Framework/Spring

인프런 스프링 핵심 원리-기본편 #5 싱글톤 컨테이너 / 의존관계 자동 주입

MINGYUM 2022. 1. 5. 12:39

스프링 컨테이너에서 싱글톤은 기본적으로 동작한다. 

    @Test
    @DisplayName("스프링 컨테이너와 싱글톤")
    void springContainer(){

        // 1. 조회 : 호출할 떄마다 객체 생성
        ApplicationContext ac = new AnnotationConfigApplicationContext(AppConfig.class);

        // 2. 조회 : 호출할 때마다 객체 생성
        MemberService memberService1 = ac.getBean("memberService", MemberService.class);
        MemberService memberService2 = ac.getBean("memberService", MemberService.class);

        // 참조 값이 다른 것을 확인
        System.out.println("memberService1 = " + memberService1);
        System.out.println("memberService2 = " + memberService2);

        // memberService != memberService2
        assertThat(memberService1).isNotSameAs(memberService2);
    }

따라서 이 코드에서 memberService1, 2는 같은 객체로 생성되므로 assertThat에서 오류가 발생하게 된다. 

 

이렇게 싱글톤 컨테이너로 인해 발생하는 문제가 또 있다. 

바로 같은 객체임을 이용해 다음과 같이 간단한 Servive의 Test를 구현해보면, 

package hello.core.singletone;

public class StatefulService {
    private int price;

    public void order(String name, int price){
        System.out.println("name = " + name + " price = " + price);
        this.price = price;
    }

    public int getPrice(){
        return price;
    }
}
    @Test
    void statefulServiceSingleton(){
        AnnotationConfigApplicationContext ac = new AnnotationConfigApplicationContext(TestConfig.class);
        StatefulService statefulService1 = ac.getBean(StatefulService.class);
        StatefulService statefulService2 = ac.getBean(StatefulService.class);

        // ThreadA : A 사용자가 10000원 주문
        statefulService1.order("userA", 10000);
        // ThreadB : B 사용자가 20000원 주문
        statefulService2.order("userB", 20000);

        // ThreadA : A 사용자가 주문 금액 조회
        int price = statefulService1.getPrice();
        System.out.println("price = " + price); // 20000

        Assertions.assertThat(statefulService1.getPrice()).isEqualTo(20000);

    }

같은 객체를 공유하고 있기 때문에 price도 다르게 설정됨을 알 수 있다.

 

    @Test
    void configurationTest(){
        AnnotationConfigApplicationContext ac = new AnnotationConfigApplicationContext(AppConfig.class);

        MemberServiceImpl memberService = ac.getBean("memberService", MemberServiceImpl.class);
        OrderServiceImpl orderService = ac.getBean("orderService", OrderServiceImpl.class);
        MemberRepository memberRepository = ac.getBean("memberRepository", MemberRepository.class);

        MemberRepository memberRepository1 = memberService.getMemberRepository();
        MemberRepository memberRepository2 = orderService.getMemberRepository();

        System.out.println("memberService -> memberRepository = " + memberRepository1);
        System.out.println("orderService -> memberRepository = " + memberRepository2);
        System.out.println("memberRepository = " + memberRepository);

        Assertions.assertThat(memberService.getMemberRepository()).isSameAs(memberRepository);
        Assertions.assertThat(orderService.getMemberRepository()).isSameAs(memberRepository);
    }

이 Test의 결과로 출력된 세 가지 객체가 모두 동일한 것을 알 수 있다. 

 

 

자바 코드의 @Bean이나 XML의 <bean>을 통해서 스프링 빈을 등록함

=> 자동으로 스프링 빈을 등록하는 컴포넌트 스캔이라는 기능을 사용

 

프로젝트의 시작 부분에 있는 Config에 @ComponentScan을 입력하면 하위 디렉토리에 존재하는 모든 클래스의 @Component가 붙은 클래스를 모두 스프링 컨테이너에서 빈으로 관리하게 된다. 

 

 

