내 코드가 이렇게 이상한가요? 1부

devty·2023년 11월 2일
0

BookReview

목록 보기
1/2
post-thumbnail

1. 잘못된 구조의 문제 깨닫기

  • 의미를 알수 없는 네이밍은 지향하자
    • 클래스, 메소드 등에 번호를 붙히지 않고 어떤 작업을 하는지 기술 중심에 명명법으로 사용하자
  • 이해하기 어럽게 많든 조건문은 지향하자
    • 예를들어 if문이 하나의 비즈니스 로직에 적게는 5개 많게는 수십개가 들어간 코드를 만들지 말자
  • 데이터 클래스
    • 데이터 밖에 없는 클래스는 다른 클래스에 의해 핸들링 된다.
    • 하지만 다른 클래스 1개가 아닌 여러 클래스에 핸들링 된다.
    • 이러한 상황은 데이터를 담고 있는 클래스와 데이터를 사용해서 계산하는 클래스가 떨어져 있을 경우를 응집도가 낮은 구조라고 한다.
    • 아래는 응집도가 낮으면 생기는 문제점들을 나열했다.
      1. 코드 중복 → 데이터 클래스를 의존하는 여러 클래스 모두 중복된 코드를 갖게 됨.
      2. 수정 누락 → 비즈니스 로직이 변경 된다면 여러 클래스들은 모두 코드 수정이 불가피 할 것이다. 코드가 많아지면 놓치는 부분이 존재할 수도 있을 것이다.
      3. 잘못된 값 할당 → 데이터 클래스에 유효성 검사가 따로 없기에 모든 유효성 검사는 데이터 클래스를 의존하는 여러 클래스는 모두 유효성 검사를 해줘야한다.
  • 응집도와 결합도
    • 응집도
      • 멍청한 이야기지만…나는 여태 응집도랑 결합도가 거의 유사한 개념이라고 생각했었다.
      • 응집도는 하나의 모듈 또는 클래스 내부의 요소들이 서로 얼마나 밀접하게 관련되어 있는지를 나타낸다.
      • 높은 응집도는 클래스나 모듈의 모든 메서드와 속성이 단일 목적 또는 기능에 집중되어 있다는 것을 의미한다.
      • 예를 들어, OrderService 클래스가 Order 클래스의 내부 필드에 직접 접근하고 있다. 두 클래스 사이의 결합도는 높다고 볼 수 있다.
        public class Order {
            public double price;
            public int quantity;
        
            public double getTotalPrice() {
                return price * quantity;
            }
        }
        
        public class OrderService {
            public void printOrderTotalPrice(Order order) {
                // 직접 Order 클래스의 내부 구조에 접근
                System.out.println(order.price * order.quantity);
            }
        }
    • 결합도
      • 결합도는 두 모듈 또는 클래스가 서로 얼마나 강하게 연결되어 있는지를 나타낸다.
      • 낮은 결합도는 각 클래스나 모듈이 독립적이며, 다른 클래스나 모듈의 내부 구현에 대한 지식 없이도 변경이나 확장이 가능하다는 것을 의미한다.
      • 예를 들어, OrderService 클래스가 Order 클래스의 메서드만 호출하고, Order 클래스의 내부 구조나 로직에 의존하지 않는다면, 두 클래스 사이의 결합도는 낮다고 볼 수 있다.
        public class Order {
            private double price;
            private int quantity;
        
            public double getTotalPrice() {
                return price * quantity;
            }
        }
        
        public class OrderService {
            public void printOrderTotalPrice(Order order) {
                // Order 클래스의 제공하는 메서드만 사용
                System.out.println(order.getTotalPrice());
            }
        }
    • 응집도가 높고 결합도가 낮은 설계의 장점
      • 유지 보수성 : 각 컴포넌트나 모듈이 독립적이므로, 한 부분을 수정하거나 확장해도 다른 부분에 미치는 영향이 최소화된다.
      • 재사용성: 응집도가 높은 컴포넌트나 모듈은 특정 기능에 집중하기 때문에 다른 프로젝트나 문맥에서도 재사용하기 쉽다.
      • 확장성: 결합도가 낮은 설계는 새로운 기능이나 요구 사항을 추가할 때 유연성을 가진다.

