빙응의 공부 블로그

[Spring]스프링 핵심 원리2 - 객체 지향 원리 적용 본문

Spring/인프런_개념

[Spring]스프링 핵심 원리2 - 객체 지향 원리 적용

빙응이 2023. 12. 8. 17:09

📝인터페이스 호출 방식의 문제점 

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

위 코드처럼 우리는 인터페이스를 호출한다. 빈 인터페이스에 구현체를 넣어주는 것이다. 

하지만 이 방법을 사용 시 구현체가 자주 바뀌는 상황에서 ServiceImpl 본문을 수정해야 한다.

우리는 인터페이스 방법을 사용하면서 SOLID를 지켰다.

  • 역할과 구현을 충실하게 분리했다.
  • 다형성도 활용하고, 인터페이스와 구현 객체를 분리했다.

그러나 아직 큰 문제점이 있다.

DIP 위반

 

해당 코드의 초기 설계는 이렇게 되어있었다.

 

하지만 실제 의존 관계는 이렇게 되어있다.

잘보면 클라이언트인 Service가 인터페이스 뿐만 아니라 구현 클레스도 함께 의존한다. 

DIP : 구현체가 아닌 추상체에 의존해야한다. - 위반

 

 

OCP 위반

개발자는 요구사항에 따라 만들지만 언제든지 요구사항이 변경이 될 수 있다.

위 코드에서는 구현 기능을 바꾸는 순간에서도 문제가 발생한다.

 

구현 기능을 변경하는 순간 Service에서 소스 코드도 함께 변경해야 한다. 

OCP : 변화에는 닫혀있고, 확장에는 열려있어야 한다. - 위반

 

 

📝해결 방법 - 팩토리 패턴과 IoC

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

우리는 이 코드의 문점을 알아보았다. 그러면 어떻게 문제를 해결할 수 있을까?

제일 근본적인 것은 인터페이스에만 의존하도록 설계를 하는 것이다.

 

public class OrderServiceImpl implements OrderService {
 private DiscountPolicy discountPolicy;
}
  • 인터페이스에만 의존하도록 설계와 코드를 변경했다.
  • 그러나 이 코드는 구현체가 없어 NPE 오류가 발생한다. 
  • 이 문제를 해결하기 위해 관심사의 분리를 활용한다. 

 

관심사의 분리 - 팩토리 패턴

관심사의 분리란? 구현체는 구현만 연결은 연결만 책임지는 별도의 설정 클래스를 만드는 것이다.

구현 클래스를 변경 시 연결을 책임지는 클래스만 바꾸면 되게 하는 것이다. 

 

팩토리 패턴이란 객체 생성을 캡슐화하고 객체를 생성하는 책임을 특정 클래스나 메서드에 위임하는 것이다.

 

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 DiscountPoilcy discountPoilcy;

  public OrderServiceImpl(MemberRepository memberRepository, DiscountPoilcy discountPoilcy) {
    this.memberRepository = memberRepository;
    this.discountPoilcy = discountPoilcy;
  }
}

위 코드처럼 OrderServiceImpl은 인터페이스에만 의존하고 AppConfig라는 팩토리가 대신 의존성을 주입(DI)하고 있다.

팩토리의 등장으로 구현클래스는 구현만 생성클래스는 생성만 하게 분리되었다. 

 

 

📝좋은 객체 지향 설계의 3가지 원칙 적용

SRP 단일 책임 원칙

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

  • 클라이언트 객체는 직접 구현 객체를 생성하고, 연결하고, 실행하는 다양한 책임을 가지고 있음
  • 구현 객체를 생성하고 연결하는 책임은 AppConfig(팩토리)가 담당한다.
  • 클라이언트 객체는 실행하는 책임만 담당하게 된다. 
DIP 의존관계 역전 원칙

프로그래머는 "추상화에 의존해야지, 구체화에 의존하면 안된다." 의존성 주입은 이 원칙을 따르는 방법 중 하나다.

  • 요구사항이 바뀌었을 때 클라이언트의 소스 코드도 바꿔야 했다.
  • 클라이언트 코드를 인터페이스만 의존하도록 수정
  • 그러나 인터페이스는 구현이 안되어있어 에러가 발생한다. 
  • AppConfig(팩토리)를 이용해 의존성 주입(DI)를 해서 DIP 원칙을 지켰다.
OCP

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

  • 다형성 사용하여 클라이언트의 DIP를 지켰다.
  • AppConfig(팩토리)를 이용하여 의존 관계를 주입하므로 클라이언트 코드를 변경하지 않고 확장에 용이해졌다.

 