package hello.core.scan.filter;

import org.assertj.core.api.Assertions;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.NoSuchBeanDefinitionException;
import org.springframework.context.annotation.AnnotationConfigApplicationContext;
import org.springframework.context.annotation.ComponentScan;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.FilterType;

import java.lang.annotation.Annotation;

public class ComponentFilterAppConfigTest {

    @Test
    void filterScan(){
        AnnotationConfigApplicationContext ac = new AnnotationConfigApplicationContext(ComponentFilterAppConfig.class);
        BeanA beanA = ac.getBean("beanA", BeanA.class);
        Assertions.assertThat(beanA).isNotNull();

        ac.getBean("beanB", BeanB.class); // 존재하지 않는다.
        org.junit.jupiter.api.Assertions.assertThrows(
                NoSuchBeanDefinitionException.class,
                () -> ac.getBean("beanB", BeanB.class)
        );
    }

    @Configuration
    @ComponentScan(
            includeFilters = @ComponentScan.Filter(type= FilterType.ANNOTATION, classes = MyIncludeComponent.class),
            excludeFilters = @ComponentScan.Filter(type= FilterType.ANNOTATION, classes = MyExcludeComponent.class)
    )
    static class ComponentFilterAppConfig {

    }

}

 

 

https://twofootdog.github.io/Spring-%ED%95%84%ED%84%B0(Filter)%EB%9E%80-%EB%AC%B4%EC%97%87%EC%9D%B8%EA%B0%80/ 

 

[Spring]필터(Filter)란 무엇인가 | 두발로걷는개

두발로걷는개의 Blog

twofootdog.github.io

 

ComponentScan의 Filter를 이용해, 특정 조건을 만족하는 클래스만을 스캔하도록 설정한다. 

생성자 주입

 

수정자 주입은 Setter이라는 필드의 값을 변경하는 수정자 메서드를 통해서 의존관계를 주입하는 방법이다. 

수정자 주입

 

필드 주입

@Configuration 같은 곳에서만 특별한 용도로 사용한다. 

 

 

 

일반 메서드 주입

일반 함수를 만들어서 의존관계 주입

 

 

 

롬복을 사용한 생성자 주입 간결화 

 

@RequiredArgsConstructor

어노테이션을 클래스 상단에 붙이면, final로 선언된 모든 멤버 변수에 대한 생성자가 자동 생성되고, 생성자 주입이 이루어지게 된다. 

 

조회 빈이 2개 이상인 경우

 

FixDiscountPolicy, RateDiscountPolicy 두 클래스에 모두 @Component를 붙이면 생기는 문제점, 

조회 빈이 2개가 되어 OrderServiceImpl에서 하위 타입 두 개 중 무엇을 선택할지 에러가 발생한다 .

 

그래서 필드 이름으로 이를 지정해주면 되는데,

이렇게 rateDiscountPolicy 혹은 fixDiscountPolicy로 이름을 설정해주면

정상적으로 돌아가는 것을 알 수 있다. 

 

@Quilifier 은 추가 구분자를 설정하는 것.

구분자를 설정해 그때 그때 다르게 할인율을 적용할 수 있다. 

 

혹은 Primary 어노테이션을 붙여서, 두 개 이상의 클래스 중 우선권을 정해 의존관계를 주입한다. 

Primary보다 Qualifer의 우선순위가 더 높다. 

 

 

 

어노테이션 직접 만들기

Qualifer의 안에 있는 어노테이션을 가져와, @Inteface 형식인 MainDiscountPolicy에 붙인다. 

Qualifer의 사용 없이 어노테이션을 만들어 컴파일시 타입 체크를 가능하게 할 수 있다. 

문자는 컴파일 시간에 체크가 되지 않기 때문이다. 

 

 

조회와 빈이 모두 필요할 때 

 

클라이언트가 rate, fix 중 선택하게끔 설계하려면?

package hello.core.autowired;