2. 설계 첫 걸음

  • 의도를 분명히 전달할 수 있는 이름 설계하기

  • 변수를 계속해서 재할당 하는것이 아닌 목적별로 변수를 따로 생성하기

    • 가독성 저하: 변수의 값이 여러 번 바뀌면 코드를 읽는 사람이 그 변수의 현재 값이 무엇인지 추적하기 어려워진다.
    • 불변성 손실: 불변성(Immutability)은 데이터의 안정성을 보장하고 함수형 프로그래밍 패러다임에서 중요하게 여겨진다. 재할당을 많이 사용하면 불변성을 유지하기 어려워진다.
    • 리팩토링 어려움: 변수가 재할당되는 코드는 리팩토링이나 코드의 수정이 필요할 때 변경의 범위를 예측하기 어려워진다.
  • 데이터와 로직이 묶여 있는 좋은 클래스

    class OrderItem {
        private String productName;
        private double price;
        private int quantity;
    
        public OrderItem(String productName, double price, int quantity) {
            if (price < 0) {
                throw new IllegalArgumentException("Price cannot be negative");
            }
            if (quantity < 0) {
                throw new IllegalArgumentException("Quantity cannot be negative");
            }
    
            this.productName = productName;
            this.price = price;
            this.quantity = quantity;
        }
    
        // 해당 품목의 총 가격을 계산
        public double calculateTotalPriceForItem() {
            return price * quantity;
        }
    }
    • 생성자를 통해 유효성 검사를 완료하였다.
    • calculateTotalPriceForItem → 메소드 명만 확인해도 해당 품목의 총 가격을 계산하는 로직인 걸 알수 있다.

3. 클래스 설계 : 모든 것과 연결되는 설계 기반

  • 잘 만들어진 클래스는 두가지로 구성 되어야한다.
    1. 인스턴스 변수
      • 객체가 생성될 때마다 독립적인 메모리 공간을 갖게 되는 변수이다.
      • Java에서는 접근 제한자로 Private, Public, Protected로 사용되며 일반적으로 캡슐화 원칙에 따라 private로 설정되어 외부로부터 직접 접근이 제한된다.
    2. 인스턴스 변수에 잘못된 값이 할당되지 않게 막고, 정상적으로 조작하는 메소드
      • 이는 캡슐화 원칙의 일환이다. 캡슐화는 객체의 상태를 외부에서 직접 변경하지 못하게 하고, 대신 정해진 메서드를 통해서만 상태를 변경하도록 제한하는 원칙이다.
      • 예를 들어, 인스턴스 변수가 private로 선언되었다면, 해당 변수를 직접 조작할 수 없다. 대신, 이 변수의 값을 조회하거나 변경하기 메서드를 사용한다.
  • 모든 클래스가 갖추어야 하는 자기 방어 임무
    • 다른 클래스를 사용해서 초기화와 유효성 검사를 해야 하는 클래스는 그 자체로는 안전하게 사용할 수 없는 미성숙한 클래스이다.
    • 클래스는 스스로 자기 방어 임무(혼자서 초기화와 유효성 검사 가능해야 함)를 수행할 수 있어야 소프트웨어의 품질을 높이는 데 도움이 된다.
  • 성숙한 클래스로 성장시키는 설계 기법
    • 잘못된 값을 들어간 인스턴스가 생성되지 못하게 생성자를 통해 유효성을 검사한다.
      class OrderItem {
          private String productName;
          private double price;
          private int quantity;
      
          public OrderItem(String productName, double price, int quantity) {
              if (price < 0) {
                  throw new IllegalArgumentException("Price cannot be negative");
              }
              if (quantity < 0) {
                  throw new IllegalArgumentException("Quantity cannot be negative");
              }
      
              this.productName = productName;
              this.price = price;
              this.quantity = quantity;
          }
      }
    • 불변 변수로 만들어 예상하지 못한 동작 막기
      class OrderItem {
          private final String productName;
          private final double price;
          private final int quantity;
      }
      • 이렇게 되면 생성자를 통해서만 생성이 가능하다.
    • 값을 변경하고 싶다면 새로운 인스턴스 만들기
      class OrderItem {
          private final String productName;
          private final double price;
          private final int quantity;
      
          public OrderItem updatedQuantity(int newQuantity) {
              return new OrderItem(productName, price, newQuantity);
          }
      }
      • 이렇게 하면 불변을 유지하면서도 값을 변경할 수 있다.
      • 또한 불변 객체는 여러 스레드 간에 공유 되더라도, 그 상태가 변경되지 않으므로 동시성 문제에서 안전하다.
      • 객체의 상태가 한번 설정되면 변경되지 않기 때문에, 시스템의 동작을 예측하기 쉬워진다. 이는 디버깅과 유지보수를 용이하게 해준다.
  • 위와 마찬가지로 메소드의 매개변수, 지역 변수도 불변으로 만들수 있다.
  • 값 객체
    • 값 객체란 값을 클래스(자료형)로 나타내는 디자인 패턴이다.
    • 금액, 날짜 등을 사용하게 되면 각각의 값과 로직이 있기에 응집도가 높은 구조로 만들 수 있다.
    • 예를 들어 금액을 단순한 int형으로 두었다면 상품의 개수 가격등에 사용이 되며 실수로 의미가 다른 값이 섞을수도 있다.
    • 값 객체 + 완전 생성자는 객체 지향 설계에서 많이 사용되는 기법이다.

