[스프링] Validation에 대하여

2025. 9. 27. 16:28·스프링

웹을 개발하다 보면 회원가입이나 상품 등록 같은 입력 화면에서 이런 메시지를 흔히 본다.

  • 이메일을 잘못 입력했을 때 → “이메일 형식으로 입력해주세요”
  • 수량을 너무 크게 입력했을 때 → “최대 수량은 9999개까지 가능합니다”

만약 이런 검증이 없다면 이름에 숫자가 들어가거나, 상품 수량이 비정상적으로 들어가거나, 가격×수량의 총액이 터무니없는 값으로 계산되는 등 여러 문제가 발생할 것이다.
이러한 문제를 해결하는 과정이 바로 Validation(검증) 이다.

 

과거 방식 — if문 검증

예전에는 이런 검증을 전부 if 문으로 직접 처리했다.

// 특정 필드 검증
if (item.getQuantity() == null || item.getQuantity() > 10000) {
    errors.rejectValue("quantity", "max", new Object[]{9999}, null);
}

// 전체 비즈니스 규칙 검증
if (item.getPrice() != null && item.getQuantity() != null) {
    int resultPrice = item.getPrice() * item.getQuantity();
    if (resultPrice < 10000) {
        errors.reject("totalPriceMin", new Object[]{10000, resultPrice}, null);
    }
}

이 방식은 간단하지만, 화면과 조건이 늘어날수록 코드가 복잡해지고 중복이 쌓여 유지보수에 큰 비용이 든다.

 

따라서 오늘은

  1. 스프링이 제공하는 Bean Validation 이 무엇인지,
  2. Validation Groups로 상황별 제약을 처리하는 방법과 한계,
  3. 그리고 실무에서 널리 쓰이는 DTO 분리 방식

까지 차례대로 알아보겠다.

 

2. Bean Validation — 어노테이션 기반 검증

스프링은 JSR-303/380 표준인 Bean Validation을 지원한다.
검증 로직을 코드로 직접 쓰는 대신 어노테이션을 붙여 선언만 하면 된다.

 

예를 들어 보자

@Data
public class Member {
    @NotBlank               // 공백 불가 Null 불가
    private String name;

    @Email                  // 이메일 형식
    @NotBlank
    private String email;

    @NotNull
    @Range(min = 1, max = 100)
    private Integer age;
}

이처럼 어노테이션만으로도 검증 규칙을 선언할 수 있다.
하지만 단순히 선언만으로 끝나는 것이 아니라, 컨트롤러에서 @Valid 또는 @Validated를 함께 사용해야 한다.
즉, 해당 애노테이션을 지정하면 스프링이 해당 객체를 대상으로 실제 검증을 수행하고, 그 결과를 BindingResult에 담아준다.

@PostMapping("/members")
public String saveMember(@Valid @ModelAttribute Member member,
                         BindingResult bindingResult) {
    if (bindingResult.hasErrors()) {
        return "members/addForm";
    }
    return "redirect:/members";
}

이제 검증 규칙은 어노테이션이 담당하고, 실행 시점에 스프링이 자동으로 검증을 수행한다.

 

이를 사용함으로써 중복되고 가독성을 떨어뜨리는 많은 if문들이 해결되어 개발자는 더 편리하게 개발 할 수 있다.

 

하지만 이는 한계가 존재한다.

 

3. Validation Groups — 상황별 제약과 한계

실무에서는 단순히 한두 개의 필드만 검증하는 수준을 넘는 경우가 많다.
예를 들어 기획자가 이렇게 요구했다고 해보자.

  • “상품 등록할 때는 수량을 9999개 이하로 제한해 주세요.”
  • “그런데 상품 수정할 때는 수량 제한을 풀어주세요. 이미 등록된 상품을 막으면 안 됩니다.”

즉, 같은 객체라도 등록과 수정에서 제약 조건이 달라야 한다.

 

Groups로 해결하기

스프링은 이를 위해 Validation Groups 기능을 제공한다.

public interface SaveCheck {}
public interface UpdateCheck {}
@Data
public class Item {
    @NotNull(groups = UpdateCheck.class) // 수정 시에는 ID 필요
    private Long id;

    @NotBlank(groups = {SaveCheck.class, UpdateCheck.class})
    private String itemName;

