DI Container와 @Configuration 그리고 싱글톤에 대해 알아보자

스프링을 적용안한 DI Container

public class AppConfig {

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

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

스프링을 적용한 Spring Container

@Configuration
public class SpringConfig {
    @Bean
    public MemberService memberService() {
        return new MemberService(memberRepository());
    }
    @Bean
    public MemberRepository memberRepository() {
        return new MemoryMemberRepository();
    }
}

두개 다 의존관계 주입한다(Dependency Injection)

그렇다면 차이점은?

  • 스프링 컨테이너는 싱글톤을 보장한다

싱글톤

스프링을 적용안한 DI 컨테이너를 테스트

public class SingletonTest {
	@Test
	@DisplayName("스프링 없는 순수한 DI 컨테이너")
	void pureContainer() {

	  AppConfig appConfig = new AppConfig();

		//1. 조회: 호출할 때 마다 객체를 생성
		MemberService memberService1 = appConfig.memberService();
		//2. 조회: 호출할 때 마다 객체를 생성
		MemberService memberService2 = appConfig.memberService();

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

image

  • 다른 인스턴스이다
  • 즉 요청을 할 때마다 객체를 새로 생성
  • 만약 고객 트래픽이 몰릴면 여러 객체가 생성되어 메모리 낭비가 심하다
  • 이를 해결하기 위해 싱글톤 패턴 등장

스프링 컨테이너를 사용하는 테스트 코드

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

  ApplicationContext ac = new AnnotationConfigApplicationContext(AppConfig.class);

	//1. 조회: 호출할 때 마다 같은 객체를 반환
  MemberService memberService1 = ac.getBean("memberService", MemberService.class);
	//2. 조회: 호출할 때 마다 같은 객체를 반환
  MemberService memberService2 = ac.getBean("memberService", MemberService.class);

	//참조값이 같은 것을 확인
	System.out.println("memberService1 = " + memberService1); 
	System.out.println("memberService2 = " + memberService2);
	
	//memberService1 == memberService2
  assertThat(memberService1).isSameAs(memberService2);
}
image
스프링 컨테이너 덕분에 클라이언트 요청이 올때마다 객체를 생성하는 것이 아니라, 이미 만들어진 객체를 공유해서 효율적으로 재사용

싱글톤 주의점

여러 클라이언트가 하나의 같은 객체 인스턴스를 공유하기 때문에 싱글톤 객체는 무상태로 설계해야 한다

  • 특정 클라이언트가 값을 변경할 수 있는 필드가 있으면 안된다

문제점 예시


문제가 있는 Bean(Stateful)

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

}

테스트 코드

public class StatefulServiceTest {

	@Test
	void statefulServiceSingleton() {

	    ApplicationContext ac = new AnnotationConfigApplicationContext(TestConfig.class);
	    StatefulService statefulService1 = ac.getBean("statefulService", StatefulService.class);
	    StatefulService statefulService2 = ac.getBean("statefulService", StatefulService.class);
	
			//ThreadA: A사용자 10000원 주문 
			statefulService1.order("userA", 10000);
			//ThreadB: B사용자 20000원 주문 
			statefulService2.order("userB", 20000);
			//ThreadA: 사용자A 주문 금액 조회
			int price = statefulService1.getPrice();

			//ThreadA: 사용자A는 10000원을 기대했지만, 기대와 다르게 20000원 출력 System.out.println("price = " + price);
	    Assertions.assertThat(statefulService1.getPrice()).isEqualTo(20000);

	}

	static class TestConfig {
	    @Bean
	    public StatefulService statefulService() {
	        return new StatefulService();
	    }
	} 

}
  • price는 공유필드인데, 특정 클라이언트가 값을 변경한다
  • Stateless유지하자

Stateless 테스트 코드

public class StatefulService {

	private int price; //상태를 유지하는 필드

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

  

}
public class StatelessServiceTest {

	@Test
	void statefulServiceSingleton() {

	    ApplicationContext ac = new AnnotationConfigApplicationContext(TestConfig.class);
	    StatefulService statefulService1 = ac.getBean("statelessService", StatelessService.class);
	    StatefulService statefulService2 = ac.getBean("statelessService", StatelessService.class);
	
			//ThreadA: A사용자 10000원 주문 
			int userAPrice = statelessService1.order("userA", 10000);
			//ThreadB: B사용자 20000원 주문 
			int userBPrice = statelessService2.order("userB", 20000);
			//ThreadA: 사용자A 주문 금액 조회
			int price = statefulService1.getPrice();
			System.out.println("price = " + userAPrice);

	}

	static class TestConfig {
	    @Bean
	    public StatelessService statelessService() {
	        return new StatelessService();
	    }
	} 

}

@Configuration

@Configuration이 있는 경우

@Configuration
public class AppConfig {

