패키지 경로부터 시작해서 아예 관리자 컨트롤러 내부에 있는 메서드까지 명시하니까 에러가 사라지고 잘 작동했다!
발생한 문제 : HttpServletRequest의 getInputStream() 호출 시 에러 발생
requestBody내역을 로그에 찍어야 하기 때문에 LogAspect에서 getInputStream()을 호출하는 코드를 작성했다!
public String getRequestBody(HttpServletRequest request)throws IOException {
ObjectMapper objectMapper = new ObjectMapper();
return objectMapper.readTree(request.getInputStream()).toString();
}
하지만 에러가 발생했다
Request processing failed: java.lang.IllegalStateException: getInputStream() has already been called forthis request
HTTP 요청에 대해 getInputStream() 메서드를 한 번 이상 호출했을 때 발생하는 에러가 발생한 것이다!
찾아보니 HttpServletRequest에서 Stream형태로 RequestBody에 접근해 데이터를 가져갈경우에 스트림이 소비되면서 딱 한번만 읽을 수 있기 때문에 getInputStream() 또는 getReader()를 호출하면, 스트림이 소비되어 이후 다시 읽을 수 없다는 것이다
AOP를 사용하면서 먼저 RequestBody를 읽어서 스트림을 소비하고, 그 다음에 컨트롤러에서 한번 더 읽으려고 했는데 이미 스트림은 소비된 후라서 발생하게 된 것!!
해결 : HttpServletRequest의 Body를 여러번 읽을 수 있도록 바꾸기
RequestWrapper 추가
@GetterpublicclassRequestWrapperextendsHttpServletRequestWrapper{
// 요청 바디를 저장하는 변수 (다시 읽을 수 있도록 캐싱)privatefinal String body;
publicRequestWrapper(HttpServletRequest request){
super(request);
StringBuilder stringBuilder = new StringBuilder();
try (BufferedReader bufferedReader = request.getReader()) { // 요청 바디를 읽을 BufferedReader 생성char[] charBuffer = newchar[128]; // 128자씩 읽어올 버퍼 생성int bytesRead;
while ((bytesRead = bufferedReader.read(charBuffer)) > 0) {
// 읽어온 데이터를 StringBuilder에 추가
stringBuilder.append(charBuffer, 0, bytesRead);
}
} catch (IOException e) {
// 요청 바디를 읽다가 예외가 발생하면 400 Bad Request 예외 발생thrownew BadRequestException();
}
// 읽은 요청 바디를 문자열로 변환하여 저장
body = stringBuilder.toString();
}
@Overridepublic ServletInputStream getInputStream(){
// 저장된 body 문자열을 ByteArrayInputStream으로 변환 (다시 읽을 수 있도록 함)final ByteArrayInputStream byteArrayInputStream = new ByteArrayInputStream(body.getBytes());
returnnew ServletInputStream() {
@OverridepublicbooleanisFinished(){
returnfalse; // 스트림이 끝났는지 여부 (false로 설정하여 계속 읽을 수 있도록 함)
}
@OverridepublicbooleanisReady(){
returnfalse; // 스트림이 준비되었는지 여부 (false로 설정)
}
@OverridepublicvoidsetReadListener(ReadListener readListener){
thrownew UnsupportedOperationException(); // 비동기 읽기 미지원
}
@Overridepublicintread(){
return byteArrayInputStream.read(); // 저장된 요청 바디를 한 글자씩 읽어서 반환
}
};
}
@Overridepublic BufferedReader getReader(){
// getInputStream()을 기반으로 BufferedReader 생성하여 반환 (요청 바디를 여러 번 읽을 수 있도록 함)returnnew BufferedReader(new InputStreamReader(this.getInputStream()));
}
}
여러번의 시도를 해봤지만 응답객체의 바디값은 나오지 않았고.. 시도했던 이상한 중구난방 클래스들과 메서드들을 모두 지우고 처음부터 다시 찾아보기로 마음을 잡았다
사실 저 첫 번째 시도때 사용했던 코드들은 이해가 잘 되지 않았다 (gpt에게 냅다 물어봐서 얻은 코드이기 때문..)
이걸 쓰면서도 이게 뭔지도 모르고 자꾸 에러나고 하라는대로 하면서 계속 삽질을 했기 때문이다
다시 구글링을 열심히 하면서 찾아본 결과!!
// 메서드 실행
Object result = joinPoint.proceed();
이 result객체가 메서드를 실행한 이후의 응답 객체라는 것이다!!!!
엥?? 그러면 그냥
// 메서드 실행
Object result = joinPoint.proceed();
// 메서드 실행 후
LocalDateTime responseTime = LocalDateTime.now();
String responseBody = getResponseBody(result);
응답 body를 출력하기 위해서 구글링을 하던 도중 내가 원하는 자료가 많이 보이지 않아서 그냥 gpt에게 물어보면서 해결하려 했는데(첫 번째 시도 : RequestWrapper 사용 부분), 무슨 코드인지도 모르는 상태에서 계속 질문하고 답은 안되고 뱅글뱅글 반복하면서 시간을 너무 허비한 것 같았다..
사실 gpt를 사용해서 코드를 짜는 걸 별로 좋아하지 않는데 (내가 짠 코드도 아니고 이게 뭔지도 모르면서 쓰는 것도 찝찝하고 감당이 안되서.. ) 역시 직접적인 문제해결은 내가 일단 이해하고 뭔지 안 다음에 되는 것 같다는 생각이 들었다
Object result = joinPoint.proceed();이것만 잠시 생각해 봐도 금방 해결할 수 있는 문제를 오히려 gpt를 사용하면서 더 헛수고를 하게 된 것이다..!!!
지금 단계에서는 역시 내가 감당하고 이해할 수 있는 코드만 만들고 gpt를 사용한 문제해결을 최대한 지양해야겠다는 생각이 더 들게 됐다
다양한 경우의 수의 에러 메시지가 비지니스 코드 내에서 직접 작성되어있기 때문에, 어떤 에러 메시지가 있는지 한눈에 알아보기 힘듦
@Transactionalpublic SignupResponse signup(SignupRequest signupRequest){
if (userRepository.existsByEmail(signupRequest.getEmail())) {
thrownew InvalidRequestException("이미 존재하는 이메일입니다.");
}
public SigninResponse signin(SigninRequest signinRequest){
User user = userRepository.findByEmail(signinRequest.getEmail()).orElseThrow(
() -> new InvalidRequestException("가입되지 않은 유저입니다."));
// 로그인 시 이메일과 비밀번호가 일치하지 않을 경우 401을 반환합니다.if (!passwordEncoder.matches(signinRequest.getPassword(), user.getPassword())) {
thrownew AuthException("잘못된 비밀번호입니다.");
}
동일한 내용의 에러 메시지가 코드 내에서 여러번 재사용되고 있음
만약 같은 내용이지만 여러 곳에 분포되어 있는 에러 메시지가 추후에 변경된다면 직접 그곳을 찾아다니며 수정해줘야 함
@Transactionalpublic CommentSaveResponse saveComment(AuthUser authUser, long todoId, CommentSaveRequest commentSaveRequest){
User user = User.fromAuthUser(authUser);
Todo todo = todoRepository.findById(todoId).orElseThrow(() ->
new InvalidRequestException("Todo not found"));
public TodoResponse getTodo(long todoId){
Todo todo = todoRepository.findByIdWithUser(todoId)
.orElseThrow(() -> new InvalidRequestException("Todo not found"));
@Transactional(readOnly = true)public List<ManagerResponse> getManagers(long todoId){
Todo todo = todoRepository.findById(todoId)
.orElseThrow(() -> new InvalidRequestException("Todo not found"));
에러 메시지를 Enum Class에 담아서 관리하여 개선
에러 객체 분리에 대한 고민
ServerException을 좀더 구체적으로 나눌까(관리에러, 날씨 api에러 등..) 고민
구체적으로 나눈다면 추후에 기능이 추가될 때마다 예외 객체들 또한 무수히 많아질 것 같고, 에러를 발생시킨 책임의 주체만 분명히 하고(서버인지, 클라이언트인지) 어차피 구체적인 내용은 에러 메시지를 통하여 전달하면 된다고 생각함
기존에 AuthException(인증, 인가 에러), ServerException(서버에서 발생하는 에러), InvalidRequestException(클라이언트가 잘못된 요청으로 인해 발생하는 에러)를 유지함
Enum Class 생성
common 패키지 하위에 errorcode를 만들고 에러 메시지를 한번에 관리할 수 있는 ErrorCode Enum Class를 생성
각각의 Exception Class별로 구분하여 에러 메시지를 배치
publicenumErrorCode{
// AuthException
EMAIL_ALREADY_EXISTS("이미 사용 중인 이메일입니다."),
USER_NOT_FOUND_BY_EMAIL("해당 이메일로 등록된 유저가 없습니다."),
INVALID_PASSWORD("비밀번호가 잘못되었습니다."),
AUTH_AND_AUTHUSER_REQUIRED("@Auth와 AuthUser 타입은 함께 사용되어야 합니다."),
// ServerException
WEATHER_DATA_FETCH_FAILED("날씨 데이터를 가져오는데 실패했습니다."),
NO_WEATHER_DATA_FOUND("날씨 데이터가 없습니다."),
NO_WEATHER_DATA_FOR_TODAY("오늘에 해당하는 날씨 데이터를 찾을 수 없습니다."),
TOKEN_NOT_FOUND("토큰을 찾을 수 없습니다."),
// InvalidRequestException
TODO_NOT_FOUND("해당하는 할일이 없습니다."),
MANAGER_NOT_FOUND("관리자가 없습니다"),
NOT_ASSIGNED_TO_SCHEDULE("해당 일정에 등록된 담당자가 아닙니다."),
CANNOT_REGISTER_AS_SELF("일정 작성자는 본인을 담당자로 등록할 수 없습니다."),
USER_NOT_FOUND("유저가 존재하지 않습니다."),
INVALID_SCHEDULE_CREATOR("담당자를 등록하려고 하는 유저가 일정을 만든 유저가 유효하지 않습니다."),
INVALID_USER_ROLE("유효하지 않은 UserRole입니다."),
USING_PASSWORD("이미 사용 중인 비밀번호 입니다.");
private String message;
privateErrorCode(String message){
this.message = message;
}
public String getMessage(){
return message;
}
}