[스프링 핵심 원리 - 기본편] 3. 스프링 핵심 원리 이해2 - 객체 지향 원리 적용

HJ·2022년 7월 29일
0

김영한 님의 스프링 핵심 원리 - 기본편 강의를 보고 작성한 내용입니다.
https://www.inflearn.com/course/%EC%8A%A4%ED%94%84%EB%A7%81-%ED%95%B5%EC%8B%AC-%EC%9B%90%EB%A6%AC-%EA%B8%B0%EB%B3%B8%ED%8E%B8/dashboard


1. 할인 정책 변경 시, 문제 상황


< 회원 서비스 구현체 >

public class OrderServiceImpl implements OrderService {

    //private final DiscountPolicy discountPolicy = new FixDiscountPolicy();

    private final DiscountPolicy discountPolicy = new RateDiscountPolicy();
}
  • 할인 정책 변경 시, DIP와 OCP를 지키지 못하는 문제 발생

  • DIP 위반 : 클라이언트인 OrderServiceImplDiscountPolicy 인터페이스에 의존하지만 동시에 구현 클래스인 FixDiscountPolicy, RateDiscountPolicy에도 의존하고 있음

  • OCP 위반 : FixDiscountPolicy에서 RateDiscountPolicy로 변경하려면 OrderServiceImpl의 코드를 수정해야함




2. 해결 방안


< 회원 서비스 구현체 >

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

        private DiscountPolicy discountPolicy;
}
  • DIP 위반 해결 : 클라이언트가 인터페이스에만 의존하도록 변경

  • 그러나 구현 객체가 없기 때문에 NullPointerException 발생

  • NullPointerException을 해결하려면 클라이언트인 OrderServiceImplDiscountPolicy 구현 객체를 대신 생성하고 주입해주는 누군가가 필요




3. AppConfig

3-1. AppConfig 생성


< AppConfig >

public class AppConfig {

    public MemberService memberService() {
        return new MemberServiceImpl(new MemoryMemberRepository());
    }
}
  • 어플리케이션의 전체 동작 방식을 구성(config)하기 위해, 구현 객체를 생성하고, 연결하는 책임을 가지는 별도의 설정 클래스를 생성

  • 기존에 MemberServiceImpl에서 MemoryMemberRepository를 생성하던 것을 AppConfig에서 진행

  • 누군가가 MemberService를 불러서 쓴다면 AppConfigMemberServiceImpl을 생성하는데 이 때 MemoryMemberRepository가 같이 생성되어 연결된다


3-2. 회원 서비스 구현체 코드 변경


< 회원 서비스 구현체 >

public class MemberServiceImpl implements MemberService{

    private final MemberRepository memberRepository;

    public MemberServiceImpl(MemberRepository memberRepository) {
        this.memberRepository = memberRepository;
    }
}
  • AppConfig를 통해 MemoryMemberRepository를 주입 받기 위해 생성자 추가

  • 생성자를 통해 MemoryMemberRepository가 들어오면 위에서 선언한 memberRepository에 할당된다

  • MemberServiceImpl 입장에서 생성자를 통해 어떤 구현 객체가 들어올지(주입될지)는 알 수 없다

  • MemberServiceImpl 의 생성자를 통해서 어떤 구현 객체를 주입할지는 오직 외부( AppConfig )에서 결정된다

  ➡️ 추상화에만 의존하기 때문에 DIP 원칙을 지키게 된다


3-3. 정리

  • AppConfig를 통해 MemberService를 불러서 사용

  • MemberService의 구현체 (MemberServiceImpl) 가 생성되는 동시에 MemoryMemberReposityory가 생성되어 생성자에 전달되고

  • MemberServiceImpl의 생성자를 통해 의존관계 인터페이스인 MemberRepository에 구현체인 MemoryMemberReposityory가 주입

    ➡️ 생성자 주입

  • MemberServiceImplMemberRepository 인터페이스만 의존하고 있다

  • 할인 정책에 의존하는 OrderServiceImpl 역시 동일한 방식으로 변경하면 DIP 위반을 해결할 수 있다

  • 즉, DIP를 해결하기 위해 인터페이스에만 의존하도록 하고, AppConfig에서 구현 객체를 생성하고, 생성한 객체 인스턴스의 참조 ( 레퍼런스 )를 생성자를 통해 주입한다


