본문 바로가기
Lecture

자바 코드 리팩토링: 객체에게 꼬치꼬치 묻지 말고 시켜라 - 종속적인 관계가 아닌 자율적인 관계 유지하기

by Renechoi 2023. 7. 9.

 

문제점 

 

다음과 같은 Coupon Legacy 코드가 있다고 치자. 

 

@Getter
@Setter
@NoArgsConstructor(access = AccessLevel.PROTECTED)
public class CouponLegacy {

   private long id;

   private boolean used;

   private double amount;

   private LocalDate expirationDate;

   public CouponLegacy(double amount, LocalDate expirationDate) {
      this.amount = amount;
      this.expirationDate = expirationDate;
      this.used = false;
   }
}

 

해당 객체는 실제로 엔티티지만 별도의 로직을 담당하고 있지 않다. 이 쿠폰을 사용하는 쪽에서는 다음과 같이 사용하고 있다. 

 

public class FirstOrderCouponLegacy {

   /**
    * 안티 패턴:
    * 꼬치꼬치 캐묻고 있습니다.
    * 1. 개체간의 협력관계에서는 상대 객체에 대한 정보를 꼬치꼬치 묻지 않아합니다. 묻지말고 시켜라 ->
    */
   public void apply(final long couponId) {
      if (canIssued()) {
         final CouponLegacy coupon = getCoupon(couponId);

         if (LocalDate.now().isAfter(coupon.getExpirationDate())) {
            throw new IllegalStateException("사용 기간이 만료된 쿠폰입니다.");
         }

         if (coupon.isUsed()) {
            throw new IllegalStateException("이미 사용한 쿠폰입니다.");
         }
      }
   }

   // 실제는 데이터베이스 조회..
   private CouponLegacy getCoupon(final Long id) {
      return new CouponLegacy(1000, LocalDate.now().plusDays(3));
   }

   private boolean canIssued() {
      // TODO: 첫 구매인지 확인 하는 로직 ...
      return true;
   }
}

위의 코드에서  apply  메서드에서는  CouponLegacy의 게터를 통해 다양한 정보를 묻고 있다. 이는 객체 간의 협력 관계에서 상대 객체에 대한 정보를 묻는 것이므로 "묻지말고 시켜라"라는 원칙에 위배된다.

 

예를 들어 쿠폰의 종류가 다양하다면 어떨까? 첫 쿠폰, 생일 쿠폰, 이벤트 쿠폰 등등 다양한 쿠폰들이 존재한다면 매 서비스에서 똑같은 코드의 중복이 발생할 것이다. 

 

또한 만약 만료기간이 하조금이라도 달라졌다면 어떨까? 모든 코드를 다시 고쳐야 하는 문제가 생긴다. 

 

이렇게 객체를 가져오고 게터를 통해서 다양한 정보들을 물어서 처리하는 방식은 안티패턴으로서 객체 지향 설계 관점에서 적절하지 않다고 볼 수 있다. 

 

 

 

제안 

 

다음과 같이 변경해보자. 

 

@Getter
@Setter
@NoArgsConstructor(access = AccessLevel.PROTECTED)
public class Coupon {

   private long id;

   private boolean used;

   private double amount;

   private LocalDate expirationDate;

   public Coupon(double amount, LocalDate expirationDate) {
      this.amount = amount;
      this.expirationDate = expirationDate;
      this.used = false;
   }

   public void apply() {
      verifyCouponIsAvailable();
      this.used = true;
   }

   private void verifyCouponIsAvailable() {
      verifyExpiration();
      verifyUsed();
   }

   private boolean isExpiration() {
      return LocalDate.now().isAfter(expirationDate);
   }

   private void verifyExpiration() {
      if (isExpiration()) {
         throw new IllegalArgumentException("만료된 쿠폰입니다.");
      }
   }

   private void verifyUsed() {
      if (this.used) {
         throw new IllegalArgumentException("이미 사용한 쿠폰입니다.");
      }
   }
}

 

객체 스스로가 본인이 해야 하는 일에 대해서 스스로 책임지고 있다. 이제 쿠폰을 사용하는 쪽에서는 쿠폰의 만료시점이 언제인지, 사용이 되었는지 등에 대한 정보를 알 필요가 없다. 그저 다음과 같이 시키기만 하면 된다. 

 

public class FirstOrderCoupon {

   /**
    * 좋은 패턴
    * 묻지 말고 시켜라. 쿠폰 객체의 apply() 메서드를 통해서 묻지 말고 쿠폰을 적용하고 있습니다.
    */
   public void apply(final long couponId) {
      if (canIssued()) {
         final Coupon coupon = getCoupon(couponId);
         coupon.apply();
      }
   }

   // 실제는 데이터베이스 조회..
   private Coupon getCoupon(final Long id) {
      return new Coupon(1000, LocalDate.now().plusDays(3));
   }

   private boolean canIssued() {
      // TODO: 첫 구매인지 확인 하는 로직 ...
      return true;
   }
}

 

 

결론 

 

위의 예시에서처럼 쿠폰의 만료일이나 사용 여부 등의 상태를 직접 묻지 않고 쿠폰 객체 스스로가 판단하도록 코드를 작성함으로써, 객체간 충분히 협력적인 관계를 유지한다. 즉, 객체는 서로에게 종속적이거나 복종하는 관계가 아니라 스스로 자율적인 책임을 갖고 역할을 해내도록 변경되었다. 또한 쿠폰을 관리하는 코드가 간결해지고 유연성이 증가한다.

 

이처럼 객체의 상태를 직접 묻지 않고 메서드를 통해 요청하는 패턴을 동해 원할한 객체 간 협력을 디자인하고 보다 클린한 코드를 작성할 수 있다.

 

 

 


 

참고자료

- 패스트 캠퍼스 (한 번에 끝내는 Spring 완.전.판 초격차 패키지 Online - Part9)

 

반응형