3. 스프링 핵심 원리 이해2 - 객체지향원리 적용

jinhxxxxkim·2023년 3월 6일
0

Spring핵심

목록 보기
3/9
post-thumbnail

3. 스프링 핵심 원리 이해2 - 객체지향원리 적용

1. 새로운 할인 정책

설계

  • 기존의 정액할인정책 FixDiscountPolicy에서 정률할인정책 RateDiscountPolicy으로 변경하고자 한다.

구현

할인정책(정률) 구현체

  • 정액할인정책과 똑같이 VIP고객에 한한다.
  • 상품가격의 10%에 해당하는 금액을 할인한다.
public class RateDiscountPolicy implements DiscountPolicy {

    private int discountPercent = 10;

    @Override
    public int discount(Member member, int price) {...}
}

새로운 할인 정책 적용과 문제점

적용

  • 할인정책의 변경을 위해서는 클라이언트인 OrderServiceImpl의 코드를 고쳐야한다.
public class OrderServiceImpl implements OrderService {
	// private final DiscountPolicy discountPolicy = new FixDiscountPolicy();
    // 기존 코드
	private final DiscountPolicy discountPolicy = new RateDiscountPolicy();
}

문제점 발견

  • 역할과 구현을 충실하게 분리하였는가?
    • YES!
  • 다형성을 활용하고, 인터페이스와 구현 객체를 분리하였는가?
    • YES!
  • DIP, OCP를 준수하였는가?
    • YES?
    • 준수한 것 같아 보이지만 NO!

문제점

  1. DIP(Depedency inversion principle) 위반
    • "추상화에 의존해야하며, 구체화에 의존하면 안된다"
    • 얼핏 보기에 OrderServiceImpl는 인터페이스 discountPolicy에 의존하며 DIP를 지킨 것 같아보인다.
    • 클래스 의존관계를 분석해 보면, 추상(인터페이스) 뿐만 아니라 구체(구현) 클래스에도 의존하고 있다.
      • 추상(인터페이스) 의존: discountPolicy
      • 구체(구현) 클래스: FixDiscountPolicy, RateDiscountPolicy
  2. OCP(Open/Closed Principle) 위반
    • "소프트웨어 요소는 확장에는 열려 있으나 변경에는 닫혀 있어야 한다"
    • 위의 코드를 보게되면, 기능을 확장하여 변경하기 위해서는 클라이언트 코드의 변경이 필요하다

의존관계 분석

  • 기대한 의존관계
    • 단순히 OrderServiceImpl가 인터페이스 discountPolicy에만 의존한다고 생각했다.

  • 실제 의존관계
    • 클라이언트인 OrderServiceImplDiscountPolicy 인터페이스 뿐만 아니라 FixDiscountPolicy 인 구체 클래스도 함께 의존하고 있다
    • private final DiscountPolicy discountPolicy = new FixDiscountPolicy();

  • 정책 변경 시 OCP 위반 문제 발생
    • 그래서 FixDiscountPolicyRateDiscountPolicy 로 변경하는 순간 OrderServiceImpl 의 소스 코드도 함께 변경해야 한다

해결 방안 1

  • 클라이언트 코드인 OrderServiceImplDiscountPolicy 의 인터페이스 뿐만 아니라 구체 클래스도 함께 의존한다
  • 따라서 추상에만 의존하도록 변경(인터페이스에만 의존)
public class OrderServiceImpl implements OrderService {
 	//private final DiscountPolicy discountPolicy = new RateDiscountPolicy();
 	private DiscountPolicy discountPolicy;
}
  • 하지만, 구현제가 없으므로 당연하게도 NPE(Null Pointer Exception)이 발생한다.

해결 방안 2

  • 누군가가 클라이언트인 OrderServiceImplDiscountPolicy 의 구현 객체를 대신 생성하고 주입해주어야 한다

2. 관심사의 분리

  • 애플리케이션을 하나의 "공연"이라고 생각하자
  • 인터페이스 = 배역
  • 구현체 = 배우
  • 배우는 상대 배우가 누군지는 중요하지 않으며, 상대 '배역'에 의존해야한다.
  • 기존의 코드는 배우(구현체)가 상대 배역의 배우(구현체)를 선정하는 것이다
  • 이는 하나의 구현체가 너무 "다양한 책임"이 부여된 것이다.

따라서 별도의 배우(구현체)를 선정해주는 기획자가 필요하다

AppConfig

  • 애플리케이션의 전체 동작 방식을 구성(config)하기 위해, 구현 객체를 생성하고, 연결하는 책임을 가지는 별도의 설정 클래스다

구현

