본문 바로가기

Spring에서 유효성 검사는 어디서 해야 할까?

@cojoop 2025. 7. 12. 09:40

클라이언트가 보내는 요청 데이터에 대해 필수 여부, 길이 제한, 형식 검사 등을 수행해야 할 때, 그 책임을 어디에 둘 것인지에 따라 코드의 가독성, 재사용성, 테스트성이 크게 달라지게 된다. 

 

이번 글에서는 Spring 기반의 프로젝트에서 유효성 검사를 어디에 두는 것이 적절한지, 그리고 각 위치별 장단점과 사용 시점에 대해 정리해보려 한다.


✅ 유효성 검사란?

유효성 검사(Validation)는 클라이언트가 보낸 데이터가 시스템이 요구하는 조건을 만족하는지 확인하는 과정이다.

 

예시

  • 빈 값 검사
  • 숫자 범위 검사
  • 날짜 형식 검사
  • enum 값 제한
  • JSON 구조의 필수 속성 체크

 

📍 유효성 검사를 할 수 있는 위치들

Spring에서는 보통 다음과 같은 위치에서 유효성 검사를 수행한다.

위치 설명
1. Controller 계층 @Valid, @Validated를 이용한 검증
2. Service 계층 비즈니스 로직 수행 전 검증
3. Validator 클래스 책임 분리된 전용 클래스에서 검증
4. DTO 내부 필드에 어노테이션으로 제약 명시
5. Interceptor/Filter 요청 전체에 대한 사전 필터링 수행

1. Controller 계층에서의 유효성 검사

@PostMapping("/messages/basic")
public ResponseEntity<?> sendMessagesBasic(@Valid @RequestBody MessageRequestDto dto, BindingResult result) {
    if (result.hasErrors()) {
        return ResponseEntity.badRequest().body(result.getAllErrors());
    }
    messageService.save(dto);
    return ResponseEntity.ok().build();
}

✅ 장점

  • 간단한 입력 검증에 적합
  • @Valid를 이용해 선언적으로 처리 가능

❌ 단점

  • 컨트롤러가 검증 로직으로 인해 비대해질 수 있음
  • 복잡한 검증 조건에는 부적합
💡단순한 null 체크, 길이 제한 등 입력값 검증에 적합

2. Service 계층에서의 유효성 검사

public void save(UserRequestDto dto) {
    if (!dto.getEmail().contains("@")) {
        throw new ValidationException("이메일 형식이 올바르지 않습니다.");
    }
    // 저장 로직
}

✅ 장점

  • 도메인 규칙과 함께 검증 가능
  • DB 조회 후 상태 기반 검증 가능 (ex. 중복 체크)

❌ 단점

  • 검증과 비즈니스 로직이 섞여 관심사 분리에 위배
  • 테스트와 재사용이 어려움
💡 "탈퇴한 유저는 저장 불가" 같은 비즈니스 조건 검증에 적합

3. Validator 클래스를 분리하는 방식

@Component
public class UserValidator {
    public void validate(UserRequestDto dto) {
        if (!dto.getEmail().contains("@")) {
            throw new ValidationException("이메일 형식 오류");
        }
    }
}
userValidator.validate(dto);

✅ 장점

  • 관심사 분리로 구조가 명확
  • 테스트, 재사용 용이
  • 복잡한 검증 로직을 메서드 단위로 세분화 가능

❌ 단점

  • Validator 클래스 수가 많아질 경우 관리 복잡도 증가
  • 명확한 설계 기준이 필요
💡도메인별 검증 로직이 많거나 복잡한 경우, 다양한 계층(Service, Handler, Event 등)에서 공통 검증이 필요한 경우 사용

4. DTO에 어노테이션 사용

public class UserRequestDto {

    @NotBlank
    private String name;

    @Min(1)
    @Max(99)
    private int age;
}

✅ 장점

  • 선언적으로 조건을 명시할 수 있어 가독성이 높음
  • 공통 제약을 간결하게 표현 가능

❌ 단점

  • 복잡한 조건은 표현 불가
  • 조건이 자주 바뀌면 DTO를 재사용하기 어려움
💡 null, 길이, 범위 같은기본 제약 조건에 적합

5. Interceptor/Filter에서의 유효성 검사

public class ApiKeyInterceptor implements HandlerInterceptor {
    @Override
    public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) {
        String apiKey = request.getHeader("X-API-KEY");
        if (!"SECRET".equals(apiKey)) {
            response.setStatus(HttpServletResponse.SC_UNAUTHORIZED);
            return false;
        }
        return true;
    }
}

✅ 장점

  • 전역적인 요청 검증 가능
  • 인증, 보안 등 비즈니스 외적 검증에 적합

❌ 단점

  • JSON 바디는 읽기 어려워 Payload 검증에는 부적절
  • 비즈니스 검증은 절대 포함하면 안 됨
💡 API 키, 토큰, 헤더 등 보안 관련 전처리에 적합

 

마무리

유효성 검사는 단순한 조건 체크를 넘어서 시스템의 신뢰성과 유지보수성, 확장성까지 좌우하는 핵심 설계 요소라는 것을 다시금 깨달았다.


앞으로는 각 계층의 역할에 맞게 검증 책임을 명확히 나누고, 검증 로직 또한 깔끔하고 유연하게 설계할 수 있도록 구조적인 접근을 습관화해야겠다.

cojoop
@cojoop :: cojoop.dev

공감하셨다면 ❤️ 구독도 환영합니다! 🤗

목차