Spring

[Spring] 필드의 유효성 검증 3가지 방법

우주유령 2025. 3. 27. 16:22
728x90
반응형

도메인 필드의 유효성 검증(validation)을 할 때 가장 좋은 방법은 각 필드에 대한 검증을 한 곳에서 관리하는 거.
이를 위해 3가지 방법을 고려할 수 있어:

  1. DTO 레벨에서 @Valid 어노테이션을 활용한 검증 (Spring Validation)
  2. Value Object를 활용하여 검증 로직을 캡슐화
  3. User 엔티티 생성 시, 생성자 내부에서 검증 실행

User 도메인을 예로 들어서 설명해볼게.


✅ 1. DTO 레벨에서 @Valid 어노테이션 활용

가장 일반적인 방식으로, Spring의 @Valid 및 @NotBlank, @Size, @Email 등을 활용하는 방법이야.

📌 DTO에서 유효성 검증

import jakarta.validation.constraints.*;

@Getter
public class UserRequestDto {
    
    @Email(message = "Invalid email format")
    @NotBlank(message = "Email cannot be empty")
    private String email;

    @Size(min = 8, message = "Password must be at least 8 characters long")
    @NotBlank(message = "Password cannot be empty")
    private String password;

    @NotBlank(message = "Name cannot be empty")
    private String name;

    @NotBlank(message = "Address cannot be empty")
    private String address;
}

📌 Controller에서 @Valid 사용

@RestController
@RequestMapping("/users")
@RequiredArgsConstructor
public class UserController {
    private final UserService userService;

    @PostMapping
    public ResponseEntity<UserResponseDto> createUser(@Valid @RequestBody UserRequestDto dto) {
        UserResponseDto response = userService.createUser(dto);
        return ResponseEntity.ok(response);
    }
}

장점

  • 코드가 간결하고 유지보수하기 쉬움
  • Spring Boot의 검증 기능을 최대한 활용 가능
  • 필드별로 검증 메시지를 쉽게 설정할 수 있음

단점

  • DTO에서만 검증이 이루어짐 → 즉, 도메인 객체(User)에서 직접 생성될 때는 유효성 검증이 적용되지 않음
  • DTO를 거치지 않고 직접 엔티티를 조작하면 검증이 무시될 수 있음

✅ 2. Value Object에서 검증 로직을 캡슐화

모든 필드에 대해 **VO (Value Object)**를 만들어서 내부에서 검증을 수행하는 방식이야.
이 방식은 도메인 계층에서 유효성 검증을 강제할 수 있어.


📌 Value Object 정의

 
@Embeddable
@Getter
@NoArgsConstructor(access = AccessLevel.PROTECTED)
public class Email {
    private String value;

    public Email(String value) {
        if (value == null || !value.matches("^[A-Za-z0-9+_.-]+@(.+)$")) {
            throw new IllegalArgumentException("Invalid email format");
        }
        this.value = value;
    }
}
@Embeddable
@Getter
@NoArgsConstructor(access = AccessLevel.PROTECTED)
public class Password {
    private String value;

    public Password(String value) {
        if (value == null || value.length() < 8) {
            throw new IllegalArgumentException("Password must be at least 8 characters long");
        }
        this.value = value;
    }
}
@Embeddable
@Getter
@NoArgsConstructor(access = AccessLevel.PROTECTED)
public class Name {
    private String value;

    public Name(String value) {
        if (value == null || value.trim().isEmpty()) {
            throw new IllegalArgumentException("Name cannot be empty");
        }
        this.value = value;
    }
}
@Embeddable
@Getter
@NoArgsConstructor(access = AccessLevel.PROTECTED)
public class Name {
    private String value;

    public Name(String value) {
        if (value == null || value.trim().isEmpty()) {
            throw new IllegalArgumentException("Name cannot be empty");
        }
        this.value = value;
    }
}

📌 User 엔티티에서 Value Object 사용

@Getter
@Entity
@Table(name = "users")
@NoArgsConstructor(access = AccessLevel.PROTECTED)
public class User {
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;

    @Embedded
    private Email email;

    @Embedded
    private Password password;

    @Embedded
    private Name name;

    @Embedded
    private Address address;
    
    private LocalDateTime createTime;
    private LocalDateTime updateTime;

    public User(Email email, Password password, Name name, Address address) {
        this.email = email;
        this.password = password;
        this.name = name;
        this.address = address;
        this.createTime = LocalDateTime.now();
        this.updateTime = LocalDateTime.now();
    }

    public void updateInfo(Name name, Address address) {
        this.name = name;
        this.address = address;
        this.updateTime = LocalDateTime.now();
    }
}

장점

  • 검증 로직이 엔티티 내부에서 수행되므로 비즈니스 로직과 일관성이 유지됨
  • 어디서든 User 객체를 생성할 때 유효성 검증이 강제됨
  • 불변성 보장 (Value Object는 생성 후 변경이 불가능)

단점

  • 모든 필드에 대해 VO를 만드는 것은 코드가 많아지고 복잡해질 수 있음
  • 엔티티가 복잡해질 수 있음

✅ 3. User 엔티티 생성 시, 생성자 내부에서 검증

위 방식이 너무 무겁다고 생각되면, 엔티티의 생성자 내부에서 검증하는 방법도 가능해.
이 경우, Value Object를 만들지 않고, User 내부에서 직접 검증 로직을 처리할 수 있어.

 
@Getter
@Entity
@Table(name = "users")
@NoArgsConstructor(access = AccessLevel.PROTECTED)
public class User {
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;

    private String email;
    private String password;
    private String name;
    private String address;
    
    private LocalDateTime createTime;
    private LocalDateTime updateTime;

    public User(String email, String password, String name, String address) {
        validateEmail(email);
        validatePassword(password);
        validateName(name);
        validateAddress(address);

        this.email = email;
        this.password = password;
        this.name = name;
        this.address = address;
        this.createTime = LocalDateTime.now();
        this.updateTime = LocalDateTime.now();
    }

    private void validateEmail(String email) {
        if (email == null || !email.matches("^[A-Za-z0-9+_.-]+@(.+)$")) {
            throw new IllegalArgumentException("Invalid email format");
        }
    }

    private void validatePassword(String password) {
        if (password == null || password.length() < 8) {
            throw new IllegalArgumentException("Password must be at least 8 characters long");
        }
    }

    private void validateName(String name) {
        if (name == null || name.trim().isEmpty()) {
            throw new IllegalArgumentException("Name cannot be empty");
        }
    }

    private void validateAddress(String address) {
        if (address == null || address.trim().isEmpty()) {
            throw new IllegalArgumentException("Address cannot be empty");
        }
    }
}

장점

  • 검증 로직이 엔티티 내부에 있어 한눈에 보기 좋음
  • 코드가 상대적으로 간결함

단점

  • VO보다 재사용성이 떨어짐
  • 불변성(immutability)을 완벽히 보장할 수 없음

🎯 결론: 어떤 방법이 가장 좋은가?

유효성 검증을 강제해야 한다면 → "Value Object" 방식 추천
DTO에서만 검증하면 충분하다면 → "Spring Validation(@Valid)" 방식 추천
간단한 검증이라면 → "User 생성자에서 검증" 방식 추천

🚀 상황에 따라 가장 적절한 방법을 선택하면 돼! 😃

 

나는 DTO, Controller에서 @Valid를 써서 들어올 때 검증하고, 필요한 경우만 생성자에서 검증하는 방법을 택함.

 

728x90
반응형