4. 불변 활용하기 : 안정적으로 동작하게 만들기

  • 재할당
    • 변수에 값을 다시 할당하는 것을 재할당이라하며 재할당은 변수의 의미를 바꿔 추측하기 어렵고 언제 어떻게 변경되었는지 추척하기가 어렵다.
  • 재할당을 방지하기 위해 불변 변수로 만들기
    • 지역변수는 물론 메소드의 매개변수도 불변 변수로 변경한다.
  • 가변으로 인해 발생하는 의도하지 않은 영향
    class OrderItem {
        private String productName;
        private double price;
        private int quantity;
    }
    
    class Order {
        final OrderItem orderItem;
    
    		Order(OrderItem orderItem) {
            this.orderItem = orderItem;
        }
    }
    
    public class OrderServiceDemo {
        public static void main(String[] args) {
            OrderItem sharedOrderItem = new OrderItem("Laptop", 1000, 1);
    
            Order order1 = new Order(sharedOrderItem);
    				Order order2 = new Order(sharedOrderItem);
    
            // order1의 가격 변경
            order1.orderItem.price(1200);
    
            System.out.println("Order1 Total Price after: " + order1.OrderItem.price);
            System.out.println("Order2 Total Price after: " + order2.OrderItem.price);
        }
    }
    • 지금 보면 order1의 가격만 변경을 하였는데, 출력을 하게 되면 order1, order2 두 객체 모두 가격이 1200으로 변경되었다.
    • 이처럼 가변 인스턴스 변수는 예상하지 못한 동작을 일으킨다.
  • 부수 효과의 단점
    • 함수의 부수 효과는 '함수가 매개변수를 전달받고, 값을 리턴하는 것' 이외에 외부 상태(인스턴스 변수 등)을 변경하는 것을 가르킨다.
    • 상태 변경이란 함수 밖에 있는 상태를 변경하는 것을 의미한다.
      • 인스턴스 변수 변경
      • 전역 변수 변경
      • 매개변수 변경
      • 파일 읽고 쓰기 같은 I/O 조작
  • 함수의 영향 범위 한정하기
    • 함수는 아래 항목을 만족하도록 설계하는 것이 좋다.
      • 상태는 매개변수로 받는다.
      • 상태를 변경하지 않는다.
      • 같은 함수의 리턴 값으로 돌려준다.
    • 잘못된 예시
      public class OrderItem {
          private double price;
          private int quantity;
          private static double discount = 0.1;  // 전역적인 할인율
      
          public void calculateDiscountedPrice() {  // 상태를 매개변수로 안 받는다.
              this.price * (1 - discount);  // 상태를 변경한다.
          }
      }
    • 올바른 예시
      public class OrderItem {
          private final double price;  // 불변성을 위해 final 키워드 사용한다.
          private final int quantity;  // 불변성을 위해 final 키워드 사용한다.
      
          public OrderItem(double price, int quantity) {
              this.price = price;
              this.quantity = quantity;
          }
      
          public double calculateDiscountedPrice(double discount) {  // 상태는 매개변수로 받는다.
              return price * (1 - discount);  // 상태를 변경하지 않고, 같은 입력에 대해 항상 같은 값을 반환한다.
          }
      }
      • calculateDiscountedPrice에 새로운 객체를 반환하지 않았던 이유는 기존 객체의 상태를 변경하거나 새로운 OrderItem 객체를 생성하는 것이 아니라, 단순히 계산된 값만 반환하기 위함이다.
      • 메소드의 목적에 따라 다를수 있지만 해당 메소드는 객체의 상태를 변경하는 것이 아니라 특정 값만 계산하여 반환하는거라 새로운 객체를 반환하여 불변성을 유지시킬 이유가 없다. 라고 생각했다.
      • 만약 상품의 개수(quantity)를 변경하는 거라면 새로운 객체를 생성해서 불변상태를 유지 시켜주는 것이 좋을 것이다.
        public OrderItem updateQuantity(final int newQuantity) {
            return new OrderItem(this.price, newQuantity);
        }
  • 기본적으로는 불변으로
    • 장점
      • 변수의 의미가 변하지 않으므로, 혼란을 줄일 수 있음
      • 동작이 안정적이게 되므로, 결과를 예측하기 쉬움
      • 코드의 영향 범위가 한정적이므로, 유지 보수가 편리해짐
  • 가변으로 설계해야 하는 경우
    • 대량의 데이터를 빠르게 처리해야하는 경우
    • 이미지를 처리하는 경우
    • 위와 같이 가변으로 설계해야하는 이유는?
      • 불변이라면 값을 변경할 때 인스턴스를 새로 생성해야한다.
      • 만약 크기가 큰 인스턴스를 새로 생성하면서 시간이 오래 걸려 성능에 문제가 있다면, 불변보단 가변을 사용해야한다.
  • 상태를 변경하는 메서드 설계하기
    • 상태를 변화시키는 메소드를 뮤테이터(Mutater)라고 부른다.
    • 뮤테이터의 예시는 아래와 같다.
      public class OrderItem {
          private final double price;
          private int quantity;
      
          // 수량 변경 메서드 (가변성 도입)
          public void updateQuantity(int newQuantity) {
              this.quantity = newQuantity;
          }
      }
      • 웹 개발자라면 한번쯤 이렇게 코드를 짜보지 않을까 한다.
      • 해당 도메인 엔티디에 대해 값 변경을 메소드를 통해 이뤄지고, Repo를 통해 저장을 할 것이다.
      • 이런 경우도 있다라는 예시로 들어보았다.

