목차
- 웹 애플리케이션과 싱글톤
- 싱글톤 패턴
- 싱글톤 컨테이너
- 싱글톤 방식의 주의점 ★
- @Configuration과 싱글톤
- @Configuration과 바이트코드 조작
스프링은 기업용 온라인 서비스 기술을 지원하기 위해 탄생
웹 애플리케이션은 보통 여러 고객이 동시에 요청
스프링 없는 순수한 DI 컨테이너 테스트
test > singleton > SingletonTest
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);
Assertions.assertThat(memberService1).isNotSameAs(memberService2);
//same : ==
}
}
해결방안으로 객체가 1개만 생성되고 그 객체를 공유하도록 한다 -> 싱글톤 패턴
test > singleton > SingletonServiceTest
public class SingletonService {
//static 영역에 객체를 1개만 생성
private static final SingletonService instance = new SingletonService();
// public으로 열어서 객체 인스턴스가 필요하면 이 static 메서드를 통해서만 조회 허용
public static SingletonService getInstance() {
return instance;
}
// 생성자를 private로 선언해서 외부에서 new 키워드를 사용한 객체 생성을 막음
private SingletonService() {
}
public void logic() {
System.out.println(" 싱글톤 객체 로직 호출");
}
}
테스트
test > singleton > singletonTest
...
@Test
@DisplayName("싱글톤 패턴을 적용한 객체 사용")
void singletonServiceTest() {
SingletonService singletonService1 =SingletonService.getInstance();
SingletonService singletonService2 =SingletonService.getInstance();
System.out.println("singletonService1 = " + singletonService1);
System.out.println("singletonService2 = " + singletonService2);
Assertions.assertThat(singletonService1).isSameAs(singletonService2);
}
}
같은 인스턴스를 반환하는것을 확인
싱글톤 패턴을 적용하면 요청이 올때마다 객체를 생성하는것이 아닌 이미 만들어진 객체를 공유해서 효율적으로 사용할 수 있다. 하지만 많은 문제점을 가지고 있다..
- 구현하는 코드 자체가 많이 들어감
- 의존 관계상 구현체 클래스에 의존 -> DIP에 위반
- 유연한 테스트가 어려움
- 설정이 끝난 객체를 가져오기 때문
- 내부 속성을 변경하거나 초기화 하기 어려움
- private 생성자를 사용하기 때문에 자식 클래스를 만들기 어려움
- 위 문제들로 인해 유연성이 떨어짐
스프링 컨테이너는 싱글톤 패턴의 문제를 해결 하면서 싱글톤( 1개의 객체 )으로 관리
지금까지 학습한 스프링 빈이 바로 싱글톤으로 관리되는 빈
- 싱글톤 패턴을 적용하지 않아도 인스턴스를 싱글톤으로 관리
- 지금까지 설명한 스프링 컨테이너를 생각 해보자. ( 스프링 빈 )
- 싱글톤 객체를 생성하고 관리하는 기능 -> 싱글톤 레지스트리
- 싱글톤 패턴의 모든 단점을 해결하면서 객체를 싱글톤으로 유지
- 싱글톤 패턴을 위한 지저분한 코드가 필요 없음.
- DIP, OCP, 테스트, private 생성자로부터 자유로움
test > singleton > SingletonTest
...
@Test
@DisplayName("스프링 컨테이너와 싱글톤")
void springContainer() {
// AppConfig appConfig = new AppConfig();
ApplicationContext ac = new AnnotationConfigApplicationContext(AppConfig.class);
MemberService memberService1 = ac.getBean("memberService",MemberService.class);
MemberService memberService2 = ac.getBean("memberService",MemberService.class);
System.out.println("memberService1 = " + memberService1);
System.out.println("memberService2 = " + memberService2);
Assertions.assertThat(memberService1).isSameAs(memberService2);
}
}
싱글톤 컨테이너 적용 후
스프링 컨테이너로 요청이 올 때마다 객체를 생성하는 것이 아닌, 이미 만들어진 객체를 공유해서 효율적으로 재사용
실무에서 이로 인해 해결하기 어려운 문제가 터지므로 잘 알아 두자.
싱글톤 패턴이든, 스프링 같은 싱글톤 컨테이너를 사용하든, 객체 인스턴스를 하나만 생성해서 공유하는 싱글톤 방식은 여러 클라이언트가 하나의 같은 객체 인스턴스를 공유하기 때문에 싱글톤 객체는 상태를 유지하게 설계하면 안된다.
지역변수, 파라미터, ThreadLocal 등 사용해야한다.
test > singleton > StatefulService
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 > singlton > StatefulServiceTest
class StatefulServiceTest {
@Test
void statefulServiceSingleton() {
ApplicationContext ac = new AnnotationConfigApplicationContext(TestConfig.class);
StatefulService statefulService1 = ac.getBean(StatefulService.class);
StatefulService statefulService2 = ac.getBean(StatefulService.class);
statefulService1.order("userA", 10000);
statefulService2.order("userB",20000);
int price = statefulService1.getPrice();
System.out.println("price = " + price);
Assertions.assertThat(statefulService1.getPrice()).isEqualTo(20000);
}
static class TestConfig {
@Bean
public StatefulService statefulService() {
return new StatefulService();
}
}
}
statefulService1의 price값을 출력 할 경우 10000을 기대 했지만 20000이 출력 되었다 -> statefulService2의 20000이 price값으로 유지
해결 방법 - 지역 변수
간단하게 코드를 수정해서 무상태로 만든다.
int userAPrice = statefulService1.order("userA", 10000);
int userBPrice = statefulService2.order("userB",20000);
//int price = statefulService1.getPrice();
System.out.println("price = " + userAPrice);
System.out.println("price = " + userBPrice);
Assertions.assertThat(userAPrice).isEqualTo(10000);
Assertions.assertThat(userBPrice).isEqualTo(20000);
public int order(String name, int price){
System.out.println("name = " + name + "price = "+ price);
return price;
}
지금까지 설명한걸 토대로 AppConfig를 보면 이상한점이 있다.
main > AppConfig
@Configuration
public class AppConfig {
@Bean
public MemberService memberService(){
return new MemberServiceImpl(memberRepository());
}
@Bean
public OrderService orderService() {
return new OrderServiceImpl(
memberRepository(),
discountPolicy());
}
@Bean
public MemberRepository memberRepository() {
return new MemoryMemberRepository();
}
@Bean
public DiscountPolicy discountPolicy() {
//return new FixDiscountPolicy();
return new RateDiscountPolicy();
}
}
memberService 빈을 만드는 코드에서 memberRepository를 호출한다.
orderService 빈을 만드는 코드에서도 memberRepository를 호출한다.
호출하게 되면 각각 다른 2개의 MemoryMemberRepository를 호출한다.
결과적으로 2개의 MemoryMemberRepository가 생성되면서 싱글톤이 깨지는 것 처럼 보인다.
검증 용도의 코드
@Bean
public MemberService memberService() {
//1번
System.out.println("call AppConfig.memberService");
return new MemberServiceImpl(memberRepository());
}
@Bean
public OrderService orderService() {
//1번
System.out.println("call AppConfig.orderService");
return new OrderServiceImpl(
memberRepository(),
discountPolicy());
}
@Bean
public MemberRepository memberRepository() {
//2번? 3번?
System.out.println("call AppConfig.memberRepository");
return new MemoryMemberRepository();
}
@Bean
public DiscountPolicy discountPolicy() {
return new RateDiscountPolicy();
}
스프링 컨테이너가 @Bean을 호출해서 빈을 생성하므로 memberRepository는 총 3번 호출 되어야 할까?
- 스프링 컨테이너가 스프링 빈에 등록하기 위해 @Bean이 붙어있는 memberRepository() 호출
- memberService() 로직에서 memberRepository() 호출
- orderService() 로직에서 memberRepository() 호출
생각한 호출 값
// call AppConfig.memberService
// call AppConfig.memberRepository
// call AppConfig.memberRepository
// call AppConfig.orderService
// call AppConfig.memberRepository
// call AppConfig.memberService
// call AppConfig.memberRepository
// call AppConfig.orderService
테스트 코드
test > singleton > ConfigurationSingletonTest
public class ConfigurationSingletonTest {
@Test
void configurationTest() {
ApplicationContext 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);
//모두 같은 인스턴스를 참고하고 있다.
System.out.println("memberService -> memberRepository = " +
memberService.getMemberRepository());
System.out.println("orderService -> memberRepository = " +
orderService.getMemberRepository());
System.out.println("memberRepository = " + memberRepository);
//모두 같은 인스턴스를 참고하고 있다.
assertThat(memberService.getMemberRepository()).isSameAs(memberRepository);
assertThat(orderService.getMemberRepository()).isSameAs(memberRepository);
}
}
결과는 memberRepository 1개 즉, 같은 인스턴스 1개만 출력
어떻게 스프링은 어떤 상황에서도 싱글톤을 보장을 해줄까?
모든 비밀은 @Configuration을 적용한 AppConfig에 있다.
AppConfig 스프링 빈조회
test > singleton > ConfigurationSingletonTest
...
@Test
void configurationDeep() {
ApplicationContext ac = new
AnnotationConfigApplicationContext(AppConfig.class);
//AppConfig도 스프링 빈으로 등록된다.
AppConfig bean = ac.getBean(AppConfig.class);
System.out.println("bean = " + bean.getClass());
//출력: bean = class hello.core.AppConfig$$EnhancerBySpringCGLIB$$bd479d70
}
...
AnnotationConfigApplicationContext 에 파라미터로 넘긴 값은 스프링 빈으로 등록
순수한 클래스 였다면?
class hello.core.AppConfig 만 출력
예상과는 다르게 클래스 명에 xxxCGLIB가 붙으면서 복잡해짐
AppConfig 클래스를 상속받은 임의의 다른 클래스를 만들고, 그 다른 클래스를 스프링 빈으로 등록
@Configuration 삭제 후 테스트를 해보면
순수 AppConfig가 스프링 빈에 등록된 것을 확인할 수 있음
테스트 진행
- 1번은 @Bean에 의해 컨테이너로 등록하기 위해
- 2번 3번은 각각 호출하면서 발생
인스턴스가 같은지 테스트 진행
- @Bean만 사용해도 스프링 빈으로 등록되지만, 싱글톤을 보장하지 않음
- memberRepository() 처럼 의존관계 주입이 필요해서 메서드를 직접 호출할 때 싱글톤을 보장하지 않음
- 스프링 설정 정보는 항상 @Configuration 을 사용하자