3-4. 클래스 및 객체 인스턴스 다이어그램

  • 객체의 생성과 연결은 AppConfig 가 담당한다

  • AppConfigMemberServiceImplMemoryMemberRepository를 생성한다

  • AppConfigmemoryMemberRepository 객체를 생성하고 그 참조값을 memberServiceImpl 을 생성하면서 생성자로 전달

  • DIP 완성 : MemberServiceImplMemberRepository 인 추상에만 의존하고 구체 클래스를 몰라도 된다

  • 객체를 생성하고 연결하는 역할과 실행하는 역할이 명확히 분리되었다

  • 클라이언트인 memberServiceImpl 입장에서 보면 의존관계를 마치 외부에서 주입해주는 것 같다고 해서 DI ( Dependency Injection ) 우리말로 의존관계 주입 또는 의존성 주입이라 한다




4. AppConfig 사용


< MemberApp >

public class MemberApp {

    public static void main(String[] args) {

        AppConfig appConfig = new AppConfig();
        MemberService memberService = appConfig.memberService();
    }
}
  • memberService에는 MemberServiceImpl이 할당되어진다


< MemberServiceTest >

class MemberServiceTest {

    MemberService memberService;

    @BeforeEach
    public void beforeEach() {
        AppConfig appConfig = new AppConfig();
        memberService = appConfig.memberService();
    }
}



5. AppConfig Refactoring


< Refactoring 이전 AppConfig >

public class AppConfig {

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

    public OrderService orderService() {
        return new OrderServiceImpl(new MemoryMemberRepository(), new FixDiscountPolicy());
    }
}
  • new MemoryMemberRepository()가 중복되고, 역할에 따른 구현이 잘 안보인다


< Refactoring 이후 AppConfig >

public class AppConfig {

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

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

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



6. 할인 정책 변경

  • 할인 정책 변경 시, AppConfig의 코드만 변경하면 된다

  • FixDiscountPolicy ➡️ RateDiscountPolicy 로 변경해도 구성 영역만 영향을 받고, 사용 영역은 전혀 영향을 받지 않는다




7. 좋은 객체 지향 설계의 5가지 원칙 적용

7-1. SRP (단일 책임 원칙)

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

  • 구현 객체를 생성하고 연결하는 책임은 AppConfig가 담당

  • 클라이언트 객체는 실행하는 책임만 담당


7-2. DIP (의존 관계 역전 원칙)

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

  • 클라이언트 코드가 추상화 인터페이스에만 의존하도록 코드를 변경했다

  • 하지만 인터페이스만으로는 아무것도 실행할 수 없다 ( NullPointerException )

  • AppConfigFixDiscountPolicy 객체 인스턴스를 클라이언트 코드 대신 생성해서 클라이언트 코드에 의존관계를 주입하면서 DIP 원칙을 지키고 문제도 해결했다


7-3. OCP ( 개방 / 폐쇄 원칙 )

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

  • 다형성 사용하고 클라이언트가 DIP를 지킴

  • 애플리케이션을 사용 영역과 구성 영역으로 나눔

  • AppConfig가 의존관계를 FixDiscountPolicy에서 RateDiscountPolicy 로 변경해서 클라이언트 코드에 주입하므로 클라이언트 코드는 변경하지 않아도 됨

  • 소프트웨어 요소를 새롭게 확장해도 사용 영역의 변경은 닫혀 있다




8. IoC (Inversion of Control, 제어의 역전)

  • 기존 프로그램은 클라이언트 구현 객체가 스스로 필요한 서버 구현 객체를 생성하고, 연결하고, 실행했다

  • 예를 들어, MemberServiceImpl이 new 키워드를 통해 MemoryMemberRepository를 직접 생성하는 것처럼

  • 즉, 구현 객체가 프로그램의 제어 흐름을 스스로 조종했다


  • 하지만 AppConfig가 등장한 이후에 프로그램에 대한 제어 흐름에 대한 권한은 모두 AppConfig가 가지고 있고 구현 객체는 자신의 로직을 실행하는 역할만 담당한다

  • 프레임워크가 객체를 호출(생성)한다

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

  • 예를 들어, OrderServiceImpl 은 필요한 인터페이스들을
    호출하지만 인터페이스에 어떤 구현 객체들이 할당될지 모른다


  • 프레임워크 vs 라이브러리

    • 내가 작성한 코드를 제어하고, 대신 실행하면 프레임워크 (JUnit)

    • 반면에 내가 작성한 코드가 직접 제어의 흐름을 담당한다면 라이브러리




9. DI ( Dependency Injection, 의존관계 주입 )

