본문 바로가기

개발공부/원티드 챌린지 정리

9월 백엔드 챌린지 정리 : 클린 코드 (3)

728x90

SOLID - 1

1. SRP : Single Responsibility Principle

- 객체를 변경시키는 요인은 무조건  하나여야 한다

책임이란, 객체에 의해 정의되는 응집도(행동과 관련된 상태를 한 클래스에 모이는 정도) 있는 행위의 집합으로, 객체가 유지해야 하는 정보(상태)와 수행할 수 있는 행동(메서드)에 대해 추상적으로 서술한 문장이다. 책임과 기능은 구분이 가능하고 그 크기가 다르다. 책임은 객체가 수행할 수 있는 행동을 종합적이고 간략하게 서술하기 때문에 기능보다 추상적이고 개념적으로 더 크다

 

커피 주문 서비스를 설계하면서, 필요한 기능으로 1) 주문받기 -> 2) 커피 제조 -> 3) 준비된 커피 전달의 3가지 기능에 대한 책임을 "Cashier" 객체에 할당하도록 구현했다

public class Cashier {
	private Orderbook orderbook;

	public boolean takeOrders(String menuName, int quantity){
		Coffee makedCoffee = makeCoffee(menuName, quantity);
		deliveryOrder(orderbook.getUserName(), makedCoffee);
	}

	public long calculatePrice(String menuName, int quantity){

	}

	public Coffee makeCoffee(String menuName, int quantity){

	}

	public void deliveryOrder(Strint toCustomer, Coffee coffee){

	}
}

이렇게 설계할 경우, 가격 계산방식 변경이나 메뉴 추가의 경우에 클래스를 변경해야 하는 문제가 생긴다. Cashier 클래스에 두 개 이상의 책임이 할당되어 있어 다음과 같은 문제를 가진다

1) 각자 다른 책임의 기능이 강한 결합도를 가지게 되어, 변화 발생 시 서로 영향을 끼친다

2) 적절한 관심사 분리가 되지 않아, 코드 가독성이 저해된다. 너무 많이 클래스를 나누는 것에 대해 소스 파악이 어려워 진다고 SRP에 대한 반론을 제기되기도 하지만, 나누는 행위로 인해 소스의 크기가 커지는 것은 아니므로 충분한 반론이 되기 어려우며, 다중 책임 클래스일수록 하나의 변화에 대해 다른 코드까지 영향을 미치므로 복잡도와 가독성 및 재사용성이 떨어질 수 밖에 없다.

 

SRP의 핵심은 클래스를 변경해야만 할 경우, 그 이유는 하나뿐이어야 한다는 것이다. 그러므로 클래스 설계 시, 다음을 지켜야 한다

- 클래스는 오직 하나의 책임(변경 요소)을 가진다

- 하나의 책임이 여러 클래스로 분산된 경우, 하나의 클래스에 모아 그 책임은 그 클래스에만 할당한다

다음의 규칙을 준수한 Cashier 클래스는 이렇게 구현한다

public class Cashier {
	private Orderbook orderbook;
	private Beverage beverage;

	public boolean takeOrders(String menuName, int quantity){
		Coffee makedCoffee = makeCoffee(menuName, quantity);
		deliveryOrder(orderbook.getUserName(), makedCoffee);
	}

	public long calculatePrice(String menuName, int quantity){
		return beverage(menuName, quantity);
	}

	public boolean sendToOrderInfoToBarista(Barista barista){
		barista.receiveOrder(orderbook.getOrder());

	}

	public void deliveryOrder(Strint toCustomer, Coffee coffee){

	}
}

Cashier 클래스는 오직 주문 관련 책임만 할당받는다. 음료수 정보 관련 책임은 Beverage 클래스로 할당하며 음료 제조와 련된 책임은 Barista 클래스로 할당된다

2. OCP : Open and Close Principle

- 구체화가 아닌 추상화에 의존하라

열기와 닫기란 ? : 클래스는 확장에 열려있고, 변경에 닫혀있어야 한다. 즉, 클래스를 수정하지 않고 확장할 수 있어야 한다.

새로운 기능을 추가해야 할 때, 코드 또는 클래스를 수정하지 않고 기능을 추가하는 기법을 열기와 닫기 기법이라고 한다

 

