삽질인가 고찰인가

❓ init {}에서 유효성 검사하면 왜 ExceptionHandler에서 안 잡힐까?

우당탕 오리의 개발모험 2025. 7. 24. 10:22

Spring에서 @RestController를 만들 때, 요청 객체(Request DTO) 안에 init {} 블록으로 유효성 검사를 넣는 경우가 종종 있다.
예를 들어 아래처럼 말이다.

data class MyRequest(
    val name: String,
    val age: Int
) {
    init {
        require(name.isNotBlank()) { "이름은 필수입니다." }
        require(age > 0) { "나이는 1살 이상이어야 합니다." }
    }
}

그리고 이렇게 작성하면 당연히 유효성 실패 시 @RestControllerAdvice에서 잡히겠거니… 했는데?

 

❗ 안 잡힌다???

 

🤔 왜 안 잡히는 걸까?

Spring은 요청 본문(@RequestBody)을 JSON → 객체로 바꾸는데 이 작업을
컨트롤러에 진입하기 전에 Jackson 등의 HttpMessageConverter에서 먼저 처리한다.

즉,

  1. JSON → 객체 변환 (여기서 init {} 실행됨)
  2. 이때 예외 발생 시 → Spring이 자체적으로 HttpMessageNotReadableException 같은 걸 던짐
  3. 우리가 만든 예외는 무시되고, @ExceptionHandler(MyCustomException::class)에서는 안 잡힘

 

✅ 해결 방법 3가지

1. Bean Validation을 쓰자 (@Valid, @NotBlank, etc.)

Spring 공식 추천 방식이다.
요청 객체에 애노테이션만 붙여도 자동 검증이 되고, 예외도 잘 잡힌다.

import jakarta.validation.constraints.*

data class MyRequest(
    @field:NotBlank
    val name: String,

    @field:Min(1)
    val age: Int
)
@PostMapping
fun create(@RequestBody @Valid request: MyRequest) {
    // 여긴 유효한 요청만 옴
}

@ControllerAdvice에 MethodArgumentNotValidException 잡아서 예외 포맷 통일 가능.

 

2. 검증 로직은 init 말고 별도 validate() 함수로 빼기

DTO 생성 자체는 허용하되, 검증은 Controller나 Service에서 직접 호출한다.

data class MyRequest(val name: String?, val age: Int?) {
    fun validate() {
        require(!name.isNullOrBlank()) { "이름은 필수입니다." }
        require(age != null && age > 0) { "나이는 1살 이상이어야 합니다." }
    }
}
@PostMapping
fun create(@RequestBody request: MyRequest) {
    request.validate() // 여기서 실패 시, 우리가 만든 예외 잘 잡힘
}

 

 

3. Controller 바깥 계층에서 검증 (도메인 규칙은 service 이후에서)

복잡한 비즈니스 검증은 DTO → 도메인 모델 변환 이후, Service 계층에서 처리하는 게 더 깔끔하다.
Request는 그저 데이터만 옮기는 "메신저" 역할로 두는 것.

 

🌱 결론

방법 장점 단점
Bean Validation (@Valid) 간편하고 Spring 기본방식 복잡한 조건 처리 어려움
validate() 함수 분리 유연하게 검증 가능 validate 호출 깜빡할 수 있음
Service에서 검증 도메인 로직에 충실 단순한 필드검증에는 과함

 

🔚 마무리

init {}에서 예외 던지면 @RestControllerAdvice로 못 잡는다.
검증은 Controller 이후에서 하자!