본문 바로가기

Spring/웹

Lombok의 좋은 사용법

1. @Data 지양하기

@ToString

@EqualsAndHashCode

@Getter

@Setter

@RequiredArgsConstructor

 

@Data는 위와 같은 모든 어노테이션을 따로 선언해줄 필요없습니다.

왜냐하면, @Data안에 이 모든 어노테이션이 포함되어있기 때문이죠! 

그런데, 이 간편한 @Data를 사용하는 것을 왜 지양해야 할까요?

이에 대한 여러가지 이유가 있습니다.

 

첫 번째, @Setter는 안전하지 못합니다

Setter를 사용하게 된다면 객체를 언제든 수정할 수 있기 때문에 객체의 안전성이 보장받기 힘듭니다. 

만약, '사용자의 실명'이 변경되지않아야 하는 웹이라면, 아예 그 기능을 제공하지 않는 것이 안전합니다. 그러나, @Setter를 사용하게 된다면, 이 기능을 원천적으로 차단하지 못하겠죠?

 

두 번째, 양방향 연관관계에서 순환 참조 Because of.... @ToString

@ToString을 사용하였는데 해당 DTO에 양방향연관관계가 설정되어있다...?

이 경우, ToString을 하는 과정에서 1번 엔티티가 연관관계의 2번 엔티티를 찾아 ToString하게 뿌리고, 2번 엔티티가 다시 또 연관관계에 있는 1번 엔티티를 찾아 뿌리고... 무한대로 반복하게 됩니다..  이러면 안되겠죠...?

...

그럼 @ToString은 사용할 수 없는건가요...?

아니요 ! 그에 대한 해결방법은 다음과 같습니다. 양방향 연관관계에 있는 것을 제외시키면 됩니다. 

@Data를 쓰지않고 따로 @ToString을 삽입하여 exclude 시켜줘야합니다.

@ToString(exclude = "blog")
public class Post{}

 


2. @NoArgsConstructor  접근 권한 부여

저는 그동안 @NoArgsConstructor를 그냥 속성 없이 사용했는데요...이러면 안된다고합니다.

@NoArgsConstructor 에 ( access = AccessLevel.PROTECTED) 라는 속성을 부여하고 Builder 패턴을 사용하여 생성자를 대신하여 사용해야 한다고 하는데요. 

먼저, Builder 패턴이 나오게 된 유래(?)를 설명드리겠습니다.

 

Builder 패턴의 유래

 

보통 이러한 생성자를 많이 쓰곤 합니다. 

public Post(String title, String writer, String category, String content) {
    this.title = title;
    this.writer = writer;
    this.category = category;
    this.content = content;
}

하지만,  프로젝트 진행 시에 category나 content가 null인 경우 , 이런식으로 생성자를 지정해주는데,

생성자의 매개변수가 많아지면 뭐가 category고 어디가 content인지 잘 알 수 없겠죠..?

Post post = new post("제목", "riimy", null, null);

 

그렇다고 해서 아래와 같이 점층적 생성자 패턴(telescoping constructor pattern)을 사용하게 되면...

일일이 다 필요하게 된다면... 2^4=16개인데.. 너무 비효율적입니다.

그리고, 생성자를 많이 호출해야 하는 것은 절대 좋지 않습니다. 생성자는 한번 호출하여 그걸 계속 재활용해야 좋다고 합니다.

public Post(String title) {
    this.title = title;
}

public Post(String title, String writer) {
    this.title = title;
    this.writer = writer;
}

public Post(String title, String writer, String category) {
    this.title = title;
    this.writer = writer;
    this.category = category;
}

public Post(String category, String content) {
    this.category = category;
    this.content = content;
}

 

그렇다면 자바 빈 패턴(Java Bean pattern)은 어떨까요?

프로젝트 진행 시에 제가 많이 사용했던 방법인데요... 위에서 말했듯이 @Getter, @Setter(Lombok의 경우)를 사용하거나 변수에 대해 모두 Get, Set Method를 생성하고 

아래와 같이 사용하는 것입니다.

Post post = new Post();
post.setTitle("제목입니다");
post.setWriter("riimy");
post.setCategory("Lombok");
post.setContent("lombok의 올바른 활용법 내용");

 

이제 각각의 인자의 의미들을 파악하기 쉬워졌고 여러 개의 생성자를 만들지 않아도 됩니다!