최초 개발이 끝나고 이미 구현이 완료된 코드에 새로운 기능을 추가하기 위해 코드를 수정해야 한다면, 기존 코드를 무작정 손대는 것은 리스크가 있다. 다른 코드에 영향이 갈 수 있으며, 테스트 또한 완전히 새롭게 진행해야 하기 때문이다.

 

어떻게 적용할까?

1) 변경할 것과 변경하지 않을 것을 구분한다

ex) 새로운 할인 정책을 추가한다

-> 할인 정책은 변경된다.(변경할 것)

-> 모든 할인 정책은 적용 여부를 확인하고 할인 금액을 계산한다 ~ 할인 정책의 본질(변하지 않을 것)

 

2) 공통된 특징을 기반으로 추상화 또는 인터페이스 정의

- 적용 여부 판단과 할인 금액 계산은 모든 할인 정책 객체에서 응답할 수 있어야 하는 메세지이다.

~ 공통으로 사용될 상태(인스턴스 변수)가 없기 떄문에 인터페이스를 사용한다

 

3) 구현이 아닌 추상화에 의존하기

구현에 의존하는 경우, 변경에 유연하게 대응할 수 없다. 추상화, 인터페이스와 다형성이 OCP를 적용하는 데 필요한 가장 중요한 매커니즘이다. 이를 적용할 경우, 변경에 유연해지지만 코드의 가독성이 떨어진다

// 추상화 또는 인터페이스
public interface class DiscountPolicy {
    public boolean isSatisfied();
    public long    calculateDiscountAmount();
}

// 팩토리얼
public DiscountPolicyFactory {
	public static DiscountPolicy makeDiscountPolicyBy(String type){
			switch(type){
				case "telecome":
						return new TelecomeDiscountPolicy();
				case "membership":
						return new MembershipDiscountPolicy();
				case "payco":
						return new PaycoDiscountPolicy();
				default:
					return new NoneDiscountPolicy();
	}
}

// 호출부
String discountType = @RequestParam type;
DiscountPolicy discountPolicy = 
		DiscountPolicyFactory.makeDiscountPolicyBy(discountType)
getDiscountAmount(discountPolicy);

** 코드의 가독성이 더 중요한 경우엔 분기처리를 통한 구현을, 변경 유연성이 더 중요한 경우엔 다형성을 선택하는 것이 가장 효율적인 의사결정이다

LSP : Liskov Substitution Principle

기능 명세를 지켜라(OCP의 기본이 되는 원칙)

1) 리스코프 원칙이란

상위 객체를 하위 객체로 치환해도 상위 타입을 사용하는 프로그램은 정상적으로 작동해야 한다는 원칙을 의미한다

즉, 부모 객체를 호출하는 동작에서 자식 객체가 부모 객체를 대체할 수 있어야 하고, 올바른 상속을 위해 자식 객체의 확장이 부모 객체의 방향을 온전히 따르도록 권하는 원칙

public class ParentClass {
	public void upcasting(){
		System.out.println("Up Casting");
	}
}

public class ChildClass extends ParentClass {
		@Override
		public void upcasting(){
			System.out.println("Down Casting");
		}
}

public static void main(String[] args) {
		ParentClass p = new ChildClass();
		p.upcasting(); // expect output: "Up Casting"
}

 

2) 중요한 이유

리스코프 치환 원칙이 위반되면 변경 유연성을 보장해주는 열기와 닫기 원칙(개방 폐쇄 원칙) 또한 지킬 수 없다

public class CopyUtil {
  public static void copy(InputStream is, OutputStream out) {
    byte[] data = new byte[512];
    int len = -1;

    // InputStream.read() 메서드는 스트림의 끝에 도달하면 -1을 리턴
    while((len = is.read(data)) != -1) {
      out.write(data, 0, len);
    }
  }
}

read()메서드는 InputStream is의 데이터가 더 이상 없는 경우 -1을 반환하고, copy() 메서드는 -1일 때까지 out.write()로 데이터를 쓴다

public class CustomInputStream implements InputStream {
  public int read(byte[] data) {
    ...
    return 0; // 데이터가 없을 때 0을 리턴하도록 구현
  }
}

그러나 만약 InputStream을 상속한 CustomInputStream에서 read()의 끝 값을 0으로 재정의하고 기존의 copy()메서드에서 InputStream 객체를 하위 객체인 CustomInputStream으로 치환해서 호출할 경우 -1이 나올 수 없기 때문에 루프를 돌게 된다

