3 분 소요

Reference

스프링 입문을 위한 자바 객체 지향의 원리와 이해, 위키북스, 김종민 지음

개발자가 반드시 정복해야 할 객체 지향과 디자인 패턴, 인투북스, 최범균 지음


📌 What’s DIP?

“추상화에 의존해야지 구체화에 의존하면 안된다.”

”고수준 모듈은 저수준 모듈의 구현에 의존해서는 안 된다. 저수준 모듈이 고수준 모듈에서 정의한 추상 타입에 의존해야 한다.”

”자주 변경되는 구체(Concrete) 클래스에 의존하지 마라.”

  • 의존 역전 원칙: 자신보다 변하기 쉬운 것에 의존하던 것을 추상화된 인터페이스나 상위 클래스를 두어 변화하기 쉬운 것의 변화에 영향받지 않게 하는 원칙
    • 고수준 모듈: 어떤 의미 있는 단일 기능을 제공하는 모듈(인터페이스나 상위 클래스)
    • 저수준 모듈: 고수준 모듈의 기능을 구현하기 위해 필요한 하위 기능의 실제 구현(구현체나 하위 클래스)
  • 자신보다 변하기 쉬운 것에 의존하지 마라!!!

고수준 & 저수준 모듈 예제: 암호화

  • 고수준 모듈: 의미있는 단일 기능
    • 바이트 데이터를 읽어와 암호화 하고 결과 바이트 데이터를 쓴다.
    • 의미 있는 단일 기능: 바이트 데이터를 암호화한다.
    • 필요한 하위 기능: 데이터 읽기, 암호화, 데이터 쓰기
  • 저수준 모듈: 고수준 모듈에서 필요한 하위 기능을 실제로 구현
    • 데이터 읽기: 파일에서 바이트 데이터를 읽어온다.
    • 암호화: AES 알고리즘을 이용해 데이터를 암호화한다.
    • 데이터 쓰기: 파일에 바이트 데이터를 쓴다.


📌 고수준 모듈이 저수준 모듈에 의존할 때의 문제

고수준 모듈은 상대적으로 큰 틀(상위 문제)에서 프로그램을 다루고, 저수준 모듈은 각 개별 요소(상세 문제)가 어떻게 구현될지에 대해서 다룬다.

프로젝트가 안정기에 접어들면 상위 문제를 변경하기 보다는 상세한 문제를 변경할 가능성이 높아진다. 이때 고수쥰 모듈이 저수준 모듈에 의존하면 변경이 어려워진다.

할인 정책의 변경

상품의 가격을 결정하는 정책을 생각해 보면 상위 수준에서 어떻게 할인 정책을 적용할지 큰 틀을 잡아 결정을 내린다.

  • 쿠폰을 적용해서 가격 할인을 해 준다.
  • 쿠폰은 상품당 한 개만 사용할 수 있다.

위 내용은 고수준 모듈의 정책이다. 고수준 모듈의 의미 있는 단일 기능은 쿠폰을 적용해 할인을 해주는 것이다. 이 기능을 구현하기 위해 필요한 하위 기능은 고정 할인 쿠폰을 사용할지, 정률 할인 쿠폰을 사용할지, 사용자의 등급에 따라 어떤 정률을 적용할지 등이 존재한다.

쿠폰을 적용해 할인을 해주는 정책은 크게 변경될 일이 없지만, 세부적인 할인 방법은 상황에 따라 변경이 될 수 있다. 만약 고정 할인 쿠폰을 사용하다가 사용자 등급에 따라 정률 할인 쿠폰을 제공한다면 어떻게 될까?

  • 처음 정책은 고정 할인 쿠폰을 사용한다.
public int calculate(Item item) {

	int fixedDiscountAmount = 1000;

	return item.getPrice() - fixedDiscountAmount;
}
  • 여기에서 사용자 등급이 VIP 이상이면 10% 할인 쿠폰을 사용할 수 있도록 변경한다면…
