본문 바로가기
Spring/스프링 핵심 원리 - 기본편

3-1. 스프링 핵심 원리 이해2 - 객체 지향 원리 이용(AppConfig)

by hk27 2022. 1. 2.
OCP, DIP를 지키려면 클라이언트가 구체 클래스에 의존하지 않아야 하는데,
이것이 어떻게 가능할까요?

 

안녕하세요.

오늘은 '섹션 3. 스프링 핵심 원리 이해 2 - 객체 지향 원리 이용' 중 앞부분을 리뷰하겠습니다. 

저번 시간에 회원, 주문, 할인 서비스를 자바 코드로 만들어보았습니다.

인터페이스와 구현체를 나눠서 잘 설계된 것으로 보였는데요. 

여전히 좋은 객체 지향 설계 원칙 중 SRP, OCP, DIP를 지키지 못하는 문제가 있어서 AppConfig 를 활용해 해결하는 것이 오늘 강의의 핵심 내용입니다. 

 

저번 시간에 완성한 도메인 다이어그램은 아래와 같습니다.

 

 

할인 정책 변경

할인 정책에 초점을 두고 생각해봅시다. 

만약 할인 정책이 정액 할인 정책(정해진 금액만큼 할인)에서 정률 할인 정책(정해진 비율만큼 할인)으로 변경된다면, 할인 서비스의 클라이언트인 OrderServiceImpl 코드를 변경해야 합니다.

public class OrderServiceImpl implements OrderService {
	// private final DiscountPolicy discountPolicy = new FixDiscountPolicy();
	private final DiscountPolicy discountPolicy = new RateDiscountPolicy();
}

 

DIP, OCP

이 방식은 DIP와 OCP를 지키지 못한다는 문제가 있습니다. 

https://passionate.tistory.com/4 <- 에서 좋은 객체 지향 설계의 5가지 원칙을 학습하였습니다.

그중 DIP는 의존관계 역전 원칙 (Dependency Inversion Principle) 으로, 구현 클래스에 의존하지 말고, 인터페이스에 의존하라는 것이며, OCP는 개방-폐쇄 원칙 (Open/Closed Principle)으로, 기능을 변경 또는 확장할 수 있으면서 그 기능을 사용하는 코드는 수정하지 않아야 한다는 것입니다. 

OrderServiceImple이 DiscountPolicy라는 인터페이스에 의존하는 것처럼 보이나, 사실은 Fix/RateDiscountPolicy라는 구현 클래스에도 의존하고 있어서 DIP를 어기고 있으며, 따라서 기능을 변경할 때 서비스코드도 변경해야 하므로 OCP를 지키지 못하고 있습니다. 아래 사진은 실제 코드의 의존 관계입니다. 인터페이스뿐만 아니라 구현체에도 의존하고 있습니다. 

 

어떻게 하면 인터페이스에만 의존할 수 있을까요?

public class OrderServiceImpl implements OrderService {
	//private final DiscountPolicy discountPolicy = new RateDiscountPolicy();
	private DiscountPolicy discountPolicy;
}

이렇게 하면 구현체가 없기 때문에 null point exception 에러가 발생합니다.

이 문제를 해결하려면 누군가가 클라이언트인 OrderServiceImpl에 DiscountPolicy의 구현 객체를 대신 생성하고 주입해주어야 합니다.

 

SRP

그리고 DIP, OCP를 지키지 못한다는 것 외에도 하나의 문제가 더 있는데요.

바로 SRP, 즉 단일 책임 원칙 (Single Responsibility Principle)도 지키지 못하고 있다는 것입니다. 서비스 구현체가 어떤 할인 구현체를 사용할지도 정하고 있습니다. 서비스 구현체는 서비스하는 하나의 책임만을 가져야 하고, 구현체를 정하는 책임을 갖는 것은 SRP를 위반하는 것입니다. 

 

AppConfig

DIP, OCP, SRP를 한 번에 해결하기 위해서는 구현 객체를 생성하고, 연결하는 책임을 갖는 클래스가 필요합니다. 

그것이 바로 AppConfig 입니다. AppConfig는 Application Configuration에서 나온 단어로, 애플리케이션을 설정하고, 구성하는 클래스입니다. 

