입력 값 검증 ✅❎

Lzhtk·2025년 6월 19일

오늘은 각 계층에서 수행되는 입력값의 검증 그 중에서 중복 검증을 피하면서도 안정성을 확보하는 전략과 트레이드 오프에 대해 알아보자 ❗ ❕


계층별 검증은 왜 필요할까? 🤷‍♂️

  • 입력값 검증을 단일 계층에서만 처리하지 않고 각 계층별로 수행해야 하는 이유는 다음과 같다.
    1️⃣ 관심사 분리
    • 각 계층은 서로 다른 관점에서 데이터를 바라보며, 이에 따라 검증해야 할 항목도 달라지기 때문이다.
    • 모든 검증을 한 계층에서 처리하면 해당 계층의 책임이 과도하게 커지고, 코드의 응집도가 떨어진다.
      2️⃣ 방어 계층의 필요성
    • 외부 요청이 내부로 전파될수록 처리 비용이 증가하므로, 최대한 외부에서 잘못된 데이터를 걸러내는 것이 효율적이다.
    • 각 계층이 방어 계층으로서 작동하면서 시스템의 안정성을 높일 수 있기 때문이다.

계층별 입력값 검증 역할 🧗‍

  • Controller(프레젠테이션 계층) : 잘못된 요청을 방지하고 빠른 피드백을 목적으로 형식적이고 구문적인 검증을 한다.
  • Service(비즈니스 계층) : 도메인 규칙 위반 방지를 목적으로 비즈니스 정책 검증을 한다.
  • Entity(도메인 모델) : 도메인의 무결성 보장을 목적으로 하고 객체 상태의 불변성을 검증한다.
  • Repository(DB 계층) : 최종 방어선으로 일관성 유지를 목적으로 스키마 제약 검증을 한다.

중복 검증을 피하면서도 안정성을 확보하는 전략 🧐

  • 전략 1 : 확실한 관심사 분리 원칙

    • 각 계층이 고유 책임만 수행하도록 한다.
    • ex ) 형식 검증은 Controller, 정책 검증은 Service, 상태 유지는 Entity에서!
  • 전략 2 : Bean Validation + 커스텀 유효성 검사

    • Javax.validation 기반 어노테이션을 Controller에서 활용! ( @Valid )
    • Service나 Domain에서는 비즈니스 요구사항에 맞춘 별도 로직을 사용한다.
    @PostMapping(path = "public")
    public ResponseEntity<ChannelDto> create(@Valid @RequestBody PublicChannelCreateRequest request) {
      ChannelDto createdChannel = channelService.create(request);
      return ResponseEntity
          .status(HttpStatus.CREATED)
          .body(createdChannel);
    }
  • 전략 3 : 불변 객체 설계

    • 엔티티나 VO를 유효한 상태로만 생성 가능하게 만들어 내부 중복 검증 제거

트레이드오프 ⚖️

  • 입력값 검증은 중복을 최소화하며 유지보수를 용이하게 해야 하면서도, 시스템의 무결성과 보안은 절대 포기할 수 없는 영역이다. 그렇기에 각 계층에 어떠한 검증을 둘 것인지 결정할 때 아래 항목과 같은 트레이드오프를 고려해야 한다.

    1. 중복 방지 vs 안정성 확보
    • 중복 방지에 치우친 경우
      • 검증 로직이 단일 책임으로 응집되어 유지보수에 좋다!
      • 실수로 검증이 누락될 경우 보안 문제나 예외 발생이 가능하다😥
      • ex ) email 중복 체크를 Controller에서만 하는 경우
    • 안정성 확보에 치우친 경우
      • 다층 방어로 내부 호출이나 악의적 요청도 방어 가능하다.
      • 같은 검증이 여러 계층에 존재하여 코드 중복 및 유지보수 비용이 증가한다.
      • ex ) Service와 Domain 모두에서 중복 검사 수행
    1. UX 향상 vs 시스템 무결성 우선
    • UX 중심 설계
      • 사용자에게 빠르고 구체적인 오류 메시지 제공!
      • 모든 경로에 적용되기 어려움
      • ex ) @Valid로 필드 에러를 세밀하게 반환하는 경우
    • 시스템 무결성 중심 설계
      • 데이터 무결성과 시스템 일관성 보장
      • 사용자는 DB 제약 위반이나 NPE같은 불필요한 메시지 경험
      • ex ) DB UNIQUE 제약에만 의존해서 ConstraintViolationException 발생
    1. 개발 속도 vs 확장성과 유지보수성
    • 빠른 구현 위주 설계
      • 검증 로직을 한 곳에 모아 빠르게 개발 가능!
      • 모든 검증을 Controller나 Service에 몰아넣었기에 코드가 커져 재사용이 힘듦..
      • ex ) 서비스 단에서 모든 필드 유효성 + 정책 Check하는 경우
    • 확장성과 구조화된 설계
      • 계층 간 책임 분리로 구조가 명확하고 테스트가 용이하다!
      • 초기 구현 시간이 증가하고 설계 복잡도가 증가한다..
      • ex ) 각 유효성 검사를 Domain, Validator 클래스로 분리
    1. 보안/회복력 강화 vs 성능 최적화
    • 보안과 회복력 중시
      • 잘못된 데이터가 어디서 들어오든 항상 방어 가능
      • 모든 계층에서 검증하므로 처리 비용 증가
      • ex ) 내부 배치 호출에서도 Service/Domain 검증 필수
    • 성능 중시
      • 중복 로직 생략으로 요청 처리 속도 개선 !
      • 외부 요청 아닌 내부 호출에서도 예외 발생시 취약..
      • ex ) REST API에만 @Valid 사용하고 내부 로직은 생략하는 경우
  • 결국 검증은 안정성, 성능, 유연성 사이의 문제이며, 각 계층의 책임을 명확히 정의하지 않으면 중복 또는 누락이라는 또 다른 문제가 발생할 수 있다.


    마무리 🔚

    입력값 검증은 단순히 @Valid를 붙이는 것이 아닌, 각 계층이 맡아야 할 책임을 명확히 정의하고 신뢰 가능한 시스템을 구축하는 기초작업이다. 모든 검증을 한 곳에서 처리하려는 쉬운 길을 택하기 보단 시스템의 무결성을 다중 계층에서 방어할 수 있도록 설계해보자🤗

0개의 댓글