💡 싱글턴 패턴(Signleton Pattern) - 소프트웨어 디자인 패턴 중 하나로 클래스의 ‘인스턴스(Instance)가 오직 하나만 생성’되도록 보장하는 패턴으로 ‘전역 변수를 사용하지 않고도 단일 인스턴스에 접근’ 할 수 있도록 합니다.
- 애플리케이션이 실행될 때 ‘최초 한 번만 메모리를 할당’하고 여러 차례 호출이 되더라도 실제로 생성되는 객체는 하나이고 최초 생성된 이후 호출된 생성자는 최초 생성한 객체를 리턴합니다. - 이를 통해 여러 곳에서 공유하여 사용할 수 있으므로 메모리 절약 및 자원 관리 등의 장점을 얻을 수 있습니다. - 또한 이를 통해 생성된 리소스에 대한 중복 액세스를 피하고 객체 간의 일관성을 유지하기 위해 사용되며 주로 로깅, 드라이버 개체, 캐싱 및 스레드 풀, 데이터베이스 연결에 사용됩니다.
[ 더 알아보기 ] 💡 싱글턴 패턴의 사용예시
1️⃣ 데이터베이스 연결 - 데이터베이스 연결은 일반적으로 오버헤드가 큰 작업이므로, 매번 연결 객체를 새로 생성하는 것은 비효율적입니다. 이런 경우 싱글턴 패턴을 사용하여 하나의 연결 객체를 생성하고, 다른 클래스에서 이 객체를 공유하여 사용할 수 있습니다. - 여러 객체가 공유하는 단일 DB 연결은 모든 객체에 대해 별도의 DB 연결을 생성하는 데 비용이 많이 들 수 있습니다. 그렇기에 단일 인스턴스를 사용하는 싱글턴 패턴을 이용합니다.
2️⃣ 로깅 시스템 - 애플리케이션 전반에서 로그를 기록하고 관리하기 위해 사용되는데, 이때 싱글턴 패턴을 사용하면 여러 곳에서 동시에 로깅 시스템에 접근하여 로그를 기록할 수 있습니다. 이를테면, 여러 스레드에서 동시에 로그를 작성하더라도 싱글턴 패턴을 사용하면 하나의 로깅 인스턴스를 통해 동시에 로그를 기록할 수 있습니다.
싱글턴 패턴은 클래스의 인스턴스가 오직 하나만 생성되도록 보장합니다. 이를 통해 메모리 낭비를 방지하고, 여러 곳에서 일관된 방식으로 인스턴스에 접근할 수 있습니다.
안전한 인스턴스 생성
다중 스레드 환경에서 싱글턴 패턴을 사용하면 인스턴스 생성이 안전하게 이루어집니다. 여러 스레드에서 동시에 인스턴스를 생성하려는 경우, 동기화 메커니즘을 사용하여 하나의 인스턴스만 생성되도록 보장할 수 있습니다.
전역적인 접근성
싱글턴 패턴을 사용하면 어디서든 쉽게 인스턴스에 접근할 수 있습니다. 전역 변수로 선언될 수 있어 다른 객체들이 인스턴스에 접근할 수 있고, 필요한 경우 인스턴스를 공유하여 사용할 수 있습니다.
[ 더 알아보기 ] 💡 비즈니스 로직에서 싱글턴 패턴이 사용되는 경우 1️⃣ 공유 리소스에 대한 접근 제한 - 여러 스레드 또는 다른 객체가 동시에 공유 리소스에 접근해야 할 때, 싱글턴 패턴을 사용하여 하나의 인스턴스를 공유하여 동시에 접근할 수 있습니다. 이를테면, 데이터베이스 연결, 로깅 기능 등이 있습니다.
2️⃣ 상태 유지 - 어떤 객체의 상태를 유지해야 하는 경우, 싱글턴 패턴을 사용하여 하나의 인스턴스를 생성하고 상태를 공유할 수 있습니다. 이를테면, 애플리케이션 설정 정보, 캐시 등이 있습니다.
3️⃣ 리소스 절약 - 객체 생성과 소멸에 대한 오버헤드가 큰 경우, 싱글턴 패턴을 사용하여 하나의 인스턴스를 생성하고 재사용함으로써 리소스를 절약할 수 있습니다. 이를테면, 스레드 풀, 프린터 스풀링 등이 있습니다.
- 아래의 그림은 설명을 위해 작성한 그림입니다. Singleton Class와 Test Class1, Test Class2의 3개의 클래스가 존재한다고 가정합니다. - Singleton Class 내에서는 Client A, B, C라는 메서드가 존재하고, Singleton Object라는 인스턴스를 구성하였습니다. - Test Class 내에서는 이 Singleton Class의 인스턴스를 가져와서 Client A, B, C에 접근이 가능합니다.
💡 해당 과정을 통해 static method를 호출하여 '하나의 인스턴스'를 생성하여 메서드에 접근할 수 있습니다.
💡 동작 과정에 대한 예시
1. 클래스의 변수를 선언하는데 내부적으로 접근이 가능하며 클래스 내부에서만 공유가 가능하도록 변수를 구성하였습니다.
2. 외부 클래스에서 해당 클래스의 인스턴스화를 방지하기 위해 빈 형태의 생성자를 구성하였습니다.
3. 외부에서 접근이 가능하며 구성된 인스턴스를 의미합니다.
4-1, 4-2, 4-3 클래스 내에서 구성한 메서드입니다.
package com.adjh.multiflexapi.modules.practies.singleton;
/**
* 싱글턴 패턴을 연습하기 위한 모듈
*/publicclassSingletonBasic {
// 1. 내부적만 접근이 가능한 변수를 구성privatestaticfinalSingletonBasicinstance=newSingletonBasic();
// 2. 외부에서 인스턴스화를 방지하기 위해 생성자 구성.privateSingletonBasic() {
//
}
// 3. 구성된 인스턴스를 호출 할 수 있는 메서드publicstatic SingletonBasic getInstance() {
return instance;
}
// 4-1. Client A MethodpublicvoidclientA() {
System.out.println("[+] Client A 메서드입니다.");
}
// 4-2. Client B MethodpublicvoidclientB() {
System.out.println("[+] Client B 메서드입니다.");
}
// 4-3. Client C MethodpublicvoidclientC() {
System.out.println("[+] Client C 메서드입니다.");
}
}
💡 호출 부분 예시
- 해당 예시에서는 싱글턴 패턴으로 구성한 클래스를 외부 클래스에서 호출하는 방법입니다.
- Singleton 클래스를 인스턴스화하는데, getInstance()를 통해 단일 인스턴스를 반환하도록 하였습니다. - 반환받은 인스턴스에서 .clientA(), .clientB(), .clientC() 함수를 호출합니다.
SingletonBasicsingletonBasic= SingletonBasic.getInstance();
singletonBasic.clientA(); // [+] Client A 메서드입니다.
singletonBasic.clientB(); // [+] Client B 메서드입니다.
singletonBasic.clientC(); // [+] Client C 메서드입니다.
[ 더 알아보기 ] 💡private static
- 클래스 내부에서만 접근 가능하고, 클래스의 모든 인스턴스가 공유하는 변수로 사용됩니다.
- 일반 메서드는 싱글턴 패턴과 비교하면 일반 메서드에서는 중복된 리소스가 발생할 수 있습니다.
💡 일반 클래스인 MyClass는 각각 독립적인 속성을 가지는 인스턴스를 생성합니다. - instance1과 instance2는 서로 다른 이름을 가지고 있으며, 각각의 printName() 메서드를 호출하면 해당 인스턴스의 이름을 출력합니다.
💡 싱글턴 패턴인 SingletonClass는 인스턴스가 오직 하나만 생성되도록 보장합니다. - getInstance() 메서드를 통해 싱글턴 클래스의 인스턴스를 반환하게 됩니다. - 이렇게 반환된 인스턴스는 모든 인스턴스가 동일한 속성을 공유하게 됩니다. 따라서 singletonInstance1과 singletonInstance2는 동일한 인스턴스를 참조하고 있으므로, 두 인스턴스가 같은지 비교하면 true를 반환합니다.
// =============================================== 일반 클래스 =============================================== publicclassMyClass{
privateString name;
publicMyClass(String name) {
this.name = name;
}
// 인스턴스의 이름 출력publicvoidprintName() {
System.out.println(this.name);
}
}
// 일반 클래스의 인스턴스 생성
MyClass instance1 = new MyClass("Instance 1");
MyClass instance2 = new MyClass("Instance 2");
// 각 인스턴스의 독립적인 속성 출력
instance1.printName(); // Output: Instance 1
instance2.printName(); // Output: Instance 2// =============================================== 싱글턴 패턴 =============================================== publicclassSingletonClass{
privatestatic SingletonClass _instance;
privateSingletonClass() {}
// 싱글턴 클래스의 인스턴스 반환publicstatic SingletonClass getInstance() {
if (_instance == null) {
_instance = new SingletonClass();
}
return _instance;
}
}
// 싱글턴 클래스의 인스턴스 생성
SingletonClass singletonInstance1 = SingletonClass.getInstance();
SingletonClass singletonInstance2 = SingletonClass.getInstance();
// 모든 인스턴스가 동일한 속성을 공유
System.out.println(singletonInstance1 == singletonInstance2); // Output: true
💡 지연 초기화 방식 (Lazy Initialization) - 인스턴스가 클래스가 로드되는 시점이 아닌 사용자가 사용하기 전까지 생성을 지연하는 방법을 의미합니다. - 이를 통해 자원의 낭비를 방지하고 필요한 시점에 인스턴스를 생성할 수 있습니다.
💡 지연 초기화 방식 (Lazy Initialization) 동작 방식
1. 처음으로 싱글턴 클래스를 참조할 때, 인스턴스가 생성되지 않은 경우에만 인스턴스를 생성합니다. 2. 인스턴스가 생성되면, 이후에는 생성된 인스턴스를 반환합니다. 3. getInstance() 메서드를 호출할 때, 인스턴스가 생성되지 않은 경우에만 인스턴스를 생성하고 반환합니다.
💡 지연 초기화 방식 (Lazy Initialization)에 대한 문제점 1. 동기화 문제 - 멀티스레드 환경에서는 동기화 문제가 발생할 수 있으므로 안전하지 않을 수 있습니다. 2. 초기화 시간 - 이른 초기화에 비해 상대적으로 초기화 시간이 오래 걸릴 수 있습니다.
💡 이른 초기화 방식(Eager Initialization) - 인스턴스를 필요한 시점과 상관없이 ‘클래스가 로드될 때 인스턴스를 생성’하는 방법을 의미합니다. - Lazy Initialization의 동기화 문제를 회피할 수 있지만, 애플리케이션이 시작될 때부터 인스턴스가 생성되므로 자원의 낭비가 발생할 수 있습니다.
💡 스레드 안전 지연 초기화(Thread safe Lazy initialization) - Lazy Initialization 방식을 개선한 방식으로 ‘멀티 스레드 환경에서 안전하게 인스턴스를 생성’할 수 있도록 구성한 방법을 의미합니다. - 다중 스레드 환경에서는 두 개 이상의 스레드가 동시에 인스턴스를 생성하려고 할 수 있기 때문에 인스턴스 생성 메서드에 동기화(synchronization)를 적용하여 한 번에 한 스레드만 인스턴스를 생성하도록 합니다.
- Java에서 멀티스레드 환경에서의 동기화를 달성하기 위해 사용됩니다. 또한 이를 사용하면 여러 스레드가 동일한 객체 또는 메서드를 동시에 액세스 하지 못하도록 할 수 있습니다. - 이를 통해 스레드 간의 충돌이나 경쟁 조건을 방지하고 데이터 일관성을 유지할 수 있습니다. - Synchronized 키워드는 객체나 메서드의 선언부에 추가됩니다. - 예를 들어 한 스레드가 해당 변수의 접근 중인 동안 다른 스레드가 접근하지 못하도록 할 수 있습니다.
💡 스레드 안전 지연 초기화(Thread safe Lazy initialization) 문제점
1. 동시에 여러 스레드가 초기화를 수행할 경우 경합 상태 (race condition)가 발생할 수 있습니다. 2. 여러 스레드가 동시에 초기화 코드를 실행하면서 원치 않은 결과를 가져올 수 있음을 의미합니다. 3. 스레드 안전 지연 초기화는 매번 인스턴스를 생성할 때마다 초기화 코드를 실행하게 됩니다. 이는 성능 저하를 초래할 수 있습니다. 4. 코드의 복잡성을 증가시킬 수 있습니다. 동기화와 관련된 추가적인 코드를 작성해야 하며, 이는 가독성과 유지보수성을 저하시킬 수 있습니다.
💡 정적 블록 초기화 (Static block initialization) - 이른 초기화(Eager Initialization) 방식과 유사하지만 클래스가 로딩될 때 생성되지 않고 ‘정적 블록(static block) 내에서 생성되고 예외 처리가 가능한 방법’을 의미합니다. - 클래스의 정적 블록은 클래스가 로드될 때 실행되는 특별한 블록으로 클래스 변수의 초기화나 다른 초기화 작업을 수행하는 데 사용합니다.
💡 정적 블록 초기화 (Static block initialization) 동작방식
1. static 블록을 사용하여 Singleton 클래스의 인스턴스를 초기화합니다. 2. static 블록은 클래스가 로드될 때 실행되므로, Singleton 클래스가 사용되기 전에 한 번만 실행되고 인스턴스가 생성됩니다.
💡 정적 블록 초기화 (Static block initialization)의 문제점 1. 순서 의존성 문제 - 클래스 내에 여러 개의 정적 블록이 있을 경우, 이들의 실행 순서가 중요합니다. 만약 순서를 잘못 설정하거나 의존성이 있는 경우, 예상치 못한 동작이 발생할 수 있습니다. 2. 예외 처리 문제 - 정적 블록은 예외를 던질 수 있습니다. 예외가 발생하면 클래스의 초기화가 완료되지 않아, 해당 클래스를 사용할 수 없는 상태가 됩니다. 3. 가독성과 유지 보수 문제 - 정적 블록은 클래스의 초기화 과정을 복잡하게 만들 수 있습니다. 코드가 길어지고 가독성이 떨어지며, 유지 보수가 어려워질 수 있습니다.
💡 더블 체크 락킹(Double-Checked Locking) - 멀티스레드 환경에서 Lazy Initialization을 구현하기 위한 동기화 메커니즘 중 하나로 두 번 검사를 수행하여 인스턴스의 생성 여부를 확인합니다. - 첫 번째 검사는 동기화 블록에 진입하기 전에 수행되며, 인스턴스가 이미 생성되어 있는지 확인합니다. - 두 번째 검사는 동기화 블록 내부에서 수행되며, 인스턴스가 아직 생성되지 않은 경우에만 인스턴스를 생성합니다.
💡 더블 체크 락킹(Double-Checked Locking) 동작방식
1. 첫 번째 검사와 두 번째 검사를 수행하는 블록 내부에 synchronized 키워드를 사용합니다 2. 인스턴스가 이미 생성되었는지 확인하기 위해 동기화 블록에 진입하기 전에 수행 3. 동기화 블록 내부에서 인스턴스를 생성
[ 더 알아보기 ] 💡volatile - 모든 스레드가 해당 변수에 대한 작업을 메인 메모리에서 직접 수행하게 됩니다. 이는 변수의 값을 읽고 쓸 때 CPU 캐시를 거치지 않고 항상 메인 메모리를 통해 이루어진다는 것을 의미합니다. - volatile 변수를 사용하면 스레드 간의 데이터 동기화가 보장됩니다. 한 스레드에서 변수의 값을 변경하면, 다른 스레드에서는 변경된 값을 즉시 읽을 수 있습니다. 이는 스레드 간의 데이터 일관성을 유지하기 위해 매우 중요합니다.
💡 더블 체크 락킹(Double-Checked Locking) 문제점 1. 스레드 안전성 문제 - 초기화되지 않은 객체에 동시에 접근할 수 있는 상황이 발생할 수 있습니다. 이로 인해 예상치 못한 동작이 발생할 수 있습니다. 2. 메모리 가시성 문제 - 객체가 생성되기 전에 다른 스레드에서 접근할 수 있는 경우, 초기화되지 않은 객체에 접근할 수 있습니다. 이는 예기치 않은 결과를 초래할 수 있습니다. 3. 코드 복잡성 문제
💡 빌 퓨 솔루션(Bill Pugh Solution) - 스레드 안정성(thread-safety)과 지연 초기화(lazy initialization)를 보장하는 Singleton 객체를 생성하는 데 사용됩니다. - 이 방법은 내부 정적 클래스를 사용하여 Singleton 인스턴스를 생성하고, 이를 외부에서 접근할 수 있는 public 메서드를 통해 제공합니다. - 이 방식은 다중 스레드 환경에서 안전하고 성능이 우수한 Singleton 객체를 생성하는 데 도움이 됩니다
💡 빌 퓨 솔루션(Bill Pugh Solution) 문제점 1. 클래스 복잡성 - 빌 퓨 솔루션은 내부적으로 정적 중첩 클래스를 사용하여 스레드 안전성을 보장합니다. 이로 인해 클래스의 구조가 복잡해지고, 코드 가독성이 떨어질 수 있습니다. 2. 초기화 시간 - 빌 퓨 솔루션은 초기화되지 않은 객체에만 동기화를 적용하기 때문에, 초기화되지 않은 객체에 접근하는 시간이 더 오래 걸릴 수 있습니다. 3. 내부 클래스 사용 - 빌 퓨 솔루션은 내부적으로 정적 중첩 클래스를 사용하여 스레드 안전성을 보장합니다. 이는 클래스의 구조를 더 복잡하게 만들고, 유지 보수를 어렵게 할 수 있습니다.
💡 Enum 활용방식 - 스레드 안정성과 직렬화(serialization) 문제를 자동으로 처리하며, 코드가 간결하고 명확하게 사용할 수 있는 방식입니다. - enum 내부에 인스턴스를 정의하고 해당 인스턴스를 사용하여 싱글턴 객체에 접근합니다. - enum은 JVM에서 싱글턴을 보장하므로 다중 스레드 환경에서 안전하게 사용할 수 있습니다.
💡 Enum 동작 방식 : 생성
1. Singleton.INSTANCE을 통해 싱글턴 객체에 접근할 수 있습니다. 2. enum은 JVM에서 보장되는 유일한 인스턴스를 제공하므로, 스레드 안전성과 직렬화 문제를 걱정하지 않고 싱글턴 객체를 사용할 수 있습니다.
publicenumSingleton {
INSTANCE; // 싱글턴 객체를 의미합니다.// 필요한 멤버 변수와 메서드를 선언할 수 있습니다.publicvoidperformOperation() {
// 싱글턴 객체의 동작을 정의합니다.
}
}