public class AppConfig {
	public MemberService memberService() {
		return new MemberServiceImpl(memberRepository());
	}
	public OrderService orderService() {
		return new OrderServiceImpl(
			memberRepository(),
			discountPolicy());
	}
	public MemberRepository memberRepository() {
		return new MemoryMemberRepository();
	}
	public DiscountPolicy discountPolicy() {
		// return new FixDiscountPolicy();
		return new RateDiscountPolicy();
	}
}

AppConfig는 애플리케이션의 실제 동작에 필요한 구현 객체를 생성합니다. MemberServiceImpl, OrderServiceImpl, MemoryMemberRepository, RateDiscountPolicy를 여기서 만듭니다!

그리고 객체 인스턴스의 참조(레퍼런스)를 생성자를 통해 주입(연결)해줍니다. MemberServiceImple에는 MemoryMemberRepository를, OrderServiceImple에는 MemoryMemberRepository와 RateDiscountPolicy를 연결해주었습니다.

이제 할인정책이 정액 할인에서 정률 할인으로 바뀌어도 AppConfig 클래스만 고쳐주면 됩니다. 

 

아직 각 클래스에 생성자가 없기 때문에, 아래와 같이 생성자를 만들어야 합니다.

public class MemberServiceImpl implements MemberService {
	// private final MemberRepository memberRepository = new MemoryMemberRepository();
	private final MemberRepository memberRepository;
    
	public MemberServiceImpl(MemberRepository memberRepository) {
		this.memberRepository = memberRepository;
	}
}

주석으로 처리한 부분이 이전 코드입니다. 이렇게 constructor를 만들어주면, MemberServiceImpl 클래스가 MemoryMemberRepository를 의존하지 않습니다! 의존관계를 주입해주는 것은 AppConfig가 처리합니다. 이 클래스는 단지 MemberRepository 인터페이스에만 의존하고 있습니다!  

 

AppConfig를 어떻게 사용하는지 예시를 봅시다.

public class MemberApp {
	public static void main(String[] args) {
    		// MemberService memberService = new MemberServiceImpl();
		AppConfig appConfig = new AppConfig();
		MemberService memberService = appConfig.memberService();
		Member member = new Member(1L, "memberA", Grade.VIP);
		memberService.join(member);
		System.out.println("new member = " + member.getName());
	}
}

5번 줄에서 appConfig.memberService()로 객체를 만들어주고 있습니다. 원래는 new MemberServiceImple()로 불렀는데 말이죠! 이제 더이상 구체 클래스에 의존하지 않습니다. 

 

DI(Dependency Injection)

AppConfig의 역할을 도식화하면 아래와 같습니다.

appConfig 객체는 memoryMemberRepository 객체를 생성하고 그 참조 값을 memberServiceImple을 생성하면서 생성자로 전달합니다. memberServiceImpl 입장에서 보면 의존관계를 마치 외부에서 주입해주는 것 같다고 해서 이를 DI(Dependency Injection), 우리말로 '의존관계 주입'이라고 부릅니다.

 

기존의 방식은 좋은 객체 지향 설계의 5가지 원칙 중 SRP, OCP, DIP를 어기는 문제가 있어서 이를 해결하기 위해 AppConfig 클래스를 도입하였습니다. 문제가 해결되었는지 확인해볼까요? 
SRP는 한 클래스가 직접 구현 객체를 선택하는 일까지 하지 않고, 각자의 기능만 수행해도 되므로 지켜지고 있습니다. 
OCP는 다른 기능을 확장해도 클라이언트 코드를 바꾸지 않으므로 지켜지고 있고, 
DIP는 추상화(인터페이스)에 의존하고, 구체화(구현체)에 의존하지 않으므로 지켜지고 있습니다. 

 

드디어 문제를 해결하였는데요!

그런데 생각해보면 이것은 순수 자바 코드입니다. 아직 Spring을 활용하지 않았죠.

다음 시간에는 Spring을 활용해 DI를 적용해보겠습니다! 

 

 

인프런  '스프링 핵심 원리 - 기본편' 강의를 듣고 공부하며 정리한 자료입니다. 

잘못된 부분은 피드백 주시면 감사하겠습니다. 

글 읽어주셔서 감사합니다 :-)

 

참고자료
[1] 스프링 핵심 원리 - 기본편, 섹션 3. 스프링 핵심 원리 이해2 - 객체 지향 원리 적용

https://www.inflearn.com/course/스프링-핵심-원리-기본편

 

 

 

댓글