public class AppConfig {
 	public MemberService memberService() {
 		return new MemberServiceImpl(new MemoryMemberRepository());
 	}
    
 	public OrderService orderService() {
 		return new OrderServiceImpl(new MemoryMemberRepository(), new FixDiscountPolicy());
 	}
}
  • AppConfig는 애플리케이션의 실제 동작에 필요한 구현 객체를 생성한다.
    1. MemberServiceImpl
    2. MemoryMemberRepository
    3. OrderServiceImpl
    4. FixDiscountPolicy
  • AppConfig는 생성한 객체 인스턴스의 참조(레퍼런스)를 생성자를 통해서 주입(연결)해준다
    • MemberServiceImpl \to MemoryMemberRepositor
    • OrderServiceImpl \to MemoryMemberRepositor, FixDiscountPolicy
  • AppConfig를 구현한 후, 기존의 구현체 또한 변경이 필요하다
  • 생성자를 통해 인스턴스를 전달받아야한다.

MemberServiceImpl.java

public class MemberServiceImpl implements MemberService{
    private final MemberRepository memberRepository;
	
    // 생성자 주입
    public MemberServiceImpl(MemberRepository memberRepository) {
        this.memberRepository = memberRepository;
    }
    ...
}
  • 설계 변경으로 MemberServiceImplMemoryMemberRepository 를 의존하지 않는다.
  • 단지 MemberRepository 인터페이스만 의존한다. 즉, MemberRepository 인터페이스를 구현한 구현체 어떤 것이 들어와도 된다.
  • MemberServiceImpl 입장에서 생성자를 통해 어떤 구현 객체가 들어올지(주입될지)는 알 수 없다.
  • MemberServiceImpl 의 생성자를 통해서 어떤 구현 객체를 주입할지는 오직 외부(AppConfig)에서 결정된다.
  • MemberServiceImpl 은 이제부터 의존관계에 대한 고민은 외부에 위임하고 실행(단 하나의 책임)에만 집중하면 된다

클래스 다이어그램

  • 객체의 생성과 연결은 AppConfig의 역할이다.
  • DIP 위반 해결
    \to MemberServiceImplMemberRepository 인 추상에만 의존할 뿐, 구체화에 의존하지 않는다.
  • 관심사의 분리
    \to 객체를 생성하고 연결하는 역할( AppConfig )과 실행하는 역할( MemberServiceImpl )이 명확히 분리되었다

인스턴스 다이어그램

  • appConfig 객체는 memoryMemberRepository 객체를 생성하고 그 참조값을 memberServiceImpl 을 생성하면서 생성자로 전달한다.
  • 클라이언트인 memberServiceImpl 입장에서 보면 의존관계를 마치 외부에서 주입해주는 것이므로 DI(Dependency Injection) 의존관계 주입이라 한다.

OrderServiceImpl.java

public class OrderServiceImpl implements OrderService{

    private final MemberRepository memberRepository;
    private final DiscountPolicy discountPolicy;

    public OrderServiceImpl(MemberRepository memberRepository, DiscountPolicy discountPolicy) {
        this.memberRepository = memberRepository;
        this.discountPolicy = discountPolicy;
    }
    ...
}
  • 설계 변경으로 OrderServiceImplMemoryMemberRepositoryFixDiscountPolicy를 의존하지 않는다.
  • 단지 MemberRepositoryDiscountPolicy 인터페이스만 의존한다. 즉, MemberRepository , DiscountPolicy 인터페이스를 구현한 구현체 어떤 것이 들어와도 된다.
  • OrderServiceImpl 입장에서 생성자를 통해 어떤 구현 객체가 들어올지(주입될지)는 알 수 없다.
  • OrderServiceImpl 의 생성자를 통해서 어떤 구현 객체를 주입할지는 오직 외부(AppConfig)에서 결정된다.
  • OrderServiceImpl 은 이제부터 의존관계에 대한 고민은 외부에 위임하고 실행(단 하나의 책임)에만 집중하면 된다.
  • OrderServiceImpl 에는 MemoryMemberRepository , FixDiscountPolicy 객체의 의존관계가 주입된다

중간 정리

  • AppConfig를 통해서 관심사를 확실하게 분리했다
  • AppConfig = 공연 기획자
  • 인터페이스(MemberRepository, DiscountPolicy etc.) = 배역
  • 구현체(MemoryMemberRepository, FixDiscountPolicy etc.) = 배우
  • AppConfig이 전체 애플리케이션의 구성을 담당한다.
  • 각 구현체(배우)는 자신의 기능의 실행에만 집중하면 된다.

AppConfig 리펙토링

  • 현재 AppConfig는 중복이 있고, 역할에 따른 구현이 잘 안보인다.