5. 응집도 : 흩어져 있는 것들

  • static 메소드 오용
    • static 메소드는 전역적으로 사용이 가능하다. → 클래스 인스턴스를 생성하지 않는다.
    • static 메소드의 구조적 문제
      • 데이터와 비즈니스 로직이 분리되어 있어서 응집도가 낮다.
    • static 메소드를 사용한 예시
      public class Order {
          private List<OrderItem> orderItems;
      
          // 응집도가 낮은 static 메소드
          public static double calculateVAT(double price) {
              return price * 0.1;  // 부가세 10% 계산
          }
      }
      
      // 인스턴스를 생성하지 않고 호출
      double vatAmount = Order.calculateVAT(totalPrice);
  • 인스턴스 메서드인 척 하는 static 메소드 주의하기
    public class Order {
        private List<OrderItem> orderItems;
    
    		// 인스턴스 메소드인 척 하는 static 메소드
        public double calculateDiscountedPrice(double originalPrice, double discountRate) {
            return originalPrice * (1 - discountRate);
        }
    }
    • calculateDiscountedPrice → orderItems을 전혀 사용하지 않는 메소드이다.
    • 이렇게 되면 앞에서 예시로 보여줬던 calculateVAT와 거의 동일하다.
  • static 메소드를 왜 사용할까?
    • static 메소드는 절차 지향 언어의 접근 방법에서 사용한다.
    • 절차 지향 언어에서는 데이터와 비즈니스 로직이 별개로 존재하도록 설계한다.
    • 따라서 클래스의 인스턴스를 생성하지 않고도 사용할 수 있는 static 메소드를 사용한다.
    • static 메소드는 클래스의 인승턴스를 만들지 않아도 되기에 간단하게 사용이 가능하지만, 응집도가 낮아지는 문제가 있다. 적재적소에 사용하는 것이 중요하다.
  • 어떤 상황에서 static 메소드를 사용해야 좋을까?
    • 로그 출력 전용 메소드, 포맷 변환 전용 메소드 등에서 쓰인다.
    • 해당 메소드들은 응집도와 관계가 없기에 static으로 사용하는게 좋다.
  • 매개변수가 너무 많은 경우
    • 매개변수가 많아지면 응집도가 낮아질 수 있다.
    • 그 이유는 아래와 같다.
      • 해당 메소드에서 너무 많은 일을 하려고 할 수 있기에 이는 저 응집도의 신호이다. 즉, 하나의 메소드가 여러 가지 책임을 지니고 있을 가능성이 높다.
  • 기본 자료형에 대한 집착
    • 기본 자료형에 대한 집착을 하게 되는 경우 데이터와 비즈니스 로직이 분산된다.
    • 그렇게 되면 중복된 코드가 많이 생기므로 응집도가 낮아진다.
    • 잘못된 예시 → 기본 자료형만 사용한 코드
      public class OrderService {
          
          public double applyDiscount(double orderAmount, double discountRate) {
              if (orderAmount < 0) {
                  throw new IllegalArgumentException("Amount cannot be negative");
              }
              if (discountRate < 0) {
                  throw new IllegalArgumentException("discountRate cannot be negative");
              }
              return orderAmount * (1 - discountRate);
          }
      
          public double calculateVAT(double orderAmount) {
              if (orderAmount < 0) {
                  throw new IllegalArgumentException("Amount cannot be negative");
              }
              return orderAmount * 0.1;  // 10% VAT
          }
      }
      • 매개변수 orderAmount의 유효성 검사를 중복되게 하는 것을 볼 수 있다.
    • 올바른 예시 → 기본 자료형을 클래스로 응집한 코드
      
      public class OrderAmount {
          private final double amount;
      
          public OrderAmount(double amount) {
              if (amount < 0) {
                  throw new IllegalArgumentException("Amount cannot be negative");
              }
              this.amount = amount;
          }
      }
      
      // DiscountRate 클래스
      public class DiscountRate {
          private final double rate;
      
          public DiscountRate(double rate) {
              if (rate < 0 || rate > 1) { // 0 ~ 1 사이의 값만 유효하다고 가정
                  throw new IllegalArgumentException("Discount rate must be between 0 and 1");
              }
              this.rate = rate;
          }
      
          public double applyTo(double amount) {
              return amount * (1 - rate);
          }
      }
      
      // OrderService 클래스
      public class OrderService {
          private final OrderAmount orderAmount;
          private final DiscountRate discountRate;
      
          public OrderService(OrderAmount orderAmount, DiscountRate discountRate) {
              this.orderAmount = orderAmount;
              this.discountRate = discountRate;
          }
      
          public double applyDiscount() {
              return discountRate.applyTo(orderAmount.getAmount());
          }
      
          public double calculateVAT() {
              return orderAmount.getAmount() * 0.1;
          }
      }
      • 이렇게 만들어진 코드는 다양한 장점이 있다.
        1. 캡슐화 : OrderAmount, DiscountRate 클래스는 자신의 상태와 관련된 유효성 검사와 동작을 자체적으로 처리한다. 이는 외부에서 이 클래스들의 내부 상태나 구현 방식을 알 필요 없이 사용할 수 있다는 것을 의미한다.
        2. 응집도 : OrderAmount는 주문 금액과 관련된 로직만, DiscountRate는 할인율과 관련된 로직만 책임지게 된다. 각 클래스는 그에 해당하는 역할만을 수행하므로 응집도가 높아진다.
        3. 재사용성 : OrderAmount나 DiscountRate와 같은 클래스는 다른 서비스나 기능에서도 사용될 가능성이 있다. 이렇게 독립적으로 잘 정의된 클래스는 다른 부분에서도 재사용하기 쉽다.
  • 묻지 말고 명령하기
    • 다른 객체의 내부 상태(변수)를 기반으로 판단하거나 제어하려고 하지 말고, 메소드로 명령해서 객체가 알아서 판단하고 제어하도록 설계하라는 의미이다.
    • 잘못된 예시
      public class Order {
          private ShippingStatus shippingStatus;
      
          public ShippingStatus getShippingStatus() {
              return shippingStatus;
          }
      
          public void setShippingStatus(ShippingStatus status) {
              this.shippingStatus = status;
          }
      }
      
      public class OrderService {
          public void shipOrder(Order order) {
      				// order 객체에게 묻고 있다.
              if (order.getShippingStatus() == ShippingStatus.PENDING) {
                  order.setShippingStatus(ShippingStatus.SHIPPED);
              }
          }
      }
    • 올바른 예
      public class Order {
          private ShippingStatus shippingStatus;
      
          public void ship() {
              if (this.shippingStatus == ShippingStatus.PENDING) {
                  this.shippingStatus = ShippingStatus.SHIPPED;
              }
          }
      }
      
      public class OrderService {
          public void shipOrder(Order order) {
      				// order 객체에게 묻는 것이 아닌 order 객체가 주체적으로 제어한다.
              order.ship();
          }
      }