public int calculate(Item item, Member member) {

	int rateDiscountAmount;
	int fixedDiscountAmount = 1000;

	if (member.getGrade.equals("VIP") {
		rateDiscountAmount = item.getPrice * 0.1;
		return item.getPrice() - Math.max(fixedDiscountAmount, rateDiscountAmount);
	}

	return item.getPrice() - fixedDiscountAmount;
}

쿠폰을 이용한 가격 계산 모듈이 개별적인 쿠폰 구현에 의존하게 되면 새로운 쿠폰 구현이 추가되거나 변경될 때마다 코드가 변경된는 상황이 발생한다.

  • 이런 상황은 프로그램의 변경을 어렵게 만든다. 저수준 모듈이 변경되더라도 고수준 모듈은 변경되지 않도록 지원하는 것이 의존 역전 원칙(DIP)이다.


📌 DIP: 유연함 확보

  • DIP는 저수준 모듈이 고수준 모듈에 의존하게 만들어서 변경시 발생하는 문제를 해결한다.

고수준 모듈이 저수준 모듈을 사용하는 것은 고수준 모듈이 저수준 모듈에 의존한다는 뜻인데, 어떻게 저수준 모듈이 고수준 모듈을 의존하게 만들 수 있을까? 답은 추상화에 있다.

public interface DiscountPolicy {

	/**
	 *
	 * @param member, 회원
	 * @param price, 상품 가격
	 * @return 할인 대상 금액
	 */
	int calculate(Member member, Item item);

}

public class FixDiscountPolicy implements DiscountPolicy {

	private int fixedDiscountAmount = 1000;

	@Override
	public int calculate(Member member, Item item) {				
		return item.getPrice() - fixedDiscountAmount;
	}

}

public class RateDiscountPolicy implements DiscountPolicy {

	private int discountPercent = 10;
	private int fixedDiscountAmount = 1000;

	@Override
	public int calculate(Member member, Item item) {
		if (member.getGrade().equals("VIP")) {
			return item.getPrice() - Math.max(item.getPrice() * discountPercent / 100, fixedDiscountAmount);
		} else {
			return 0;
		}
	}

}

실제 할인 정책을 가져다 쓰는 클라이언트에서는 세부적으로 고정 할인 정책을 사용할지, 정률 할인 정책을 사용할지 몰라도 할인 정책을 제공한다는 큰 틀만 알아도 된다. 이는 저수준 모듈(구현체)이 고수준 모듈(인터페이스)에 의존하기 때문에 발생할 수 있는 일이다.

이때 새로운 할인 정책이 추가되더라도 고수준 모듈의 변경 없이 저수준 모듈만 새로 추가하고 변경하면 되어 변경의 유연함을 얻게 된다.

즉, 의존 역전 원칙(DIP)은 리스코프 치환 원칙(LSP)과 함께 개방 폐쇄 원칙(OCP)을 따르는 설계를 만들어 주는 기반이 된다.


📌 소스 코드 의존과 런타임 의존

  • 의존 역전 원칙(DIP)은 소스 코드에서의 의존을 역전시키는 원칙이다.

암호화 예제에서 FlowController 클래스가 ByteSource 인터페이스에 의존하고 있고, ByteSource의 구현체인 FileDataReader 클래스도 ByteSource 인터페이스에 의존하고 있다고 하자.

소스 코드의 의존

  • 두 클래스 모두 고수준 모듈인 ByteSource 인터페이스에 의존하고 있다.

런타임에서의 의존

  • 고수준 모듈인 FlowController 클래스가 저수준 모듈인 FileDataReader 클래스에 의존하고 있다.

DIP는 런타임의 의존이 아닌 소스 코드의 의존을 역전시킴으로써 변경의 유연함을 확보할 수 있도록 만들어 주는 원칙이지, 런타임에서의 의존을 역전시키는 것은 아니다.


📌 DIP와 패키지

  • DIP는 타입의 소유도 역전시킨다.

DIP가 적용되기 전 암호화 예제는 데이터 읽기 타입은 FileDataReader를 소유한 패키지가 소유하고 있었다.

DIP가 적용되고 데이터 읽기 타입은 고수준 모듈이 소유하게 된다.

  • 타입의 소유 역전은 각 패키지를 독립적으로 배포할 수 있도록 만들어 준다.
    • 독립적으로 배포한다는 건 jar 파일이나 DLL 등의 파일로 배포한다는 것을 뜻함

의존 역전 원칙(DIP)은 개방 폐쇄 원칙(OCP)을 클래스 수준뿐만 아니라 패키지 수준까지 확장시켜 주는 디딤돌이 된다.

댓글남기기