lombok 잘! 사용하기

김다혜·2020년 2월 16일
4

spring-guide

목록 보기
2/2

2019년에 했던 '스프링 가이드' (스터디 자료로 공유된 내용은 비공개이므로 해당 repository를 공유) 스터디 내용을 바탕으로 내용을 정리하고자 한다.

2회차 내용은 lombok 사용법라는 주제였다. 그리고 객체지향에 대한 내용도 다루었다. 그때 당시 객체의 사용과 객체 지향에 대해 관심이 많았지만, 이해가 부족했던 부분들이 '객체지향의 사실과 오해'를 읽고 다시 발표자료를 보니 더욱 공감가는 내용들이 많았다.

아래에서 다룰 내용은 lombok 사용법이지만, 나아가서 객체를 올바르게 사용하려면? 이라는 생각을 가지고 읽어나가면 lombok을 사용하지 않는 누군가에게도 분명히 도움이 될 내용이라고 생각된다.

lombok

  • compile 시점에 어노테이션으로 특정 코드 추가할 수 있게 도와주는 java library

lombok 자~알 사용하기

@Data 지양하기

/**
 * Generates getters for all fields, a useful toString method, and hashCode and equals implementations that check
 * all non-transient fields. Will also generate setters for all non-final fields, as well as a constructor.
 * <p>
 * Equivalent to {@code @Getter @Setter @RequiredArgsConstructor @ToString @EqualsAndHashCode}.
 * <p>
 * Complete documentation is found at <a href="https://projectlombok.org/features/Data">the project lombok features page for &#64;Data</a>.
 * 
 * @see Getter
 * @see Setter
 * @see RequiredArgsConstructor
 * @see ToString
 * @see EqualsAndHashCode
 * @see lombok.Value
 */
@Target(ElementType.TYPE)
@Retention(RetentionPolicy.SOURCE)
public @interface Data {
	...
}

@Data는 위의 docs에서 보이느 것과 같이 @Getter, @Setter..를 한방에 주입할 수 있는 만능 어노테이션이다. 언젠간 대가를 치루게 된다는 단점이 있다.
한방에 사용할 수 있다는 장점도 있을 수 있겠지만, 수면위에 드러나지 않는 문제가 많을 수 있기 때문에 지양하자.

무분별한 @Setter 지양하기

  • setter는 코드의 의도를 가지기 힘들다.
    ex) 변경되지 않는 인스턴스에 대해서는 setter (변경포인트)가 제공되지 않는 것이 맞다. 즉, 인스턴스의 변경 포인트를 제공하지 않음으로써, 변경 기능이 없다는 것을 명시적으로 전달할 수 있음
  • 객체의 일관성 / 안정성이 보장받기 힘들다. (setter가 열린다는 말은, immutable(불변)하지 않다는 것이다.)

💁‍♀️ @setter 대신에 이렇게 사용하자

public class MyAccountDto {

    public static class SignUpReq {
        private Email email;
        private String name;
        private String password;
        private Address address;

        @Builder
        public SignUpReq(Email email, String name, String password, Address address) {
            Assert.notNull(email, "email must not be null");
            Assert.notNull(name, "name must not be null");
            Assert.notNull(password, "password must not be null");
            Assert.notNull(address, "address must not be null");

            this.email = email;
            this.name = name;
            this.password = password;
            this.address = address;
        }

        public Account toEntity() {
            return Account.builder()
                    .email(this.email)
                    .name(this.name)
                    .password(Password.builder().value(this.password).build())
                    .address(this.address)
                    .build();
        }
    }
}

위의 코드는 회원의 회원가입을 위한 객체
👉 회원 가입 시 일관성 있게, 객체를 생성할 수 있다.

@ToString 양방향 순환 참조 문제 피하기

  • JPA 사용 시, 객체가 양방향 영관 관계 일 경우 ToString을 호출하게 되면 무환 순환 참조 발생

👉 @ToString(of={"..."}) 권장

  • exclude보다 비용이 적음 (새로 객체를 추가해 줄 때마다 수정 비용이 적다.)
  • @ToString 사용할 때 특정 항목을 제외하여 사용할 수 있음

@EqualsAndHashCode 남발 지양하기

  • 성능 이슈가 발생할 수 있음 (Set 자료 구조)

