개요
싱글턴이 잘 와닿지 않는 개발자를 위한 포스팅
왜 우리는 자바를 배우고 있는 와중에 싱글턴 패턴을 배울까? 많은 디자인 패턴이 있지만 왜 싱글턴부터 배우게 될까?
이건 추후에 자바로 쓰인 프레임워크인 스프링을 공부하기 때문에도 있습니다.
싱글턴 패턴은 한 번만 생성되는 객체를 보장하는 기법으로 많은 자바 개발자들이 싱글턴을 직접 구현하는 것보다 더 자주 접하는 경우가 있습니다.
바로 스프링에서 제공하는 "싱글턴 빈" 개념입니다.
스프링을 처음 접하는 주니어 개발자들은 "@Component, @Service, @Bean으로 등록하면 알아서 객체를 관리해준다!"
라는 걸 배우지만, 이게 사실 싱글턴 패턴의 원리와 연결된다는 걸 놓치기 쉽습니다.
이 글에서는 싱글턴 패턴의 기본 원리를 먼저 이해한 후, 마지막에 스프링이 어떻게 이를 활용하는지 살펴보겠습니다.
싱글턴: 오직 하나의 객체만 보장하자
먼저 아무런 제약이 없는 일반 클래스에서 시작해보자.
여기서 “객체가 하나만 존재해야 하는” 필요성이 어떻게 생기는지 과정을 살펴볼 것입니다.
1. 일반 클래스
아래처럼 생성자를 제한하지 않은 일반 클래스는 new 키워드를 통해 원하는 만큼 객체를 만들 수 있습니다.
public class NormalClass {
// 생성자를 따로 제한하지 않음
}
이 클래스를 테스트할 때 new NormalClass()
로 여러 객체를 만들면, 서로 다른 인스턴스가 생성됩니다.
public class NormalClassTest {
public static void main(String[] args) {
NormalClass obj1 = new NormalClass();
NormalClass obj2 = new NormalClass();
System.out.println("obj1: " + obj1);
System.out.println("obj2: " + obj2);
System.out.println("두 객체는 같은가? " + (obj1 == obj2));
}
}
- new NormalClass()로 생성하면 객체가 계속 새로 만들어집니다.
- obj1 == obj2는 당연히 false입니다.
2. 생성자 접근 제한
싱글턴 패턴을 위해서는 생성자를 외부에서 막아야 합니다.
-> 생성자를 private으로 만들면, 외부에서 함부로 new
로 객체를 생성할 수 없게 됩니다.
public class NotSingleton {
private NotSingleton() {
// 외부에서 new 불가
}
}
- 이 코드는 “new”로 객체를 만들 수 없게 막았지만, 이제 객체를 전혀 만들 수 없다는 문제가 생깁니다.
- 이대로는 클래스 내부에서만 객체를 만들수 있고, 외부에 반환하는 무언가가 필요합니다.
외부에 반환할 방법이 없다면 인스턴스 사용 자체가 불가능해집니다.
3. 특정 메서드로 인스턴스 반환하기
하나의 객체를 내부에서 생성하고, 이를 반환하는 정적 메서드를 마련해 봅시다. 그래야 외부에서도 사용할 수 있죠.
public class NotSingleton {
private static NotSingleton instance;
private NotSingleton() {
// 외부에서 new 불가
}
public static NotSingleton getInstance() {
if (instance == null) {
instance = new NotSingleton();
}
return instance;
}
}
- 내부에 static 필드를 하나 두고, 그걸 반환합니다.
- 생성자는 여전히 private이므로 new NotSingleton()은 외부에서 불가능합니다.
- getInstance()가 있는 이상, “최대 하나의 인스턴스”만 만들어지는 구조입니다
이제 이 클래스를 테스트하면 어떻게 될까요?
public class NotSingletonTest {
public static void main(String[] args) {
NotSingleton obj1 = NotSingleton.getInstance();
NotSingleton obj2 = NotSingleton.getInstance();
System.out.println("obj1: " + obj1);
System.out.println("obj2: " + obj2);
System.out.println("두 객체는 같은가? " + (obj1 == obj2));
}
}
- 최초 getInstance() 호출 시 객체가 생성되고, 이후에는 재사용됩니다.
- 이제 obj1 == obj2는 true가 됩니다.
이런 형태를 지연 초기화(Lazy Initialization)라고 부릅니다. 실제로 필요할 때 생성하는 방식이므로, 객체 생성 비용이 큰 경우 유리합니다.
*단, 멀티스레드 환경에서는 동기화 처리를 해줘야 안전합니다.
4. 이른 초기화(Eager Initialization)로 더 간단하게
지연 초기화보다 구현이 더 간단한 방법이 바로 이른 초기화입니다. 클래스가 로드될 때 미리 객체를 만들어 두는 방식입니다.
static final 변수는 클래스가 메모리에 로드될 때 단 한 번 초기화되기 때문에 static final 변수에 new Singleton()을 바로 할당하여 클래스가 로드될 때 객체가 즉시 생성되기 때문인데
이거에 대한 이유는 아래에 자세히 작성해뒀다.
2025.01.22 - [Java] - 예제로 배우는 결국 알아야하는 JVM 메모리 구조(순한맛)
public class Singleton {
private static final Singleton instance = new Singleton();
private Singleton() {
// 외부에서 new 불가
}
public static Singleton getInstance() {
return instance;
}
}
- private static final Singleton instance에서 이미 new로 객체가 만들어집니다.
- 클래스가 처음 로드될 때(=정적 필드가 처음 사용될 때) 생성이 이루어집니다.
- *멀티스레드 상황에서도 별도의 동기화 로직이 필요 없습니다.
- 객체를 사용하지 않아도 미리 만들어 두므로 메모리가 낭비될 수 있지만, 로직이 단순하다는 장점이 있습니다.
이렇게 하면 “오직 하나의 객체만 보장”이라는 싱글턴의 핵심 특징을 만족합니다.
최종 코드: 싱글턴 패턴(이른 초기화)
public class Singleton {
private static final Singleton instance = new Singleton();
private Singleton() {}
public static Singleton getInstance() {
return instance;
}
}
public class SingletonTest {
public static void main(String[] args) {
Singleton s1 = Singleton.getInstance();
Singleton s2 = Singleton.getInstance();
System.out.println("s1: " + s1);
System.out.println("s2: " + s2);
System.out.println("두 객체는 같은가? " + (s1 == s2));
}
}
성능과 장단점 비교
- 이른 초기화(Eager Initialization)
- 장점: 구현이 매우 간단하고 멀티스레드에서 안전.
- 단점: 애플리케이션 실행 시점에 무조건 객체를 생성해 메모리 낭비 가능성.
- 지연 초기화(Lazy Initialization)
- 장점: 객체를 실제 사용할 때만 생성하므로 메모리 절약.
- 단점: 멀티스레드 환경에서 동기화 처리가 필요해 코드가 복잡해질 수 있음.
보통 싱글턴은 “한 번만 쓸” 확률이 높은 경우가 많아, 이른 초기화를 많이 사용합니다.
하지만 객체가 크거나, 애플리케이션 전체에서 사용되지 않을 수도 있다면 지연 초기화도 충분히 고려해야 합니다.
마무리
일반 클래스로 시작하면 new 키워드로 계속 객체가 생성되어 싱글턴이 아님.
생성자 private + 정적 메서드(getInstance) 로 “하나의 객체”를 강제한다.
*이제 싱글턴 패턴의 핵심을 이해했다면, 스프링이 이 개념을 기반으로 어떻게 객체를 관리하는지 생각해봅시다.
우리가 스프링에서 @Component나 @Service 같은 어노테이션을 붙이면 스프링 컨테이너(ApplicationContext)가 자동으로 싱글턴 객체를 생성하고 관리한다.
즉, 직접 new로 객체를 만들지 않아도 스프링 내에서 한 번만 생성된 인스턴스를 재사용한다.
(이런 동작이 스프링의 기본적인 "빈 스코프"가 싱글턴인 이유다.)
싱글턴 패턴을 이해하면, 스프링이 어떻게 객체를 효율적으로 관리하는지 자연스럽게 이해할 수 있고,
자바에서 싱글턴을 직접 구현할 일은 많지 않지만, 원리를 알아두면 스프링의 동작을 더 깊이 이해할 수 있다.
'CS' 카테고리의 다른 글
Kent Beck 구현패턴 Ch02-03; 원칙과 패턴: 복잡성을 다루는 방법 (0) | 2025.01.31 |
---|---|
CORS 에러 발생 시나리오 정리 (0) | 2024.08.21 |
DNS, TCP, TLS 통신에 대한 WireShark를 이용한 패킷 분석 (1) | 2024.06.30 |