리펙토링 전

public class AppConfig {
 	public MemberService memberService() {
 		return new MemberServiceImpl(new MemoryMemberRepository());
 	}
    
 	public OrderService orderService() {
 		return new OrderServiceImpl(new MemoryMemberRepository(), new FixDiscountPolicy());
 	}
}

리펙토링 후

public class AppConfig {
    
    public MemberService memberService(){
        return new MemberServiceImpl(memberRepository());
    }

    public MemberRepository memberRepository() {
        return new MemoryMemberRepository();
    }

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

    public DiscountPolicy discountPolicy() {
        return new RateDiscountPolicy();
    }
    // 리펙토링 => 역할과 구현클래스가 한눈에 보임
    // 어플리케이션의 전체의 구성파악에 용이하다
}
  • new MemoryMemberRepository() 중복을 제거되었다.
  • 한눈에 어떤 역할이 존재하며, 각 역할에 대한 구현 클래스가 보인다.
  • MemberService역할에 대한 구현은 MemberServiceImpl
  • MemberRepository역할에 대한 구현은 MemoryMemberRepository
  • OrderService역할에 대한 구현은 OrderServiceImpl
  • DiscountPolicy역할에 대한 구현은 RateDiscountPolicy

3. 사용, 구성의 분리

  • 할인 정책으로 FixDiscountPolicy를 선택할 경우
  • 할인 정책으로 RateDiscountPolicy로 변경할 경우
  • FixDiscountPolicy RateDiscountPolicy 로 변경해도 구성 영역만 영향을 받고, 사용 영역은 전혀 영향을 받지 않는다

구현

public class AppConfig {
    ...
    private DiscountPolicy discountPolicy() {
        // return new FixDiscountPolicy();
        return new RateDiscountPolicy();
    }
    ...
}
  • AppConfig 에서 할인 정책 역할을 담당하는 구현을 FixDiscountPolicy RateDiscountPolicy 객체로 변경했다.
  • 할인 정책을 변경해도, 애플리케이션의 "구성" 역할을 담당하는 AppConfig만 변경하면 된다.
  • 클라이언트 코드인 OrderServiceImpl 를 포함해서 사용 영역의 어떤 코드도 변경할 필요가 없다.
  • 구성 영역은 당연히 변경된다. 구성 역할을 담당하는 AppConfig를 애플리케이션이라는 공연의 기획자로 생각하면, 공연 기획자는 공연 참여자인 구현 객체들을 모두 알아야 한다

4. SOLID 적용

단일 책임 원칙 SRP(Single responsibility principle)

"한 클래스는 하나의 책임만 가져야 한다"

  • SRP 단일 책임 원칙을 따르면서 관심사를 분리함
  • AppConfig = 구현 객체를 생성하고 연결하는 책임 담당
  • 클라이언트 객체 = 실행하는 책임 담당

개방-폐쇄 원칙 OCP(Open/Closed principle)

“소프트웨어 요소는 확장에는 열려 있으나 변경에는 닫혀 있어야 한다.”

  • 애플리케이션을 사용 영역과 구성 영역으로 나눔
  • AppConfig가 의존관계를 변경하여도 클라이언트 코드에 주입하므로 클라이언트 코드는 변경하지 않아도 됨
  • 소프트웨어 요소를 새롭게 확장해도 사용 영역의 변경은 닫혀 있다

의존관계 역전 원칙 DIP(Depedency inversion principle)

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

  • 클라이언트 코드가 추상화 인터페이스에만 의존하도록 코드를 변경했다
  • AppConfig가 객체 인스턴스를 클라이언트 코드 대신 생성해서 클라이언트 코드에 의존관계를 주입했다

5. IoC, DI, 컨테이너

제어의 역전 IoC(Inversion of Control)

  • AppConfig의 등장으로 구현 객체는 자신의 로직을 실행하는 역할만 할 뿐, 프로그램의 제어 흐름은 AppConfig가 가져간다.
  • 구현객체들은 필요 인터페이스들을 호출하지만, 어떤 구현 객체들이 실행 될지 알 수 없다.
  • 프로그램의 제어흐름을 직접 제어하는 것이 아니라 외부에서 관리하는 것을 제어의 역전(IoC)이라 한다.

프레임워크 vs 라이브러리

  • 프레임워크: 내가 작성한 코드를 제어하고, 대신 실행한다 (JUnit)
  • 라이브러리: 내가 작성한 코드가 직접 제어의 흐름을 제어한다.