하지만!! 이는 함수 호출 한 번으로 객체 생성을 끝내지 못하고 4번이나 호출하고 있죠? 그래서 객체 일관성(consistency)이 일시적으로 깨질 수 있습니다. 또한, Set Method가 존재하고 있기 때문에 immutable 객체를 생성할 수 없다는 큰 단점이 생기게 됩니다.

//Immutable 관련 예시 코드 (참고: meetup.toast.com/posts/217)

public class OrderService{
    public FinalPrice calculate(Long memberId, Long productId){

        // CASE #1. 기본 생성자(default constructor)로 객체를 생성하고 맴버변수 값이 비어있는 priceTag 객체 생성.
        PriceTag priceTag = new PriceTag();

        //... 코드 생략
        this.applyMemberShip(priceTag);
        //... 코드 생략

        return priceTag.getDiscountPrice();
    }

    private void applyMemberShip(PriceTag priceTag){
        // CASE #2 인자의 값을 변경하는 경우. 첫번째 글에서 하지 말아야 하는 액션이라고 말씀드렸네요.
        BigDecimal memberShipPrice = // 생략
        priceTag.setMemberShipPrice(memberShipPrice);
    }
}

 

이러한 단점을 해소한 것이 빌더 패턴 인데요.

 

클라이언트 코드에서 필요한 객체를 직접 생성하는 대신,

그 전에 필수 인자들을 전달하어 빌더 객체를 만든 뒤,

빌더 객체에 정의된 설정 메서드들을 호출하여 인스턴스를 생성하는 것입니다.

보통 Private로 생성자를 막아놓지만,  Protected로 구현하여 Entity 클래스를 프로젝트 코드상에서 기본생성자로 생성하는 것은 막되, JPA에서 Entity 클래스를 생성하는것은 허용합니다.

public class Post {
    private String title;    // 필수
    private String writer;    // 필수
    private String category;    // 선택
    private String content;        // 선택


   public static class Builder {
        private String title;   
        private String writer;    
        private String category = "Lombok"    //선택적 인자 기본값
        private String content ="Lombok의 올바른 활용법"   // 위와 동일한 이유

	   //필수적인 것들로만 이루어짐
       public Builder(String title, String writer) {
           this.title = title;
           this.writer = writer;
       }

       public Builder category(String category) {
           this.category = category;
           return this;
       }
       public Builder content(String content) {
           this.content = content;
           return this;
       }

       public Post build() {
           return new Post(this);
       }
   }

   public Post(Builder builder) {
       this.title = builder.title;
       this.writer = builder.writer;
       this.category = builder.category;
       this.content = builder.content;
   }
   
	///// OR /////
@Builder
public class Post {
    private final String title;
    private final String writer;
    @Builder.Default private String category = "Lombok"; // 선택적 인자 기본값 설정 가능
    @Builder.Default private String content = "Lombok의 올바른 활용법";
}



    public static void main(String[] args) {
        Post post = new Builder("제목", "riimy")
                .category("롬복")
                .content("롬복내용이다")
                .build();
    }
}
Effective Java 규칙 중 하나인 생성자 인자가 많을 때는 Builder 패턴 적용을 고려하라는 부분도 만족!

 

 

하지만,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,

Builder를 @AllArgsConstructor대신 클래스 위에 생성하기도 하는데요.

@AllArgsConstructor의 문제는 모든 필드에 대해서 매개변수를 받는 기본 생성자를 만들 수 있다는 것입니다.

 id와 같은 경우는 자동으로 increment되기때문에 생성자에 넣지 않는 것이 좋습니다.

 

따라서, 아래와 같이 Builder를 따로 생성해줘 매개변수를 최소화 해주는 것이 좋습니다.

@Entity
@Table(name = "post")
@ToString(exclude = "categories")
@Getter
@NoArgsConstructor(access = AccessLevel.PROTECTED)
public class Post {
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private long id;
    
    private String title;    // 필수
    private String writer;    // 필수
    private String category;    // 선택
    private String content; //선택

    @OneToMany
    @JoinColumn(name = "category")
    private List<Category> categories = new ArrayList<>();

    @Builder
    public Post(String title, String writer, String category, String content)  {
        this.title = title;
        this.writer = writer;
        this.category = category;
        this.content = content;
    }
}

'Spring > ' 카테고리의 다른 글

Spring Container & Bean 생명주기  (0) 2021.04.04
Container란? (Servlet / Spring Container)  (0) 2021.04.03
AOP란? (Spring AOP)  (0) 2021.03.26
Spring Security  (1) 2021.02.06
Web Server vs Web Application Server  (0) 2021.01.24