Spring - Lombok을 사용한 생성자 전략

조예슬·2023년 5월 5일
0

Server

목록 보기
5/6
post-thumbnail

Entity 개발을 하다가 문득 @NoArgsConstructor을 아무 생각없이 붙이고 있는 나 자신을 발견했다. 그러면서 @AllArgsConstructor과 @RequiredArgsConstructor을 잘 구분해서 알맞게 사용하고 있는 것인지 헷갈리기 시작했다...😶‍🌫️ 정리가 필요할 것 같아서 이렇게 글을 써본다 !


@NoArgsConstructor

  • JPA에서는 프록시를 생성을 위해서 기본 생성자를 반드시 하나를 생성해야한다.

  • 사실 기본 생성자는 @Entity 어노테이션만 붙여도 자동으로 생성해주지만, @NoArgsConstructor 를 붙여주는 이유는 AccessLevel 옵션값을 부여해서 접근 제한을 하도록 해 기본 생성자의 무분별한 생성을 막아서 의도하지 않은 엔티티를 생성하는 것을 막을 수 있기 때문이다.

  • 근데 보통 @NoArgsConstructor(access = AccessLevel.PROTECTED) 와 같이 접근 제한을 PROTECTED로 해주는데 그건 왜 그런걸까 ? 안전하게 PRIVATE으로 하면 안돼 ?

  • JPA에서 연관 관계에 있는 엔티티를 조회할 때 보통 지연 로딩(LAZY)으로 값을 조회하여 리소스 낭비를 줄인다. 이때 지연 로딩은 프록시 객체를 생성해서 엔티티 값을 참조할 수 있게 하는데 접근 제한이 PRIVATE이라면 이 프록시 객체를 생성할 수 없기 때문이다.

  • 사용 시 고려해야 할 점

    • 필드들이 final로 생성되어 있는 경우에는 필드를 초기화할 수 없기 때문에 생성자를 만들 수 없고 에러가 발생한다. → @NoArgsConstructor(force=true) 옵션을 이용해 final 필드를 강제 초기화 시켜 생성자를 만들 수 있다.
    • @Nonnull 같이 필드에 제약조건이 설정되어 있는 경우, 추후 초기화를 진행하기 전까지 생성자 내 null-check 로직이 생성되지 않는다.

@Builder@NoArgsConstructor의 사용

  • Builder 개념이 궁금하다면 다음 포스팅을 참고하자 !

    🖇 빌더 패턴이 뭐야 ?

  • @Builder 의 경우, 해당 어노테이션이 붙은 클래스에 생성자가 없는 경우 모든 멤버 변수를 파라미터로 받는 기본 생성자를 생성하고, 생성자가 있다면 따로 생성자를 생성하지 않는다.

  • 이 때 @NoArgsConstructor 어노테이션이 붙어 있다면 기본 생성자가 이미 생성이 되는 것이므로, 따로 생성자를 생성하지 않는데 이렇게 되면 매개변수를 일치하게 받는 생성자가 없어 에러가 발생한다. 따라서 모든 필드를 파라미터로 가지는 @AllArgsConstructor 를 붙여주어 해결한다.

  • 또 다른 방법으로는 직접 클래스 내에 생성자를 만들고, 클래스에 @Builder 를 붙여서 선언하는 것이 아니라 생성자에 붙여서 선언하는 방법이 있다. 코드로 설명하면 다음과 같다.

    @Entity
    @Getter
    @NoArgsConstructor(access = AccessLevel.PROTECTED)
    public class User {
    
        @Id
        @GeneratedValue(strategy = GenerationType.IDENTITY)
        private Long id;
    
        @Column(nullable = false)
        private String nickname;
        @Column(nullable = false)
        private String email;
        @Column(nullable = false)
        private String password;
    
        @Builder
        private User(String nickname, String email, String password) {
            this.nickname = nickname;
            this.email = email;
            this.password = password;
        }
    }
  • 사실 후자의 방법처럼 생성자를 직접 만들어서 생성자에 @Builder 어노테이션을 붙이는 것을 더 지향하는 편이다. 그 이유는 @AllArgsConstructor의 사용은 되도록 지양하고, 생성자를 직접 선언해주고 필요에 따라 Builder 패턴을 사용할 것을 권장하기 때문이다. 자세한 건 아래에서 설명하겠다 !


