• 티스토리 홈
starryeye
  • 프로필사진
    starryeye
    • 분류 전체보기 (189)
      • C++ (17)
      • Java (24)
      • OOP (5)
      • Spring Reactive Stack (12)
        • Reactive Streams (3)
        • Netty (4)
        • Reactor (1)
        • Webflux (3)
        • DB, Cache 연동 (1)
      • Spring (90)
        • Core (17)
        • MVC (33)
        • Client (2)
        • Security (4)
        • DB, Cache 연동 (33)
      • DataBase (12)
        • RDBMS (2)
        • NoSQL (10)
      • Message Broker (6)
      • Web (4)
      • Network (4)
      • 대규모 시스템 설계 (15)
  • 방문자 수
    • 전체:
    • 오늘:
    • 어제:
  • 최근 댓글
      등록된 댓글이 없습니다.
    • 최근 공지
        등록된 공지가 없습니다.
      # Home
      # 공지사항
      #
      # 태그
      # 검색결과
      # 방명록
      • 불변 객체 validation
        2023년 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

        https://stackoverflow.com/questions/67092847/java-16-annotation-elementtype-record-component-cannot-be-reflected

        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
        다음글
        다음 글이 없습니다.
        이전글
        이전 글이 없습니다.
        댓글
      조회된 결과가 없습니다.
      스킨 업데이트 안내
      현재 이용하고 계신 스킨의 버전보다 더 높은 최신 버전이 감지 되었습니다. 최신버전 스킨 파일을 다운로드 받을 수 있는 페이지로 이동하시겠습니까?
      ("아니오" 를 선택할 시 30일 동안 최신 버전이 감지되어도 모달 창이 표시되지 않습니다.)
      목차
      표시할 목차가 없습니다.
        • 안녕하세요
        • 감사해요
        • 잘있어요

        티스토리툴바