    @NotNull(groups = {SaveCheck.class, UpdateCheck.class})
    @Range(min = 1000, max = 1000000)
    private Integer price;

    @Max(value = 9999, groups = SaveCheck.class) // 등록 시에만 제약
    private Integer quantity;
}

컨트롤러에서 그룹을 지정해 사용한다.

@PostMapping("/items/add")
public String add(@Validated(SaveCheck.class) @ModelAttribute Item item,
                  BindingResult bindingResult) { ... }

@PostMapping("/items/{itemId}/edit")
public String edit(@Validated(UpdateCheck.class) @ModelAttribute Item item,
                   BindingResult bindingResult) { ... }

이렇게 하면 등록과 수정 요구사항을 구분할 수 있다.

 

그러나 한계가 있다

문제는, 실무에서는 요구사항이 등록/수정뿐만 아니라 계속 늘어난다는 점이다.
예를 들어 “관리자만 등록할 수 있는 특수 상품”, “이벤트 한정 상품”, “수정 시 특정 필드만 허용” 등 조건이 복잡해진다.

그때마다 그룹이 추가되고, 엔티티의 검증 규칙이 화면 요구사항에 종속되어 점점 꼬인다.
결국 엔티티가 오염되고 유지보수가 어려워지는 문제가 생긴다.

 

그래서 Validation Groups는 가능은 하지만, 실무에서는 권장되지 않는다.

 

4. 실무 방식 — DTO 분리

실제 현장에서는 등록 DTO, 수정 DTO를 따로 만들어 검증을 분리하는 방식을 더 많이 사용한다.

이 방식은

  • 검증 규칙을 상황에 맞게 각각 선언할 수 있고,
  • 엔티티를 직접 노출하지 않아 API 스펙을 안전하게 관리할 수 있으며,
  • 화면 요구사항이 바뀌어도 엔티티는 안정적으로 유지된다는 장점이 있다.

등록 DTO

@Data
public class ItemSaveForm {
    @NotBlank
    private String itemName;

    @NotNull
    @Range(min = 1000, max = 1000000)
    private Integer price;

    @NotNull
    @Max(9999)
    private Integer quantity;
}

 

수정 DTO

@Data
public class ItemUpdateForm {
    @NotNull
    private Long id;

    @NotBlank
    private String itemName;

    @NotNull
    @Range(min = 1000, max = 1000000)
    private Integer price;

    // 수정에서는 수량 제한 없음
    private Integer quantity;
}
 

컨트롤러 예시

@PostMapping("/{itemId}/edit")
public String edit(@PathVariable Long itemId,
                   @Validated @ModelAttribute("item") ItemUpdateForm form,
                   BindingResult bindingResult) {

    // 복합 검증: 총액은 최소 10000원 이상
    if (form.getPrice() != null && form.getQuantity() != null) {
        int total = form.getPrice() * form.getQuantity();
        if (total < 10000) {
            bindingResult.reject("totalPriceMin",
                                 new Object[]{10000, total}, null);
        }
    }

    if (bindingResult.hasErrors()) {
        return "validation/v4/editForm";
    }

    Item itemParam = new Item();
    itemParam.setItemName(form.getItemName());
    itemParam.setPrice(form.getPrice());
    itemParam.setQuantity(form.getQuantity());

    itemRepository.update(itemId, itemParam);
    return "redirect:/validation/v4/items/{itemId}";
}

 

즉, 등록용 폼과 수정용 폼을 각각 분리해서 검증 규칙을 따로 선언하면 상황에 맞는 제약 조건을 깔끔하게 적용할 수 있다.
이 방식은 단순히 검증을 편리하게 한다는 것 이상의 장점이 있다.

 

우선, 폼별로 검증을 수행할 수 있기 때문에 등록 시에는 엄격한 제약을 걸고, 수정 시에는 완화된 규칙을 적용하는 등 요구사항을 유연하게 반영할 수 있다.


또한, 엔티티를 직접 컨트롤러나 화면과 연결하지 않고 폼 전용 DTO를 따로 두게 되므로, 엔티티의 내부 구조나 불필요한 필드가 그대로 외부에 노출되는 문제도 막을 수 있다.


