자바 및 코틀린 싱글톤(singleton) 패턴이란?
인스턴스가 하나 뿐인 특별한 객체를 만들 수 있게 해주는 패턴입니다.
즉, 필요할 때만 객체를 만들 수가 있습니다.
사용 이유?
: 인스턴스를 두 개 이상 만들게 되면, 프로그램이 이상하게 돌아간다든가, 불필요하게 자원을 잡아먹는다든가,
결과에 일관성이 없어지는 것 같은 심각한 문제가 생길 수 있습니다.
고전적인 싱글턴 패턴 구현법
public class Singleton {
//2개이상의 인스턴스를 가지면 안되기 때문에 private으로 유일한 인스턴스를 저장할 정적 변수.
private static Singleton uniqueinstance;
//생성자를 private으로 선언했기 때문에 singleton에서만 클래스의 인스턴스를 만들 수 있다.
//만약 public으로 만들게 되면 new Singleton()으로 인스턴스를 하나가 아닌 더 생성할 수 있게된다.
private Singleton() { }
//클래스의 인스턴스를 만들어서 리턴 해줌.
public static Singleton getInstance() {
if(uniqueinstance == null) {
uniqueinstance = new Singleton();
}
return uniqueinstance;
}
}
.
.
.
싱글톤 객체가 필요할 때는 인스턴스를 직접(new 객체()) 만드는게 아니고, 인스턴스를 달라고 요청을 해야 됩니다.
클래스에는 그런 용도로 쓰이는 getInstance()라는 정적 메소드가 있습니다.
getInstance()메소드를 호출하면 제가 일할 준비를 갖추고 바로 나타나죠.
사실 이미 만들어져 있는 상태에서 어떤 객체를 도와주다가 호출을 받고 다른 객체한테 불려가는 경우도 종종 있습니다.
.
.
.
가정) 초콜릿 공장
요즘은 거의 모든 초콜릿 공장에서 초코릿을 끓이는 장치를 컴퓨터로 제어합니다.
이 장치에서는 초콜릿과 우유를 받아서 끓이고 초코바를 만드는 단계로 넘겨줍니다.
과정
1. 우유와 초콜릿을 혼합한 재료를 집어넣음
2. 재료를 끓임
3. 끓인 재료를 다음 단계로 넘김
public class ChocolateBoiler {
//끓이는 장치 내부가 비어있는지 확인
private boolean empty;
//끓이기
private boolean boiled;
public ChocolateBoiler() {
//생성될 때 비어있고, 끓이지 않는 상태.
empty = true;
boiled = false;
}
public void fill() {
//비어있다면, 재료를 채워줌.
if (isEmpty()) {
empty = false;
boiled = false;
}
}
public void boil() {
//비어있지 않고, 끓이지 았았다면
if(!isEmpty() && !isBoiled()) {
//끓임
boiled = true;
}
}
public void drain() {
//비어있지 않고, 끓였다면
if(!isEmpty() && isBoiled()) {
// 끓인 재료를 다음 단계로 이동
empty = true;
}
}
public boolean isEmpty() {
return empty;
}
public boolean isBoiled() {
return boiled;
}
}
.
.
.
싱글톤 패턴 : 싱글톤(singleton)패턴은 해당 클래스의 인스턴스가 하나만 만들어지고, 어디서든지 그 인스턴스에 접근할 수 있도록 하기 위한 패턴입니다.
.
.
.
문제)
가동중에 fill() 메소드에서 아직 초콜릿이 끓고 있는데, 재료를 집어넣고 말았습니다. 왜 그럴까요!?
이유)
만약 다중 스레드에서 여기에 있는 코드를 실행히킨다고 가정해 봅시다.
코드 사용중에 스레드가 겹치게 되어 동작 에러가 발생할 수 있습니다.
(쉽게 설명하자면, 이 클래스를 사용하는 시간이 겹치게 되어 에러가 발생합니다.)
1번 스레드 : 3초에 한번 동작
2번 스레드 : 5초에 한번 동작
15초당 한번씩 사용하는 코드(메소드)가 겹치게 됩니다.
.
.
.
해결방법
1. synchronzed(동기화) 사용
( synchronzed : 한 스레드가 사용을 끝내기 전까지 다른 스레드는 기다려야 함)
//예제
public static synchronized Singleton getInstance() {
if(uniqueinstance == null) {
uniqueinstance = new Singleton();
}
return uniqueinstance;
}
사용 시 문제 : 속도 문제 (기다려야되니까.)
2. 인스턴스를 필요할 때 생성하지 말고, 처음부터 만들어 버립니다.
//예제
public class Singleton {
private static Singleton = uniqueInstance = new Singleton();
private Singleton() {}
public static Singleton getInstance() {
return uniqueinstance;
}
}
정적 초기화 부분(static initializer)부분에서 싱글톤의 인스턴스를 생성합니다.
이렇게하면 스레드를 써도 문제 없습니다.
이렇게하면, JVM(Java Vertual Machine)에서 싱글톤의 유일한 인스턴스를 생성 해주빈다.
유일한 인스턴스를 생성하기 전까지는 그 어떤 스레드도 uniqueInstance 정적 변수에 접근할 수 없습니다.
3. DCL(Double-Checking-Locking)을 써서 getInstance()에서 동기화되는 부분을 줄입니다.
(일단 인스턴스가 생성되어 있는지 확인한 다음, 생성되어 있지 않았을 때만 동기화를 할 수 있습니다.)
-> 이렇게하면 처음에만 동기화를 하고 나중에는 동기화를 하지 않아도 됩니다.
public class Singleton {
// volatile 추가!
private volatile static Singleton uniqueinstance;
private Singleton() { }
public static Singleton getInstance() {
if(uniqueinstance == null) {
//이렇게하면 처음에만 동기화 됨.
synchronized (Singleton.class) {
if(uniqueInstance == null) {
uniqueinstance = new Singleton();
}
}
}
return uniqueinstance;
}
}
volatile이란?
1. java변수를 main memory에 저장하겠다라는 것을 명시하는 것
2. 멀티스레드는 성능향상(속도)을 위해 main memory의 변수 값을 CPU cache에 저장하고 값을 가져온다.
3. 멀티스레드가 값을 가져올때, CPU cache에서 가져온 값이 불일치 할 수 있다(가동중에 fill() 메소드에서 아직 초콜릿이 끓고 있는데, 재료를 집어넣고 말았습니다. 와 같은.)
4. 그래서 volatile을 사용하여 main memory에서 값을 가져옴으로서, 값의 불일치를 해결한다.
코틀린
//object을 이용하면 싱글톤을 사용할 수 있다.
object Singleton {
//만약 파마메터(예 : context)를 받는 싱글톤이라면 Companion object를 사용한다.
private var uniqueInstance:Singleton? = null
fun getInstance() : Singleton {
if (uniqueInstance == null) {
return uniqueInstance ?: synchronized(this) {
uniqueInstance ?: Singleton.also {
uniqueInstance = it
}
}
}
return uniqueInstance!!
}
}