Back-end/스프링 핵심원리 - 기본편

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

조 수빈 2023. 12. 21. 23:03

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

☁️ 새로운 할인 정책 그리고 문제점

새로운 정률 할인 정책 추가 (FixDiscountPolicy -> RateDiscountPolicy)
DiscountPolicy 인터페이스를 상속 받아 RateDiscountPolicy 구현

...

할인 정책을 변경하려면 클라이언트OrderServiceImpl 로직을 수정해야 함

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

Why? 클라이언트(OrderSeriveImpl)에서 추상(인터페이스) 뿐만 아니라 구체(구현) 클래스에도 의존하고 있음!!

  • 추상(인터페이스): DiscountPolicy
  • 구체(구현) 클래스: FixDiscountPolicy, RateDiscountPolicy

구체 클래스를 변경할 때 클라이언트 코드도 함께 변경해야 하는 문제점..

= DIP 위반, OCP 위반

  • 구체 클래스에도 의존하고 있음
  • 변경하지 않고 확장 불가 (클라이언트 코드에 영향)

📍 관심사의 분리

위의 상황을 공연에 비유하면, 각각의 인터페이스는 배역임
실제 배역에 맞는 배우를 선택하는 것은 배우의 몫? X
배우는 본인의 역할인 배역을 수행하는 것에만 집중해야 함
공연 기획자 필요

해결 방법: DIP를 위반하지 않도록, 인터페이스에만 의존하도록 의존관계를 변경하면 됨

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

인터페이스에만 의존하도록 설계와 코드 변경 => 구현체는?

결국 누군가는 클라이언트인 OrderServiceImpl에 DiscountPolicy의 구현 객체를 대신 생성하고 주입해줘야 함


AppConfig 등장

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

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

  public OrderService orderService() {
    return new OrderServiceImpl(new MemoryMemberRepository(), new FixDiscountPolicy());
  }
}
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;
  }
  ...
}

실제 동작에 필요한 **구현 객체를 생성** - MemberServiceImpl - MemoryMemberRepository - OrderServiceImpl - FixDiscountPolicy

생성자를 통해 주입(연결)

  • MemberServiceImpl MemoryMemberRepository
  • OrderServiceImpl MemoryMemberRepository , FixDiscountPolicy

설계 변경으로 OrderServiceImpl은 FixDiscountPolicy를 의존하지 않음
DiscountPolicy 인터페이스만 의존
생성자를 통해 어떤 구현 객체가 주입될지는 알 수 없으며, 오직 외부(AppConfig)에서 결정됨
=> 의존관계에 대한 고민은 외부에 맡기고, 실행에만 집중하면 됨 = DIP 완성, 관심사의 분리


AppConfig 실행 및 리팩토링

AppConfig appConfig = new AppConfig();
MemberService memberService = appConfig.memberService();
OrderService orderService = appConfig.orderService();

기존 AppConfig
public class AppConfig {

  public MemberService memberService() {
    return new MemberServiceImpl(new MemoryMemberRepository());
  }

  public OrderService orderService() {
    return new OrderServiceImpl(new MemoryMemberRepository(), new FixDiscountPolicy());
  }
}

리팩터링 후 AppConfig
중복 제거, 역할에 따른 구현이 보이도록.

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();
  }
}

📍 좋은 객체 지향 설계의 5가지 원칙

SRP(단일 책임 원칙): 한 클래스는 하나의 책임만 가져야 함
DIP(의존관계 역전 원칙): 추상화에 의존해야 함, 구체화에 의존하면 안됨 // 의존성 주입은 이 원칙을 따르는 방법 중 하나
OCP(개방 폐쇄 원칙): 소프트웨어 요소는 확장에는 열려 있으나 사용 영역의 변경에는 닫혀 있어야 함


📍 IoC, DI, 그리고 컨테이너

제어의 역전 IoC(Inversion of Control)

AppConfig가 등장한 이후에 구현 객체는 자신의 로직을 실행하는 역할만 담당함
프로그램의 제어 흐름은 AppConfig가 가져감

프로그램의 제어 흐름을 직접 제어하는 것이 아닌 외부에서 관리하는 것을 제어의 역전(IoC) 라고 함

의존관계 주입 DI(Dependency Injection)

OrderServiceImpl은 DiscountPolicy 인터페이스에 의존함
실제 DiscountPolicy의 구현 객체는 어떤 것이 사용될 것인지 모름

의존관계는 정적인 클래스 의존관계, 실행 시점에 결정되는 동적인 객체(인스턴스) 의존관계로 나눌 수 있음

정적인 클래스 의존관계는 클래스가 사용하는 import 코드만 보고도 의존관계를 판단할 수 있음 애플리케이션 실행과 상관없이 분석이 가능하며 클래스 다이어그램으로 판단 가능

동적인 객체 인스턴스 의존 관계는 애플리케이션 실행 시점에 실제 생성된 객체 인스턴스의 참조가 연결된 의존 관계

IoC 컨테이너, DI 컨테이너

AppConfig 처럼 객체를 생성하고 관리하면서 의존관계를 연결해 주는 것 = IoC 컨테이너 or DI 컨테이너
의존관계 주입에 초점을 맞추어 최근에는 주로 DI 컨테이너라 함
또는 어샘블러, 오브젝트 팩토리 등으로 불리기도 함


📍 스프링으로 전환하기

AppConfig에 설정을 구성한다는 뜻의 @Configuration 붙여줌
각 메서드에 @Bean 붙여줌 => 스프링 컨테이너에 스프링 빈으로 등록

// 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를 구현한 AnnotationConfigApplicationContext 구현체를 사용하면 스프링 컨테이너에 AppConfig를 등록해 사용할 수 있음

스프링 컨테이너는 @Configuration 이 붙은 AppConfig 를 설정(구성) 정보로 사용함
@Bean이라 적힌 메서드를 모두 호출해서 반환된 객체를 스프링 컨테이너에 등록함
=> 스프링 컨테이너에 등록된 객체를 스프링 빈이라 함

빈 이름은 @Bean이 붙은 메서드의 명, @Bean(name="...")으로 네임 지정 가능

applicationContext.getBean()을 사용하면 내부에 등록된 빈 객체를 가져올 수 있음


스프링 핵심 원리 - 기본편