    @Bean
    public MemberService memberService() {
				System.out.println("call AppConfig.memberService");
        return new MemberServiceImpl(memberRepository());
    }

    

    @Bean
    public MemberRepository memberRepository() {
				System.out.println("call AppConfig.memberRepository");
        return new MemoryMemberRepository();
    }

		@Bean
    public OrderService orderService() {
				System.out.println("call AppConfig.orderService")
        return new OrderServiceImpl(
                memberRepository(),
                discountPolicy());
    }

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

}

다음 순서대로 실행된다 가정 : memberService()memberRepository()

  1. memberService()
    1. System.out.println("call AppConfig.memberService"); 호출
    2. return new MemberServiceImpl(memberRepository()); 에서 memberRepository() 호출
    3. memberRepository()로 이동 후, System.out.println("call AppConfig.memberRepository"); 호출
    4. memberRepository()를 스프링 컨테이너에 스프링 빈으로 등록(이때 빈이름 : memberRepository() -> 빈 객체: MemoryMemberRepository@CGLIB로 등록)
    5. memberService() 를 스프링 컨테이너에 의존관계 주입된 상태로 스프링 빈으로 등록(이때 빈이름 : memberService() -> 빈 객체: MemberServiceImpl@CGLIB로 등록
  2. memberRepository() → 스프링 컨테이너에 이미 등록되어 있어서 건너뛴다

System.out.println("call AppConfig.memberRepository"); 한번만 호출된다(싱글톤을 보장해줌)

@Configuration이 없는 경우

//@Configuration
public class AppConfig {

    @Bean
    public MemberService memberService() {
				System.out.println("call AppConfig.memberService");
        return new MemberServiceImpl(memberRepository());

    }

    

    @Bean
    public MemberRepository memberRepository() {
				System.out.println("call AppConfig.memberRepository");
        return new MemoryMemberRepository();
    }

		@Bean
    public OrderService orderService() {
				System.out.println("call AppConfig.orderService")
        return new OrderServiceImpl(
                memberRepository(),
                discountPolicy());

    }

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

}

위와 똑같이 순서대로 실행된다 가정 : memberService()memberRepository()OrderService()

  1. memberService()
    1. System.out.println("call AppConfig.memberService"); 호출
    2. return new MemberServiceImpl(memberRepository()); 에서 memberRepository() 호출
    3. memberRepository()로 이동 후, System.out.println("call AppConfig.memberRepository"); 호출
    4. memberRepository()를 스프링 컨테이너에 스프링 빈으로 등록되지않고 new MemoryMemberRepository()가 실행되어 주입관계를 넣는다

      즉, 단순히 DI만 한다

      아래코드와 같다

       @Bean
           public MemberService memberService() {
       				System.out.println("call AppConfig.memberService");
               return new MemberServiceImpl(new MemoryMemberRepository());
           }
      
    5. memberService()를 스프링 컨테이너에 의존관계가 주입된 상태로 스프링 빈 등록하면서 객체 생성(이때 빈이름 : memberService() -> 빈 객체: MemberServiceImpl로 등록, 의존관계 주입
  2. memberRepository()
    1. System.out.println("call AppConfig.memberRepository"); 호출
    2. memberRepository()를 스프링 컨테이너에 스프링 빈으로 등록(이때 빈이름 : memberRepository() -> 빈 객체: MemoryMemberRepository로 등록)
  3. OrderService()
    1. System.out.println("call AppConfig.orderService") 호출
    2. return new OrderServiceImpl(memberRepository(), discountPolicy());에서 memberRepository() 호출
    3. memberRepository()로 이동 후, System.out.println("call AppConfig.memberRepository"); 호출
    4. 다시 한번 new MemoryMemberRepository() 실행

System.out.println("call AppConfig.memberRepository"); 3번 호출된다(싱글톤을 보장X)

해결방법

public class AppConfig {

		@Autowired MemberRepository memberRepository;

    @Bean
    public MemberService memberService() {
				System.out.println("call AppConfig.memberService");
        //return new MemberServiceImpl(memberRepository());
				return new MemberServiceImpl(memberRepository);
    }

    

    @Bean
    public MemberRepository memberRepository() {
				System.out.println("call AppConfig.memberRepository");
        return new MemoryMemberRepository();
    }

		@Bean
    public OrderService orderService() {
				System.out.println("call AppConfig.orderService")
        return new OrderServiceImpl(
                memberRepository,
                discountPolicy());
    }

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

}
@Bean
    public MemberRepository memberRepository() {
				System.out.println("call AppConfig.memberRepository");
        return new MemoryMemberRepository();
    }
  • 위 코드를 통하여 Bean으로 등록되고
  • @Autowired MemberRepository memberRepository; 를 통하여 다른 함수(memberService(), orderService())에서도 등록된 Bean을 사용하게 만든다
  • 즉 전부 같은 하나의 Instance이다

@Configuration을 붙이자

댓글 쓰기