싱글톤 패턴은 객체 지향 프로그래밍에서 특정 클래스가 단 하나의 인스턴스를 생성하여 사용하기 위한 패턴이다.
생성자를 여러 번 호출하더라도 인스턴스가 하나만 존재하도록 보장, 애플리케이션에서 동일한 객체 인스턴스에 접근 할 수 있도록 한다.
public class Singleton {
//클래스가 로드될 떄 인스턴스를 생성
private static final Singleton instance = new Singleton();
//private 생성자로 외부에서의 인스턴스 생성을 방지
private Singleton() {}
// 인스턴스를 반환하는 메소드
public static Singleton getInstatnce() {
return instance;
}
}
싱글톤 패턴을 사용하는 이유
- 커넥션 풀, 스레드 풀, 디바이스 설정 객체 등 인스턴스를 여러 개를 만들게 되면 불필요한 자원을 사용하게 된다.
- 따라서 프로그램이 예상치 못한 결과를 낳게된다.
- 객체를 필요할 떄 마다 생성하는 것이 아닌 단 한 번만 생성하여 전역에서 이를 공유하고 사용할 수 있게 하기 위해 싱글톤 패턴을 사용한다.
싱글톤 패턴의 장단점
장점
- 유일한 인스턴스 : 싱글톤 패턴이 적용된 클래스의 인스턴스는 전역에서 단 하나만 존재하도록 보장한다. 이는 객체의 일관성을 유지하고 전역에서 접근 가능하도록 한다.
public class Singleton {
private static Singleton instance = new Singleton();
private Singleton() {}
public static Singleton getInstance() {
return instance;
}
}
public class Main {
public static void main(String[] args) {
Singleton instance1 = Singleton.getInstance();
Singleton instance2 = Singleton.getInstance();
// 두 인스턴스는 동일하다.
System.out.println(instance1 == instance2); // true
}
}
- 메모리 절약 : 인스턴스가 단 하나뿐이므로 메모리가 절약된다. 생성자를 여러 번 호출하더라도 새로운 인스턴스가 생성되지 않아 메모리 점유 및 해제에 대한 오버헤드를 줄인다.
public class Singleton {
private static Singleton instance = new Singleton();
private Singleton() {
System.out.println("Singleton instance created");
}
public static Singleton getInstance() {
return instance;
}
}
public class Main {
public static void main(String[] args) {
Singleton instance1 = Singleton.getInstance();
Singleton instance2 = Singleton.getInstance();
// 생성자는 한 번만 호출됨
// "Singleton instance created" 는 한번만 출력된다.
}
}
- 지연 초기화 : 인스턴스가 실제로 사용되는 시점에 생성하여 초기 비용을 줄일 수 있다.
public class LazySingleton {
// 유일한 인스턴스를 저장하기 위한 정적 필드, null
private static LazySingleton instance;
private LazySingleton() {
System.out.println("LazySingleton instance created");
}
// 유일한 인스턴스를 반환하는 메서드
public static LazySingleton getInstance() {
if (instance == null) { // 인스턴스가 null이면
instance = new LazySingleton(); // 새로운 인스턴스를 생성
}
return instance;
}
}
public class Main {
public static void main(String[] args) {
// 첫 번째 getInstance() 호출 시 인스턴스가 생성, 메세지 출력
LazySingleton instance1 = LazySingleton.getInstance();
// 두 번째 호출 시 이미 생성된 인스턴스를 반환
LazySingleton instance2 = LazySingleton.getInstance();
// 생성자는 처음 호출 시에만 호출됨
}
}
단점
- 결합도 증가 : 싱글톤 패턴은 전역에서 접근을 허용하기 때문에 인스턴스에 의존하는 경우 결합도가 증가된다.
public class Singleton {
private static Singleton instance = new Singleton();
private Singleton() {}
public static Singleton getInstance() {
return instance;
}
public void doSomething() {
System.out.println("Doing something");
}
}
public class DependentClass {
//performAction()은 Singleton 인스턴스에 의존한다.
public void performAction() {
Singleton singleton = Singleton.getInstance();
singleton.doSomething();
}
}
- 테스트 복잡성 : 단 하나의 인스턴스만을 공유하기 때문에 싱글톤 클래스를 의존하는 클래스는 결합도가 증가되면서 테스트가 어려 울 수 있다.
import org.junit.jupiter.api.Test;
import static org.mockito.Mockito.*;
public class DependentClassTest {
@Test
public void testPerformAction() {
// 테스트가 Singleton 인스턴스에 의존하고 있음
DependentClass dependent = new DependentClass();
dependent.performAction();
// 여기서 테스트가 어려운 이유는 Singleton 인스턴스를 모킹할 수 없다.
// Singleton.getInstance()가 항상 실제 Singleton 인스턴스를 반환
// 따라서, 이 코드는 Singleton의 doSomething() 메서드가 제대로 동작하는지 확인하는 테스트가 아니라
// 실제 Singleton 인스턴스에 의존하게 됨
}
}
DI를 사용하면 단위 테스트 작성이 쉬워진다, DependentClass가 Singleton 클래스에 직접 의존하지 않도록 한다.
public class DependentClass {
private Singleton singleton;
// 생성자를 통해 Singleton 인스턴스를 주입받음
public DependentClass(Singleton singleton) {
this.singleton = singleton;
}
public void performAction() {
singleton.doSomething();
}
}
import org.junit.jupiter.api.Test;
import static org.mockito.Mockito.*;
public class DependentClassTest {
@Test
public void testPerformAction() {
// Mock 생성
Singleton mockSingleton = mock(Singleton.class);
// DependentClass 인스턴스 생성
DependentClass dependent = new DependentClass(mockSingleton);
dependent.performAction();
// performAction 호출 시 mockSingleton의 doSomething이 호출되었는지 확인
verify(mockSingleton).doSomething();
}
}
- 상태 관리의 어려움 : 싱글톤 클래스가 상태를 가지고 있는 경우 전역에서 사용되어 변경 될 수 있다. 이로 인해 예상치 못한 동작이 발생할 수 있다.
- 전역에서 접근 가능 : 애플리케이션 내 어디서든 접근이 가능한 경우, 무분별한 사용을 막기 힘들다.
public class Singleton {
private static Singleton instance = new Singleton();
private Singleton() {}
public static Singleton getInstance() {
return instance;
}
public void doSomething() {
System.out.println("Doing something");
}
public int getState() {
return state;
}
public void setState(int state) {
this.state = state;
}
}
public class Main {
public static void main(String[] args) {
Singleton singleton = Singleton.getInstance();
singleton.setState(10);
// 다른 곳에서 상태가 변경될 수 있음
System.out.println(singleton.getState()); // 10
}
}
public class UncontrolledAccess {
public void misuseSingleton() {
// 다른 클래스에서도 무분별하게 접근 가능
Singleton singleton = Singleton.getInstance();
singleton.doSomething();
}
}
싱글톤 패턴을 사용할 때에는 장단점을 고려하여 상황에 맞게 적절히 사용해야 한다. 메모리 효율성은 높일 수 있지만 그에 따를 수 있는 문제점들이 있다.
싱글톤 패턴의 주의사항
- 싱글톤 패턴은 단일 객체이기 때문에 공유 객체로 사용된다.
- 따라서 상태 값은 가지지 않는것이 좋다.
- 특정 참조 변수가 상태를 변경했을 때, 다른 참조 변수에도 영향을 끼친다.
- 상태 값이 아닌 읽기 전용 속성을 가지거나 또 다른 단일 객체를 참조하는 속성을 가지는 경우에는 문제가 되지 않는다.
또한 멀티 스레드 환경에서 Thread Safe하지 않는 문제점이 있다.
싱글톤 패턴은 여러 스레드가 동시에 접근하는 경우 문제가 발생한다.
public class Singleton {
private static Singleton instance;
private Singleton() {
System.out.println("Singleton instance created");
}
public static Singleton getInstance() {
if (instance == null) { // 첫 번째 체크
instance = new Singleton();
}
return instance;
}
}
getInstance()는 Thread Safe하지 않다. 여러 스레드가 동시에 호출하면 instance 필드가 여러 번 초기화 될 수 있다.
여러 스레드가 동시에 생성
public class Main {
public static void main(String[] args) {
Runnable task = () -> {
Singleton singleton = Singleton.getInstance();
System.out.println(singleton);
};
// 여러 스레드를 생성하여 동시에 싱글톤 인스턴스를 요청
for (int i = 0; i < 10; i++) {
new Thread(task).start();
}
}
}
Singleton instance created
Singleton instance created
Singleton@1a2b3c4d
Singleton@5e6f7g8h
Singleton@1a2b3c4d
Singleton@5e6f7g8h
...
두 개 이상의 메세지가 보이면 싱글톤 인스턴스가 여러번 생성되었음을 의미한다.
Thread Safe Singleton
public class Singleton {
private static Singleton instance;
private Singleton() {
System.out.println("Singleton instance created");
}
public static synchronized Singleton getInstance() {
if (instance == null) {
instance = new Singleton();
}
return instance;
}
}
- synchronized는 스레드가 동시에 접근할 수 있는 공유 자원에 대한 동시성을 제어하기 위해 사용 된다.
- 특정 메서드나 블록이 한 번에 하나의 스레드만 접근할 수 있도록 보장한다.
Singleton instance created
Singleton@1a2b3c4d
Singleton@1a2b3c4d
Singleton@1a2b3c4d
...
이제 실행 결과는 다음과 같이 하나의 인스턴스만 생성되었음을 알 수 있다.
'Design pattan' 카테고리의 다른 글
MVC(Model-View-Controller) Pattern (0) | 2023.12.15 |
---|