본문 바로가기

WEB/Spring

AppConfig와 DI

스프링 어린이(스린이)로서 프로젝트를 할 때 괜찮은 코드를 참고 위해 구글링하면서 여기 저기 돌아다니다보면

"AppConfig" 이 녀석을 많이 보게 된다. 이전 뭐지? 뭐 설정해주는건가? 하고 넘겼다.

게다가 스린이 입장으로 봤을 때

'한 번의 선언으로 해결할 수 있는데 쓸데없이 코드가 한 번 더 있네? 빼야하는 부분 아닌가? 뭐 가독성은 있는데 빼는 게 더 나을듯 후훗 역시 나는 효율적이야' 라고 생각했다. (바보같은 생각)

 

하지만 알고보니 이 녀석은 아주 좋은 객체지향 코드가 되기 위한 핵심 역할을 해주는 좋은 녀석(마치 찐빵의 팥)이었다!!

비유적으로 접근하자면, 배역은 인터페이스이고 배우는 이를 구현하는 구현체라고 하자.
이때, AppConfig는 "공연 기획자, 캐스팅 담당자"로 배역에 맞는 배우를 연결하는 역할을 하는 것이다. 

 

더 자세히 예시 코드를 보면서 더 알아보자.

(상황은 주문 서비스가 있고 할인 정책이 있는데 손님이 주문을 하면 할인 정책을 적용된 가격을 지불해야 한다)

구조는 다음과 같다. 

[클라이언트] → [주문 서비스 구현체 OrderServiceImpl] → 1) 고정 할인 정책 구현체 2) 비율 할인 정책 구현체

 

✔️[스프링의 핵심 개념]

먼저, 스프링을 관통하는 핵심 개념은 "다형성을 위해 객체 설계 시 역할(인터페이스)을 먼저 부여하고, 그 역할을 수행하는 구현 객체 만들자"이다.(역할을 먼저 → 구현체를 나중에 덧붙이기!)

 

이러한 개념이 적용된 코드를 짜기 위해서 먼저 추상적으로 인터페이스를 생각하고 (ex discountPolicy 할인 정책이라는 인터페이스) 이를 구체적으로 적용된 것(고정 금액 할인 혹은 비율 할인과 같은 구현체)들을 생각해볼 수 있다.

이렇게 인터페이스와 구현체를 나누게 된다면 정책의 변경 사항에 잘 대응할 수 있다. (마치 애플워치 스트랩 바꾸듯 일부분만 바꿔끼울 수 있다)

 

[예시]

하지만 이렇게 생각한 구현체를 직접 코드에 적용하기 위해서 다음과 같이 코드를 작성할 수 있다. 

public class OrderServiceImpl implements OrderService{
   ...

	private final DiscountPolicy discountPolicy = new RateDiscountPolicy();
	//DIP 위반! -> 구현체인 RateDiscountPolicy에도 의존하고 있음

	@Override
	public Order createOrder(Long memberId, String itemName, int itemPrice) {
		...
        
		int discountPrice = discountPolicy.discount(member, itemPrice);
		return new Order(memberId,itemName, itemPrice, discountPrice );
	}
}

OrderServiceImpl에서 할인 정책에 적용하기 위해서 할인 정책 구현체를 선언했다. 

 

하!지!만! 여기서 2가지 문제점이 있다.

[문제점 1]

첫 번째 문제점은 바로바로바로 OrderServiceImpl가 지금 discountPolicy 와 이의 구현체인 RateDicountPolicy에도 동시에 의존하고 있다는 점이다. 이는 DIP 위반이다. 

 

참고 : 좋은 객체지향의 5가지 원칙 중 DIP(의존관계 역전 원칙)은 “추상화에 의존해야지, 구체화에 의존하면 안된다”라는 것이며5가지 원칙 중 중요한 원칙에 속한다.

 

[문제점 2]

만약 할인 정책의 구현체를 바꾼다고 한다면(고정 금액 -> 비율로 변경)

할인 정책을 바꾸는 순간 OrderServiceImpl 의 코드를 변경했다 .

하지만 변경이 있는 순간 구현체의 코드가 바뀌면 안 된다. 이는 전기차로 바꾸었는데 따로 면허를 다시 따는 격이라고 할 수 있다.

이는 OCP 위반이다. 

 

참고 : 좋은 객체지향의 5가지 원칙 중 OCP(개방 폐쇄 원칙)은 "소프트웨어 요소는 확장에는 열려 있으나, 변경에는 닫혀있어야 한다. "라는 것이다. 기능을 확장할 때 기존 코드를 변경하는 것이 아니라 인터페이스를 구현한 새로운 클래스를 하나 만들어서 새로운 기능을 구현한다. 

 

