목차
싱글톤 패턴은 어떠한 클래스에 대한 객체(=인스턴스)를 해당 프로그램에서 단 하나만 가지도록 강제하는 구현 패턴 입니다. 단 하나만 있지 않으면 문제가 생기거나, 하나만 있어도 문제가 없을 때 사용하면 됩니다.
자바로 싱글톤 패턴을 구현하는 방법들을 점차 이전의 단점을 해결하면서 변화하는 코드들로 짧게 얘기해보려 합니다. 주관적 견해도 들어가 있으므로 정답은 아닙니다.
1. 가장 기초가 되는 구현 방법
class ConnectionManager {
private final static ConnectionManager instance = new ConnectionManager();
private ConnectionManager() {}
public static ConnectionManager getInstance() {
return instance;
}
}
private 생성자로 다른 곳에서 new로 생성되는 것을 막습니다. 자바 실행 시 instance가 생성되고 이후 다른 곳에서 getInstance()로 하나만 생성되어 있는 instance를 받아 사용합니다.
이 경우 사용하지 않을 생각이더라도, 실행 시 instance에 객체가 생성되어 있다는 단점이 있습니다. 예를들어 DB 연결과 관련된 싱글톤 클래스인데, DB를 사용하지 않더라도 항상 메모리에 올라와 있게 됩니다.
2. 사용하려고 할 때 생성하자
class ConnectionManager {
private static ConnectionManager instance = null;
private ConnectionManager() {}
public static ConnectionManager getInstance() {
if (instance == null)
instance = new ConnectionManager();
return instance;
}
}
'1'의 instance를 null로 두고 있다가, getInstance()를 통해 누군가 사용하려 할 때 instance가 null일 때만 한 번 객체를 생성해줍니다. 이후 하나만 생성된 instance를 리턴해주는 방식입니다.
싱글 스레드라면 문제가 없으나, 멀티 스레드 환경에서 Thread safe를 만족하지 못합니다. 우연히 근접한 시간에 두 스레드가 getInstance()를 호출할 경우 new 가 2번 호출될 수 있습니다.
3. synchronized로 해결해보자
class ConnectionManager {
private static ConnectionManager instance = null;
private ConnectionManager() {}
public static synchronized ConnectionManager getInstance() {
if (instance == null)
instance = new ConnectionManager();
return instance;
}
}
getInstance() 함수에 synchronized를 붙이면 한 스레드에서 접근 시 다른 스레드의 접근을 막을 수 있어서 Thread safe를 만족하게 됩니다.
다만 synchronized가 getInstance()를 호출할 때 마다 수행되므로 동작 시간이 오래걸립니다. 멀티 스레드에 대해 동기화 시키는 부분은 어느 언어나 흔히 말하는 'expensive'한 연산입니다. 물론 여기 하나 붙였다고 그렇게 유의미하게 차이가 나냐고 하면 또 그렇진 않은데, 이후 더 좋은 방법이 있긴 하니 넘어가겠습니다!
4. 객체 생성시에만 synchronized를 적용시켜 보자
class ConnectionManager {
private static ConnectionManager instance = null;
private ConnectionManager() {}
public static ConnectionManager getInstance() {
if (instance == null) {
synchronized (ConnectionManager.class) {
if (instance == null) {
instance = new ConnectionManager();
}
}
}
return instance;
}
}
synchronized를 getInstance() 함수 자체에 걸지 않고 안쪽으로 가져갔습니다. 그렇다면 멀티스레드 환경에서 우연히 충돌이 났을 경우에만 synchronized를 적용할 수 있으므로 '3'에서의 성능 문제도 해결되었습니다.
다만 Serializable을 implements한 경우 역직렬화 시 새로운 객체가 생성될 수 있고, 리플렉션을 통한 공격에 취약하다.
5. enum을 사용하자
enum ConnectionManager {
INSTANCE;
}
직렬화 및 리플렉션 문제를 해결하기 위해 enum을 싱글톤 객체처럼 사용할 수도 있다. 이 경우 1~4의 모든 문제가 해결된다.
다만 enum의 원래 enum의 사용법에 맞지 않다. 개인적으로 설계 후 클래스 다이어그램만 봐도 어느정도 동작이 이해되야 한다고 생각한다. enum을 싱글톤처럼 사용한 부분은 별도로 설명하지 않는 이상 이해할 수 없다.
6. 내부 클래스를 통해 구현해보자
class ConnectionManager {
private ConnectionManager() {}
private static class ConnectionManagerHolder {
private static final ConnectionManager instance = new ConnectionManager();
}
public static ConnectionManager getInstance() {
return ConnectionManagerHolder.instance;
}
}
'4'의 경우 코드가 이쁘지 않은 것 같다. 내부 클래스를 통해서도 싱글톤 구현이 가능하다. 내부 클래스에 있는 필드값의 경우 프로그램 실행 단계에서 생성되지 않으므로, 누군가 getIntacne()를 호출할 때 생성되게 된다. 또한 멀티 스레드에 대한 동기화는 JVM이 보증해준다.
다만 '5'에서는 해결된 직렬화, 리플렉션이 해결되진 않는다. 그래도 enum을 사용해 용법에 맞지 않는걸 사용하는 것 보다는 개인적으로 낫다고 생각한다. 직렬화 및 리플렉션 문제도 사실 짜고 있는 프로그램에서 문제가 될 것 같다면 별도의 방어코드를 넣어 해결 가능하다.
References
1. 면접을 위한 CS 전공지식 노트 (주홍철 저)
2. 이펙티브 자바 Effective Java 3/E (조슈아 블로크 저)
3. https://dzone.com/articles/another-singleton-implementation
'CS > Design Pattern' 카테고리의 다른 글
[디자인 패턴] 프록시 패턴 (Proxy Pattern) (0) | 2023.06.18 |
---|---|
[디자인 패턴] 전략 패턴 (Strategy Pattern) (0) | 2023.06.17 |
댓글