싱글톤 패턴
- 게임 개발했던 것처럼, 하나의 객체를 전역적으로 생성하고 공유해서 사용하는 것.
- 구체 클래스.getInstance() 해서 가져와야 하므로, OCP와 DIP를 위반함.
- 유연성이 떨어져서 안티패턴으로 불림. 게임 개발땐 많이 썼는데, 백엔드 개발은 좀 다른 것 같다.
public class SingletonService {
private static final SingletonService instance = new SingletonService();
// 외부 생성 막음
private SingletonService() {}
public static SingletonService getInstance() {
return instance;
}
public void logic() {
System.out.println("싱글톤 객체 로직 호출");
}
}
// ---------------------------------------------------------------------
public class SingletonTest {
// 호출할 때 마다 객체를 생성하는 문제 발생 -> 메모리 비효율성
@Test
@DisplayName("스프링 없는 순수한 DI 컨테이너")
void pureContainer() {
AppConfig appConfig = new AppConfig();
MemberService memberService1 = appConfig.memberService();
MemberService memberService2 = appConfig.memberService();
System.out.println("memberService1 = " + memberService1);
System.out.println("memberService2 = " + memberService2);
assertThat(memberService1).isNotSameAs(memberService2);
}
@Test
@DisplayName("싱글톤 패턴을 적용한 객체 사용")
void singletonServiceTest() {
SingletonService singletonService1 = SingletonService.getInstance();
SingletonService singletonService2 = SingletonService.getInstance();
assertThat(singletonService1).isSameAs(singletonService2);
}
}
싱글톤 컨테이너
- 스프링 컨테이너는 싱글톤 패턴을 적용하지 않아도, 객체 인스턴스를 싱글톤으로 관리함.
- 덕분에, 싱글톤 패턴의 문제점을 해결하면서 장점을 이용할 수 있게 됨.
@Test
@DisplayName("스프링 컨테이너와 싱글톤")
void springContainer() {
AnnotationConfigApplicationContext ac = new AnnotationConfigApplicationContext(AppConfig.class);
MemberService memberService1 = ac.getBean("memberService", MemberService.class);
MemberService memberService2 = ac.getBean("memberService", MemberService.class);
assertThat(memberService1).isSameAs(memberService2);
}
싱글톤 컨테이너
싱글톤 방식의 문제점
- 값은 외부에서 변경되어선 안되며, 가급적 읽기만 가능해야 한다. 즉, 동시성 문제가 발생하지 않도록 해야 함.
- 즉, 클라이언트에 의존적인 필드와 값을 변경할 수 있는 필드가 있으면 안된다. -> 스프링 빈은 무상태(stateless)로 설계.
- 이와 관련해서 실무에서 해결하기 어려운 큰 문제들이 실제로 발생함.
package hello.core.singleton;
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;
}
}
class StatefulServiceTest {
@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 주문 금액 조회 -> 만원이 아니라 2만원으로 의도치 않게 나옴
// 동시성 문제
int price = statefulService1.getPrice();
assertThat(price).isEqualTo(20000);
}
static class TestConfig {
@Bean
public StatefulService statefulService() {
return new StatefulService();
}
}
}
// ---------------------------해결 후------------------------------
public class StatefulService {
// 상태를 유지하지 않도록 변경
// private int price;
public int order(String name, int price) {
System.out.println("name = " + name + ", price = " + price);
return price;
}
}
class StatefulServiceTest {
@Test
void statefulServiceSingleton() {
AnnotationConfigApplicationContext ac = new AnnotationConfigApplicationContext(TestConfig.class);
StatefulService statefulService1 = ac.getBean(StatefulService.class);
StatefulService statefulService2 = ac.getBean(StatefulService.class);
// ThreadA: A사용자 10000원 주문
int userAPrice = statefulService1.order("userA", 10000);
// ThreadB: B사용자 20000원 주문
int userBPrice = statefulService2.order("userB", 20000);
// ThreadA: 사용자A 주문 금액 조회 -> 만원이 아니라 2만원으로 의도치 않게 나옴
// int price = statefulService1.getPrice();
assertThat(userAPrice).isNotSameAs(userBPrice);
}
static class TestConfig {
@Bean
public StatefulService statefulService() {
return new StatefulService();
}
}
}
@Configuration
- 해당 어노테이션을 통해, 하나의 스프링 빈을 생성하고 이후에 새로 생성하지 않고 동일 객체 참조 값을 반환하게 함.
- 즉, 생성자는 한 번만 호출되는 것임. 또한 AppConfig도 스프링 빈이 됨.
- 이 어노테이션을 사용하지 않으면, 동일한 객체 참조를 반환하지 않고 새로 객체를 생성해서 반환 -> 싱글톤 깨짐
@Configuration
public class AppConfig {
// call AppConfig.memberService
// call AppConfig.memberRepository
// call AppConfig.memberRepository
// call AppConfig.orderService
// call AppConfig.memberRepository
// 위 처럼 되지 않고 세 번만 호출 됨.
// call AppConfig.memberService
// call AppConfig.memberRepository
// call AppConfig.orderService
@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 FixDiscountPolicy();
return new RateDiscountPolicy();
}
}
// ---------------------------------------------------------------------------
public class ConfigurationSingeltonTest {
@Test
void configuration() {
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();
assertThat(memberRepository1).isSameAs(memberRepository);
assertThat(memberRepository2).isSameAs(memberRepository);
}
}