추가 포스팅 사유
[Language/Java] 싱글톤(Singleton) 패턴 - #1
저번 시간에 싱글톤 패턴에 대한 대략적인 내용과 싱글톤 패턴을 이용하여 객체를 생성하는 예시 코드를 포스팅 했었는데 어떤분께서 댓글로 싱글톤 패턴에는 객체 선언방법이 여러 가지가 있다고 알려주셔서 이와 관련된 부분을 추가로 공부하여 포스팅을 하고자 한다. 또한 싱글톤 패턴의 단점 또한 포스팅을 요청하셨기 때문에 이번 포스팅에서 같이 다뤄볼 예정이다.
싱글톤(Singleton) 패턴을 이용한 객체 선언방법
1. 즉시 초기화 (Eager Initialization)
public class Singleton {
private static final Singleton instance = new Singleton();
private Singleton() {}
public static Singleton getInstance() {
return instance;
}
}
저번 시간에 포스팅한 글에서 예시 코드로 다뤘던 부분이다. 사실 구독자 분께서 댓글을 남겨주시기 전까지는 싱글톤 패턴을 이용하여 객체를 선언하는 방법이 이 방법 외에 다른 방법도 존재할 거라는 생각은 하지 못했었다. 저번 시간에 다뤘던 예시 코드이기 때문에 길게 설명하지는 않겠다.
다만, 위 방법으로 싱글톤 패턴을 적용하면 클래스가 로딩될 때 인스턴스를 즉시 생성하게 되는데 이 때문에 아래와 같은 장점이자 단점을 갖는 다는 점을 알아두자.
클래스 로딩 시점에서 인스턴스를 생성하므로 getInstance() 메서드 호출 시 추가적인 시간이 소요되지 않으며, 멀티 스레드 환경에서도 인스턴스가 단 한 번만 생성되는 것을 보장한다는 장점 이 되기도 하는 반면, 싱글톤 인스턴스가 필요하지 않은 경우에도 무조건 인스턴스를 생성하기 때문에 메모리 리소스를 낭비하게 될 수 있는 단점 이 되기도 한다.
2. 정적 블록 초기화 (Static Block Initialization)
public class Singleton {
private static Singleton instance;
static {
try {
instance = new Singleton();
} catch (Exception e) {
throw new RuntimeException("Exception occured in creating singleton instance");
}
}
private Singleton() {}
public static Singleton getInstance() {
return instance;
}
}
정적 블록 초기화 (Static Block Initialization) 방법은 Static 블록을 활용하여 싱글톤 인스턴스를 생성하는 방법이다. 즉시 초기화(Eager Initialization) 방식과 비슷하지만, Static 블록을 활용하여 예외 처리 작업 등 좀 더 상세한 코드를 작성할 수 있다는 것이 장점 이다. 하지만 이 방법 역시 초기 로딩 시 싱글톤 인스턴스를 생성하기 때문에 싱글톤 인스턴스가 필요하지 않은 경우에도 인스턴스를 생성하여 리소스를 낭비할 수 있는 단점 이 있다.
3. 게으른 초기화 (Lazy Initialization)
public class Singleton {
private static Singleton instance;
private Singleton() {}
public static Singleton getInstance() {
if (instance == null) {
instance = new Singleton();
}
return instance;
}
}
게으른 초기화 (Lazy Initialization) 방식은 클래스 로딩 시 싱글톤 인스턴스를 즉시 생성하지 않는다. 싱글톤 인스턴스가 필요한 시점에 getInstance() 메서드를 호출하려 싱글톤 인스턴스를 반환 받으려고 할 때 싱글톤 인스턴스의 주소값을 갖는 참조변수가 null 이면, 즉 싱글톤 인스턴스가 생성된 적이 없으면 비로소 그 때서야 싱글톤 인스턴스를 생성하여 참조변수를 초기화 하고 싱글톤 인스턴스를 리턴하는방식이다.
따라서, 즉시 초기화 (Eager Initailization) 방식이나 정적 블록 초기화 (Static Block Initialization) 방식에 비해 좀 더 리소스를 효율적으로 다룰 수 있다는 장점 이 있다. 하지만 멀티 스레드 환경에서는 동기화 문제가 발생할 수 있다는 단점 이 존재한다.
4. 스레드 안전 싱글톤 (Thread Safe Singleton)
public class Singleton {
private static Singleton instance;
private Singleton() {}
public static synchronized Singleton getInstance() {
if (instance == null) {
instance = new Singleton();
}
return instance;
}
}
스레드 안전 싱글톤 (Thread Safe Singleton) 방식은 3번의 게으른 초기화 (Lazy Initialization) 방식에서 getInstance() 메서드 에 synchronizaed 키워드를 붙여서 메서드에 락(Lock)을 걸어 동기화를 보장한다.
따라서, 멀티 스레드 환경에서 동기화 문제가 발생할 수 있는 문제점을 해결한 방식이지만 한번에 하나의 스레드만 접근가능하게 되기 때문에 접근하지 못하는 다른 스레드들이 대기하게 되는 과정에서 오버헤드(병목현상) 이 발생할 수 있다.
간단히 정리하자면, 게으른 초기화 (Lazy Initialization) 방식의 장점 + 멀티 스레드 환경하에서도 비교적 안전하다는 장점을 갖지만 동기화 처리로 인해 오버헤드 (병목현상) 이 발생하여 성능이 저하될 수 있다는 단점 이 존재한다. 따라서, 위 방식은 싱글톤 객체가 자주 요청되는 상황에서는 효율적이지 않은 방식이다.
5. 빌 푸 싱글톤 (Bill pugh Singleton) or 요청 시 초기화 홀더 이디엄 (Initialization - on - demand holder idiom)
public class Singleton {
private Singleton() {}
private static class Holder {
private static final Singleton INSTANCE = new Singleton();
}
public static Singleton getInstance() {
return Holder.INSTANCE;
}
}
빌 푸 싱글톤 (Bill Pugh Singleton) 방식은 '게으른 초기화(Lazy Initialization) 방식의 장점' 과 '스레드 안전(Thread Safety)'이 동시에 보장된다는 장점 을 가진다. 하지만 초기화 과정이 무거운 작업을 수행하는 경우 사용자 경험에 부정적인 영향을 미칠 수 있다는 단점 을 갖는다.
게으른 초기화 (Lazy Initialization) 방식의 장점이 보장되는 이유
SIngleton 클래스 안에 중첩 정적 클래스 (Nested Static Class) 를 별도로 선언하고 해당 정적 클래스에서 Singleton 인스턴스를 초기화하는 코드를 작성하였기 때문에 '게으른 초기화(Lazy Initialization) 방식의 장점' 이 보장된다. 내부 정적 클래스는 외부 클래스가 로드될 때 함께 로드되는 것이 아니라, 내부 정적 클래스가 처음으로 참조될 때 로드되기 때문이다.
스레드 안전이 보장되는 이유
Java에서는 클래스 로딩과 초기화 과정이 원자적(atomic) 으로 이루어지며, 이 과정은 내부적으로 동기화되어 있다. 즉, 한 번에 오직 하나의 스레드만이 클래스를 로드하고 초기화할 수 있다.
따라서, Holder 클래스가 로드되고 초기화되는 시점에는 오직 하나의 스레드만이 싱글톤 인스턴스를 생성하는 것이 보장되며 이와 동시에 클래스는 단, 한번만 로딩되기 때문에 싱글톤 인스턴스가 단 한 번만 생성되는 것도 보장되는 것이다.
이러한 이유로 멀티스레드 환경에서도 한 번만 인스턴스가 생성되는 것이 보장되기 떄문에 동기화를 명시적으로 처리하지 않아도 되므로 코드가 간결해지며, 동기화로 인한 오버헤드 (병목현상) 도 없게 된다는 엄청난 장점이 되는 것이다!
사용자 경험에 부정적인 영향을 미칠 수 있는 이유
빌 푸 싱글톤 (Bill Pugh Singleton) 방식은 처음으로 싱글톤 인스턴스가 필요한 시점에 싱글톤 인스턴스를 초기화 하기 때문에 일반적으로 싱글톤 인스턴스의 초기화 시점을 직접 제어할 수 없다.
예를 들어 사용자가 어떤 특정 작업을 요청시 해당 요청이 싱글톤 인스턴스가 필요하다면 최초로 싱글톤 인스턴스의 초기화가 이루어지게 되는데 싱글톤 인스턴스를 초기화하는 과정이 무거운 작업(큰 파일을 로드하거나 복잡한 계산을 수행하는 등) 을 포함하고 있다면, 사용자는 자신이 요청한 작업에 대한 처리가 느린줄 알게되므로 사용자 경험에 부정적인 영향을 미칠 수 있다 는 것이다.
추가적으로 말하자면, 이러한 단점은 비단 빌 푸 싱글톤 (Bill Pugh Singleton) 방식에만 해당되는 것이 아니라 처음으로 필요한 시점에 싱글톤 인스턴스를 생성하는 방식인 '게으른 초기화 (Lazy Initialization)' 와 '스레드 안전 싱글톤 (Thread Safe Singleton)' 방식에 모두 해당되는 단점이다.
* 원자적(atomic) 이란?
원자적(atomic) 이라는 것은 '나누어 질 수 없는' 이라는 의미로 프로그래밍에 연산이 여러 단계로 이루어져 있더라도 한 번에 수행되며 모든 단계의 연산 과정이 완료될 때 까지 이 과정 중간에 다른 작업이 끼어들 수 있다는 것이다. 따라서, Java에서는 클래스 로딩과 초기화 과정이 원자적(atomic) 으로 이루어진다는 것은 Java에서 한 번 클래스 로딩과 초기화 과정이 수행되면, 이 과정은 중간에 중단되거나 다른 스레드에 의해 방해받지 않고 반드시 완료된다는 의미이다.
6. 열거형 싱글톤 (Enum Singleton)
private Connection connection;
private DatabaseSingleton() {
try {
String url = "jdbc:mysql://localhost:3306/my_database?useSSL=false";
String username = "root";
String password = "password";
connection = DriverManager.getConnection(url, username, password);
} catch (SQLException e) {
throw new RuntimeException("Database Connection Creation Failed : " + e.getMessage());
}
}
public Connection getConnection() {
return connection;
}
Java의 열거형 타입은 각 열거형 값이 프로그램 내 단 하나의 인스턴스만 존재하도록 보장하기 때문에 열거형 타입을 사용하여 싱글톤 인스턴스를 선언하면 싱글톤 패턴에 대한 안전성이 자연스럽게 보장된다. 이렇게 열거형 타입을 사용하여 싱글톤 인스턴스를 선언했을 때의 장점으로는 코드가 간결하고 명확해지며, '스레드 안전성' 이 보장된다는 장점 을 갖는다.
열거형의 필드들은 JVM에 의해 생성되고 초기화되는데, JVM은 열거형 타입의 각 값이 한번만 생성되어 사용되도록 보장하기 때문에 여러 스레드가 동시에 열거형 값을 참조하더라도 그 값은 항상 동일하다. 따라서, 열거형 타입은 별도의 동기화 처리 없이도 멀티 스레드 환경에서 안전하게 사용할 수 있는 것이다.
하지만, 열거형 타입은 다른 클래스를 상속 받을 수 없다.(Java의 열거형 클래스는 기본적으로 Enum클래스를 상속받으며 자바는 다중상속 지원 X ) 따라서, 만약 싱글톤 클래스가 다른 클래스를 상속해야하는 경우에는 사용할 수 없다는 단점 과 열거형 싱글톤 패턴 역시 '이른 초기화 (Eager Initialization) 방식' 과 같이 클래스가 로드되는 시점에 생성되기 때문에 싱글톤 인스턴스가 필요하지 않아도 무조건 생성되게 되어 메모리 리소스를 낭비할 수 있다는 단점 을 갖는다.
싱글톤 패턴의 공통적인 문제점
- 첫 번째로, 싱글톤 인스턴스가 너무 많은 역할을 맡게 되면 이른 바 '신의 객체 (God Object)' 로 불리며 각종 문제를 야기할 수 있다. 싱글톤 인스턴스는 전역 상태로 Application 전반에 걸쳐 공유되기 때문에 과도하게 사용되면 Application 전체적으로 결합도가 높아질 수 있는 문제가 발생할 수 있다.(이는 유지보수와 테스팅을 어렵게 만드는 지름길)
- 두 번째로, 싱글톤 클래스는 확장에 제한적이다. 쉽게 말해 기존의 싱글톤 클래스를 상속받아 새로운 기능을 추가하거나 변경하는 것이 어렵다는 것이다.
- 위와 같은 이유들로 싱글톤 패턴은 반드시 필요한 경우에만 사용하고 가능한 한 사용을 자제하는 것이 좋다.
'[Language] Java' 카테고리의 다른 글
[Language/Java] 봉인된 클래스 (Sealed Class) (38) | 2024.01.10 |
---|---|
[Language/Java] final클래스와 final메소드 (68) | 2024.01.06 |
[Language/Java] 싱글톤(Singleton) 패턴 - #1 (33) | 2023.12.30 |
[Language/Java] 자바(Java)의 메모리 구조 (0) | 2023.11.29 |
[Language/Java] SDK, JDK, JRE, JVM, JIT 란? (1) | 2023.11.28 |