싱글톤 패턴
- 클래스의 인스턴스가 딱 한 개만 생성되도록 보장하는 디자인 패턴
- 생성자가 여러 차례 호출되더라도 실제로 생성되는 객체는 하나임
- 최초 생성 이후에 다시 생성자가 호출되었을 때, 객체를 새로 생성하지 않고 최초의 생성자가 생성한 객체를 리턴한다.
- 주로 공통된 객체를 여러개 생성해서 사용하는 DBCP(Database Connection Pool)와 같은 상황에서 많이 사용된다.
- 스프링에서는 스프링 컨테이너에서 빈을 싱글톤 패턴으로 관리한다.
- 싱글톤 패턴으로 객체를 관리하지 않으면, 고객의 트래픽 만큼 객체를 생성하고 소멸해야 한다. (초당 고객 트래픽이 500이면 객체를 500개 생성하고 소멸되어야 한다. -> 메모리 낭비)
- 따라서 객체를 1개만 생성하고 공유하도록 설계하여 보다 효율성을 높인다.
- 싱글톤 패턴을 적용하면 고객의 요청이 올 때 마다 객체를 생성하는 것이 아니라 이미 만들어진 객체를 공유한다.
싱글톤 패턴 구현 코드
public class SingletonService{
// 1. static final 로 지정하여 객체를 1개만 생성하고 공유될 수 있도록 한다.
private static final SingletonService instance = new SingletonService();
// 2. instance를 조회하는 메서드를 static으로 만든다.
// instance 객체가 필요하면 이 static 메서드를 통해서만 조회하도록 허용한다.
public static SingletonService getInstance(){
return instance;
}
// 3. 생성자를 private로 선언하여 외부에서 생성자를 호출하지 못하도록 막는다.
private SingletonServie(){}
}
- static: 클래스가 로드될 때 한 번만 메모리에 할당, 모든 인스턴스에서 공유할 수 있도록 함.
- final: 변수/메소드/클래스에 대한 변경을 금지. 상수값
싱글톤 패턴의 문제점
- 싱글톤 패턴을 구현하는 코드가 많이 들어간다
- 의존관계상 클라이언트가 구체 클래스에 의존한다: OCP, DIP를 위반
- 싱글톤 패턴을 구현하기 위해서는 생성자를 private로 막아야 하기 때문에 클래스 내부에서 직접 객체를 생성해야 함. 따라서 클라이언트가 객체를 호출할 때 특정 클래스를 직접적으로 의존할 수밖에 없음.
- static 메소드는 오버라이딩이 불가하기 때문에, 클라이언트에서 인터페이스가 아닌 구현 클래스를 직접 호출해야 한다. 따라서 DIP를 위반.
- 클라이언트가 구체 클래스에 직접적으로 의존하므로, 호출하는 클래스가 달라진다면 클라이언트의 코드를 수정할 수밖에 없다. OCP를 위반.
- 테스트하기 어렵다.
- 내부 속성을 변경하거나 초기화하기 어렵다.
- private 생성자로 자식 클래스를 만들기 어렵다.
- 결론적으로 유연성이 떨어진다.
싱글톤 컨테이너
- 스프링 컨테이너는 싱글톤 패턴의 문제점을 해결하면서 객체 인스턴스를 싱글톤으로 관리한다
- 스프링 컨테이너에서 싱글톤으로 관리되는 객체 인스턴스가 바로 스프링 빈이다.
- 스프링 컨테이너는 클래스에 직접 싱글톤 패턴을 적용하지 않아도 객체 인스턴스를 싱글톤으로 관리한다.
- 스프링 컨테이너는 싱글톤 컨테이너의 역할을 하며, 이렇게 싱글톤 객체를 생성하고 관리하는 기능을 싱글톤 레지스트리라고 한다.
- 스프링 컨테이너는 싱글톤 패턴의 단점을 해결하면서 객체를 싱글톤으로 유지할 수 있다.
- 싱글톤 패턴을 위한 코드가 따로 필요하지 않다.
- DIP, OCP, 테스트, private 생성자로부터 자유롭게 싱글톤 패턴을 사용할 수 있다.
싱글톤 패턴 적용 전/후
싱글톤 패턴을 적용하지 않으면 클라이언트가 요청을 할 때마다 객체를 새로 생성
싱글톤 패턴을 적용하면 클라이언트가 요청할 때마다 같은 객체를 리턴. (하나의 객체를 공유)
싱글톤 방식 사용 시 주의점
- 싱글톤 패턴은 여러 클라이언트가 하나의 같은 객체를 공유하기 때문에 싱글톤 객체는 상태를 유지(stateful)하는 것이 아닌, 무상태(stateless) 로 설계해야 한다.
- 무상태(stateless) 설계란,
- 특정 클라이언트에 의존적인 필드가 있으면 안된다.
- 특정 클라이언트가 값을 변경할 수 있은 필드가 있으면 안된다.
- 가급적 읽기만 가능해야 한다.
- 필드 대신 자바에서 공유되지 않는 지역변수, 파라미터, ThreadLocal 등을 사용해야 한다.
- 스프링 빈의 필드에 공유 값을 설정하면 정말 큰 장애가 발생할 수 있기 때문에 주의해야 한다.
상태 유지(stateful)의 문제점 예시
상태를 유지하는 문제점을 가진 클래스
public class StatefulService{
private String name; // 상태를 유지하는 필드
pubilc void callName(String className, String name){
System.out.println("반 이름: " + className + ", 학생이름: " + name);
this.name = name; // 필드 값 변경 <- 문제 발생
}
public String getName(){
return name;
}
}
테스트코드, stateful 클래스 호출부
public class StatefulServiceTest{
@Test
void statefulServiceTest(){
ApplicationContext ac = new AnnotationConfigApplicationContext(TestConfig.class);
// 스프링 싱글톤 컨테이너에서 StatefulService 객체를 꺼냄. statefulService1과 2는 같은 인스턴스임.
StatefulService statefulService1 = ac.getBean("statefulService", StatefulService.class);
StatefulSerfice statefulService2 = ac.getBean("statefulService", StatefulService.class);
// ThreadA - A사용자: 1반 홍길동 학생 호출
statefulService1.callName("1반", "홍길동");
// ThreadB - B사용자: 2반 김철수 학생 호출
statefulSerfice2.callName("2반", "김철수");
// ThreadA: A사용자가 부른 학생 조회 -> "홍길동" 기대
String name1 = statefulService1.getName();
// ThreadA: A사용자는 홍길동을 기대했지만 김철수 출력: 필드값을 변경하고 유지하기 때문(stateful)
System.out.println("반 이름: " + className + ", 학생이름: " + name);
// 객체를 공유하기 때문에 사용자 A와 B, ThreadA, B는 최종적으로 저장된 이름, "김철수"를 공유
Assertions.assertThat(statefulService1.getName()).isSameAS(statefulService2.getName());
}
static class TestConfig{
@Bean
public StatefulService statefulService(){
return new StatefulService();
}
}
}
- 위의 예시에서, StatefulService의 name은 공유필드이다. (statefulService 인스턴스 하나를 여러 클라이언트가 공유하므로 필드 값도 공유된다.)
- 그런데 callName()의 로직에서 이 공유필드의 값을 변경하는 코드가 있다. (특정 클라이언트의 상태를 저장한다.)
- 하나의 인스턴스를 여러 클라이언트가 참조하는 구조이므로, 공유 필드의 값이 계속 변경된다.
- 따라서 위의 테스트 코드에서, 사용자 A가 호출한 학생은 1반 홍길동인데, 확인을 해보면 1반 김철수가 출력된다. 사용자 B의 값이 저장되었기 때문이다.
- 실무에서 종종 공유필드로 인해 큰 문제가 생기므로 주의하자.
- 스프링 빈은 항상 무상태(stateless)로 설계해야 한다.
'Spring' 카테고리의 다른 글
스프링 핵심원리: 빈 스코프 (1) | 2025.02.07 |
---|---|
빈 생명주기 콜백 (1) | 2025.02.04 |
스프링 컨테이너와 스프링 빈(ApplicationContext, BeanFactory, BeanDefinition) (1) | 2024.11.27 |
객체 지향 설계와 SOLID 원칙, 스프링 (0) | 2024.10.28 |
Spring Bean, DI, Component Scan (0) | 2024.09.25 |