그렇게 되면 인스턴스가 무수히 많이 생성하게 되어 메모리 낭비가 심해 성능 저하 이슈가 발생할 수 있다.
-> 해당 객체를 딱 1개만 생성하고, 공유하도록 설계하는 싱글톤 패턴을 적용하여 해결할 수 있다.
Java에서의 싱글톤 패턴
우선 Java에서 싱글톤 패턴을 구현하기 위해서는 다음과 같이 구현할 수 있다.
클래스의 인스턴스가 한 개만 생성되는 것을 보장하는 디자인 패턴
생성자를 private으로 선언하여 외부에서 new 키워드를 사용한 객체 생성을 막음
public class SingletonService {
// static 영역에 객체 instance를 하나만 생성
private static final SingletonService instance = new SingletonService();
// 생성자를 private으로 선언하여 외부에서 new 키워드를 사용한 객체 생성을 막음
private SingletonService() {
}
// 해당 객체의 인스턴스가 필요할 경우 static 메서드인 getInstance()를 통해 이 instance를 공유
public static SingletonService getInstance() {
return instance;
}
public void logic() {
System.out.println("싱글톤 객체 로직 호출");
}
}
위 방법으로 순수 Java로 싱글톤 패턴을 구현할 수 있지만 여러 단점이 생겨나게 된다.
싱글톤 패턴을 구현하는 코드 자체가 늘어남
의존관계상 클라이언트가 구체 클래스에 의존 -> DIP 위반
클라이언트가 구체 클래스에 의존해서 OCP, 테스트, private 생성자로 자식 클래스 생성 불가 등의 문제점이 발생할 수 있음
테스트의 어려움
내부 속성을 변경하거나 초기화 하기 어려움
private 생성자로 자식 클래스를 만들기 어려움
유연성이 떨어짐
싱글톤 레지스트리(Singleton Registry)
스프링 컨테이너에 객체들이 빈으로 등록되면서 객체를 생성하고 관리하는데, 이 때 싱글톤 레지스트리 패턴을 사용한다.
때문에 스프링 컨테이너에 빈으로 등록되면 싱글톤으로 객체를 관리하게 되면서 위에 언급 된 문제점을 해결할 수 있다.
싱글톤 패턴을 위한 getInstance()와 같은 코드가 필요하지 않음
DIP, OCP, 테스트, private 생성자로 부터 자유롭게 싱글톤을 사용할 수 있음
싱글톤 방식의 주의점
싱글톤 방식은 여러 클라이언트가 하나의 같은 객체 인스턴스를 공유하기 때문에 무상태(stateless)로 설계해야 한다.
특정 클라이언트에 의존적인 필드 존재 X
특정 클라이언트가 값을 변경할 수 있는 필드 존재 X
가급적 읽기만 가능하도록 설계
필드 대신에 자바에서 공유되지 않는, 지역변수, 파라미터, ThreadLocal 등을 사용
문제 발생 예시
public class StatefulService {
private int price; // Stateful Field
public void order(String name, int price) {
System.out.println("name = " + name + " price = " + price);
this.price = price; // Problem!
}
public int getPrice() {
return price;
}
}
public class StatefulServiceTest {
@Test
void statefulServiceSingleton() {
ConfigurableApplicationContext ac = new SpringApplicationBuilder(TestConfig.class)
.run();
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);
assertThat(statefulService1.getPrice()).isEqualTo(10000); // fail
}
static class TestConfig {
@Bean
public StatefulService statefulService() {
return new StatefulService();
}
}
}
위 코드는 statefulService1과 statefulService2가 같은 price 공유
statefulService1이 price를 변경하면 statefulService2도 변경되는 문제 발생
@Configuration과 싱글톤
@Configuration
public class AppConfig {
@Bean
public MemberService memberService() {
System.out.println("call AppConfig.memberService");
return new MemberService(memberRepository()); // 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 OrderService(memberRepository(), discountPolicy()); // memberRepository() 호출
}
@Bean
public DiscountPolicy discountPolicy() {
return new FixDiscountPolicy();
}
}
위의 Config로 스프링 컨테이너에 등록되면 자바 코드 상으론 여러 번의 memberRepository() 호출을 하여 여러 개의 인스턴스를 생성하게된다.
하지만 실제로는 한 번만 호출되며, 스프링이 CGLIB라는 라이브러리를 통해 @Configuration이 붙은 클래스를 프록시 클래스를 만들어 싱글톤을 보장한다.
public class ConfigurationSingletonTest {
@Test
void configurationDeep() {
ApplicationContext ac = new AnnotationConfigApplicationContext(AppConfig.class);
AppConfig bean = ac.getBean(AppConfig.class);
System.out.println("bean = " + bean.getClass()); // bean = class hello.core.AppConfig$$EnhancerBySpringCGLIB$$d7f7f2a2
}
}
Configuration 프록시 클래스가 스프링 빈으로 등록되어 다른 스프링 빈을 등록할 때, 이미 등록되어있는 스프링 빈은 그대로 반환하고 없으면 생성하여 반환하는 코드가 동적으로 만들어 싱글톤을 보장한다.