클래스 상단의 @Builder는 지양하기

  • 클래스 위에 @Builder 사용 시 @AllArgsConstructor효과 발생한다. (모든 멤버 필드에 대해 매개변수를 받는 기본 생성자 생성)

👉 생성자 위 @Builder에 적절한 책임을 부여하자
주문에 대해 신용카드취소, 계좌 기반 환불이 있다고 가정하자.
= 신용카드 취소 / 계좌 기반 환불에 대해 각각 정의하여 @Builder설계

public class Refund {
   private Long id;
   private Account account;
   private CreditCard creditCard;
   private Order order;

   @Builder(builderMethodName = "of", builderClassName = "of")
   private Refund(Account account, CreditCard creditCard, Order order) {
       this.account = account;
       this.creditCard = creditCard;
       this.order = order;
   }

   // 계좌 기반 환불
   @Builder(builderClassName = "byAccountBuilder", builderMethodName = "byAccountBuilder")
   public static Refund byAccount(Account account, Order order) {
       Assert.notNull(account, "account must not be null");
       Assert.notNull(order, "order must not be null");

       return Refund.of().account(account).order(order).build();
   }

   // 신용카드 환불
   @Builder(builderClassName = "byCreditBuilder", builderMethodName = "byCreditBuilder")
   public static Refund byCredit(CreditCard creditCard, Order order) {
       Assert.notNull(creditCard, "creditCard must not be null");
       Assert.notNull(order, "order must not be null");

       return Refund.of().creditCard(creditCard).order(order).build();
   }
}

생성자의 접근 지시자는 최소한으로 하기

reflection 의 이유로 기본 생성자가 꼭 필요한 경우가 있다.
하지만, 기본 생성자를 만든다는 것은, 우리에게 또다른 null 처리의 의무를 부여하게 된다. (다른 어딘가에서 기본생성자를 호출하여 null 이 되면 안되는 필드가 null이 되어 돌아다닐 수 있다..)

👉 가장 낮은 레벨로 지정하여 사용하자
@NoArgsConstructor(access = AccessLevel.PRIVATE)

가능하다면, lombok.config 설정을 통해 제한하자

  • lombok.config 설정 파일을 통해 lombok어노테이션을 제한할 수 있음
# 해당 어노테이션을 사용한다면 Build 시 error 발생하여 어노테이션 제한 가능
lombok.setter.flagUsage=error
lombok.allArgsConstructor.flagUsage=error
lombok.data.flagUsage=error

lombok 마치며

  • 코드의 유지보수를 위해 사소한 객체 생성에서부터 생각해 보는 것은 많은 도움이 될 것이다.
  • 자동으로 해주는 것들은 그 비용을 지불하고 있지 않지만, 언젠가는 그 비용을 지불하게 될 수 있다.

객체 지향

  • 객체 지향의 핵심은 역할, 책임, 협력이 아닐 까 싶다.

쿠폰 객체를 통한 자율적인 객체 알아보기

🙅‍♀️ 이렇게는 사용하지 말자

쿠폰을 사용하기 위해,

  • 쿠폰을 사용할 수 있니 ?
  • 이미 사용했니 ?
  • 기한이 지났니 ?
  • ...
    와 같이 꼬치꼬치 캐묻는 코드는 좋지 않다.

🙅‍♀️ 이렇게 코딩하는 것은 어떨까?

@Data
public class Coupon {
    private CouponCode couponCode;
    private boolean used;
    private double discount;
    private LocalDateTime expirationDate;
    private LocalDateTime createAt;
    private LocalDateTime updateAt;
    private Account account;

    @Builder
    public Coupon(CouponCode couponCode, double discount, LocalDateTime expirationDate, Account account) {
        this.couponCode = couponCode;
        this.discount = discount;
        this.expirationDate = expirationDate;
        this.account = account;
        this.used = false;
    }

    public boolean isExpiration() {
        return LocalDateTime.now().isAfter(expirationDate);
    }

    public void use() {
        verifyExpiration();
        verifyUsed();
        this.used = true;
    }

    private void verifyExpiration() {
        if(isExpiration()) throw new CouponAlreadyUsedException();
    }

    private void verifyUsed() {
        if(used) throw new CouponExpireException();
    }

}
  • 해당 쿠폰을 사용하고자 하는 코드에서 Coupon.use()만 호출하면 된다.

0개의 댓글