📝제어의 역전 IoC(Inversion of Control)

  • AppConfig 적용 전
    • 기존 프로그램이 클라이언트 구현 객체가 스스로 필요한 서버 구현 객체를 생성하고, 연결하고, 실행했다.
    • 구현 객체가 프로그램의 제어 흐름을 스스로 조종했다. 
  • AppConfig 적용 후
    • 반면에 AppConfig(팩토리) 등장 이후 구현 객체는 자신의 로직만 수행하는 역할을 담당하게 되었다. 
    • 프로그램의 제어 흐름을 AppConfig(팩토리)가 가져갔다.
    • 프로그램에 대한 제어 흐름 권한을 모두 AppConfig(팩토리)가 가져갔다. 

이렇듯 프로그램이 직접 제어 흐름을 제어하는 것이 아니라 외부에서 관리하는 것을 제어의 역전(IoC)라고 한다.

 

 

📝의존 관계 주입 DI

의존관계는 정적인 클래스 의존관계와, 실행 시점에 결정되는 동적인 객체(인스턴스) 의존 관계 둘을 분리해서 생각해야 한다.

 

정적인 클래스 의존관계
  • 클래스가 사용하는 import 코드만 보고 의존관계를 쉽게 판단할 수 있다. 
  • 애플리케이션을 실행하지 않아도 분석할 수 있다.
public class OrderServiceImpl implements OrderService{
  private final MemberRepository memberRepository;
  private final DiscountPolicy discountPoilcy;
}

해당 코드에서 정적인 것은 implements로 OrderService를 받고 있고 MemberRepository, DiscountPolicy와 의존관계를 맺고 있다.

동적인 클래스 의존 관계

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

  • 애플리케이션 실행 시점(런타임)에 외부에서 실제 구현 객체를 생성하고 클라이언트에 전달해서 클라이언트와 서버의 실제 의존관계가 연결 되는 것을 의존관계 주입이라고 한다.
  • 객체 인스턴스를 생성하고, 그 참조값을 전달해서 연결된다.
  • 의존관계 주입을 사용하면 정적인 클래스 의존관계를 변경하지 않고, 동적인 객체 인스턴스 의존관계를 쉽게 변경할 수 있다.

📝IoC 컨테이너, DI 컨테이너

스프링에는 AppConfig처럼 객체를 생성하고 관리하는 것이 존재한다. 이것이 DI 컨테이너이다.

DI 컨테이너는 Bean이라는 객체를 자동으로 생성, 관리, 주입하는데 중점을 둔다. 이러한 빈들 간의 의존성은 스프링이 자동으로 관리하며 주입해준다.

  • AppConfig처럼 객체를 생성하고 관리하면서 의존관계를 연결해 주는 것을 IoC컨테이너, DI 컨테이너라고 한다.
  • 의존관계 주입에 초점을 맞추어 최근에는 주로 DI 컨테이너라고 한다. 

 

스프링 컨테이너

ApplicationContext 를 스프링 컨테이너라 한다.

  • 기존 개발자가 AppConfig를 사용해서 직접 객체를 생성하고 DI를 했지만, 이제부터는 스프링 컨테이너를 통해서 사용한다. 
  • 스프링 컨테이너는 @Configuration이 붙은 AppConfig를 설정 정보로 사용한다. 여기서 @Bean 이라 적힌 메소드를 모두 호출해서 반환된 객체를 스프링 컨테이너에 등록한다.
  • 이전에 개발자는 필요한 객체를 직접 조회했지만, 이제부터는 스프링 컨테이너를 통해서 스프링 빈을 찾아야 한다.
@Configuration
public class AppConfig {
  
  @Bean
  public MemberService memberService(){
    return new MemberServiceImpl(memberRepository());
  }
  @Bean
  public OrderService orderService(){
    return  new OrderServiceImpl(memberRepository(),discountPolicy());
  }
  /**
   *MemberRepository 구현
   *
   */
  @Bean
  public MemberRepository memberRepository(){
    return new MemoryMemberRepository();
  }
  /**
   *DiscountPolicy 구현
   *
   */
  @Bean
  public DiscountPolicy discountPolicy(){
    return new RateDiscountPolicy();
  }
}

 

 


✔해당 포스트는 김영한님의 스프링 로드맵 강의 공부 입니다.

우아한형제들 최연소 기술이사 출신 김영한의 스프링 완전 정복 - 인프런 | 로드맵 (inflearn.com)