스프링 없는 순수한 DI 컨테이너 AppConfig는 고객이 요청하는 만큼 객체를 생성해 반환 - 메모리 낭비
싱글톤: 클래스의 인스턴스가 딱 1개만 생성되는 디자인 패턴
private 생성자를 사용해 외부에서 임의로 new 키워드를 사용하지 못하도록 함
public class SingletonService {
private static final SingletonService instance = new SingletonService();
// JVM이 맨처음 실행할 때 static 영역을 초기화 하면서 딱 한번 객체를 하나 생성
public static SingletonService getInstance(){ // 객체 인스턴스가 필요하면 이 static 메서드를 통해서만 조회하도록 허용
return instance;
}
private SingletonService(){ // 생성자를 private으로 선언해 외부에서 new 키워드를 사용한 객체 생성을 못하도록
}
public void logic(){
System.out.println("싱글톤 객체 로직 호출");
}
}
public class SingletonTest {
// new SingletonService() -> private 생성자 호출해 컴파일 오류
@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();
System.out.println("singletonService1 = " + singletonService1);
System.out.println("singletonService2 = " + singletonService2);
// 서로 같은 객체 출력
assertThat(singletonService1).isSameAs(singletonService2);
// same : == 비교 (참조값비교)
// equal : equal 비교
}
}
문제점
- 싱글톤 패턴을 구현하는 코드 자체가 많이 들어감
- 클라이언트가 구체 클래스에 의존함(구체클래스 .getInstance()로 접근) → DIP, OCP 위반
- 테스트, 내부 속성 변경, private 생성자로 자식 클래스를 만들기 어렵다
- 유연성이 떨어짐
싱글톤 컨테이너
스프링 컨테이너는 위의 문제점을 해결하며 객체를 싱글톤으로 관리한다.
클라이언트의 코드를 고칠 필요가 전혀 없다.
- 컨테이너는 객체를 하나만 생성해서 관리
- 스프링 컨테이너 = 싱글톤 컨테이너 역할
고객의 요청이 올때마다 이미 만들어진 객체를 공유해서 재사용
싱글톤 방식의 주의점
하나의 객체를 공유하기 때문에 싱글톤 객체는 상태를 유지하게 설계하면 안된다.
특정 클라이언트가 값을 변경할 수 있는 필드가 있으면 안된다.
같은 객체이므로 값이 덮어 씌워질 수 있기 때문이다.
무상태(stateless)로 설계해야한다.
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(){
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();
Assertions.assertThat(statefulService1.getPrice()).isEqualTo(20000); // test 통과
}
@Configuration
static class TestConfig{
@Bean
public StatefulService statefulService(){
return new StatefulService();
}
}
}
해당 코드에서 스레드A가 userA, 스레드B가 userB의 코드를 호출한다 가정
같은 객체이므로 값이 덮어 씌워짐
공유 필드 price를 공유하기 때문에 값이 덮어씌워져 버린다.
💡 공유필드 조심
@Configuration
@Test
void configurationDeep(){
ApplicationContext ac = new AnnotationConfigApplicationContext(AppConfig.class);
AppConfig bean = ac.getBean(AppConfig.class);
System.out.println("bean = " + bean.getClass());
// bean = class com.hello.core.AppConfig$$SpringCGLIB$$0
}
@Configuration을 붙이면
스프링이 CGLIB라는 바이트코드 조작 라이브러리를 사용해서
AppConfig 클래스를 상속받은 임의의 다른 클래스를 만들고, 그 다른 클래스를 스프링 빈으로 등록한다.
이 임의의 클래스에서는
@Bean 이 붙은 메서드마다 이미 스프링 빈이 존재하면 존재하는 빈을 반환하고
없으면 생성해서 스프링 빈으로 등록하고 반환하는 코드가 만들어진다.
⇒ 싱글톤 보장
@Configuration을 제거하면 순수한 AppConfig로 스프링 빈에 등록되어 싱글톤이 적용되지 않는다.
💡 @Bean 만 사용해도 스프링 빈으로 등록되지만, 의존 관계 주입이 필요해 메서드를 직접 호출할 때 싱글톤을 보장하지 않는다. 💡 @Configuration을 사용하자
참고) 스프링 핵심 원리 기본편
'BACK > SPRING' 카테고리의 다른 글
[Spring] 스프링 핵심 원리 - 의존관계 자동 주입 (0) | 2024.01.31 |
---|---|
[Spring] 스프링 핵심 원리 - 컴포넌트 스캔 (0) | 2024.01.31 |
[Spring] 스프링 핵심 원리 - 스프링 컨테이너와 스프링 빈 (0) | 2024.01.30 |
[Spring] 스프링 핵심 원리 - 객체 지향 원리 적용(+ DI, IoC, 스프링 컨테이너) (0) | 2024.01.30 |
[Spring] 스프링 핵심 원리 - 객체 지향 설계와 스프링 (0) | 2024.01.30 |