싱글톤 패턴이란 인스턴스를 오직 한개만 제공하는 클래스를 의미한다.
시작하기 전에 개념을 살짝 알아보면
클래스(Class) 란
객체를 만들어 내기 위한 설계도 혹은 틀
객체(Object) 란
소프트웨어 세계에 구현할 대상
인스턴스(Instance) 란
설계도를 바탕으로 소프트웨어 세계에 구현된 구체적인 실체
public class App {
public static void main(String[] args) {
Car car1 = new Car();
Car car2 = new Car();
System.out.println(car1);
System.out.println(car2);
System.out.println(car1 == car2);
}
}
다음 코드는 Car
라는 객체를 동일하게 생성하여 두개의 인스턴스가 같은 것인지 확인해보려고 한다.
결과는 다음과 같이 아예 다른 객체임을 확인할 수 있다. 그 이유는 java에서 new
를 사용하여 객체를 생성하면 new
를 사용할 때마다 새로운 메모리 공간에 새로운 객체를 만들어서 반환해주기 때문이다.
그럼 우리는 이 new
를 막아버리자!
new
를 통해 생성자에서 새로운 객체를 반환받아서 사용하기 때문에 생성자를 private으로 막아버리면 더이상 new를 사용할 수 없다.
public class Car {
private Car() {
}
}
그러면 이제 문제는
다음과 같이 new를 통해 새로운 객체를 반환받을 수 없어졌기 때문에 에러가 난다.
public class Car {
private static Car car;
private Car() {
}
public static Car getCar(){
if(car == null) return car = new Car();
return car;
}
}
public class App {
public static void main(String[] args) {
Car car1 = Car.getCar();
Car car2 = Car.getCar();
System.out.println(car1);
System.out.println(car2);
System.out.println(car1 == car2);
}
}
코드를 다음과 같이 수정하여 car라는 객체를 static으로 등록해두고 getCar()
메서드가 호출되었을 때 해당 인스턴스가 구현이 되어 있다면 동일한 인스턴스로 반환하도록 코드를 수정하면 같은 인스턴스가 출력되는 것을 확인할 수 있다.
하지만 해당 부분은 동시성 문제에서 자유롭지 못하다. 코드를 더 수정해보자!
public class Car {
private static Car car;
private Car() {
}
public static synchronized Car getCar(){
if(car == null) return car = new Car();
return car;
}
}
가장 안전하고 간단하게 만드는 방법이지만 성능 이슈가 있다. synchronized
키워드를 통해 쓰레드를 검사하는 과정이 많은 리소스를 필요로 하기 때문이다.
public class Car {
private static final Car CAR = new Car();
private Car() {
}
public static synchronized Car getCar(){
return CAR;
}
}
객체 생성 비용이 크지 않다면 미리 정의해두면 된다.
하지만 객체 자체가 미리 만드는 비용이 크다면 어플리케이션 로드를하는 과정에서 해당 class가 정말로 사용될지 안될지 모르지만 우선 class를 로드하면서 객체를 생성해야하기 때문에 성능 이슈가 발생할 수 있다.
public class Car {
private static volatile Car car;
private Car() {
}
public static Car getCar(){
if(car == null){
synchronized (Car.class){
if(car == null){
car = new Car();
}
}
}
return car;
}
}
volatile
는 java 1.5이상부터 지원되는 기능으로 위 synchronized보다 성능이 조금 더 보장된다. 그 이유는 getCar()
를 호출할 때마다 synchronized를 매번 체크하지 않고 극소수의 상황에서 발생하는 synchronized를 null일 경우에만 체크하기 때문이다.
하지만 volatile
를 이해하는 것과 코드 자체가 굉장히 복잡하고 java 1.5 이상에서만 동작한다는 단점이 있다.
public class Car {
private Car() {
}
private static class CarHolder{
private static final Car INSTANCE = new Car();
}
public static Car getCar(){
return CarHolder.INSTANCE;
}
}
inner class를 사용하게되면 synchronized
을 사용하지 않아 성능적인 이슈를 걱정하지 않아도 된다. 또한 이른 초기화
에서 문제가 되었던 어플리케이션이 시작될 때 바로 인스턴스를 생성하지 않고 getCar()
가 최초로 호출되었을 때 inner class가 로드되며 실제로 해당 인스턴스를 사용하기 위해 호출되었을 때 세팅이 되므로 성능적인 이슈를 해결할 수 있다.
public class App {
public static void main(String[] args) throws IllegalAccessException, InvocationTargetException, InstantiationException {
Car car1 = Car.getCar();
Car car2 = null;
Constructor<?>[] declaredConstructors = Car.class.getDeclaredConstructors();
for (Constructor<?> declaredConstructor : declaredConstructors) {
declaredConstructor.setAccessible(true);
car2 = (Car) declaredConstructor.newInstance();
}
System.out.println(car1);
System.out.println(car2);
System.out.println(car1 == car2);
}
}
java의 Reflection API를 사용하여 private으로 접근을 막아둔 생성자를 강제로 접근이 가능하도록 변경한 뒤 newInstance()
메서드를 통해 새롭게 받아오면 싱글톤을 깰수도 있다.
참고
이 외에 직렬화와 역직렬화를 통해 패턴을 깰수도 있긴 하다.
public enum Car {
INSTANCE;
}
public class App {
public static void main(String[] args) throws IllegalAccessException, InvocationTargetException, InstantiationException {
Car car1 = Car.INSTANCE;
Car car2 = Car.INSTANCE;
Constructor<?>[] declaredConstructors = Car.class.getDeclaredConstructors();
for (Constructor<?> declaredConstructor : declaredConstructors) {
declaredConstructor.setAccessible(true);
car2 = (Car) declaredConstructor.newInstance();
}
System.out.println(car1);
System.out.println(car2);
System.out.println(car1 == car2);
}
}
enum을 통해 선언해둔 class를 실행해보면
Reflection 자체에서 막아둬서 enum을 사용하면 가장 안전하게 singleton을 구현할 수 있다.
하지만 단점으로는 이른 초기화
와 동일하게 어플리케이션이 우선 class를 로드하면서 객체를 생성해야하기 때문에 성능 이슈가 발생할 수 있다.
또한 상속을 사용하지 못한다. -> 하지만 상속에서 발생하는 문제도 있기 때문에 이 부분은 고려할만 할거 같다.