관심사의 분리 및 IOC

최주영·2024년 3월 14일
0

스프링 핵심 원리

목록 보기
3/9
post-thumbnail

✅ 할인 정책 변경

기존에 사용하고 있던 FixDiscountPolicy 인터페이스가 아닌 RateDiscountPolicy 로 바꾸려고 함

고정 금액 할인이 아니라 좀 더 합리적인 주문 금액당 할인하는 정률 % 할인으로 변경해보자. 예를 들어서 기존 정책은 VIP가 10000원을 주문하든 20000원을 주문하든 항상 1000원을 할인했는데, 이번에 새로 나온 정책은 10%로 지정해두면 고객이 10000원 주문시 1000원을 할인해주고, 20000원 주문시에 2000원을 할인해주는 식이다.

✅ 문제점

  • 할인정책을 변경하려면 클라이언트인 OrderServiceImpl 코드를 고쳐야함 -> OCP 위반
public class OrderServiceImpl implements OrderService {
    // private final DiscountPolicy discountPolicy = new FixDiscountPolicy();
    private final DiscountPolicy discountPolicy = new RateDiscountPolicy();
}

클래스 의존 관계를 보면, 추상(인터페이스) 뿐만 아니라 구체(구현) 클래스에도 의존하고 있다
OrderServiceImpl -> FixDiscountPolicy 클래스와 RateDiscountPolicy 클래스를 의존 -> DIP 위반

public class OrderServiceImpl implements OrderService {

    //private final DiscountPolicy discountPolicy = new RateDiscountPolicy(); // 인터페이스 + 구현 클래스에 까지 의존하는 상황
    private DiscountPolicy discountPolicy; // 
}

위 코드는 인터페이스에만 의존하도록 설계와 코드를 변경함
하지만 구현체가 없어서 실행하면 Null Pointer Exception 발생
문제 해결 방법은 누군가가 클라이언트인 OrderServiceImplDiscountPolicy의 구현객체를
대신 생성하고 주입해야함


✅ 해결 방법

애플리케이션을 하나의 공연으로 생각했을때, 각각의 인터페이스를 배역 이라 하자

로미오 역할(인터페이스) -> 원빈 (구현 클래스) 이라 할때
이전 코드는 마치 원빈(구현 클래스)가 여자 주인공(구현 클래스)를 직접 초빙하는 것과 같다
원빈은 공연도 하고, 여자 주인공도 직접 초빙해야하는 다양한 책임을 가지고 있다

그래서 관심사를 분리 해야 한다
-> 공연기획자를 만들고, 배우와 공연 기획자의 책임을 확실히 분리해야함


✅ AppConfig 등장

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

AppConfig는 실제 동작에 필요한 필요한 구현 객체를 생성

  • MemberServiceImpl
  • MemoryMemberRepository
  • OrderServiceImpl
  • FixDiscountPolicy

AppConfig는 생성한 객체를 인스턴스의 참조(레퍼런스)를 생성자를 통해서 주입(연결)해줌

  • MemberServiceImpl -> MemoryMemberRepository
  • OrderServiceImpl -> MemoryMemberRepository, FixDiscountPolicy

public class MemberServiceImpl implements MemberService{

    private final MemberRepository memberRepository;

    public MemberServiceImpl(MemberRepository memberRepository) { // 생성자 주입
        this.memberRepository = memberRepository;
    }
}

위 코드의 변경으로 MemberServiceImplMemoryMemberRepository 를 의존하지 않고
MemberRepository 인터페이스만 의존한다
MemberServiceImpl 의 입장에서 생성자를 통해 어떤 구현 객체가 들어오질 알 수 없다
MemberServiceImpl 의 생성자를 통해서 어떤 구현 객체가 주입될지는 외부(AppConfig)에서 결정됨
MemberServiceImpl은 이제 의존관계에 대한 고민은 외부에 맡기고 실행에만 집중하면 됨

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

  • DIP 완성 : MemberServiceImplMemberRepository에만 의존하면되며, 구체클래스를 몰라도 됨
  • 관심사의 분리 : 객체를 생성하고 연결하는 역할과 실행하는 역할이 명확히 분리되었다

OrderServiceImpl - 생성자 주입

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

OrderServiceImplFixDiscountPolicy를 의존하지 않고 DiscountPolicy 인터페이스만 의존한다.
OrderServiceImpl 입장에서 생성자를 통해 어떤 구현 객체가 들어올지(주입될지)는 알 수 없다.
OrderServiceImpl의 생성자를 통해서 어떤 구현 객체을 주입할지는 오직 외부에서 결정한다.
OrderServiceImpl 은 이제부터 실행에만 집중하면 된다.

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

즉 리팩토링 후 코드에서
할인율을 고정이 아닌, 정률로 바꾸려면
클라이언트인 ServiceImpl 에서 바꿀 필요없이
AppConfig에서 return new RateDiscountPolicy(); 만 바꿔주면 된다

✅ 제어의 역전 (IoC = Inversion of Control)

위에 코드에서 AppConfig 가 프로그램 제어 흐름을 스스로 조종함
OrderServiceImpl 은 필요한 인터페이스들을 호출하지만, 어떤 구현 객체들이 실행될 지 모른다

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

✅ IoC컨테이너, DI 컨테이너

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


✅ 프레임워크 VS 라이브러리

  • 프레임워크 : 내가 작성한 코드를 제어하고, 대신 실행하는 것은 프레임워크
    ex) (JUnit)
  • 라이브러리 : 내가 작성한 코드가 직접 제어의 흐름을 담당하면 라이브러리
    ex) 자바 객체를 json으로 바꿀 때 (해당 라이브러리 직접 호출함)

✅ 스프링 컨테이너

  • ApplicationContext 를 스프링 컨테이너라 함
public class OrderApp {

    public static void main(String[] args){
        ApplicationContext applicationContext = new AnnotationConfigApplicationContext(AppConfig.class);
        MemberService memberService = applicationContext.getBean("memberService", MemberService.class);
        OrderService orderService = applicationContext.getBean("orderService", OrderService.class);

        Long memberId = 1L;
        Member member = new Member(memberId, "memberA", Grade.VIP);
        memberService.join(member);

        Order order = orderService.createOrder(memberId, "itemA", 20000);

        System.out.println("order = " + order.toString());
    }
} 
  • 기존에는 개발자가 AppConfig를 사용해서 DI를 했지만, 이제부터 스프링 컨테이너를 사용한다
  • 스프링 컨테이너는 @Configuration 이 붙은 AppConfig를 설정 정보로 사용한다
  • 여기서 @Bean 이라 적힌 메서드를 모두 호출해서 반환된 객체를 스프링 컨테이너에 등록한다
  • 스프링 컨테이너를 통해서 필요한 스프링 빈(객체)를 찾아야 한다
  • 스프링 빈은 applicationContext.getBean() 메서드를 사용해서 찾을 수 있다

정리!
1. 스프링 컨테이너에 객체를 스프링 빈으로 등록
2. 스프링 컨테이너에서 스프링 빈을 찾아서 사용

@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();
    }
}
profile
우측 상단 햇님모양 클릭하셔서 무조건 야간모드로 봐주세요!!

0개의 댓글