@AllArgsConstructor

  • 클래스에 존재하는 모든 필드를 파라미터로 받는 생성자를 만들어주는 어노테이션이다.

  • 굉장히 간단하게 생성자를 만들어주는 것 같아 오호 좋은데 ~? 할 수 있지만 지양 해야하는 어노테이션 중 하나이다.

  • 가장 흔히 문제가 될 수 있는 케이스를 봐보자.

    • 두 개의 같은 타입 맴버 멤버를 선언한 상황에서 개발자가 선언된 멤버 변수의 순서를 바꾸면, 개발자도 인식하지 못하는 사이에 lombok이 생성자의 파라미터 순서를 필드 선언 순서에 따라 변경하게 된다.
    • 이때, IDE가 제공해주는 리팩토링은 전혀 동작하지 않고, 두 필드가 동일 타입이기 때문에 기존 소스에서도 오류가 발생하지 않아 아무런 문제없이 동작하는 것으로 보이지만, 실제로 입력된 값이 바뀌어 들어가는 상황이 발생한다.
  • 다음 코드를 보면 문제점을 확실히 알 수 있다 !

    @AllArgsConstructor
    public static class Member {
       private String firstName;
       private String lastName;
    }
    // 성이 남, 이름이 주혁이라면
    Member boy = new Person("남", "주혁");
  • 여기서 만일 두 멤버 변수, firstName과 lastName의 선언 순서가 바뀐다면 ?

  • @AllArgsConstructor 는 선언된 필드의 순서대로 생성자의 파라미터 순서를 정해 만들어주기 때문에 "주혁남"이 될 수 있다..

  • 이런 문제를 미연에 방지하기 위해 다음과 같이 @builder 패턴으로 파라미터의 순서가 아닌 필드 명으로 값을 설정하도록 한다. 단순히 유연한 생성을 위해서만 builder를 사용하는 것이 아니라는 것을 알 수 있다 !

    public static class Person {
        private String firstName;
        private String lastName;
    
        @Builder
        private Person(String firstName, String lastName){
            this.firstName = firstName;
            this.lastName = lastName;
        }
    }
    // 필드 순서를 변경해도 한국식 이름이 만들어진다.
    Person me = Person.builder().lastName("현수").firstName("권").build();
    System.out.println(me);
  • 그렇기 때문에 위에서 설명한 @AllArgsConstructor@Builder를 같이 쓰는 것보다, @NoArgsConstuctor 과 생성자를 직접 만들어 그 생성자에 @Builder 을 붙이는 전략을 권장하는 것이다 !

@RequiredConstructor

  • 'Required' 즉, 꼭 필요한 객체의 변수를 인수로 받는 생성자를 구현해준다. 여기서 꼭 필요한 객체의 변수는 final 또는 @NotNull 어노테이션이 붙은 변수를 의미한다.

  • 이 어노테이션과 함께 보면 좋을 개념은 바로 @Autowired 어노테이션이다.

    • @Autowired 을 사용해서 의존성을 주입해주는 것을 필드 주입이라고 한다.
     @Service
     public class MemberService {
         @Autowired private MemberRepository memberRepository;
    
                     /* 이하 생략 */
         }
  • 따라서 생성자 주입을 해주어야 하는데 이때 필드 객체에 final 키워드를 붙이면 컴파일 시점에 해당 필드를 주입하지 않을 시에 누락된 의존성 오류를 체크할 수 있다.

  • 여기서 final 키워드와 지금 설명하고 있는 @RequiredConstructor 를 같이 쓰면 좋은 이유가 설명되는 것이다 !

  • @RequiredConstructor 가 final 변수를 위한 필수 필드를 정의하는 생성자를 대신 생성해주기 때문에 다음과 같이 코드를 작성하는 것이 바람직한 방법이다.

    @Service
    @RequiredArgsConstructor
    public class OrderService {
    
        private final OrderRepository orderRepository;
        private final MemberRepository memberRepository;
    
        public Long order(Long memberId, Long itemId) {
            Member member = memberRepository.findById(memberId).get();
            ...
            }
    }
  • Spring 컨테이너는 생성자가 1개인 경우 @Autowired를 생략하면 자동으로 생성자 주입을 해준다. 위의 OrderService 클래스는 필수 필드를 가지는 @RequiredArgsConstructor 생성자가 1개 있으므로 @Autowired 를 생략하고 다음과 같은 코드로 생성자 주입을 해주는 것이 가능하다.

  • 즉, 다른 클래스의 의존성이 주입되어야 하는 클래스 (예를 들면 Service나 Controller..) 는 위와 같이 final 키워드와 @RequiredArgsConstructor 을 함께 사용해주는 것이 좋다 !


💡 다음과 같이 Lombok을 이용해서 생성자를 쉽게 만들 수 있는 어노테이션에 대해 알아보았다.
그냥 무작정 어노테이션부터 붙이고 개발을 하는 것이 아니라, 현재 내가 설계하고 있는 클래스의 특징과 상황을 고려하여 (Entity인지 Service인지..등등) 어떤 부분을 주의해야하는지 판단하고, 알맞고 유연한 설계를 하는 것이 중요한 것 같다 !

profile
코딩 해라 스리스리 예스리 얍!

2개의 댓글

comment-user-thumbnail
2023년 5월 12일

멋진 글입니다

1개의 답글