그러므로 이런 혼란을 막기 위해 하위 타입의 객체는 각 기능의 명세를 준수해야 하는 것이다. 그렇지 못하면 이와 같이 비정상 동작이 발생하고, 분기 처리로 오류를 막을 수밖에 없어 OCP가 무너지고 코드는 변경 유연성을 상실한다

 

+ 예제

할인율을 상품가격에 계산하여 할인가를 계산하는 기능을 구현한 클래스인 Coupon은 Item 클래스에 의존한다

public class Coupon {
  public int calculateDiscountAmount(Item item) {
    return item.getPrice() * discountRate;
  }
}

// 할인을 받을 수 있는 Item
public class Item {
	private String itemName;
	private String price;
}

그러던 중 할인이 불가능한 Item을 추가하게 되어 Item 클래스를 상속받는 NoDiscountItem 클래스를 정의했다

// 기존 Item과 모든 비즈니스 로직이 동일하지만,
// 할인을 받을 수 없다는 큰 차이점을 가지고 있음.
public class NoDiscountItem extends Item {
	// 구현부...	
}

만약 할인 불가인 아이템을 Coupon.calculate(Item i)에서 호출하면 할인가는 0이어야 하므로, 분기 처리를 추가할 수밖에 없으며, 기존 코드를 수정해야 한다

	public int calculateDiscountAmount(Item item) {
		if(item instanceof NoDiscountItem)
			return 0;
		else if (item instanceof DoubleDiscountItem)
			return item.getPrice() * (discountRate * 2);
		else if (item instanceof OwnerCrayItem)
			return item.getPrice() * (discountRate * 70);
    return item.getPrice() * discountRate;
  }
// DoubleDiscoutItem
// OwnerCrayItem

이렇게 되면 LSP를 위반하는 코드 작성 사례가 된다. calculateDiscountAmount는 Item의 하위 객체인 NoDiscountItem을 알아서는 안되는데, instanceof 연산자를 통해 해당 하위 객체를 알게 되며, 하위 객체인지를 판단하는 분기 처리를 하므로 하위 객체가 상위 객체를 대체할 수 없다는 뜻이므로 LSP를 위반한다. 이 사례는 이 하위 객체에만 해당하는 것이 아니라, 

LSP를 위반하는 새 하위 객체가 등장할 때마다 분기를 추가해야 하는 비효율적인 상황이다

 

해결방법 1. 추상화

public abstract class Item {
	// 변경되지 않는 것.
	private String itemName;
	private int price;
	// 변경되는 것.
	private boolean isDiscount;

	// 변경되는 것과 변경되지 않는 것을 이어주는 인터페이스
	public boolean isSatisfied(){
		return isDiscount;
	}
}

public class EnableDiscountItem extends Item {
}
public class DisableDiscountItem extends Item {
	public DisableDiscountItem(String i, int p , boolean d){
		suprer(i,p,d);
	}
}

public class Coupon {
	private int discountRate;

  public int calculateDiscountAmount(Item item) {
		if(item.isSatisfied())
			return item.getPrice() * discountRate;
		else
			return 0;
  }
}

calculateDiscountAmount(new DisableDiscountItem("iphon15", 1500000, false);

 

해결방법 2. 인터페이스

package com.wanted.preonboarding.clean.code.solid.lsp;

public class lspInterfaceExample {
    interface Item {
        public boolean isEnableDiscount();
        public int getPrice();
    }

    class EnableDiscountItem implements Item {
        private final String itemName;
        private final int price;
        private final boolean isDiscount;
        public EnableDiscountItem(String i, int p, boolean d){
            itemName = i;
            price = p;
            isDiscount = d;
        }

        @Override
        public int getPrice(){
            return price;
        }
        @Override
        public boolean isEnableDiscount() {
            return isDiscount;
        }
    }
    class DisableDiscountItem implements Item {
        private final String itemName;
        private final int price;
        private final boolean isDiscount;

        public DisableDiscountItem(String i, int p, boolean d){
            itemName = i;
            price = p;
            isDiscount = d;
        }
        @Override
        public boolean isEnableDiscount() {
            return false;
        }
        @Override
        public int getPrice(){
            return price;
        }
    }

    public class Coupon {
        private int discountRate;

        public int calculateDiscountAmount(Item item) {
            if(item.isEnableDiscount())
                return item.getPrice() * discountRate;
            else
                return 0;
        }
    }
}

 

 

728x90