(자바) 스프링 싱글톤 객체는 어떻게 생성되고 공유되는가?
스프링 싱글톤 객체는 어떻게 생성되고 공유되는가?
1. 스프링에서 말하는 싱글톤이란?
Spring에서 "싱글톤"이란, 클래스마다 단 하나의 인스턴스를 생성하고 이를 모든 의존 주입과 요청에 재사용하는 스코프를 의미한다. 이는 GoF의 Singleton 패턴과는 구현 방식이 다르며, Spring 컨테이너 수준에서 관리되는 일종의 싱글 인스턴스다.
- 모든 @Component, @Service, @Repository, @Controller는 기본적으로 싱글톤으로 생성된다.
- 이는 @Scope("singleton")와 동일한 의미이며, 명시하지 않아도 적용된다.
싱글톤 스코프는 성능 측면에서 효율적이며, DI 컨테이너가 객체 생명주기를 직접 제어할 수 있게 해준다.
2. GoF의 싱글톤 패턴과의 차이 (+ 예제)
구분 Spring의 싱글톤 스코프 GoF Singleton 패턴
생성 시점 | 애플리케이션 컨텍스트 초기화 시 | 첫 호출 시 (lazy), 혹은 static 초기화 |
제어 주체 | Spring 컨테이너 | 개발자 (직접 구현) |
테스트 유연성 | 의존성 주입으로 유연함 | 테스트 어려움 (전역 접근) |
DI 사용 | 가능 | 어려움 (전역 참조 사용) |
활용 예 | 대부분의 Bean (@Service, @Component) | Logger, Cache 등 |
즉, Spring은 싱글톤을 직접 구현하지 않고, 컨테이너가 대신 관리하는 구조다. 반면 GoF 패턴은 개발자가 직접 private 생성자와 static 메서드를 사용해 객체 생성을 제한하고 관리한다.
✅ 예시 1. GoF 방식 Singleton 구현
public class MySingleton {
private static final MySingleton INSTANCE = new MySingleton();
private MySingleton() {}
public static MySingleton getInstance() {
return INSTANCE;
}
}
// 사용
MySingleton obj1 = MySingleton.getInstance();
MySingleton obj2 = MySingleton.getInstance();
System.out.println(obj1 == obj2); // true
✅ 예시 2. Spring 방식 Singleton Bean
@Service
public class MyService {
public String hello() {
return "hello";
}
}
// 사용
@Autowired
private MyService myService1;
@Autowired
private MyService myService2;
System.out.println(myService1 == myService2); // true (Spring이 관리)
GoF는 인스턴스를 직접 제어해야 하고 테스트가 어렵지만, Spring은 관리 주체가 Container이므로 더 유연하고 테스트 친화적이다.
3. Bean이 실제로 생성되는 시점과 흐름
Spring Boot 애플리케이션 기준:
SpringApplication.run(Main.class)
→ ApplicationContext 생성
→ BeanDefinition 등록
→ Bean 인스턴스화
→ 의존성 주입
→ 싱글톤 캐시에 저장
이 흐름은 AbstractApplicationContext#refresh() 메서드를 중심으로 진행되며, 여기서 전처리기 실행, BeanFactory 초기화, 싱글톤 Bean 인스턴스화를 모두 수행한다.
4. BeanFactory와 singletonObjects 캐시 구조
Spring 내부에서 Bean을 생성하고 관리하는 핵심 클래스는 DefaultListableBeanFactory이다. 이 클래스는 Bean을 생성할 뿐만 아니라 캐싱 기능도 제공한다.
// DefaultSingletonBeanRegistry.java
protected final Map<String, Object> singletonObjects = new ConcurrentHashMap<>(256);
모든 싱글톤 Bean은 이 Map에 저장되며, getBean() 호출 시 캐시에서 꺼내거나, 없으면 생성 후 여기에 등록한다.
5. getBean() 호출 시 내부 동작
public Object getBean(String name) {
return doGetBean(name, null, null, false);
}
protected Object doGetBean(...) {
Object sharedInstance = getSingleton(beanName);
if (sharedInstance != null) {
return sharedInstance;
}
Object bean = createBean(beanName, mbd, args);
return bean;
}
- 이미 생성된 Bean이면 그대로 반환
- 없으면 createBean()을 통해 인스턴스 생성 후 캐시에 등록
createBean()은 내부적으로 doCreateBean()을 호출하며, 이 과정에서 의존성 주입, 초기화 콜백, 후처리기 적용 등이 이루어진다.
6. Bean 생성 전처리 과정과 후처리기
Spring은 Bean을 생성하는 과정에서 다음과 같은 훅(Hook)을 제공한다:
- BeanPostProcessor → Bean 초기화 전/후 커스터마이징 가능
- @PostConstruct → 초기화 직후 동작
- @PreDestroy → 컨테이너 종료 전 콜백
이러한 훅을 통해 개발자는 객체 생성 직후 로깅, 유효성 검사, 프록시 래핑 등을 자유롭게 할 수 있다.
7. 왜 모든 Bean을 싱글톤으로 만드는가?
성능과 일관성 때문이다.
- 매 요청마다 Bean을 새로 생성하면 메모리와 GC 비용이 커진다.
- Bean을 싱글톤으로 만들면 객체 재사용, DI 최적화, 애플리케이션 수준의 공통 로직 공유가 가능해진다.
또한 싱글톤이므로 객체 간의 참조 관계도 일정하게 유지되며, 순환 참조 방지 등의 로직도 간소화된다.
8. 싱글톤의 장점과 실무 주의점
장점
- 메모리 절약, 객체 생성 비용 최소화
- DI 컨테이너와 잘 어울림
- 상태 없는(stateless) 설계에 적합
주의점
- 인스턴스 필드를 상태 저장용으로 사용하면 안 됨
- List, Map, StringBuffer 같은 mutable 객체를 필드로 가지면 모든 요청 간 공유됨
- 동시성 문제 발생 가능 → 반드시 지역 변수나 ThreadLocal로 분리 필요
9. 실전 테스트: 싱글톤 공유 확인
@SpringBootTest
class SingletonTest {
@Autowired MemberService memberService1;
@Autowired MemberService memberService2;
@Test
void testSingletonInstance() {
assertSame(memberService1, memberService2); // ✅ 동일 인스턴스
}
}
이 테스트는 DI를 통해 주입된 Bean이 항상 동일한 객체임을 증명한다.
10. 스프링 애플리케이션 실행 시 전체 생명주기 흐름
1단계: 실행
- java -jar app.jar 또는 WAS가 .war를 실행
- main() 함수 → SpringApplication.run() 호출
2단계: SpringApplication 초기화
- ApplicationContext 생성 (AnnotationConfigApplicationContext 또는 WebApplicationContext)
- prepareContext()에서 환경 정보 설정, 리스너 바인딩
3단계: Bean 정의 스캔
- @ComponentScan 기준으로 클래스 경로 탐색
- @Component, @Service, @Repository, @Controller 등 감지하여 BeanDefinition으로 등록
4단계: Bean 생성 & 의존성 주입
- AbstractApplicationContext#refresh()에서 본격적으로 생성 시작
- BeanFactory가 Bean 생성 및 의존성 주입 수행
- 생성된 Bean은 singletonObjects라는 Map에 캐시됨 (싱글톤)
5단계: 후처리 및 초기화
- @PostConstruct, BeanPostProcessor, InitializingBean.afterPropertiesSet() 호출
- AOP 프록시 래핑 등도 이 시점에 처리됨
6단계: 애플리케이션 실행 완료
- CommandLineRunner, ApplicationRunner 등이 실행됨
- 사용자는 이제 실제 서비스 기능을 사용할 수 있음
7단계: 종료 시 처리
- @PreDestroy, DisposableBean.destroy() 호출
- Bean 제거 및 자원 정리
11. 결론 및 요약
- Spring에서 대부분의 Bean은 기본적으로 싱글톤으로 생성된다.
- ApplicationContext 생성 시 모든 Bean이 미리 생성되어 singletonObjects 캐시에 등록된다.
- Bean 생성은 getBean → createBean → doCreateBean → 의존성 주입/후처리 순으로 이루어진다.
- 싱글톤 Bean은 상태를 가지지 않도록 설계해야 하며, 공유 객체 사용 시 동시성에 유의해야 한다.
- Spring의 싱글톤은 개발자가 직접 구현하지 않아도 되며, DI 환경에서 더 유연하고 테스트 친화적인 구조를 제공한다.
다음 편 예고
▶️ 2편: 인스턴스 변수 vs static 변수 – 실무에서 언제 공유되고 어떻게 피해야 하나