이번에 테코톡 주제로 싱글턴과 정적클래스
에 대해서 준비할 기회가 생겼다. 객체지향적인 설계에서는 static 메서드를 어떻게 다루는게 좋을까? 의문이 있었는데 이번 기회에 공부해봐야겠다
디자인 패턴이 생소할 수도 있는데, 쉽게 말하면 수학 공식 같은 비법이다. 소프트웨어 개발 과정에서 자주쓰이는 설계의 노하우들을 정리한 방법들이라고 생각하면 된다.
디자인 패턴의 일종으로 객체의 인스턴스가 오로지 한개만 생성되도록 설계하는 것을 의미한다.
게임의 환경 설정을 관리하는 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@2c8d66b2
game.Settings@5a39699c
false
싱글턴 패턴이 적용되어 있지 않다면, 생성할 때 마다 새로운 인스턴스를 만들기 때문에 settings1
과 settings2
는 같지 않다.
Settings
클래스 필드에 인스턴스를 저장해서 getInstance()
메서드를 통해 호출해서 받는 형식이다. 생성자를 private하게 만들면 외부에서 생성할 수 없게된다.
public class Settings {
private static Settings instance;
private Settings() {
}
public static Settings getInstance() {
if (instance == null) {
instance = new Settings();
}
return instance;
}
}
프로그램을 실행하면 아래와 같은 결과를 얻을 수 있다.
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@2c8d66b2
game.Settings@2c8d66b2
true
public static Settings getInstance() {
if (instance == null) {
instance = new Settings();
}
return instance;
}
만약 환경이 멀티쓰레드 환경으로 바뀐다면 문제가 발생한다. 두개의 쓰레드가 getInstance()
를 호출했는데 먼저 if문을 통과한 A가 인스턴스를 생성하기전에 B가 들어온다면 A랑 B는 서로 다른 인스턴스를 생성하게 된다. 따라서 스레드 세이프하지 못하다.
클래스가 로딩되는 시점에 스태틱 블럭이므로 미리 생성을 한다. 스레드 세이프하게 구현할 수 있다.
private static final Settings INSTANCE = new Settings();
private Settings() {
}
public static Settings getInstance() {
return INSTANCE;
}
미리 생성한다는 것이 문제다. 만약 인스턴스를 만드는 과정이 길고 메모리가 많이 든다면, 객체를 사용하지 않더라도 로딩할 때 리소스를 많이쓰게된다.
public class Settings {
private static Settings instance;
private Settings() {
}
public synchronized static Settings getInstance() {
if (instance == null) {
instance = new Settings();
}
return instance;
}
}
첫번째 방법에서 문제가 발생한 메서드를 synchronized를 통해 동시성 문제를 해결한다.synchronized 키워드는 현재 데이터를 사용하고 있는 스레드를 제외하고 나머지 스레드가 데이터에 접근할 수 없도록 막아준다. 동시에 객체를 사용할 때 초기화하기 때문에 사용하지 않을 때 리소스 낭비가 없다.
인스턴스가 생성된 뒤에는 getInstance()
가 스레드 세이프할 필요는 없다. 하지만 매번 호출할 때 마다 동기화때문에 불필요하게 lock이 걸리게 되어 리소스 낭비가 심해진다.
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;
}
}
synchronize 하는 시점을 뒤로 미루는 방법이다. 이미 인스턴스가 생성되어있다면 if문이 스킵되기때문에 메서드를 실행할 때 발생하는 비용낭비 문제가 없다. 이때 변수에 volatile 키워드를 통해 메인 메모리에 저장을 해서 관리해야한다. 그렇지 않으면 스레드마다 CPU Cache를 갖고 있기 때문에 다른 인스턴스를 생성할 수 있다.
JDK 5이상부터만 volatile을 사용할 수 있다. 코드도 길고 자바가 어떻게 메모리를 쓰는지 이해하고 있어야 구현할 수 있다.
public class Settings {
private Settings() {
}
private static class SettingsHolder {
private static final Settings SETTINGS = new Settings();
}
public static Settings getInstance() {
return SettingsHolder.SETTINGS;
}
}
private 생성자를 통해 외부 생성을 막고, static inner class에 인스턴스를 저장해서 static 영역을 활용하면서 객체가 필요한 시점까지 초기화를 미루는 방식이다. getInstance() 메서드가 호출하는 순간 내부 클래스의 초기화가 진행된다.
public enum Settings {
INSTANCE;
}
리플렉션과 직렬화에도 안전하다.
enum으로 구현하게되면 상속을 쓰지 못하고, 미리 만들어진다는 단점이 있다.
싱글톤 패턴을 구현하는 방법을 순수한 방법에서 문제점을 하나씩 해결해가면서 알아봤다. 결론적으로 싱글톤 패턴을 구현하기 위해서 static inner 클래스 혹은 enum을 사용하는 것이 가장 효율적이다.
https://javaplant.tistory.com/21
https://nesoy.github.io/articles/2018-06/Java-volatile
https://kdhyo98.tistory.com/70?category=971166