[10분 테코톡] 아서의 싱글턴 패턴과 정적 클래스 정리
프로그램 전역에서 사용되는 유일한 클래스를 만드는 것은 두 가지 방법
1) 싱글턴 패턴 이용
2) 정적클래스 이용
싱글턴 패턴이 뭐지?
- 객체 인스턴스가 오로지 한 개만 생성 되도록 설계하는 패턴
(애플리케이션 내에서 인스턴스가 유일해야 한다.)
When?
로그 기록, 캐싱, 사용자 설정 등
Why?
유일성, 글로벌
싱들턴 패턴을 만들어보자
문제 상황
애플리케이션의 배경 색깔을 정할 수 있는 Settings 클래스가 있다고 가정한다.
배경은 애플리케이션에서 한 개만 있어야 된다.
환경 설정을 바꿀 수 있는 인스턴스가 두 개 존재한다면 에러가 나올 것이다.
Settings settings1 = new Settings();
Settings settings2 = new Settings();
System.out.println(settings1); // test.Settings@27f674d
System.out.println(settings2); // test.Settings@1d251891
System.out.println(settings1 == settings2); // false
지금 겪은 이 상황을 코드로 풀어보면 위와 같을 것이다. Settings를 매번 생성 할때 마다 서로 다른 인스턴스를 생성 하게 된어 인스턴스를 출력해 봤을 때 각 번호가 다름을 알 수 있다.
이제부터 싱글턴 패턴으로 하나의 인스턴스만 생성될 수 있도록 구현해 보자
1. 순수한 구현
1) 인스턴스를 private static 변수
2) getInstance()에서 인스턴스 생성
3) 외부 생성자를 막는다.
<문제점>
쓰레드 세이프하지 않다.
class Settings {
private static Settings instance;
private Settings() {
}
public static Settings getInstance() {
if (instance == null) {
instance = new Settings();
}
return instance;
}
}
Settings의 인스턴스를 정적 필드에 저장하고 정적 메서드로 getInstance를 만들어서 사용자가 인스턴스를 요청할 때마다 만약에 인스턴스가 존재하지 않으면 만들어서 반환하고 만약에 있다면 그 있는 값을 그대로 반환하는 방식이다. 그리고 외부에서 인스턴스가 생성할 수 없도록 생성자를 private으로 막아두면 싱글턴 패턴을 구현할 수 있다.
public class Test {
public static void main(String[] args) {
Settings settings1 = Settings.getInstance();
Settings settings2 = Settings.getInstance();
System.out.println(settings1); // test.Settings@27f674d
System.out.println(settings2); // test.Settings@27f674d
System.out.println(settings1 == settings2); // true
}
}
위와같이 같은 인스턴스를 호출하게 된다.
하지만 문제점이 존재한다. 문제점은 바로 멀티스레드 환경에서 싱글턴이 보장되지 않는다는 것이다.
만약에 스레드 A와 B가 동시에 요청을 보냈다고 가정했을 때 A가 조건문인 'if (instance == null)'을 통과해서 인스턴스를 생성하기 전에 만약 B도 통과 한다면 각각 다른 인스턴스가 생길 가능성이 존재한다.
이를 해결하기 위해서는 어떻게 해야할까?
바로 synchronized 키워드를 이용하여 동시성 문제를 해결할 수 있다.
2. 동기화(synchronized)
1) 인스턴스를 private static 변수
2) synchronized getInstance()에서 인스턴스 생성
3) 외부 생성자를 막는다.
<문제점>
리소스 낭비
class Settings {
private static Settings instance;
private Settings() {
}
public synchronized static Settings getInstance() {
if (instance == null) {
i nstance = new Settings();
}
return instance;
}
}
하지만 이 코드에도 문제점이 있는데, 리소스가 낭비된다는 점이다.
멀티스레드 환경에서 인스턴스를 하나만 만들기 위해서 synchronized 키워드를 사용했는데 만약에 인스턴스가 존재한다면 더 이상 필요가 없게 된다.
즉 메서드를 실행 할 때마다 Lock이 걸리게 되어 리소스 낭비가 발생한다.
이것을 해결하기 위해 Double Checked Locking(DCL) 이라는 방법이 제안된다.
3. Double Checked Locking(DCL)
1) synchronized 시점 지연
2) private static volatile 인스턴스
3) 외부 생성자를 막는다.
<문제점>
- volatile(JDK 1.5이상)
- JVM에 대한 이해
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;
}
}
위와같이 한다면 getInstance 메서드를 호출할 때마다 인스턴스가 있을 때는 synchronized 블록이 스킵 된다. 그렇게 된다면 즉시 인스턴스만 반환하게 되어 리소스 낭비를 없앨 수 있다. 그리고 이때 클래스 변수에 volatile 키워드를 이용해야 된다.
스레드를 이용하게 되면 각각의 스레드는 성능을 위해서 캐시 메모리를 사용하게 된다. 첫번째 스레드가 캐시 메모리에서 메인 메모리 순서로 값을 대입해주면, 다음 스레드는 메인 메모리에 담긴 값을 캐시 메모리에 담아 들여오는 방식으로 동작한다. 여기서 문제는 첫번째 스레드가 메인 메모리의 값을 대입하기 전에 다음 스레드가 메인 메모리에서 값을 읽어 들이려고 할 때 발생한다. volatile 이라는 키워드를 사용하면 대입과 읽는 것 모두 메인 메모리에서 하도록 만들어 시간차를 극복할 수 있다.
이 코드의 문제점은 volatile 이라는 키워드 자체가 JDK 1.5 이상에서만 구현이 가능하고 JVM에 따라서 thread-safe 하지 않는 경우가 발생할 수 있다. 그리고 자바가 어떻게 메모리를 관리하는지 이해하고 있어야 되기 때문에 사용하지 않는 것이 좋다.
4. Bill Pugh Solution (Initialization on demand holder idiom)
1) static inner class 인스턴스
2) 생성자를 private
<문제점>
- 리플렉션
- 직렬화 + 역직렬화
class Settings {
private Settings() {
}
private static class SettingsHolder {
private static final Settings SETTINGS = new Settings();
}
public static Settings getInstance() {
return SettingsHolder.SETTINGS;
}
}
이 싱글턴 방법의 기반은 Initialization on demand holder idiom 개념을 이용한다. 구현방법은 Holder 역할을 하는 private static 클래스를 이용하는 것이다. 최초로 Classloader에 의해서 로드 될 때 내부로 synchronized가 실행된다. 그래서 명시적으로 synchronized를 이용하지 않고 동일한 효과를 낼 수 있다. 해당 클래스는 static 이므로 메서드가 실행될 때 JVM의 static initializer에 의해 초기화 되고 메모리로 올라가게 된다. 따라서 thread-safe와 lazy-loading을 둘 다 만족하는 싱글턴이 구현 가능하다.
하지만 해당 코드의 문제점은 클라이언트가 임의로 싱글턴을 파괴할 수 있다. 리플렉션과 직렬화를 통해 파괴할 수 있다.
4. Enum
1) Instance 끝
<문제점>
- 싱글턴을 해제할 때 번거로움
- Enum외의 클래스 상속 불가
이펙티브 자바의 저자 조슈아 블로크씨가 제안했다고 한다.
public enum Settins {
INSTANCE;
}
딱 보기에도 되게 코드가 쉬워보이는데 enum 자체가 싱글턴이기 때문에 그렇다. 생성자를 private 으로 갖게 만들고 상수만 갖는 클래스이기 때문에 싱글턴의 성질을 갖게 된다. 이렇게 구현하면 Bill Pugh의 방법과는 다르게 리플렉션과 직렬화로 깨뜨릴 수 없게 된다.
하지만 enum으로만 구현하게 됐을 때는 싱글턴에서 다시 멀티턴으로 변경하려고 할 때 코드를 다시 짜야 하고 enum 외의 클래스를 상속한다면 사용할 수 없다는 단점이 있다.
권장하는 방법 2가지
1) Bill Pugh 방법(Holder를 가지는 방법)
권장 이유
- Lazy Loading
- thread-Safe하게 구현될 수 있다.
2) Enum
권장 이유
- 싱글턴이 enum 외의 클래스를 상속하지 않는 환경이라면 enum으로 싱글턴 패턴을 쉽게 만들 수 있다.
- thread-Safe
정적 클래스와는 뭐가 다를까?
(사실 자바에는 정적클래스가 따로 존재하지 않는다. 편의상 정적메서드만을 갖고 있는 클래스를 정적클래스라 부르겠다.)
정적 클래스
정적 메서드만 갖고 있는 클래스
<싱글턴과의 공통점>
- 글로벌
- 유일성
static 메서드들은 클래스 초기화시에 메서드 영역에 등록되어 프로그램이 끝날 때 해제된다. 따라서 애플리케이션 내에서 싱글턴 패턴과 마찬가지로 전역적으로 사용할 수 있고 인스턴스를 따로 생성하지 않기 때문에 유일성을 보장받을 수 있다.
차이점으로는 인스턴스를 생성할 수 없기 때문에 클래스 메서드를 이용한다는 점이 있습니다
싱글턴과 정적클래스는 언제 쓸까?
싱글턴 패턴 | 정적 클래스 |
1. 상속받아서 가용 가능 2. 메서드 파라미터로 사용 가능 |
1. 객체 생성을 하지 않는다.(효율성) |
<권장 환경> 1. 완벽한 객체지향을 필요로 할 때 2. 레이지 로딩이 필요할 때 |
<권장 환경> 1. 유틸 메서드를 보관하는 용도로 사용할 때 2. 다형성이나 상속이 필요없는 클래스 |
싱글턴 패턴은 상속이 가능하고 메서드 파라미터로 사용을 할 수 있다. 따라서 애플리케이션 내에서 객체처럼 사용하고 싶을 때, 혹은 인스턴스 생성 할 때 리소스가 많이 드는 경우 권장되어진다.
정적클래스는 객체처럼 사용할 수는 없지만 컴파일시 정적바인딩이 되기 때문에 보통 싱글턴보다 효율이 좋다. 그래서 유틸 클래스처럼 객체 성질이 필요 없을 때 사용하는 것을 권장한다.
참고
https://www.youtube.com/watch?v=5oUdqn7WeP0&ab_channel=%EC%9A%B0%EC%95%84%ED%95%9C%ED%85%8C%ED%81%AC