6. 조건 분기 : 미궁처럼 복잡한 분기 처리를 무너뜨리는 방법

  • 조건 분기가 중첩되어 낮아지는 가독성
    • 예시
      if (조건) {
      		// 수십 ~ 수백 줄의 코드
      		if (조건) {
      				// 수십 ~ 수백 줄의 코드
      				if (조건) {
      						// 수십 ~ 수백 줄의 코드
      						if (조건) {
      								// 수십 ~ 수백 줄의 코드
      						}
      				}
      		}
      }
      • 이처럼 코드를 짠 사람도 이해하기 어려울 정도의 중첩 분기문인데, 팀 단위로 같이 작업할 땐 얼마나 더 심하겠는가
  • 조기 리턴으로 중첩 제거하기
    • 예시
      public class OrderService {
      
          public String processOrder(Order order) {
              if (!order.isPaid()) return "Order has not been paid!";
              if (!order.isInStock()) return "Item is out of stock!";
              if (!order.hasValidShippingAddress()) return "Invalid shipping address!";
              
              order.ship();
      
              return "Order has been shipped!";
          }
      }
      • 조기 리턴으로 중첩이 제거 되어서 가독성이 좋아졌다.
    • 요구사항이 추가되었다고 가정하게 된다면, 아래와 같다.
      public class OrderService {
      
          public String processOrder(Order order) {
              if (!order.isPaid()) return "Order has not been paid!";
              if (!order.isInStock()) return "Item is out of stock!";
              if (!order.hasValidShippingAddress()) return "Invalid shipping address!";
      				if (order.getItemCount() < 2) order.addShippingFee();  // 분기 요구사항
      
              order.ship();
      				order.setFreeShipping();  // 비즈니스 요구사항
      
              return "Order has been shipped!";
          }
      }
      • 위와 같이 매우 간단하게 요구사항을 추가할 수 있다.
  • 전략 패턴(정책 패턴)
    • 전략패턴이란 여러 유사한 알고리즘을 캡슐화해서 객체의 행위를 동적으로 변경 가능하게 만드는 패턴이다.
    • OCP(개방폐쇄의 원칙) : 변경엔 닫혀있고 확장엔 열려있는 객체지향 원칙이 실현된다.
      • 다른 행동 및 전략이 추가된다고 해도 기존의 코드는 변경하지 않고 확장이 가능하다.
    • 전략 패턴을 사용하지 않은 예
      public class DiscountService {
          public double applyDiscount(double orderAmount, String discountType) {
              if ("PERCENT_10".equals(discountType)) {
                  return orderAmount * 0.9;
              } else if ("PERCENT_20".equals(discountType)) {
                  return orderAmount * 0.8;
              } else if ("FLAT_50".equals(discountType)) {
                  return orderAmount - 50;
              }
              return orderAmount;
          }
      }
      • 이 방식의 문제점은 새로운 할인 유형이 추가될 때마다 applyDiscount 메서드를 수정해야 한다는 것이다.
      • 또한, DiscountService 클래스에만 이런 조건이 있는게 아니라 다른 클래스에도 유사한 조건이 있다면 모두 수정이 필요하다.
    • 전략 패턴을 사용 예
      interface DiscountStrategy {
          double apply(double orderAmount);
      }
      
      class Percent10Discount implements DiscountStrategy {
          public double apply(final double orderAmount) {
              return orderAmount * 0.9;
          }
      }
      
      class Percent20Discount implements DiscountStrategy {
          public double apply(final double orderAmount) {
              return orderAmount * 0.8;
          }
      }
      
      class Flat50Discount implements DiscountStrategy {
          public double apply(final double orderAmount) {
              return orderAmount - 50;
          }
      }
      
      public class DiscountService {
          private DiscountStrategy strategy;
      
          public DiscountService(DiscountStrategy strategy) {
              this.strategy = strategy;
          }
      
          public double applyDiscount(double orderAmount) {
              return strategy.apply(orderAmount);
          }
      }
      • 이제 새로운 할인 유형이 추가되면 DiscountService 클래스를 수정할 필요 없이 새로운 DiscountStrategy 구현만 추가하면 된다.
      • 이는 코드의 확장성을 향상시키고, 변경에 대한 영향을 최소화하는 데 도움을 줍니다.
  • 자료형 확인에 조건 분기 사용하지 않기
    • 자료형을 조건 분기로 사용하게 되면 모처럼 if문을 없애기 위해 전략 패턴을 사용했는데 다시금 생기는 것입니다.
    • 처음에 한번 생기는 건 괜찮아 질수도 있지만, 지름길을 찾게 된다면 점점 유지보수 하기 어려운 코드를 만들게 될 것입니다. 이러한 방식을 깨진 유리창 이론이라고 합니다.
      • 깨진 유리창 이론이란?

        • 품질이 떨어진 코드에서 작업할 때 더 낮은 품질의 코드를 추가하기가 쉽다.
        • 코딩 규칙을 많이 어긴 코드에서 작업할 때 또 다른 규칙을 어기기도 쉽다.
        • 지름길을 많이 사용한 코드에서 작업할 때 또 다른 지름길을 추가하기도 쉽다.
        • 즉, 레거시 코드가 될 가능성이 높습니다.
    • 자료형 확인에 조건 분기를 사용한 안 좋은 예시
      public class DiscountService {
          private DiscountStrategy strategy;
      
          public DiscountService(DiscountStrategy strategy) {
              this.strategy = strategy;
          }
      
          public double applyDiscount(double orderAmount) {
              if (strategy instanceof Flat50Discount) {
                  orderAmount += 30;
              }
              return strategy.apply(orderAmount);
          }
      }
      • 이렇게 되면 점점 분기문이 추가될 것이다.
    • 자료형 확인에 조건 분기를 사용하지 않은 좋은 예시
      interface DiscountStrategy {
          double apply(double orderAmount);
      }
      
      class Flat50Discount implements DiscountStrategy {
          public double apply(final double orderAmount) {
              return orderAmount - 50 + 30;
          }
      }
      
      public class DiscountService {
          private DiscountStrategy strategy;
      
          public DiscountService(DiscountStrategy strategy) {
              this.strategy = strategy;
          }
      
          public double applyDiscount(double orderAmount) {
              return strategy.apply(orderAmount);
          }
      }
      • 이렇게 하면 분기문을 추가하지 않고 처리가 가능하다.
    • 단, DiscountStrategy의 구현체인 다른 클래스도 공통된 로직이 추가가 된다면 DiscountStrategy에 새로운 메소드를 구현해서 모든 구현체에서도 해당 비즈니스 로직을 추가해주면 될 것이다.
profile
지나가는 개발자

0개의 댓글