import hello.core.AutoAppConfig;
import hello.core.discount.DiscountPolicy;
import hello.core.member.Grade;
import hello.core.member.Member;
import org.assertj.core.api.Assertions;
import org.junit.jupiter.api.Test;
import org.springframework.context.annotation.AnnotationConfigApplicationContext;

import java.util.List;
import java.util.Map;

import static org.assertj.core.api.Assertions.*;

public class AllBeanTest {

    @Test
    void findAllBean(){
        AnnotationConfigApplicationContext ac = new AnnotationConfigApplicationContext(AutoAppConfig.class, DiscountService.class);

        DiscountService discountService = ac.getBean(DiscountService.class);
        Member member = new Member(1L, "userA", Grade.VIP);
        int discountPrice = discountService.discount(member, 10000, "fixDiscountPolicy");

        assertThat(discountService).isInstanceOf(DiscountService.class);
        assertThat(discountPrice).isEqualTo(1000);

        int rateDiscountPolicy = discountService.discount(member,10000, "rateDiscountPolicy");
    }


    static class DiscountService{
        private final Map<String, DiscountPolicy> policyMap;
        private final List< DiscountPolicy> policies;

        public DiscountService(Map<String, DiscountPolicy> policyMap, List<DiscountPolicy> policies) {
            this.policyMap = policyMap;
            this.policies = policies;
            System.out.println("policyMap = " + policyMap);
        }

        public int discount(Member member, int price, String discountCode) {
            DiscountPolicy discountPolicy = policyMap.get(discountCode);
            return discountPolicy.discount(member, price);
        }
    }
}

 

discount 함수에서 discountCode에 어떤 문자열이 들어가냐에 따라 다르게 할인을 적용할 수 있다. 

 

 

 

빈 생명주기 콜백

 

데이터베이스 커넥션 풀, 네트워크 소켓 등 필요한 연결을 미리 해두고 종료 시점에서 연결을 모두 종료하는 작업을 진행

package hello.core.lifecycle;

public class NetworkClient  {
    private String url;

    public NetworkClient() {
        System.out.println("생성자 호출, url = " + url);
        connect();
        call("초기화 연결 메세지");
    }

    public void setUrl(String url){
        this.url = url;
    }

    // 서비스 시작
    public void connect(){
        System.out.println("connect : " + url);
    }

    public void call(String message){
        System.out.println("call : " + url + " message = " + message);
    }

    // 서비스 종료 시 호출
    public void disconnect(){
        System.out.println("close : " + url);
    }

    public void init() {
        System.out.println("NetworkClient.afterPropertiesSet");
        connect();
        call("초기화 연결 메세지 ");
    }

    public void close()  {
        System.out.println("NetworkClient.destroy");
        disconnect();
    }
}

url 이라는 변수를 가지며, init, close 함수를 가지고 있는 NetworkClient 클래스를 설계하였다. 

package hello.core.lifecycle;

import org.junit.jupiter.api.Test;
import org.springframework.context.ConfigurableApplicationContext;
import org.springframework.context.Lifecycle;
import org.springframework.context.annotation.AnnotationConfigApplicationContext;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

import java.lang.annotation.Annotation;

public class BeanLifeCycleTest {

    @Test
    public void lifeCycleTest(){
        ConfigurableApplicationContext ac = new AnnotationConfigApplicationContext(LifeCycleConfig.class);
        NetworkClient client = ac.getBean(NetworkClient.class);
        ac.close();
    }

    @Configuration
    static class LifeCycleConfig{
        @Bean(initMethod = "init", destroyMethod = "close")
        public NetworkClient networkClient(){
            NetworkClient networkClient = new NetworkClient();
            networkClient.setUrl("http://hello-spring.dev");
            return networkClient;
        }
    }
}

 

이렇게 init, close 함수에 @PostConstruct, @PreDestroy 어노테이션을 달면

Test시 @Bean의 옵션에 initMethod와 destoryMethod를 따로 지정하지 않아도 된다.