이는 곧 API 스펙을 안정적으로 유지하는 효과로 이어진다.

결과적으로, 이런 접근법은 검증의 명확성과 보안성, 그리고 유지보수성을 동시에 확보할 수 있어 실무에서도 권장되는 방식이다.

 

 

마무리하며:

오늘은 스프링에서 제공하는 Validation(검증) 기능에 대해 살펴보았다.

 

먼저, 과거에는 단순히 if 문으로 검증 로직을 직접 작성했지만, 이런 방식은 화면과 조건이 많아질수록 코드가 길어지고 중복이 생겨 유지보수에 큰 한계가 있었다.

 

이를 개선하기 위해 스프링은 Bean Validation을 지원한다. 개발자는 엔티티나 DTO에 간단히 어노테이션을 붙여 제약 조건을 선언하기만 하면 되고, 컨트롤러에서 @Valid 또는 @Validated를 지정하면 스프링이 실행 시점에 자동으로 검증을 수행한다. 덕분에 코드가 간결해지고, 검증 규칙이 눈에 잘 보이는 장점이 있다.

 

하지만 현실적인 요구사항은 단순하지 않다. 예를 들어 등록 시에는 허용되지 않는 값이 수정 시에는 허용되기도 한다. 이런 상황을 해결하기 위해 스프링은 Validation Groups 기능을 제공하지만, 시간이 지날수록 그룹이 늘어나고 엔티티가 화면 요구사항에 종속되면서 오히려 코드가 복잡해지고 유지보수가 어려워지는 문제가 발생한다.

 

그래서 실무에서는 등록용 DTO, 수정용 DTO를 아예 분리해서 관리하는 방식을 더 많이 사용한다. 이렇게 하면 검증 규칙을 상황에 맞게 명확히 나눌 수 있고, 엔티티를 직접 노출하지 않아 API 스펙도 안전하게 보호할 수 있다. 화면 요구사항이 변하더라도 DTO만 바꾸면 되므로, 엔티티는 본래의 도메인 규칙에 집중할 수 있고 결과적으로 유지보수성도 높아진다.

 

 

감사합니다.

 

'스프링' 카테고리의 다른 글

[스프링] 로그인 처리 2 - 서블릿 필터  (0) 2025.09.29
[스프링] 로그인 처리 1- 쿠키, 세션  (0) 2025.09.28
[스프링] 메시지, 국제화  (0) 2025.09.19
[스프링] 스프링 MVC - 기본기능 (마지막) HTTP 요청 메시지: text와 JSON  (1) 2025.09.03
[스프링] 스프링 MVC - 기본기능 (2) 요청 파라미터  (0) 2025.09.03
'스프링' 카테고리의 다른 글
  • [스프링] 로그인 처리 2 - 서블릿 필터
  • [스프링] 로그인 처리 1- 쿠키, 세션
  • [스프링] 메시지, 국제화
  • [스프링] 스프링 MVC - 기본기능 (마지막) HTTP 요청 메시지: text와 JSON
0kingki_
0kingki_
자바 + 스프링 웹 개발
  • 0kingki_
    0kingki_
    0kingki_
  • 전체
    오늘
    어제
    • 분류 전체보기 (134)
      • 코딩 테스트 (54)
      • 자바 (21)
      • 스프링 (27)
      • 타임리프 (16)
      • 스프링 데이터 JPA (8)
      • 최적화 (2)
      • QueryDSL (4)
      • AWS (2)
  • 블로그 메뉴

    • 홈
    • 태그
    • 방명록
  • 링크

  • 공지사항

  • 인기 글

  • 태그

    spring
    JPA
    예외처리
    스프링 컨테이너
    Java
    스프링
    백준
    쿼리dsl
    코딩 테스트
    예외 처리
    자바
    다형성
    객체지향
    재귀
    코딩테스트
    mvc
    BFS
    쿼리
    SOLID
    SpringDataJpa
    타임리프
    thymeleaf
    QueryDSL
    컬렉션
    최적화
    불변객체
    스프링 데이터 JPA
    fetch join
    dfs
    LocalDateTime
  • 최근 댓글

  • 최근 글

  • hELLO· Designed By정상우.v4.10.4
0kingki_
[스프링] Validation에 대하여
상단으로

티스토리툴바