본 글은 글쓴이의 개인적인 생각이 담겨있을 수 있습니다.
PICK 프로젝트 - pick-server-Saturn
https://github.com/DSM-PICK/pick-server-Saturn
프로젝트 PICK에서 내가 만들고 있는 서버 (pick-server-Saturn) 언어를
자바에서 코틀린으로 바꾼 뒤 처음 실제로 서비스를 하는 날이었다.
기존에 자바로 만들어진 서버 코드를 코틀린으로 바꾸는 작업을 거쳤기 때문에
문제가 발생할 것이라고는 생각했지만 서비스를 사용하는 오후 방과후 시간이 되기 전까지
많은 QA를 거쳤기 때문에 큰 문제가 발생할 것이라고는 생각 하지 않았다.
하지만 모두의 예상을 뛰어넘고 학생의 출석 상태를 변경할 때 서버는 절망적이게도 500을 뱉어냈다.
출석 상태를 변경하는 API는 Saturn 서버에게 책임이 있었기에 내가 버그 픽스를 하기 위해
자습을 하다가 바로 Swarmpit을 켜서 로그를 보면서 문제의 원인을 파악하기 시작했다.
Saturn 서버는 다음과 같이 @RestControllerAdvice를 이용하여
비지니스 에러가 아닌 RuntimeException이 발생하게 되면 500을 띄우고
printStackTrace()를 이용해 자세한 에러 상황을 남기게 된다.
@RestControllerAdvice
class ExceptionHandler {
...
@ExceptionHandler(RuntimeException::class)
fun runtimeExceptionHandler(e: RuntimeException): ResponseEntity<ExceptionResponse> {
e.printStackTrace()
return ResponseEntity(
ExceptionResponse(
code = "INTERNAL_SERVER_ERROR",
message = "큰 문제긴 한데 이거 나오면 안 되긴 함",
),
HttpStatus.INTERNAL_SERVER_ERROR,
)
}
}
그래서 로그를 보면 에러 상황을 알 수 있는데 로그에는 NullPointerException
과 함께
Converter와 관련한 에러
가 발생한 것을 확인할 수 있었다.
문제의 원인은 다음과 같았다.
Parameter나 Path로 받은 Request는 컨버터를 이용해 변환이 가능하지만,
Request Body에 있는 값은 컨버터를 이용해서 변환이 불가능하다는 것이었다.
@PatchMapping("/student-state")
fun changeAttendance(
@RequestHeader("Authorization") token: String,
@RequestBody request: StudentStateRequest,
) {
authService.validateToken(token)
attendanceService.updateAttendance(
studentNumber = request.number,
period = request.period,
attendanceState = request.state,
)
}
위 컨트롤러 메소드는 학생의 출석 상태를 변경하는 API의 일부이다.
@RequestBody를 통해 받은 Request Body의 타입인
StudentStateRequest
는 다음과 같이 구성된다.
class StudentStateRequest(
@NotNull(message = "허용하지 않는 형식 <NULL>")
val period: Period,
@NotBlank(message = "허용하지 않는 형식 <NULL, EMPTY, BLANK>")
val number: String,
@NotNull(message = "허용하지 않는 형식 <NULL>")
val state: State,
)
이렇게 Period, State와 같이 Request Body 안에 있는 필드들은
컨버터에 의해 적용이 되지 않는 다는 것이었다.
하지만 나는 웬만하면 적용시키고 싶었기 때문에 해결할 수 있는 문서들을 찾아보았다.
이에 대한 문서는 StackOverflow에서 찾을 수 있었다.
https://stackoverflow.com/questions/36219767/using-spring-converter-in-requestbody
찾은 문서에서는 HttpMessageConverters
를 사용하되,
JSON 속성들을 건드려 기본 속성을 변환하고
JSON이 불안정한 상태가 되는 것을 추천하지는 않는다고 한다.
HttpMessageConverters
에 대해서 조금 더 찾아보니
디폴트로 자동 구성되는 HttpMessageConverter
가 4개 있고,
그 중 JsonHttpMessageConverter
가 일반적으로 @RequestBody를 통해 객체를 변환시켜주며,
현재 오류를 고치기 위해서는 MappingJacksonHttpMessageConverter
를
빈으로 등록해야 한다는 것이다.
하지만 나는 현재 핫픽스 상태이기도 하고 StackOverflow 답변자의 말에도 공감하기 때문에
StudentStateRequest
의 필드 타입을 디폴트 컨버터가 변경할 수 있는 타입으로 변경하고
Service 레이어에서 한 번 변경의 작업을 거치는 것으로 결정하였다.
이걸 적으면서 생각해보는 건데 너무 Service 레이어에 많은 부담을 주는 게 아닌가 싶다.
만약 지금 나에게 이러한 문제가 떨어진다면
Controller에서 변환을 처리해서 Service 레이어에 가기 전에 처리했을 것 같다.
그래서 StudentStateRequest
클래스는 다음과 같이 변경되었다.
class StudentStateRequest(
@NotNull(message = "허용하지 않는 형식 <NULL>")
val period: Int,
@NotBlank(message = "허용하지 않는 형식 <NULL, EMPTY, BLANK>")
val number: String,
@NotNull(message = "허용하지 않는 형식 <NULL>")
val state: String,
)
이것도 적으면서 떠올랐는데, period와 state가 원시타입으로 변경됨에 따라
Validation을 적용시켜야 했다.
그래서 period는 최솟값과 최댓값을 설정해주고
state는 NotBlack로 변경하였다.
이 사실을 몰랐던 잘못도 있지만
서비스를 사용자가 사용하기 전에 QA를 확실히 해야겠다는 생각이 들었다.