스프링 프레임워크는 대부분의 빈을 싱글톤으로 관리합니다. 이번 편에서는 왜 싱글톤을 사용하는지, 직접 구현한 싱글톤 패턴과의 차이점은 무엇인지, 그리고 싱글톤 설계 시 주의할 점은 무엇인지에 대해 하나하나 짚어보겠습니다.
1. 웹 애플리케이션과 객체 생성의 문제
웹 애플리케이션에서는 수많은 사용자가 동시에 요청을 보냅니다. 예를 들어 A, B, C 사용자가 동시에 요청을 보내면 객체가 3개 생성되는 상황이 발생합니다. 이런 구조는 성능과 메모리 측면에서 매우 비효율적입니다.
객체를 매번 생성하는 것은 메모리 낭비와 GC 오버헤드로 이어지며, 결국 서버의 응답 속도까지 느려집니다.
2. 해결책: 싱글톤 패턴
객체를 매번 생성하지 않고 하나의 인스턴스를 공유하면 어떨까요? 이를 해결하는 방식이 바로 싱글톤 패턴입니다.
싱글톤 패턴 구현 예시
public class SingletonService {
private static final SingletonService instance = new SingletonService();
public static SingletonService getInstance() {
return instance;
}
private SingletonService() {}
public void logic() {
System.out.println("싱글톤 객체 로직 호출");
}
}
테스트 코드
@Test
@DisplayName("싱글톤 패턴을 적용한 객체 사용")
void singletonServiceTest() {
SingletonService s1 = SingletonService.getInstance();
SingletonService s2 = SingletonService.getInstance();
assertThat(s1).isSameAs(s2);
}
new 키워드로 객체 생성을 막고, 항상 동일한 인스턴스를 반환합니다.
3. 싱글톤 패턴의 단점
하지만 순수한 싱글톤 패턴은 다음과 같은 단점이 있습니다.
- DIP(의존 역전 원칙) 위반
- OCP(개방-폐쇄 원칙) 위반 가능성
- 테스트가 어려움
- 자식 클래스를 만들 수 없음 (생성자 private)
- 전역 상태를 가지기 쉬움
유연성이 떨어지기 때문에, 순수한 싱글톤 패턴은 종종 안티패턴으로 간주되기도 합니다.
4. 스프링 컨테이너와 싱글톤
스프링은 직접 싱글톤 패턴을 구현하지 않아도, 스프링 컨테이너가 자동으로 객체를 싱글톤으로 관리해줍니다.
ApplicationContext ac = new AnnotationConfigApplicationContext(AppConfig.class);
MemberService m1 = ac.getBean("memberService", MemberService.class);
MemberService m2 = ac.getBean("memberService", MemberService.class);
assertThat(m1).isSameAs(m2);
이처럼 getBean()으로 동일한 빈을 요청해도 항상 같은 인스턴스가 반환됩니다.
스프링 컨테이너는 내부적으로 싱글톤 레지스트리라는 저장소를 통해 싱글톤 객체를 관리합니다.
5. 싱글톤 설계 시 주의점 (매우 중요)
싱글톤 객체는 여러 클라이언트가 하나의 인스턴스를 공유합니다. 따라서 **상태를 가지는 설계(stateful)**를 하면 큰 문제가 생깁니다.
잘못된 설계 예시
public class StatefulService {
private int price; // 상태를 유지하는 필드
public void order(String name, int price) {
this.price = price; // 위험한 코드!
}
public int getPrice() {
return price;
}
}
이 설계는 여러 사용자가 동시에 주문을 하면 price 값이 서로 덮어씌워지게 됩니다.
멀티스레드 문제 발생 테스트
StatefulService service1 = ac.getBean(StatefulService.class);
StatefulService service2 = ac.getBean(StatefulService.class);
service1.order("userA", 10000);
service2.order("userB", 20000);
int price = service1.getPrice(); // 예상치 못한 값 20000 반환
실제 서비스에서는 결제 금액이 다른 사용자에게 노출되는 치명적인 문제가 발생할 수 있습니다.
6. 해결: 무상태(stateless) 설계 원칙
싱글톤 객체는 다음 원칙을 반드시 지켜야 합니다.
- 특정 클라이언트에 의존하는 필드 X
- 값을 내부에 저장하지 않고, 항상 파라미터로 전달
- 공유 상태를 두지 않기 (지역 변수, ThreadLocal 권장)
'Spring' 카테고리의 다른 글
[Spring 완전 정복 시리즈] 15편 - 컴포넌트 스캔의 시작 (0) | 2025.07.29 |
---|---|
[Spring 완전 정복 시리즈] 14편 - @Configuration과 바이트코드 조작의 마법 (1) | 2025.07.28 |
[Spring 완전 정복 시리즈] 12편 - 다양한 빈 조회와 스프링 컨테이너의 숨겨진 기능들 (2) | 2025.07.27 |
[Spring 완전 정복 시리즈] 11편 - 스프링 빈 조회와 ApplicationContext의 역할 (1) | 2025.07.27 |
[Spring 완전 정복 시리즈] 10편 - 스프링 컨테이너의 시작과 빈 등록 (1) | 2025.07.27 |