싱글턴 패턴 이란 ?
Singleton is a creational design pattern that lets you ensure that a class has only one instance, while providing a global access point to this instance.
코드 전체적으로 인스턴스 의 접근을 제공할때, 항상 하나의 인스턴스 만을 반환 해주는 것을 싱글턴 으로 정의한다.
왜 필요할까?
- 사실 코드를 작성해 오면서 문제가 되었던 부분이 없었다 이러한 디자인 패턴이 없어도, 그러나 나 자신도 모르게 썻던 기억이 있을 것이다. 예를 들어보자 피자를 좋아하니 피자를 구워보자.
우리 코드에는 오븐이 하나밖에 없다고 가정을 해보자. 그렇다면 우리 오븐에 쿠킹 을 하기 전 분명 전역변수를 이용하던 정적 필드 를 이용하던 어떻게든 현재 오븐상태 를 알아오고 그에 대한 검증이후에 도우 를 넣을것 아닌가 ? 비슷하다 이런 느낌을 좀더 우아하게 정의 내린것이 싱글턴 패턴이다. 필요한 이유에 대한 정의를 보자.
Ensure that a class has just a single instance. Why would anyone want to control how many instances a class has? The most common reason for this is to control access to some shared resource—for example, a database or a file.
1. 가장 일반적인 이유가 공유된 자원 의 접근을 제어하기 위해 사용된다고 한다 예 를든다면 데이터베이스 혹은 파일 에서
Provide a global access point to that instance. Remember those global variables that you (all right, me) used to store some essential objects? While they’re very handy, they’re also very unsafe since any code can potentially overwrite the contents of those variables and crash the app.
2. 매우 편리하게 전역 변수를 사용해서 공유자원 에 접근을 한다면, 어느 코드(어플리케이션 이 실행되는 데 필요한 모든 코드 들) 나 잠재적으로 이 전역변수를 덮어 사용할수 있어 앱에 문제를 야기한다고 한다.
오븐에 예로 들었던 사실중 하나가 사실은 앱에 문제를 야기할수 있다. 왜? 오븐 사용 전 에 검증 뿐만 아니라, 도우 를 만들때도, 토핑 을 얹을때도 무언가 잡다한 어떤한것을 할때도 모든 함수에 저 전역변수 에 접근하고 덮어서 사용할수 있다 라고 이해가 된다.
이런 잠재적인 이유를 알았으니 한번 적용해보자 도대체 어떻게 하면 될까 ? 제공 되는 예시를 내가좋아하는 피자 오븐으로 덮어서작성해보자.
코드보기
public class Testing {
public static void main(String[] args) throws InterruptedException {
System.out.println("피자도우 를 만들고 오븐에 넣어보자.");
Oven singleton = Oven.getInstance(true);
Oven anotherSingleton = Oven.getInstance(false);
System.out.println(singleton.isUse);
System.out.println(anotherSingleton.isUse);
}
}
public class Oven {
private static Oven instance;
public boolean isUse;
private Oven(boolean isUse) throws InterruptedException {
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
this.isUse = isUse;
}
public static Oven getInstance(boolean isUse) throws InterruptedException {
if(instance == null){
instance = new Oven(isUse);
}
return instance;
}
}
보이는가? 오븐을 두번 생성해서 넣어도 우리는 현재 오븐 의 상태 값을 받는다.
자 그러면 문제가 될 상황인 서로다른 방에서 피자를 여러개 만들어서 오븐을 가져온다면 ? 어떻게 해야할까 ? 싱글턴에서는 그러면 어떻게 핸들링을 할까 ?
코드보기
package Singleton;
public class Testing {
public static void main(String[] args) throws InterruptedException {
Thread threadFoo = new Thread(new Hawaiian());
Thread threadBar = new Thread(new Margherita());
threadFoo.start();
threadBar.start();
}
static class Hawaiian implements Runnable {
@Override
public void run() {
Oven singleton = null;
try {
singleton = Oven.getInstance(true);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
System.out.println(singleton.isUse);
}
}
static class Margherita implements Runnable {
@Override
public void run() {
Oven singleton = null;
try {
singleton = Oven.getInstance(false);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
System.out.println(singleton.isUse);
}
}
}
홀리몰리 ? 원하던 결과 값이 아니다 공유된 오븐이 아닌 각자 새로운 오븐을 생성한다. 이렇게 된다면 어떻게해야 동일한 오븐을 타켓할수 있을까 ? volatile 과, 싱크로나이즈 하나 띡 인스턴스 생성하는 부분에 묶어주면 된다.
Java volatile이란?
- volatile keyword는 Java 변수를 Main Memory에 저장하겠다라는 것을 명시하는 것입니다.
- 매번 변수의 값을 Read할 때마다 CPU cache에 저장된 값이 아닌 Main Memory에서 읽는 것입니다.
- 또한 변수의 값을 Write할 때마다 Main Memory에 까지 작성하는 것입니다.
코드보기
package Singleton;
public class Oven {
private static volatile Oven instance;
public boolean isUse;
private Oven(boolean isUse) throws InterruptedException {
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
this.isUse = isUse;
}
public static Oven getInstance(boolean isUse) throws InterruptedException {
synchronized(Oven.class) {
if (instance == null) {
instance = new Oven(isUse);
}
return instance;
}
}
}
똑같은 결과값을 반환하고 똑같은 오븐을 참조하기 시작했다. 그런데 굳이 이렇게 까지 해서 동기화를 시켜야하나 싶은 생각이 들기 시작한다. 이때 쉽게 작성하기 위해서 Jvm 을 이용하는 방법이 있다. 오븐 클래스 안에 이너클래스 로 스테틱을 생성하게 되면 Jvm 실행 시점에 클래스 로더에 올라가 jvm 이 나쁜 쓰레드로 부터 보호해준다. 바로 가보자.
코드보기
package Singleton;
public class Oven {
public boolean isUse;
private Oven(){}
private static class OvenKeeper{
private static final Oven instance = new Oven();;
}
public static Oven getInstance(boolean isUse){
Oven o = OvenKeeper.instance;
o.isUse = isUse;
return o;
}
}
스레드 가순서가 불안정해서 동일한 값을 매 실행마다 주진 않지만 각자 동일한 객체를 가르킨다. 오우 홀리 static 을 활용한 jvm 에 올리는 방법도 좋은거 같다.
이 외에 눈에 띄는 방법중 하나인 enum 을 이용하는 방법이다.
To overcome this situation with Reflection, Joshua Bloch suggests the use of Enum to implement Singleton design pattern as Java ensures that any enum value is instantiated only once in a Java program. Since Java Enum values are globally accessible, so is the singleton. The drawback is that the enum type is somewhat inflexible; for example, it does not allow lazy initialization.
이넘 또한 전역적으로 접근이 가능하고 한번만 인스턴스화 되기에 조슈아 씨는 이 방법을 추천한다고 합니다. 플랙시블 하지않아 지연 로딩을 허가하지 않는다는데 바로 가보자
코드보기
package Singleton;
public enum Oven {
INSTANCE;
public static boolean isUse;
public static Oven getInstance(boolean change){
isUse = change;
return Oven.INSTANCE;
}
}
와우 ... 제일 간단하면서도 위에 멀티스레드 케이스를 전부 통과한다 왜 ?
Enum은 private 생성자로 인스턴스 생성을 제어하며, 상수만 갖는 특별한 클래스이기 때문이다.
시각적으로 봐도 매우 간단하게 구현이 가능한 부분에서 보면 환상적 이다. 다만 왜 이러한 이넘 타입을 권하는지 알아보자.
일반적인 위에 구현된 싱글톤 패턴 에서는 직렬화 과정에서 싱글톤이 싱글톤이 아닌 매우 이상한 값들을 생성하고 부여한다.
따라서 일반적인 싱글톤에는 implements Serializable 추가해주고 모든 필드에 transient 를 추가해 직렬화 과정에 그대로 넘어가게 설정해 주어야 한다. readResolve() 메서드를 구현하여 현재 싱글톤의 인스턴스를 리턴해주는 이런 불필요한 과정을 거쳐야 한다.
Enum 에는 그런것이 전혀 필요하지 않다. 그냥 다 해준다. 또한 싱글톤 관련하여 검색하다 보면
Using Reflection to destroy Singleton Pattern 이라는 결과를 볼수 있는데 이는 싱글턴 패턴 을 파괴할수 있는 방법을 보여준다.
package com.journaldev.singleton;
import java.lang.reflect.Constructor;
public class ReflectionSingletonTest {
public static void main(String[] args) {
EagerInitializedSingleton instanceOne = EagerInitializedSingleton.getInstance();
EagerInitializedSingleton instanceTwo = null;
try {
Constructor[] constructors = EagerInitializedSingleton.class.getDeclaredConstructors();
for (Constructor constructor : constructors) {
//Below code will destroy the singleton pattern
constructor.setAccessible(true);
instanceTwo = (EagerInitializedSingleton) constructor.newInstance();
break;
}
} catch (Exception e) {
e.printStackTrace();
}
System.out.println(instanceOne.hashCode());
System.out.println(instanceTwo.hashCode());
}
}
이 코드를 보면 해쉬코드 의 값이 각각 다르게 나온다. 어떻게 ? 리플렉션, setAccesible 이라는 함수를 이용하면 private 생성자 호출이 가능해진다. (반면 이 리플렉션은 하이버네이트 , 스플링에서 정말 많이 사용된다.어노테이션 에서 사용되어진다.)
그러나 enum 싱글턴에서는 이 모든 반례들을 보장해준다.
만들기 제일 쉬운 Enum 이 최고의 방법이라니..
Enum 싱글턴 은 멀티쓰레드 상황, 직렬화, 리플렉션 을 이용한 싱글턴 부수기 모든 곳에서 방어가 되기 때문에 최고라고 생각한다. 무엇보다 사용하기 정말 쉽지 않은가 쉽고 직관적인게 최고다.
긴글 읽어주셔서 감사합니다.
참조 사이트
'자꾸 검색하는 내용' 카테고리의 다른 글
[Java] LinkedList 알아보기 (0) | 2023.01.02 |
---|---|
[Java] ArrayList 알아보기 (0) | 2023.01.01 |
Sql Antipatterns 스키마 및 erd (0) | 2022.12.03 |
코드 시간측정 (0) | 2022.07.25 |
AWS-EC2 서버 우분투에 MariaDB Open 하기 (0) | 2022.06.29 |