ABOUT ME

-

Today
-
Yesterday
-
Total
-
  • [Spring] Lombok에서 @Data와 @AllArgsConstructor는 지양하자.
    Backend/스프링(spring) 2024. 9. 27. 00:15

    @Data 애너테이션

    • 포함하는 애너테이션 : @Getter, @Setter, @RequireArgsConstructor, @ToString, @EqualsAndHashCode
    /**
     * @see Getter
     * @see Setter
     * @see RequiredArgsConstructor
     * @see ToString
     * @see EqualsAndHashCode
    */
    @Target({ElementType.TYPE})
    @Retention(RetentionPolicy.SOURCE)
    public @interface Data {
        String staticConstructor() default "";
    }

     

    @Data 애너테이션이 포함하는 애너테이션들에 대해서 알아보자.

     

    @Setter는 수정하면 안 되는 값을 수정하게 만든다.

    Entity에 @Setter를 사용하게 되면 수정자 메서드를 자동으로 생성한다. 모든 필드가 수정자를 갖기 때문에 비즈니스 로직에서 수정해서는 안 될 데이터마저 실수로 건드릴 수 있다. 따라서 @Setter를 지양하여 불필요하게 수정할 수 있는 가능성을 아예 배제해야 한다.

     

    @ToString은 양방향 연관관계에서 순환 참조를 발생시킨다.

    양방향 연관관계에서 @ToString를 사용하면 순환 참조 문제가 발생할 수 있다. Entity에 @ToString을 사용하게 되면 toString() 메서드를 자동으로 생성한다. 예를 들어, MemberPost1:N 관계로 설정하고 각 엔티티에 @ToString을 설정한다.

    @Entity
    @ToString
    public class Member {
    
        @Id
        @GeneratedValue(strategy = GenerationType.IDENTITY)
        private Long id;
    
        @OneToMany(mappedBy = "member")
        List<Post> posts = new ArrayList<>();
    
    }
    @Entity
    @NoArgsConstructor
    @ToString
    public class Post {
    
        @Id
        @GeneratedValue(strategy = GenerationType.IDENTITY)
        private Long id;
    
        @ManyToOne
        Member member;
    
        public Post(Member member) {
            this.member = member;
        }
    }
    @Service
    public class PostService {
    
        private PostRepository postRepository;
    
        @Transactional(readOnly = true)
        public void print() {
            Post post = postRepository.findById(1L)
                .orElseThrow(() -> new IllegalArgumentException("Post not found"));
            System.out.println(post);
        }
    }

     

    데이터베이스에서 조회한 Post 객체를 콘솔에 출력할 때 StackOverflow 예외가 발생한다. 이 예외는 메서드 호출 시 생성되는 스택 프레임이 너무 많아져 스택의 깊이가 한계에 도달했을 때 발생한다.

    1. Post 객체는 Member 객체를 참조하고 있으며, @ToString 어노테이션에 의해 Post 객체가 출력될 때 해당 Member 객체도 함께 출력된다.
    2. Member 객체는 List<Post> 형태로 여러 Post 객체를 참조하는데, 이 리스트 안에는 1번에서의 Post 객체가 포함되어 있다. 따라서 @ToString에 의해 이 리스트를 출력할 때 다시 해당 Post 객체를 출력하게 된다.
    3. 이 과정이 반복되면서, 다시 Post 객체가 출력된다.

    이와 같은 순환 참조로 인해 스택 깊이가 계속해서 증가하고, 결국 StackOverflow 예외가 발생하게 된다. 이는 PostMember 간의 순환 참조로 인해 발생하는 문제이다.

     

    따라서 Entity에는 @ToString을 지양하여 양방향 연관관계에서 순환 참조 문제가 발생하는 것을 방지해야 한다. 반대로 DTO에는 @ToString을 사용해도 크게 문제 될 것이 없다고 생각하는데, 그 이유는 DTO에는 보통 Entity를 포함하는 것이 아닌 API 스펙에 맞는 데이터를 반환하기 때문이다.

     

    @AllArgsConstructor는 치명적인 버그로 이어질 수 있다.

    @AllArgsConstructor는 클래스에 존재하는 모든 필드를 포함하는 생성자를 자동으로 생성한다.

    @AllArgsConstructor
    public class MemberResponse {
    
        private String name;
        private String email;
        
    }

     

    MemberResponse는 생성자를 통해 (name, email) 순서로 받고 있으나, 비즈니스 로직에서 (email, name) 순서로 삽입해도 오류가 발생하지 않는다. name과 email이 동일한 타입이기 때문에 컴파일 에러가 발생하지 않기 때문이다.

     

    그렇다면 @RequiredArgsConstructor도 지양해야 할까?

    사실, @RequiredArgsConstructor@AllArgsConstructor와 마찬가지로 특정 상황에서 비슷한 문제를 일으킬 수 있다. 그럼에도 불구하고 많은 개발자들이 @RequiredArgsConstructor를 사용하는 모습을 흔히 볼 수 있다.

     

    @RequiredArgsConstructor는 final로 선언된 필드들을 대상으로 생성자를 자동으로 만든다. 즉, 개발자가 @RequiredArgsConstructor를 사용한다는 것은 해당 필드들이 final로 선언되어 있음을 인지하고 사용하는 것이다.

     

    스프링 프레임워크에서는 의존성 주입 시 생성자를 통해 필요한 의존성을 전달받을 수 있는데, 이때 의존성이 필요한 객체들을 final 필드로 선언해두면 스프링이 해당 필드에 자동으로 빈을 주입한다.

     

    따라서, @RequiredArgsConstructor를 사용하는 것은 final로 선언된 필드에 의존성을 주입받기 위한 편리한 방법으로, 개발자가 필드의 final 선언을 인지한 상태에서 의도적으로 사용하는 것이다.

     

     


    참고

    https://www.inflearn.com/community/questions/1179578/%ED%98%B9%EC%8B%9C-allargsconstructor-%EB%A5%BC-%EC%A7%80%EC%96%91%ED%95%98%EC%8B%9C%EB%8A%94-%EC%9D%B4%EC%9C%A0%EA%B0%80-%EB%B9%8C%EB%8D%94-%ED%8C%A8%ED%84%B4%EC%9D%84-%EC%82%AC%EC%9A%A9%ED%95%98%EA%B8%B0-%EC%9C%84%ED%95%A8%EC%9D%B8%EA%B0%80%EC%9A%94?srsltid=AfmBOopS20HqnrsFtkdVCuDVsVTXa0ZTcgrdBZ3q4DQicbQENxmo5X-8

     

Designed by Tistory.