  • OrderServiceImplDiscountPolicy 인터페이스에만 의존하고 실제 어떤 구현 객체가 사용될지는 모른다

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


9-1. 정적인 클래스 의존관계

  • 클래스가 사용하는 import 코드만 보고 의존관계를 쉽게 판단할 수 있다

  • 정적인 의존관계는 애플리케이션을 실행하지 않고 코드만으로도 알 수 있다

  • OrderServiceImplMemberRepository , DiscountPolicy 에 의존한다는 것을 알 수 있다 ( 어떤 인터페이스에 의존하는지 알 수 있다 )

  • 그런데 클래스 의존관계 만으로는 실제로 어떤 객체가 OrderServiceImpl 가 의존하는 인터페이스에 주입될지 알 수 없다


9-2. 동적인 객체 인스턴스 의존관계

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

  • 애플리케이션 실행 시점(런타임)에 외부에서 실제 구현 객체를 생성하고 클라이언트에 전달해서 클라이언트와 서버의 실제 의존관계가 연결 되는 것을 의존관계 주입이라 한다

  • 객체 인스턴스를 생성하고, 그 참조값을 전달해서 연결된다

  • 의존관계 주입을 사용하면 클라이언트 코드를 변경하지 않고, 클라이언트가 호출하는 대상의 타입 인스턴스를 변경할 수 있다

  • 의존관계 주입을 사용하면 정적인 클래스 의존관계를 변경하지 않고, 동적인 객체 인스턴스 의존관계를 쉽게 변경할 수 있다




10. 컨테이너

  • AppConfig 처럼 객체를 생성하고 관리하면서 의존관계를 연결해 주는 것을 IoC 컨테이너 또는 DI 컨테이너라 한다

  • 의존관계 주입에 초점을 맞추어 최근에는 주로 DI 컨테이너라 한다




11. AppConfig를 스프링 기반으로 변경

11-1. AppConfig 변경


< AppConfig >

@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 FixDiscountPolicy();
        return new RateDiscountPolicy();
    }
}

11-2. 변경된 AppConfig 사용


< MemberApp >

public class MemberApp {
    public static void main(String[] args) {
        
        ApplicationContext applicationContext = new AnnotationConfigApplicationContext(AppConfig.class);

        MemberService memberService = applicationContext.getBean("memberService", MemberService.class);
    }
}
  • ApplicationContext : 스프링 컨테이너

  • 스프링 컨테이너를 생성할 때는 new AnnotationConfigApplicationContext를 사용

  • ApplicationContext applicationContext = new AnnotationConfigApplicationContext(AppConfig.class) : AppConfig에 있는 환경설정 정보를 가지고 스프링이 @Bean어노테이션이 붙은 애들의 객체를 생성해서 스프링 컨테이너에서 관리


  • 객체를 사용할 때, 스프링 컨테이너에서 가져와야함

  • applicationContext.getBean("memberService", MemberService.class) : 이름과 타입을 가지고 꺼낸다




12. 스프링 컨테이너

  • ApplicationContext : 스프링 컨테이너

  • 스프링 컨테이너를 통해 직접 객체를 생성하고 DI를 한다

  • 스프링 컨테이너는 @Configuration 이 붙은 AppConfig 를 설정(구성) 정보로 사용한다

  • 여기서 @Bean이라 적힌 메서드를 모두 호출해서 반환된 객체를 스프링 컨테이너에 등록한다

  • 이렇게 스프링 컨테이너에 등록된 객체를 스프링 빈이라 한다

  • 스프링 빈은 @Bean 이 붙은 메서드의 명을 스프링 빈의 이름으로 사용한다

  • 스프링 컨테이너를 통해서 필요한 스프링 빈(객체)를 찾아야 한다

  • 스프링 빈은 applicationContext.getBean() 메서드를 사용해서 찾을 수 있다

  • 기존에는 개발자가 직접 자바코드로 모든 것을 했다면 이제부터는 스프링 컨테이너에 객체를 스프링 빈으로 등록하고, 스프링 컨테이너에서 스프링 빈을 찾아서 사용하도록 변경되었다




참고> 인텔리제이 단축키

  • Test 생성 단축키 : Ctrl + Shift + T

  • 해당 클래스 파일로 이동 : Ctrl + B

  • 코드를 함수로 변환 : Ctrl + Alt + M

0개의 댓글