💡[해결]

자자 그럼 추상화에 의존하도록 바꿔보자 

추상화에만 의존하도록 바꿔보면 

--- 변경 전 ---

private DiscountPolicy discountPolicy;


--- 변경 후 ---
private DiscountPolicy discountPolicy;

하지만 실행하면 지금 선언만 된 상태이고 아무 것도 할당되어 있지 않기 때문에 NullPointerException 오류가 난다.

(으아아 그러면 어쩌자는거임? 뭔가 구체적인 객체가 있어야하잖아?? 다시 DIP 원칙을 어길 수도 없고 이게 뭐람 → 하지만 ! 다른 해결 방법이 있음!)

 

=> 💡 누군가가 클라이언트인 OrderServiceImpl에 DiscountPolicy의 구현 객체를 대신 생성하고 주입해야 한다. 

그 "누군가가"가 AppConfig 이다. 

 

한편, 위의 문제는 비유적으로 설명하자면 배우가 직접 캐스팅하는 꼴이라고 할 수 있다. → 캐스팅하는 역할을 본인이 한다. 하지만 공연을 올릴 때 배역과 배우만 있는 것이 아니라 공연 기획자 (캐스팅 담당자)도 있어야 한다. 캐스팅과 연기를 따로 분리하자! (배우는 연기만 하도록 공연 기획자를 추가하고 배우와 공연 기획자의 책임을 확실히 분리하자.)

 

어떤 인터페이스에 할당이 될 지에 대한 결정을 하는 부분도 추가하자 ⇒ AppConfig의 등장!

 

AppConfig

: 애플리케이션의 전체 동작 방식을 구성(config) 혹은 설정하기 위해, 선택된 구현 객체를 생성하고 연결하는 책임을 가지는 별도의 설정 클래스를 만들자!

즉, 전체적으로 사용 영역(OrderServiceImpl, DiscountPolicy 등) + 구성 영역(AppCofig)으로 나뉘게 된 것이다.

 

public class AppConfig {

	...

	public OrderService orderService()
	{
		return new OrderServiceImpl(discountPolicy());
	}

	private RateDiscountPolicy discountPolicy() { //따로 함수를 빼면 더 역할이 잘 보인다.
		//return new FixDiscountPolicy(); 이렇게 쉽게 다른 구현체 변경 가능
		return new RateDiscountPolicy();
	}


}
  • AppConfig 는 애플리케이션의 실제 동작에 필요한 구현 객체를 생성한다.
    • MemberServiceImpl
    • OrderServiceImpl
    • FixDiscountPolicy
  • AppConfig는 생성된 객체 인스턴스의 참조(레퍼런스)를 생성자를 통해서 주입(연결) 해준다.
    • OrderServiceImpl → RateDiscountPolicy, FixDiscountPolicy

✔️ DIP 위반 문제 해결 

  • OrderServiceImpl 어떤 구현 객체가 들어올지는 알 수 없다. 의존 관계를 외부에 맡긴 것이다.
  • 그리고 클라이언트인 OrderServiceImpl 입장에서도면 의존관계를 마치 외부( AppConfig)에서 주입해주는 것 같다고 해서 “DI(Dependency Injection), 의존 관계 주입”라고 한다.
  • 즉, 추상에만 의존할 수 있는 환경을 제공해줌 -> DIP 위반 문제 해결 

 

✔️ OCP 위반 문제 해결 

  • 애플리케이션을 사용 영역과 구성 영역으로 나눔
  • AppConfig가 의존 관계를 Fix→ Rate로 변경해서 클라이언트 코드에 주입하므로 클라이언트 코드는 변경하지 않아도 된다.
  • 즉 소프트웨어 요소를 새롭게 확장해도 사용 영역의 변경은 닫혀있게 된 것이다.( = 사용 영역의 수정은 필요없게 되었다)
  • 만약 구현체를 바꿀 일이 있다면 사용 영역의 코드의 변경 없이 AppConfig 영역만 바꾸면 된다.
    • 예를 들어 할인 정책이 Fix → Rate로 바뀐다고 한다면 구성영역인 AppConfig 부분만 변경하면 된다. -> OCP 위반 문제 해결 

 

 

참고로 ,  AppConfig 를 보면 역할과 구현을 나누었기 때문에 역할과 구현 클래스가 한 눈에 들어와 전체 구성을 빠르게 파악할 수 있다. 역할 = OrderService 부분, 구현 = discountPolicy 부분

 

-이 글은 김영한님의 인프런 강의를 듣고 정리한 글입니다.