- 불변 객체 validation2023년 05월 07일
- starryeye
- 작성자
- 2023.05.07.:32
반응형불변 객체, lombok @Value 와 Java record 포스팅에서 이어진다.
개발을 하다보면 불변 객체는 Dto 로 사용되며
이는 Architecture 관점에서 각 layer 의 입력 모델과 출력 모델에 해당한다.
또한, 입력 모델과 출력 모델에 대한 검증 책임은 각 layer에 있기 때문에
불변 객체의 validation 은 항상 신경 써줘야 한다.. (생성 시점)
이번 포스팅에서는 불변 객체의 생성시점에서 자주 사용되는 검증 방법을 코드로 한번 알아보겠다.
-> 선언적 유효성 검사 방법
-> 개발자가 모두 구현하는 방식은 제외
Lombok 을 사용한 방법
import lombok.Builder; import lombok.NonNull; import lombok.Value; @Value @Builder public class Person { @NonNull String firstName; @NonNull String lastName; Integer age; }
@Value 에 의해 Person 클래스는 불변객체가 된다.
@Value 에 의해 모든 필드를 인자로 받는 생성자(package-private 접근제어자)가 만들어진다.
@Builder 에 의해 Person 클래스 내부에 public static class PersonBuilder 가 만들어진다.
public Person PersonBuilder.build() 메서드에서
@Value에 의해 만들어진 Person 생성자가 호출 되어 리턴된다.
이때, @NonNull 에 의해 Person 생성자는 아래와 같이 null 체크를 하는 코드가 된다.
Person(final @NonNull String firstName, final @NonNull String lastName, final Integer age) { if (firstName == null) { throw new NullPointerException("firstName is marked non-null but is null"); } else if (lastName == null) { throw new NullPointerException("lastName is marked non-null but is null"); } else { this.firstName = firstName; this.lastName = lastName; this.age = age; } }
여기서 우리는 firstName 이 null 일 경우엔 default 값을 넣고 싶을 수도 있다.
혹은, @NonNull 말고 자체적으로 null 체크를 하고 싶을 수도 있다.
그러면..
@Value public class Person { String firstName; @NonNull String lastName; Integer age; @Builder public Person(String firstName, @NonNull String lastName, Integer age) { this.firstName = firstName == null ? "hello" : firstName; this.lastName = lastName; this.age = Objects.requireNonNull(age); } }
".class" 에서는
public Person(String firstName, @NonNull String lastName, Integer age) { if (lastName == null) { throw new NullPointerException("lastName is marked non-null but is null"); } else { this.firstName = firstName == null ? "hello" : firstName; this.lastName = lastName; this.age = Objects.requireNonNull(age); } }
이렇게 된다.
여기서 볼만한 것은..
@Value 에 의해 만들어지지 않도록 직접 생성자를 만들어 줬고 내부에 로직을 custom 하였다.
".class" 에서는 lastName은 @NonNull 검증 로직이 적용되었고
custom한 로직도 적용되어 있는 것을 볼 수 있다.
또한, 생성자 접근제어자를 public 으로 주어 생성자도 public으로 대체 된 것을 볼 수 있다.
@Builder 참고
Person person = Person.builder().build(); 와 같이 필드 초기화를 하지 않고
코드를 실행하면 null 로 초기화가 된다.
(물론, 위 로직에서는 lombok 에 의해 null check 를 하고 있으므로 NPE 터짐)
Bean Validation 을 이용한 방법
lombok 을 이용하지 않고 Bean Validation 을 이용하여 검증해보자
java 는 interface 를 제공하고 구현체는 hibernate 이다.
Person
import jakarta.validation.constraints.NotNull; import lombok.Builder; import lombok.EqualsAndHashCode; import lombok.Value; @Value @EqualsAndHashCode(callSuper=false) public class Person extends SelfValidating<Person> { @NotNull String firstName; @NotNull String lastName; Integer age; @Builder public Person(String firstName, String lastName, Integer age) { this.firstName = firstName; this.lastName = lastName; this.age = age; this.validateSelf(); } }
@NonNull 은 lombok 이 아니라
jakarta.validation 의 어노테이션이다.
SelfValidating 클래스를 상속 받았고 Generic 타입으로 Person을 넘겨줬다.
@EqualsAndHashCode(callSuper=false) 로
equals(), hashCode() 메서드 생성 시, 부모 클래스는 제외하도록 하였다.
생성자에 this.validateSelf() 메서드를 호출하여 검증을 할 수 있다.
SelfValidating
import jakarta.validation.ConstraintViolation; import jakarta.validation.ConstraintViolationException; import jakarta.validation.Validation; import jakarta.validation.Validator; import jakarta.validation.ValidatorFactory; import java.util.Set; public abstract class SelfValidating<T> { private final Validator validator; private static final ValidatorFactory factory = Validation.buildDefaultValidatorFactory(); public SelfValidating() { this.validator = factory.getValidator(); } protected void validateSelf() { Set<ConstraintViolation<T>> violations = this.validator.validate(this, new Class[0]); if (!violations.isEmpty()) { throw new ConstraintViolationException(violations); } } }
jakarta.validation 에서 제공하는 validator 를 이용하여 검증한다.
validateSelf() 메서드에서 this.validator.validate() 를 수행한 결과에는
오류가 있는지 없는지 리턴된다.
따라서, !violations.isEmpty() 로 구분할 수 있다.
참고
Spring Web 에서 Controller 에서
@Valid, @Validated 어노테이션을 적용한 Http body 데이터 검증 시에는
Validator를 직접 다룰 필요가 없다.
(Spring 이 이미 통합 해놓아서 SelfValidating 을 상속 받을 필요 없음)
자세한 내용은 SpringMVC Validation 포스팅 참고
참고
springframework.lang 의 @NonNull 은 Ide 의 도움을 받아 미연 null 을 방지 할 수 있도록 한 어노테이션이다..
record 를 이용한 방법
record 에서 선언적 유효성 검사 방법은 어떤게 있을까..
(일반적인 class 의 생성자에서 유효성 검사 처럼 record 에서도 생성자를 만들어 유효성 검사 하는 방법은 당연히 된다.)
생성자를 통한 방법
https://www.baeldung.com/java-record-keyword
먼저 record components 에 적용 가능한 어노테이션을 알아보자..
위는 다음 record class 의 설명 중 일부이다. https://openjdk.org/jeps/395
record components 의 element type은 ElementType.RECORD_COMPONENT 이다.
그런데 @NonNull 이 인식하는 것 중.. ElementType.RECORD_COMPONENT 는 없다. (아래에 나옴)
하지만..
record docs 설명에 따르면 ElementType이 전파되어 다음 타입에서 인식 할 수 있다고 한다.
ElementType.FIELD
ElementType.METHOD
ElementType.PARAMETER
ElementType.RECORD_COMPONENT
따라서 lombok 의 @NonNull 의 경우
위와 같으므로 적용된다.
lombok 이용 예시
package com.example.demo.user; import lombok.NonNull; public record User( @NonNull String firstName, @NonNull String lastName, int age ) { }
".class"
package com.example.demo.user; import lombok.NonNull; public record User(@NonNull String firstName, @NonNull String lastName, int age) { public User(@NonNull String firstName, @NonNull String lastName, int age) { if (firstName == null) { throw new NullPointerException("firstName is marked non-null but is null"); } else if (lastName == null) { throw new NullPointerException("lastName is marked non-null but is null"); } else { this.firstName = firstName; this.lastName = lastName; this.age = age; } } public @NonNull String firstName() { return this.firstName; } public @NonNull String lastName() { return this.lastName; } public int age() { return this.age; } }
하지만... Java Bean Validation 방법은 SelfValidating 을 상속 받아야한다.
-> 물론, SelfValidating 을 사용하지 않고 직접 Validator 를 이용하여
validated() 메서드를 호출하는 방법도 있지만..
생성 시점에 예외를 던져주지는 않으므로 논외로 하겠다.
record 는 상속이 안된다.
따라서, record 는 (@NotNull 어노테이션이 인식은 되지만 )
SelfValidation을 상속받지 못하므로 Java Bean Validation 방법을
생성 시점에 선언적 유효성 방법은 사용하지 못한다.
<참고>
controller 에서 요청 데이터 Json 을 객체로 바인딩 하는 상황에서 파라미터를
record 를 사용하고 Java Bean Validation 을 사용할 수 있다.
spring 에서 validator 를 이용해서 검증해주기 때문이다.
예시 코드
package com.example.demo.user; import jakarta.validation.constraints.NotNull; public record User( @NotNull String firstName, @NotNull String lastName, int age ) { }
package com.example.demo.controller; import com.example.demo.user.User; import jakarta.validation.Valid; import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RestController; @RestController @RequestMapping("/demo") public class DemoController { @GetMapping("/user") public User user(@Valid User user) { return user; } }
반응형'Java' 카테고리의 다른 글
Java 11 ~ 15 주요 변경점 (0) 2023.05.19 Java 9, 10 주요 변경점 (0) 2023.05.19 불변 객체, lombok @Value 와 Java Record (0) 2023.05.07 [Java 정리] ThreadLocal (0) 2022.10.19 [Java 정리] Thread 6 (0) 2022.08.22 다음글이전글이전 글이 없습니다.댓글