프로그램 전역에서 사용 되는 유일한 클래스를 만드는 방법 두 가지가 있습니다.
- 싱글톤 패턴
- 정적 클래스
싱글톤 패턴이 무엇인지 알아보겠습니다!
싱글톤 패턴 이란 ?
먼저 디자인 패턴이라는 말을 보겠습니다.
디자인 패턴 ?
- “바퀴를 재발명 하지마라”
- 소프트웨어 개발 과정에서 자주 쓰이는 설계의 노하우들을 정리한 방법들
그렇다면 디자인 패턴에 해당하는 싱글톤 패턴은 왜 생겨난 걸까요 ?
싱글톤 패턴의 목적 & 의의
- 객체의 인스턴스가 오로지 한개만 생성되도록 설계하는 것
- 언제 ?
- 로그 기록
- 캐싱
- 사용자 설정
- 왜 ?
- 유일성
- 글로벌
다음과 같은 상황에서 싱글톤 패턴을 적용해보겠습니다.
싱글톤 패턴 구현해보기
환경 설정을 바꿀 수 있는 인스턴스 settings1과 settings2로 배경색을 동시에 바꾸게 되면 오류가 발생하게 됩니다.
한 애플리케이션에 환경 설정이 2개가 있다는 얘기입니다.
문제 상황
- 애플리케이션 배경 설정
- Settings : 배경색을 담당하는 클래스
- Settings가 애플리케이션 내부에서 유일하지 않다면 ?? ⇒ 에러 발생
아래와 같이 인스턴스가 다르다는 것을 알 수 있습니다.
public class App {
public static void main(String[] args) {
Settings settings1 = new Settings();
Settings settings2 = new Settings();
System.out.println(settings1);
System.out.println(settings2);
System.out.println(settings1 == settings2);
}
}
/**
* 출력 내용
* game.Settings@23jgjfsda
* game.Settings@95mbjfiv9
* false
*/
이러한 상황에서 Settings 클래스의 인스턴스를 생성할 때 하나의 공동의 객체를 가져오게 된다면
환경 설정이 2개가 생기는 오류는 발생하지 않을 것입니다 !
🙋♂️ 이제부터 싱글톤 패턴으로 하나의 인스턴스만 생성되도록 함으로써 오류를 없애겠습니다 !
1. 순수한 구현
- 인스턴스를 private static 변수
- getInstance()에서 인스턴스 생성
- 외부 생성자를 막는다.
아래와 같이 Settings 클래스를 작성하면 됩니다.
public class Settings {
private static Settings instance;
private Settings() {
}
public static Settings getInstance() {
if (instance == null) {
instance = new Settings();
}
return instance;
}
}
이렇게 Settings가 싱글톤 패톤으로 구현 되었을 때
처음에 위 상황과 똑같이 Settings 클래스의 인스턴스를 2개 만들어서 작동시켜보겠습니다.
public class App {
public static void main(String[] args) {
Settings settings1 = Settings.getInstance();
Settings settings2 = Settings.getInstance();
System.out.println(settings1);
System.out.println(settings2);
System.out.println(settings1 == settings2);
}
}
/**
* 출력 내용
* game.Settings@23jgjfsda
* game.Settings@23jgjfsda
* true
*/
🙋♂️ 과연 이게 원하는 데로 작동이 될 거 같나요 ?? 아닙니다 !!!
위에서 나온 구현은 ‘쓰레드 세이프 하지 않습니다.”
즉, 각각 다른 인스턴스가 생길 가능성이 존재합니다.
이와 같이 A가 생성되지 직전에 B가 온다면 A, B 두 개의 인스턴스가 생성되고 맙니다.
저희는 이러한 상황도 막아주어야 합니다.
2. 동기화 (synchronized) - 쓰레드 세이프 하지 않은 싱글톤 패턴
1. synchronized 키워드 사용하기
- 인스턴스를 private static 변수
- synchronized getInstance()에서 인스턴스 생성
- 외부 생성자를 막는다.
synchronized 키워드를 사용하여 동시성 문제를 해결할 수 있습니다.
바로 다음 코드와 같이 사용하면 됩니다.
public class Settings {
private static Settings instance;
private Settings() {
}
public synchronized static Settings getInstance() {
if (instance == null) {
instance = new Settings();
}
return instance;
}
}
1-1. 문제점 - synchronized 남발로 인한 리소스 낭비
이미 인스턴스가 만들어진 상태에서는 synchronized가 필요 없습니다.
하지만 현재 코드에서는 계속해서 getInstance() 메서드를 실행 할 때마다 Lock이 걸리게 되어 리소스 낭비가 발생하게 됩니다 !
3. 해결 - DCL (Double Checked Locking)
- synchronized 시점 지연
- private static volatile 인스턴스
- 외부 생성자를 막는다.
DCL (Double Checked Locking) 이라는 방법이 있습니다 !!
이제 getInstance() 메서드를 호출할 때 마다 인스턴스가 있을 때는 synchronized 블록이 스킵됩니다.
이렇게 즉시 인스턴스만 반환하게 되며 리소스 낭비를 없앨 수 있습니다.
public class Settings {
private static volatile Settings instance;
private Settings() {
}
public static Settings getInstance() {
if (instance == null) {
synchronized (Settings.class) {
if (instance == null) {
instance = new Settings();
}
}
}
return instance;
}
}
이 때 클래스 변수에 아까 정의 해놨던 인스턴스를 volatile이라는 키워드를 이용해야합니다.
3-1. volatile 이 뭐고 왜 사용하는데 ??
스레드를 이용하게 되면 각각의 스레드는 성능을 위해서 캐시 메모리 (cache memory)를 사용하게 됩니다.
스레드 동작
- 첫 번째 스레드가 캐시 메모리에서 메인 메모리의 순서로 값을 대입해주면
- 다음 스레드는 메인 메모리에 담긴 값을 캐시 메모리에 담아 들어오는 방식으로 동작
문제
- 첫 번째 스레드가 메인 메모리에 값을 대입하기 전에
- 다음 스레드가 메인 메모리에서 값을 읽으려 할 때 발생합니다.
해결
- volatile이라는 키워드를 이용하게 되면
- 대입과 읽는 것 모두 메인 메모리에서 하도록 만듭니다.
- 때문에 시간차를 극복할 수 있습니다.
volatile 사용 시 주의점
- volatile (JDK 1.5 이상)
- JVM에 대한 이해
- JVM에 따라서 thread-safe 하지 않는 경우가 발생할 수 있습니다.
- 또한 자바가 어떻게 메모리를 관리하는지 이해하고 사용해야 합니다.
4. Bill Pugh Solution (Initialization on demand holder idiom)
🙋♂️ 싱글톤을 구현할 때 권장되어지는 방법 중의 하나입니다.
- static inner class 인스턴스
- 생성자를 private
이 싱글톤은 Initialization on demand holder idiom 개념을 이용한 방법입니다.
- 구현 방법은 Holder역할을 하는 private static 클래스를 이용하는 것입니다
- 해당 클래스(SettingHolder)는 static 이므로 메서드가 실행될 때 JVM의 static initializer에 의해 초기화 되고 메모리로 올라가게 됩니다.
- 따라서 thread-safe와 lazy-loading을 둘 다 만족하는 싱글턴이 구현 가능합니다.
public class Settings { private Settings() { } private static class SettingsHolder { private static final Settings SETTINGS = new Settings(); } public static Settings getInstance() { return SettingsHolder.SETTINGS; } }
- 최초로 Class Loader에 의해서 로드될 때 내부로 synchronized가 실행됩니다.
- 때문에 명시적으로 synchronized를 이용하지 않고 동일한 효과를 낼 수 있습니다.
protected Class<?> loadClass(String name, boolean resolve)
throws ClassNotFoundException
{
synchronized (getClassLoadingLock(name)) {
// First, check if the class has already been loaded
Class<?> c = findLoadedClass(name);
if (c == null) {
// ...
하지만 이 Bill Pugh Solution 방법에도 문제점이 있습니다.
4-1. 문제점
🙋♂️ 클라이언트가 임의로 싱글톤을 파괴할 수 있다는 것입니다. (리플렉션, 직렬화)
- 리플렉션
- 직렬화
그렇다면 대체 가능한 방법이 어떤 것이 있을까요 ??
바로 Enum이 있습니다.
5. Enum
- Enum 자체가 싱글톤입니다.
- 애초에 생성자를 private으로 갖게 만듭니다.
- 상수만 갖는 클래스이기 때문에 싱글톤의 성질을 갖게 됩니다.
public enum Settings {
INSTANCE;
}
이렇게 Enum으로 구현하게 되면 아까 Bill Pugh의 방법과는 다르게 리플렉션과 직렬화로 깨뜨릴 수 없게 됩니다.
하지만 그렇다고 해서 Enum이 장점만 있는 것은 아닙니다.
5-1. 문제점
Enum으로만 구현하게 됐을 때는 싱글톤에서 다시 멀티톤으로 변경하려고 할 때 코드를 다시 짜야합니다.
- 싱글톤을 해제할 때 번거로움
- Enum외의 클래스 상속 불가
6. 최종 권장하는 싱글톤 패턴 구현 방법 정리
1. Bill Pugh
1-1. 권장하는 이유
- Lazy Loading
- thread-safe
이 방법으로 즉시 로딩해서 리소스 낭비될 일도 없고 thread-safe 하게 구현될 수 있습니다.
2. Enum
2-1. 권장하는 이유
- thread-safe
- 간편하다.
싱글톤이 enum 외의 클래스를 상속하지 않는 환경이라면 enum으로 싱글톤 패턴을 쉽게 만들 수 있습니다.
7. 정적 클래스와의 차이
🙋♂️ 사실 자바에는 정적클래스가 따로 존재하지 않습니다.
편의상 정적메서드만을 갖고 있는 클래스를 정적클래스라 부르겠습니다.
static 메서드들은 클래스 초기화시에 메서드 영역(method area)에 등록되어 프로그램이 끝날 때 해제됩니다.
따라서 애플리케이션 내에서 싱글톤 패턴과 마찬가지로 전역적으로 사용할 수 있고
인스턴스를 따로 생성하지 않기 때문에 유일성을 보장받을 수 있습니다.
하지만 인스턴스를 생성할 수 없기 때문에 클래스 메서드를 이용합니다.
8. 싱글톤과 정적 클래스는 언제 사용할까 ?
8-1. 싱글톤 패턴
- 상속받아서 사용가능하다.
- 메서드 파라미터로 사용가능하다.
따라서
- 완벽한 객체지향을 필요로 할 때
- 애플리케이션 내에서 객체처럼 사용하고 싶을 때
- 레이지 로딩이 필요할 때
- 혹은 인스턴스 생성할 때 리소스가 많이 드는 경우 권장 되어집니다.
8-2. 정적 클래스
- 객체 생성을 하지 않는다.
- 정적 클래스는 객체처럼 사용할 수는 없지만 컴파일 시 정적 바인딩이 되기 때문에 보통 싱글톤보다 효율이 좋습니다.
따라서
- 유틸 메서드를 보관하는 용도로 사용할 때
- 객체 성질이 필요없을 때 사용하는 것 권장
- 다형성이나 상속이 필요없는 클래스
댓글