Java

[Java] 입력값 검증의 범위와 책임

Castle Bird 2026. 2. 1. 11:09

애플리케이션의 입력값 검증의 범위와 책임을 어떻게 나눌 것인가?


1. Presentation 계층 (@Controller + @Valid)

  • 사용자 입력의 형식, 필수값 여부, 문자열 길이 등 기본적인 유효성 검증을 수행합니다.
  • 목적은 빠른 피드백과 사용자 경험 향상입니다.
  • @Valid와 Bean Validation 어노테이션을 사용하여 구문적 검증(Syntactic Validation)을 수행합니다.
public class UserRegistrationRequest {

    @NotBlank(message = "아이디는 필수 입력값입니다.")
    @Size(min = 4, max = 20, message = "아이디는 4~20자 사이여야 합니다.")
    private String username;

    @Email(message = "이메일 형식이 올바르지 않습니다.")
    @NotBlank(message = "이메일은 필수 입력값입니다.")
    private String email;

    @Min(value = 14, message = "14세 미만은 가입할 수 없습니다.")
    private int age;

    // Getters, Setters or Constructor
}

@RestController
@RequestMapping("/api/users")
public class UserController {

    @PostMapping("/signup")
    public ResponseEntity<String> signUp(
        @Valid @RequestBody UserRegistrationRequest request
    ) {
        // @Valid에 의해 컨트롤러 진입 전 형식이 검증됨
        userService.register(request);
        return ResponseEntity.ok("가입 성공");
    }
}

 

2. Business 계층 (@Service)

  • 비즈니스 규칙(예: 재고 부족, 주문 가능 시간 등)을 검증합니다.
  • 도메인 객체(Entity) 내에서 자체적인 불변성을 보장하도록 검증 로직을 포함시키는 것이 좋습니다
  • 서비스 계층에서는 중복 확인 등 외부 시스템이 필요한 검증을, 엔티티 내부에서는 도메인 규칙(Semantic Validation)을 검증합니다.
@Service
@RequiredArgsConstructor
public class UserService {
    private final UserRepository userRepository;

    @Transactional
    public void register(UserRegistrationRequest request) {
        // 서비스 계층의 책임: 논리적 중복 검증
        if (userRepository.existsByUsername(request.getUsername())) {
            throw new DuplicateUserException("이미 존재하는 아이디입니다.");
        }

        User user = new User(
            request.getUsername(),
            request.getEmail(),
            request.getAge()
            );
        userRepository.save(user);
    }
}

 

3. Data Access 계층 (@Entity, @Table, @Column)

  • 데이터베이스의 고유 제약 조건(Unique), Null 제약, 데이터 타입 등 최종 데이터 무결성을 보장합니다
  • 애플리케이션 검증이 뚫리더라도 DB에서 최종적으로 데이터 무결성을 보장합니다.
@Entity
@Table(uniqueConstraints = {
    @UniqueConstraint(name = "uk_user_username", columnNames = "username")
})
public class User {

    @Column(nullable = false, length = 20) // DB 레벨의 NOT NULL, 길이 제한
    private String username;

    @Column(unique = true) // DB 레벨의 UNIQUE 제약 조건
    private String email;

    // ...
}

 

중복 검증을 피하면서도 안정성을 확보하는 방안, 트레이드오프 (Trade-off)


※ 중복 검증을 피하는 최선의 방법은 각 계층이 "자신만이 할 수 있는 검증"에만 집중하는 것입니다.

  1. DTO (Presentation): 구문적 검증 (Syntactic)
    • 역할: 데이터의 '형식'이 유효한지 입구에서 즉시 확인합니다.
    • 주요 도구: @NotBlank, @Email, @Positive, @Size 등
    • 핵심: 서비스 로직이 실행되기 전, 잘못된 요청을 Fail-Fast하게 차단하여 서버 리소스를 보호합니다.
  2. Service (Application): 논리적 검증 (Logical)
    • 역할: 시스템의 '상태''외부 데이터'를 참조해야 하는 규칙을 확인합니다.
    • 주요 작업: DB 조회를 통한 중복 아이디 확인, 외부 API를 통한 실명 인증 등
    • 핵심: 단순히 데이터의 형태만 봐서는 알 수 없는 비즈니스 흐름상의 제약을 담당합니다.
  3. Entity (Domain): 본질적 검증 (Invariants)
    • 역할: 객체가 생성되는 순간부터 '불변성(Invariants)'을 유지하도록 강제합니다.
    • 주요 도구: 생성자 내 가드 절(Guard Clause), @Column(unique=true, nullable=false)
    • 핵심: 어떤 경로로 객체가 생성되든 해당 객체가 비즈니스적으로 유효한 상태임을 보장합니다. DB 제약 조건은 데이터 오염을 막는 최후의 보루입니다.

※ 트레이드오프 (Trade-off)

  • 다중 계층 검증을 할 경우:
    • 장점: 매우 안전합니다. 코드 한곳이 수정되어 검증이 누락되어도 다른 계층이 막아줍니다.
    • 단점: 수정 사항이 생기면 DTO, Entity, DB 스키마를 모두 고쳐야 하므로 번거롭습니다.
  • 분리된 검증을 할 경우:
    • 장점: 중복 코드가 줄어들고 각 계층의 코드가 가벼워집니다.
    • 단점: 개발자가 계층 간 역할 분담을 명확히 이해하지 못하면 특정 검증이 아예 누락될 위험이 있습니다.

'Java' 카테고리의 다른 글

[Java] Mockito  (1) 2026.02.01
[Java] 모니터링 - Prometheus와 Grafana  (0) 2026.02.01
[Java] ORM의 N+1 문제  (0) 2026.01.13
[Java] @RestController와 HttpMassageConverter  (0) 2026.01.04
[Java] @Controller 와 @RestController  (0) 2025.12.26