의존관계 주입 DI(Dependency Injection)

  • OrderServiceImplDiscountPolicy인터페이스에 의존한다.
  • 실제 어떤 DiscountPolicy 구현객체가 올지 모른다.
  • 의존관계는 정적 의존관계, 동적 의존관계가 있다
  • 정적 의존관계
    • 애플리케이션을 실행하지 않고도 import 코드만 보고 의존관계를 파악할 수 있다.
    • OrderServiceImpl은 어떤 인터페이스에 의존하는지는 알 수 있으나, 어떤 구현객체가 주입될지는 알 수 없다
  • 동적 의존관계
    • 애플리케이션 실행시점에 실제 생성된 객체 인스턴스의 참조가 연결된 의존관계다.
    • OrderServiceImpl이 어떤 구현객체를 의존하는지 알 수 있다.
    • 애플리케이션의 실행시점(런타임)에 외부에서 실제 수현객체를 생성하고 클라이언트에 전달하여 클라이언트와 서버의 실제 의존관계가 연결되는 것을 의존관계 주입(DI)이라 한다
    • 객체 인스턴스를 생성하고, 참조값의 전달을 통해 연결된다
    • DI를 사용하면, 클라이언트 코드를 변경하지 않고, 클라이언트가 호출하는 대상의 타입 인스턴스를 변경할 수 있다.
    • DI를 사용하면, 정적인 클래스 의존관계를 변경하지 않고, 동적인 의존관계를 쉽게 변경할 수 있다.

IoC 컨테이너, DI 컨테이너

  • AppConfig와 같이 객체를 생성하고 관리하며 의존관계를 연결해 주는 것을 IoC 컨테이너, DI 컨테이너라 한다.
  • 또는 어셈블러, 오브젝트 펙토리 등으로 불린다.

6. 스프링으로의 전환

코드

AppConfig.java

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

MemberApp.java

public class MemberApp {
    public static void main(String[] args) {
//        AppConfig appConfig = new AppConfig();
//        MemberService memberService = appConfig.memberService();

        ApplicationContext applicationContext = new AnnotationConfigApplicationContext(AppConfig.class);
        MemberService memberService = applicationContext.getBean("memberService", MemberService.class);
		...
	}
}

OrderApp.java

public class OrderApp {
    public static void main(String[] args) {
//        AppConfig appConfig = new AppConfig();
//        MemberService memberService = appConfig.memberService();
//        OrderService orderService = appConfig.orderService();

        ApplicationContext applicationContext = new AnnotationConfigApplicationContext(AppConfig.class);
        MemberService memberService = applicationContext.getBean("memberService", MemberService.class);
        OrderService orderService = applicationContext.getBean("orderService", OrderService.class);
		...
	}
}

설명

ApplicationContext applicationContext = new AnnotationConfigApplicationContext(AppConfig.class);
  • ApplicationContext를 스프링 컨테이너라 한다.
  • 스프링 컨테이너를 통해 객체를 생성하며, 의존성을 주입한다.
21:39:43.568 [main] DEBUG org.springframework.beans.factory.support.DefaultListableBeanFactory - Creating shared instance of singleton bean 'appConfig'
21:39:43.578 [main] DEBUG org.springframework.beans.factory.support.DefaultListableBeanFactory - Creating shared instance of singleton bean 'memberService'
21:39:43.599 [main] DEBUG org.springframework.beans.factory.support.DefaultListableBeanFactory - Creating shared instance of singleton bean 'memberRepository'
21:39:43.602 [main] DEBUG org.springframework.beans.factory.support.DefaultListableBeanFactory - Creating shared instance of singleton bean 'orderService'
21:39:43.605 [main] DEBUG org.springframework.beans.factory.support.DefaultListableBeanFactory - Creating shared instance of singleton bean 'discountPolicy'
  • 스프링 컨테이너는 @Configuration 이 붙은 AppConfig 를 설정(구성) 정보로 사용한다.
  • @Bean이라 적힌 메서드를 모두 호출해서 반환된 객체를 스프링 컨테이너에 등록한다 (로그).
  • 스프링 컨테이너에 등록된 객체를 스프링 빈이라 한다
  • 스프링 빈은 @Bean 이 붙은 메서드의 명을 스프링 빈의 이름으로 사용한다.
  • key는 메서드 명, value는 객체 인스턴스로 컨테이너에 저장한다.
MemberService memberService = applicationContext.getBean("memberService", MemberService.class);
OrderService orderService = applicationContext.getBean("orderService", OrderService.class);
  • AppConfig를 통해 필요한 객체를 직접 조회하는 것이 아닌, 스프링 컨테이너를 통해 객체(스프링 빈)을 조회해야 한다.

출처: 인프런 스프링 핵심 원리 - 기본편 (김영한)
인프런 스프링 핵심 원리

0개의 댓글