관심사의 분리 - 스프링 컨테이너

김소희·2024년 11월 21일
2

문제점

지금까지 변화에 대비하여 다형성을 활용하여 인터페이스와 구현class를 각각 만들어서 역할과 구현을 나누어 코드를 작성하는 것까지는 좋았지만 결국 추상클래스와 구현클래스에 동시에 의존하는 상황에 다다르게 되었다.

만약 요구사항이 바뀌어서 구현클래스를 새로 만들어 바꿔끼우려고 하는 순간에 서비스 로직의 코드를 수정해야 하므로 좋은 객체 지향 설계의 원칙 - SOLID 에서 DIP(의존관계 역전 원칙), OCP(개방-폐쇄 원칙) 를 위반한 셈이다.

게다가 서비스 로직의 책임뿐만 아니라 구현체를 할당하고 연결하는 책임까지 지고 있으니 SRP(단일책임원칙)도 지켜지지 않았다고 볼 수도 있다.

이를 극복하기 위해서는 new 키워드로 직접 구현체를 생성해서 할당하지 않고 인터페이스에만 의존하게 만들어야 하는데 구현체가 없기 때문에 nullPointException이 발생하게 된다.


해결법

구현 객체를 생성하고 연결하는 책임을 가진 애플리케이션의 전체 동작 방식을 구성(config)하는 별도의 설정 클래스를 만들어서 해결해보자.

서비스 구현체 코드에서 new 키워드로 생성, 할당하던 부분을 모두 지우고 생성자를 만들었다.
그러면 코드상에는 인터페이스만 남는다. 추상화에만 의존하고 구체적인 클래스에 대해서는 전혀 모르게끔 변경된다. 어떤 구현 객체가 주입될지는 전혀 알 수 없다.
이렇게 의존관계가 외부에서 주입되는 것을 의존관계 주입(DI : Dependency Injection)이라고 한다.

이제는 DIP를 철저하게 만족시키고 있다.
또한 의존 관계에 대한 고민과 책임은 외부에 맡기고 하나의 책임인 서비스 로직에만 집중하므로 책임이 명확해지고 SRP(단일책임원칙)도 지켜진다.

그리고 config클래스에서 구현 객체를 생성하고, 생성한 인스턴스 객체의 참조(레퍼런스)를 생성자를 통해서 주입(연결)해준다.

객체를 생성하고 연결하는 역할과 실행하는 역할이 명확히 분리되었다.
관심사의 분리 완료 !!!

main method 테스트 코드에서도 이제 생성하지 않고 주입받아쓰게끔 변경해보자.

  • 변경 전

  • 변경 후

test 코드에도 @BeforeEach를 통해 적용시켜보자

모든 코드 테스트에도 이상없이 통과되는 것까지 확인할 수 있었다.


config 리팩토링

코드 상으로는 정상적으로 문제없이 잘 동작하지만
역할과 구현을 한눈에 파악하기 쉽도록 개선하기로 했다.

  • 수정 전

  • 수정 방법
    ctrl + art + m 으로 Extract Method로 빼준다.

  • 수정 후

이제 코드의 메소드 명만 봐도 역할을 알 수 있고,
나중에 할인 구현체를 변경해야 할 때가 올때 구성영역 코드의 딱 '한 줄'만 바꾸어서 적용시킬 수 있다.
사용영역 부분의 코드는 전혀 수정하지 않아도 된다. 👏👏👏

@Configuration
public class AppConfig {

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

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

    @Bean
    public static MemberRepository memberRepository() {
        return new MemoryMemberRepository();
    }

    @Bean
    public static DiscountPolicy discountPolicy() {
        return new RateDiscountPolicy();
    }

}


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);
        System.out.println("order = " + order.calculatePrice());

    }

}

스프링 컨테이너

BeanFactoryApplicationContext를 스프링 컨테이너라 부르지만 BeanFactory를 사용할 일은 거의 없으므로 ApplicationContext를 스프링 컨테이너라고 한다.

  • 스프링 컨테이너는 @Configuration이 붙은 (코드상에서는 AppConfig)를 설정(구성)정보를 지정해 주어야 한다.
  • @Bean이 붙은 메소드를 모드 호출해서 반환된 객체를 스프링 컨테이너에 등록하고 이렇게 등록된 객체를 스프링 빈이라고 한다.
  • 주의할 점으로 메소드 명이 빈 이름이 되는데 빈 이름은 항상 다른 이름으로 지어야 빈이 무시되거나 덮어버리는 오류가 발생하지 않는다.
  • 스프링 컨테이너에 빈이 등록된 이후에는 설정 정보를 참고해서 의존관계를 주입(DI)한다.
  • applicationContext.getBean()를 통해 필요한 스프링 빈을 사용할 수 있다.

컨테이너에 등록된 빈 조회하기

class ApplicationContextInfoTest {

    AnnotationConfigApplicationContext ac = new AnnotationConfigApplicationContext(AppConfig.class);

    @Test
    @DisplayName("모든 빈 출력하기")
    void findAllBean() {
        String[] beanDefinitionNames = ac.getBeanDefinitionNames();

        for (String beanDefinitionName : beanDefinitionNames) {
            Object bean = ac.getBean(beanDefinitionName);
            System.out.println("name = " + beanDefinitionName + " object = " + bean);
        }
    }

    @Test
    @DisplayName("애플리케이션 빈 출력하기")
    void findApplicationBean() {
        String[] beanDefinitionNames = ac.getBeanDefinitionNames();

        for (String beanDefinitionName : beanDefinitionNames) {
            BeanDefinition beanDefinition = ac.getBeanDefinition(beanDefinitionName);

            if (beanDefinition.getRole() == BeanDefinition.ROLE_APPLICATION) {
                Object bean = ac.getBean(beanDefinitionName);
                System.out.println("name = " + beanDefinitionName + " object = " + bean);
            }
        }
    }

}

BeanFactory

  • 빈 팩토리는 스프링 컨테이너의 최상위 인터페이스이다.
  • 스프링 빈을 관리하고 조회하는 역할을 담당한다.
  • getBean()을 제공한다.

ApplicationContext

  • ApplicationContext 는 인터페이스이다.
  • ApplicationContextBeanFactory의 모든 기능을 모두 상속받아서 제공한다.
  • 빈을 관리기능 + 부가기능을 제공한다.

ApplicationContext가 제공하는 부가기능

  • 메시지 소스를 활용한 국제화 기능 : 한국에서 들어오면 한국어로, 영어권에서 들어오면 영어로 출력
  • 환경변수 : 로컬(내pc), 개발(테스트서버), 스테이징, 운영(실제 서비스) 등을 구분해서 처리
  • 애플리케이션 이벤트 : 이벤트를 발행하고 구독하는 모델을 편리하게 지원
  • 편리한 리소스 조회 : 파일, 클래스패스, 외부 등에서 리소스를 편리하게 조회
profile
백엔드 개발자의 노트

0개의 댓글