스프링 컨테이너를 통한 생성자 주입
스프링 컨테이너=@Configuration을 통하여 스프링의 빈들의 관계를 주입해보자
스프링을 적용안한 DI Container
즉
@Configuration
과@Bean
을 적용안했을때
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 컨테이너를 테스트
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
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);
}
}
- 다른 인스턴스이다
- 즉 요청을 할 때마다 객체를 새로 생성
- 만약 고객 트래픽이 몰릴면 여러 객체가 생성되어 메모리 낭비가 심하다
- 이를 해결하기 위해 싱글톤 패턴 등장
스프링 컨테이너를 사용하는 테스트 코드
@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);
}
싱글톤 주의점
여러 클라이언트가 하나의 같은 객체 인스턴스를 공유하기 때문에 싱글톤 객체는 무상태로 설계해야 한다
- 특정 클라이언트가 값을 변경할 수 있는 필드가 있으면 안된다
문제점 예시
문제가 있는 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 주문 금액 조회
assertThat(userAPrice).isEqualTo(10000);
}
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()
memberService()
System.out.println("call AppConfig.memberService");
호출return new MemberServiceImpl(memberRepository());
에서memberRepository()
호출memberRepository()
로 이동 후,System.out.println("call AppConfig.memberRepository");
호출memberRepository()
를 스프링 컨테이너에 스프링 빈으로 등록(이때 빈이름 :memberRepository()
-> 빈 객체:MemoryMemberRepository@CGLIB
로 등록)memberService()
를 스프링 컨테이너에 의존관계 주입된 상태로 스프링 빈으로 등록(이때 빈이름 :memberService()
-> 빈 객체:MemberServiceImpl@CGLIB
로 등록
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()
memberService()
System.out.println("call AppConfig.memberService");
호출return new MemberServiceImpl(memberRepository());
에서memberRepository()
호출memberRepository()
로 이동 후,System.out.println("call AppConfig.memberRepository");
호출-
memberRepository()
를 스프링 컨테이너에 스프링 빈으로 등록되지않고new MemoryMemberRepository()
가 실행되어 주입관계를 넣는다즉, 단순히 DI만 한다
아래코드와 같다
@Bean public MemberService memberService() { System.out.println("call AppConfig.memberService"); return new MemberServiceImpl(new MemoryMemberRepository()); }
memberService()
를 스프링 컨테이너에 의존관계가 주입된 상태로 스프링 빈 등록하면서 객체 생성(이때 빈이름 :memberService()
-> 빈 객체:MemberServiceImpl
로 등록, 의존관계 주입
memberRepository()
System.out.println("call AppConfig.memberRepository");
호출memberRepository()
를 스프링 컨테이너에 스프링 빈으로 등록(이때 빈이름 :memberRepository()
-> 빈 객체:MemoryMemberRepository
로 등록)
OrderService()
System.out.println("call AppConfig.orderService")
호출return new OrderServiceImpl(memberRepository(), discountPolicy());
에서memberRepository()
호출memberRepository()
로 이동 후,System.out.println("call AppConfig.memberRepository");
호출- 다시 한번
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
을 붙이자
댓글 쓰기