싱글톤 패턴(Singleton Pattern) 이란?
- 인스턴스를 오직 하나만 제공하는 패턴을 싱글톤 패턴이라고 한다.
- 시스템 런타임, 환경 세팅에 대한 정보 등, 인스턴스가 여러개 일 때 문제가 생길 수 있는 경우가 있다.
- 생성자가 여러차례 호출되어도 실제로 생성되는 객체는 하나고 최초 생성 이후에 호출된 생성자는 최초에 생성한 객체를 리턴한다. 클래스의 인스턴스가 오직 1개만 생성되는 것을 보장하는 디자인 패턴이다.
싱글톤 패턴 문제점
- 싱글톤 패턴을 구현하는 코드 자체가 많이 들어간다
- 의존관계상 클라이언트가 구체 클래스에 의존한다 -> DIP를 위반한다
- 클라이언트가 구체 클래스에 의존해서 OCP 원칙을 위반할 가능성이 높다
- 테스트하기 어렵다
- 내부 속성을 변경하거나 초기화 하기 어렵다
- private 생성자로 자식 클래스를 만들기 어렵다
- 결론적으로 유연성이 떨어진다
- 안티패턴으로 불리기도 한다
간단한 싱글톤 구현
package me.likelen.study.singleton;
public class Settings1 {
private static Settings1 instance;
private Settings1() {
}
public static Settings1 getInstance() {
if (instance == null) {
instance = new Settings1();
}
return instance;
}
}
1. 생성자를 private으로 만든 이유?
- 오직 한 개의 인스턴스에만 접근하기 위해 생성자의 노출을 막기 위해서 이다.
2. getInstance() 메서드를 static으로 선언한 이유?
- 외부에서 생성자를 호출 못하도록 private으로 막아 놓았기 때문에 외부에서는 인스턴스를 만들지 못한다. 따라서 해당 메서드를 호출하려면 전역으로 설정해두어야 한다.
3. getInstance()가 멀티쓰레드 환경에서 안전하지 않은 이유?
- 쓰레드가 동시에 인스턴스를 생성하면 값이 달라질 수 있기 때문에 안전하지 않다.
동기화(synchronized)를 사용해 멀티쓰레드 환경에 안전하게 만드는 방법
package me.likelen.study.singleton;
public class Settings2 {
private static Settings2 instance;
private Settings2() {
}
public static synchronized Settings2 getInstance() {
if (instance == null) {
instance = new Settings2();
}
return instance;
}
}
1. 자바의 동기화 블럭 처리 방법은?
- synchronized키워드를 사용하여 여러 쓰레드가 동시에 접근하지 못하도록 막는다.
- 단점은 Synchronized 키워드 사용하면, 해당 메서드가 호출될 때마다 동기화 처리 작업을 수행해야 하는데 이는 성능에 불이익이 있을 수 있다.
- 미리 만들어놓은 인스턴스를 호출해서 사용하기만 하면 되는데 단점은 사용하지 않으면 애플리케이션이 실행될 때마다 많은 리소스를 차지하게 된다.
2. getInstance() 메소드 동기화 시 사용하는락(lock)은 인스턴스의 락인가 클래스의 락인가? 그 이유는?
- 클래스 락이다. 만약 락이 인스턴스의 락이면, 동기화시 하나의 객체를 보장할 수 없게 되기 때문이다.
이른 초기화(eager initialization)을 사용하는 방법
package me.likelen.study.singleton;
public class Settings3_2 {
private static final Settings3_2 INSTANCE = new Settings3_2();
private Settings3_2() {
}
public static Settings3_2 getInstance() {
return INSTANCE;
}
}
1. 이른 초기화가 단점이 될 수도 있는 이유?
- 생성자에 많은 리소스를 사용되는 경우에는 좋지 않다.
2. 만약 생성자에서 checked 예외를 던진다면 이 코드를 어떻게 변경해야 할까요?
- 기본적으로 함수에서 checked exception을 던지면 함수를 호출하는 쪽에서 try-catch문으로 감싸야한다. 하지만 예제처럼 변수를 초기화하는 과정에서는 try-catch문을 사용할 수 없다. 그럴 경우 static {} 블록을 이용해서 instance를 초기화하면 되는데 이경우 final 키워드를 사용할 수 없다.
double checked locking으로 효율적인 동기화 블럭 만들기
package me.likelen.study.singleton;
public class Settings3 {
private static volatile Settings3 instance;
private Settings3() {
}
public static Settings3 getInstance() {
if (instance == null) {
synchronized (Settings3.class) {
if (instance == null){
instance = new Settings3();
}
}
}
return instance;
}
}
1. double check locking이라고 부르는 이유?
- ckecking을 두 번 했기 때문. 두 개의 쓰레드가 동시에 if문을 통과했다 하더라도 synchronized때문에 하나의 쓰레드만 안으로 들어갈 수 있다. 그리고 인스턴스를 생성하고 빠져나가는데 이때 다른 if문을 통과한 쓰레드가 synchronized로 들어오더라도 인스턴스가 이미 생성돼있기 때문에 if문에서 튕겨나간다.
2. instance 변수는 어떻게 정의해야 하는가? 그 이유는?
- 변수에 volatile 키워드를 붙여야 한다. volatile을 사용하지 않은 변수는 성능 향상을 위해 CPU 캐시에 저장한다. 이 경우 쓰레드가 변숫값을 읽어올 때 각각의 CPU의 캐시에서 가져오기 때문에 값이 달라 값의 불일치가 발생한다.
static inner 클래스를 사용하는 방법
package me.likelen.study.singleton;
public class Settings4 {
private static volatile Settings4 instance;
private Settings4() {
}
private static class Settings4Holder {
private static final Settings4 INSTANCE = new Settings4();
}
private static Settings4 getInstance() {
return Settings4Holder.INSTANCE;
}
}
1. 리플렉션에 대해 설명하세요
리플렉션은 구체적인 클래스의 타입을 몰라도 안에 선언되어있는 함수, 변수들에 접근할 수 있게 해주는 자바의 api
2. setAccessible(true)를 사용하는 이유는?
기본 생성자는 이 예제에서 private로 선언되어 있다. 즉 외부에서는 호출할 수 없다는 것인데, setAccessible(true)를 통해 Constructor <Settings> 타입으로 받은 declaredConstructor, 기본 생성자를 사용 가능하게 해 newInstance()를 사용해 새로운 객체를 만들 수 있게 하기 때문이다
싱글톤 패턴 구현 깨트리는 방법 1
[방법 1] 리플렉션
public class Main {
public static void main(String[] args)
throws NoSuchMethodException, InvocationTargetException, InstantiationException, IllegalAccessException {
Settings settings = Settings.getInstance();
// [방법1]
// 리플렉션을 사용하여 new 연산자를 이용한것과 같이 인스터스를 생성했다.
Constructor<Settings> constructor = Settings.class.getDeclaredConstructor();
constructor.setAccessible(true);
Settings settings2 = constructor.newInstance();
// singleton 이 깨진것을 확인 할 수 있다.
System.out.println(settings.equals(settings2)); // fase
System.out.println(settings == settings2); // fase
}
}
1. 리플렉션에 대해 설명하세요
리플렉션은 구체적인 클래스의 타입을 몰라도 안에 선언되어있는 함수, 변수들에 접근할 수 있게 해주는 자바의 api
2. setAccessible(true)를 사용하는 이유는?
기본 생성자는 이 예제에서 private로 선언되어 있다. 즉 외부에서는 호출할 수 없다는 것인데, setAccessible(true)를 통해 Constructor<Settings>타입으로 받은 declaredConstructor, 기본생성자를 사용가능하게해 newInstance()를 사용해 새로운 객체를 만들 수 있게하기 때문이다
싱글톤 패턴 구현 깨트리는 방법 2
[방법 2] 직렬화 역직렬화
public class Main {
public static void main(String[] args) throws IOException, ClassNotFoundException {
// [방법2]
// 직렬화 & 역직렬화
// 자바는 오브젝트를 파일 형태로 디스크에 저장해뒀다가(직렬화) 다시 읽어 드릴수 있다.(역직렬화)
// 파일로 저장해뒀다가 로딩할 수 있다는 뜻이다.
Settings settings = Settings.getInstance();
Settings settings2 = null;
// 1) 직렬화
try (ObjectOutput out = new ObjectOutputStream(new FileOutputStream("settings.obj"))) {
out.writeObject(settings); // 여기서 객체가 파일에 써진다.
}
// 2) 역직렬화
try (ObjectInput in = new ObjectInputStream(new FileInputStream("settings.obj"))) {
settings2 = (Settings) in.readObject();
}
// 역직렬화를 하게되면 생성자를 사용해서 새로 인스턴스를 만들어준다.
System.out.println(settings == settings2); // false
// 하지만 대응 방안이 있다.
System.out.println(settings == settings2); // true
}
}
// 역직렬화 대응 방안.
// 역직렬화 시 해당 메서드가 자동으로 호출되는데, 원래는 Settings 인스턴스를 내려주게된다.
// 하지만 우리는 임의로 원래의 인스턴스가 내려가도록 설정해두어 singleton 을 지킬 수 있다.
public class Settings implements Serializable {
public Settings() {}
private static class SettingsHolder {
private static final Settings INSTANCE = new Settings();
}
public static Settings getInstance() {
return SettingsHolder.INSTANCE;
}
protected Object readResolve() {
return getInstance();
}
}
1. 자바의 직렬화 & 역직렬화에 대해 설명하세요
- 직렬화는 자바 시스템 내부에서 사용되는 Object 또는 Data를 외부의 자바 시스템에서도 사용할 수 있도록 byte 형태로 데이터를 변환하는 기술이며 역직렬화는 byte로 변환된 Data를 원래대로 Object나 Data로 변환하는 기술이다
2. Serializable Id란 무엇이며 왜 쓰는가?
- Serializable를 상속받는 경우 클래스의 버전 관리를 위해 serialVersionUID를 사용한다. 이 serialVersionUID변수를 명시적으로 선언해 주지 않으면 컴파일러가 계산한 값을 부여하는데 Serializable Class 또는 Outer Class에 변경이 있으면 serialVersionUID값이 바뀌게 된다. 만약 Serialize 할 때와 Deserialize 할 때의 serialVersionUID 값이 다르면 InvalidClassExcepions가 발생하여 저장된 값을 객체로 Restore 할 수 없다
3. try-resource 블럭에 대해 설명하세요
- try-resource 블럭은 기존의 try-catch-final 블럭에서 사용하고 꼭 종료해줘야 하는 resource를 사용할 때 final 블럭에서 resource를 해제하는데, try-resource 블럭을 사용하면 따로 명시적으로 resource를 해제해주지 않아도 자동으로 해제해 준다
리플렉션과 직렬화/역직렬화를 이용하여 싱글톤을 깨트리는 것까지 막고 싶은 경우엔 Enum을 사용하자.
- enum으로 singleton 구현 시 리플렉션(Reflection)에 안전한 코드가 된다.
- enum 은 리플렉션에서 new 인스턴스를 사용할 수 없도록 막아놨기 때문이다.
- enum에서도 생성자, 프로퍼티, 메서드 모두 구현이 가능하다.
- 직렬화 / 역직렬화를 통해 인스턴스 생성 시에도 안정적이다.
- enum 은 기본적으로 Serialize를 구현하고 있는데 이때 같은 인스턴스가 반환되도록 설정되어있다.
- 단점은 클래스를 로딩하는 순간 미리 만들어진다는 점과 상속을 사용하지 못한다.
- 그것이 크게 문제가 되지 않는다면, 가장 완벽한 방법일 수 있다.
싱글톤 패턴 구현 방법
public enum Settings {
INSTANCE;
}
1. enum 타입의 인스턴스를 리플렉션을 통해 만들 수 있는가?
- 만들 수 없다. enum 타입의 클래스는 리플렉션을 통해 만들 수 없도록 제한한다.
2.enum으로 싱글톤 타입을 구현할 때의 단점은?
- 단점은 이른 초기화와 같이 미리 만들어진다는 점이다. 그리고 상속을 할 수 없다.
3. 직렬화&역직렬화 시에 별도로 구현해야 하는 메서드가 있는가?
- 별다른 장치가 없어도 Enum 클래스는 직렬화&역직렬 화가 된다. 그러나, getResolves() 구현 시 역직렬화시 변경을 가할 수 있다.
4. 자바에서 enum을 사용하지 않고 싱글톤을 구현하는 방법은?
- 생성자를 private으로 설정한 다음, 인스턴스를 생성할 메서드를 하나 구현한다.
이때 인스턴스를 생성해주는 메서드는 public static으로 설정하여 외부에서 전역 메서드로 호출될 수 있게끔 설정한다. - 위 방법과 동일하게 생성하지만 synchronized 키워드 설정하여 멀티쓰레드를 막는 방법
- 이른 초기화 방법 (이는 static 한 필드들이 초기화되는 시점에 인스턴스가 생성되도록 하는 방법이다. 멀티쓰레드를 막는 방법)
- double checked locking 방법 (인스턴스가 없는 경우에만 synchronized 가 걸린다.)
syncronized block 을 설정하여 해당 block 전/후로 인스턴스 존재 여부를 확인한 후 인스턴스가 생성되도록 한다. - static inner 클래스를 사용하는 방법 (권장)
5. private 생성자와 static 메소드를 사용하는 방법의 단점은?
- 멀티쓰레드 환경에서 안전하지 않을 수 있다.
- 리플렉션과 직렬화&역직렬화를 이용하여 새로운 인스턴스 생성이 가능해진다.
6. enum을 사용해 싱글톤 패턴을 구현하는 방법의 장점과 단점은?
- 리플렉션과 직렬화&역직렬화를 이용했을 때 새로운 인스턴스 생성을 막을 수 있다. (장점)
- 즉시 로딩(eager loading) 이 되어 인스턴스가 미리 만들어진다. (단점)
- 상속을 사용하지 못한다. (단점)
7. static inner 클래스를 사용해 싱글톤 구현
package singleton;
public class Settings {
public Settings() {}
private static class SettingsHolder {
private static final Settings INSTANCE = new Settings();
}
public static Settings getInstance() {
return SettingsHolder.INSTANCE;
}
}
싱글톤 패턴, 실무에선 어떻게 쓰일까?
- 스프링에서 빈의 스코프 중에 하나로 싱글톤 스코프
- 자바 java.lang.Runtime
Runtime runtime = Runtime.getRuntime();
System.out.println(runtime.maxMemory());
System.out.println(runtime.freeMemory() );
- 다른 디자인 패턴 구현체의 일부로 쓰이기도 한다.
- Builder Parttern
- Facade Parttern
- Abstract Factory Parttern
'디자인패턴' 카테고리의 다른 글
컴포짓(composite) 패턴 (0) | 2022.07.30 |
---|---|
브릿지(Bridge)패턴 (0) | 2022.07.20 |
프로토타입(Prototype) 패턴 (0) | 2022.07.08 |
추상 팩토리 & 팩토리 메서드 패턴 & 팩토리 패턴 정의 (0) | 2022.06.19 |
팩토리 메소드 패턴(Factory Method Pattern) (0) | 2022.06.11 |