싱글톤(Singleton) 패턴
인스턴스를 오직 한 개만 제공하는 클래스
- 시스템 런타임, 환경 세팅에 대한 정보 등, 인스턴스가 여러 개 일 때 문제가 생길 수 있는 경우가 있다. 인스턴스를 오직 한 개만 만들어 제공하는 클래스가 필요할 때 사용한다.
# Try1 - 단순구현
instance에 static 키워드를 사용하여 생성자를 사용하지 않아도 접근할 수 있게 했다.
생성자를 private 하게 만들어 외부에서 새로운 인스턴스를 만들지 못하게 했다.
[-] 만약에 Runtime.instance가 없는 경우, 재생성하지 못한다.
[-] Runtime이 lazy initialization 되게 구현하면 성능적으로 더 좋을 것 같다.
public class App {
public static void main(String[] args) throws Exception {
Runtime r1 = Runtime.instance;
Runtime r2 = Runtime.instance;
System.out.println(r1 == r2);
}
}
class Runtime {
static final Runtime instance = new Runtime();
private Runtime() {
}
}
# Try 2 - lazy initialization + 동기화(synchronized) + null check
getInstance 함수로 instance에 접근할 때 생성되도록 하여 lazy initialization 처리가 되었다. null check도 같이 한다.
synchronized 키워드로 멀티쓰레드 환경도 안전하게 처리한다. 참고로 static 함수의 경우 클래스 단위로 lock이 걸리게 된다.
[-] getInstance 함수를 사용할 때마다 lock 상태를 체크하는 메커니즘을 처리해야 하므로 자주 사용할 경우 성능이 저하될 수 있다.
public class App {
public static void main(String[] args) throws Exception {
Runtime r1 = Runtime.getInstance();
Runtime r2 = Runtime.getInstance();
System.out.println(r1 == r2);
}
}
class Runtime {
private static Runtime instance;
private Runtime() {
}
public static synchronized Runtime getInstance() {
if (instance == null) {
instance = new Runtime();
}
return instance;
}
}
# Try 3 - 동기화 처리 최소화로 lock 감소(성능 향상)
- getInstance 수준에서 synchronized 했던 것에서 실제 클래스를 사용하는 시점을 syncrhonized 처리함으로써 동기화 처리로 인한 성능 저하를 최소화했다.
- instance를 volatile 변수로 변경하여 멀티스레드 환경에서 동일한 값을 참조할 수 있게 처리해야 한다. 성능을 위해 Main memory에서 읽은 변숫값을 CPU Cache에 저장해두고 사용하는데, 멀티쓰레드 환경에서 쓰레드들이 값을 읽어 올 때 각각의 CPU Cache에 저장된 값이 다를 수가 있다. 이는 변수값 불일치 문제를 발생시킨다. volatile 키워드를 사용하면 항상 Main meory에서 저장하고 읽어 오기 때문에 변수값 불일치 문제를 해결할 수 있다.
- "instance == null"을 두 번 체크함으로써 아래와 같이 객체 중복 생성을 방지할 수 있다.
첫 번째 체크 후 A, B 쓰레드가 Runtime 클래스를 차례대로 사용하게 된다.
2번 체크하는 경우__A쓰레드: 널 체크 -> 객체 생성 -> B쓰레드: 널 체크 -> 객체 미생성
1번 체크하는 경우__A쓰레드: 객체 생성 -> B쓰레드: 객체 생성
[-] volatile를 사용해야 하고, Java 1.5부터 지원하므로 사용성이 단점이 된다.
class Runtime {
private static volatile Runtime instance;
private Runtime() {
}
public static Runtime getInstance() {
if (instance == null) {
synchronized (Runtime.class) {
if (instance == null) {
instance = new Runtime();
}
}
}
return instance;
}
}
# Try 4 - static inner class 사용 (추천👍)
* synchronized 키워드를 사용하지 않고 멀티쓰레드 환경에서 안전한 싱글톤은 어떻게 구현할까?
instance를 사용할 때 생성해주면 된다. 하지만 instance를 미리 생성할 경우, lazy initialization 처리가 되지 않았었다.
static inner 클래스를 이용해서 사용할 때만 생성되게 처리해주자. static inner 클래스를 사용하면 getInstance를 호출할 때, inner class 초기화되고 그때 인스턴스가 생성되게 처리할 수 있다.
* static 키워드를 사용하더라도 initialization은 사용할 때 작동한다.(참고 docs.oracle.com)
[ The Loading Process ]
클래스 로딩은 다른 클래스가 클래스를 참조하자마자 즉시 클래스를 로드하거나 클래스 초기화가 필요할 때까지 클래스를 지연 로드하도록 구현할 수 있는 Java의 ClassLoader에 의해 수행된다. 클래스가 실제로 사용되기 전에 로드되면 초기화되기 전에 내부에 있을 수 있다. 이것은 JVM에 따라 다를 수 있다. JLS는 정적 초기화가 필요할 때 클래스가 로드되도록 보장합니다. 특히, 클래스 로더는 클래스 및 인터페이스의 이진 표현을 캐시하거나 예상 사용량에 따라 미리 가져오거나 관련 클래스 그룹을 함께 로드할 수 있다. 예를 들어 클래스 로더가 이전 버전을 캐시했기 때문에 새로 컴파일된 클래스 버전을 찾을 수 없는 경우 이러한 활동은 실행 중인 애플리케이션에 완전히 투명하지 않을 수 있다. 한 마디로 로딩은 JVM 맘대로 한다.
[ When Initialization Occus ]
클래스 또는 인터페이스 유형 T는 다음 중 하나가 처음 발생하기 직전에 초기화된다.
* T는 클래스이고 T의 인스턴스가 생성될 때.
* T는 클래스이고 T에서 선언한 정적 메서드가 호출될 때.
* T에 의해 선언된 정적 필드가 할당될 때.
* T에 의해 선언된 정적 필드가 사용되고 필드가 상수 변수가 아닐 때.
class Runtime {
private Runtime() {
}
public static Runtime getInstance() {
return LazyRuntime.instance;
}
private static class LazyRuntime {
private static final Runtime instance = new Runtime();
}
}
단순한 방법에서부터 멀티스레드를 지원하는 구현, 성능을 고려한 구현까지 싱글톤 패턴을 구현하는 다양한 방법을 알아봤다.
하지만 이렇게 구현한 패턴도 우리가 의도한 대로 사용하지 않고 싱글톤 패턴이 깨지게 사용될 수 있다. 어떻게? 🤔
# 싱글톤 파괴 1 - 리플렉션 사용
- 리플렉션이란? 자바의 리플렉션은 클래스, 인터페이스, 메소드들을 찾을 수 있고, 객체를 생성하거나 변수를 변경하거나 메소드를 호출할 수 있다. 어떤 경우에 사용되나? 프레임워크나 IDE에서 이런 동적인 바인딩을 이용한 기능을 제공한다.
- setAccesssible(true)를 사용해서 private인 Runtime 생성자에 접근할 수 있게 수정해버렸다. 그렇게 수정한 생성자로 new Instance(새로운 인스턴스)를 생성하게 되므로 두 인스턴스는 다르게 된다.
public class App {
public static void main(String[] args) throws Exception {
Runtime r1 = Runtime.getInstance();
Constructor<Runtime> declaredConstructor = Runtime.class.getDeclaredConstructor();
declaredConstructor.setAccessible(true);
Runtime r2 = declaredConstructor.newInstance();
System.out.println(r1 == r2);
}
}
# 싱글톤 파괴2 - 직렬화 & 역정렬화 사용
Serializable 하다는 것은 직렬화, 역직렬화가 가능하다는 것이고 이는 파일로 저장해뒀다가 다시 로딩할 수 있다는 말이다.
역직렬화를 할 때는 반드시 생성자를 사용해서 인스턴스를 만들어줘서 다른 객체가 된다.
public class App {
public static void main(String[] args) throws Exception {
Runtime r1 = Runtime.getInstance();
Runtime r2;
try (ObjectOutput out = new ObjectOutputStream(new FileOutputStream("runtime.obj"))) {
out.writeObject(r1);
}
try (ObjectInput in = new ObjectInputStream(new FileInputStream("runtime.obj"))) {
// 역정렬화를 할 때는 반드시 생성자를 사용해서 인스턴스를 만들어주기 때문에 다른 객체가 된다.
r2 = (Runtime) in.readObject();
}
System.out.println(r1 == r2);
}
}
- readResolve() - 역정렬화로 인한 싱글톤 파괴는 방어 가능하다.
class Runtime implements Serializable {
private Runtime() {
}
public static Runtime getInstance() {
return LazyRuntime.instance;
}
private static class LazyRuntime {
private static final Runtime instance = new Runtime();
}
// 역정렬화는 아래와 같이 방어 가능.
// 이 시그니처를 가지고 있으면 역정렬화를 할 때 아래 메소드를 사용하게 된다.
protected Object readResolve() {
return getInstance();
}
}
# Try 5 - 싱글톤 필살기
싱글톤 패턴을 구현하고 싶은 클래스를 enum으로 만들면 역정렬화를 해도 유일한 인스턴스를 유지하며, 리플렉션은 사용하지 못하게 처리된다. 구현이 매우 쉽고, 아래와 같이 구현하고 게터, 세터, 생성자, 함수 모두 구현 가능하다.
하지만 lazy initialization이 되지 않는 것과 enum만 상속할 수 있다는 점이 단점이다.
enum Runtime {
instance
}
'Develop > Fundmental' 카테고리의 다른 글
Builder Pattern (빌더 패턴) (0) | 2022.04.28 |
---|---|
팩토리 패턴(Factory Pattern) (0) | 2022.04.26 |
[cmd] 사용중인 포트 PID 찾기(netstat option) (0) | 2021.10.16 |
SIGSEGV, SIGABRT 가 뭐지?🤷♂️ (0) | 2021.09.26 |
[linux] 리눅스에서 메모리가 부족할 때 (0) | 2021.09.26 |
댓글