본문 바로가기
Language/Java

이펙티브 자바 - 싱글턴 패턴

by ocwokocw 2021. 9. 18.

- 이 글은 Effective Java 를 기반으로 작성되었습니다.

- 싱글턴

싱글턴은 객체를 하나만 만들 수 있는 클래스이다. 1.5 이전 버전의 JDK 에서는 2 가지 방법으로 싱글턴을 구현할 수 있다. 하나는 정적 멤버를 선언하는 것이고 다른 하나는 정적 팩토리 메소드를 이용하는 것이다. 이 방법으로 어떻게 싱글턴을 구현할 수 있는지 살펴보자.

 

2 가지 방법은 구현방법이 다르지만 공통점이 있는데 생성자가 private 여야 한다는 것이다. 생성자를 private 로 써야한다니 뭔가 있어 보이지만 생각해보면 객체가 하나만 존재하려면 자기자신 외에 생성자를 호출하면 안되므로 당연한것이다.


- 정적 멤버

첫번째 방법은 정적 멤버 final (static final) 멤버를 초기화하고 이를 외부에 공개하는 것이다.

 

public class ElvisStaticMember {

	private ElvisStaticMember() {}
	
	public static final ElvisStaticMember INSTANCE = new ElvisStaticMember();
}

 

위에서 public static final 인 의미를 살펴보자. 일단 생성자가 private 이기 때문에 다른 클래스에서 new 로 생성을 할 수 없다. 이 말은 객체를 생성하지 않고 다른 class 에서 INSTANCE 에 접근해야 한다는것인데 이런 상황에서 접근하려면 public static 이어야 한다. 또한 한번 생성한 싱글톤 객체가 변경되지 않도록 final 을 붙여주었다.

 

싱글턴 INSTANCE 에 접근하거나 사용할때에는 아래처럼 코드를 작성한다.

 

public class SingletonTest {

	public static void main(String[] args) {
		
		System.out.println("1: " + ElvisStaticMember.INSTANCE);
		System.out.println("2: " + ElvisStaticMember.INSTANCE);
		System.out.println("3: " + ElvisStaticMember.INSTANCE);
	}
}

- 정적 팩토리 메소드

두번째 방법은 해당 객체에 접근하는 정적 팩토리 메소드(static 메소드)를 사용하는것이다.

 

public class ElvisStaticFactoryMethod {

	private ElvisStaticFactoryMethod() {}
	
	private static final ElvisStaticFactoryMethod INSTANCE = 
		new ElvisStaticFactoryMethod(); 
	
	public static ElvisStaticFactoryMethod getInstance() {
		return INSTANCE;
	}
}

 

정적 멤버와 마찬가지의 이유로 외부 class 의 접근을 막기 위해서 생성자를 private 접근제어자로 선언해준다. 그리고 정적 멤버를 public 이 아닌 private 로 선언한다. 이 상태에서는 외부 class 에서 해당 객체에 접근할 수 없는데 정적 팩토리 메소드를 통해 접근할 수 있도록 public static 메소드를 선언한다.


- 정적 멤버와 정적 팩토리 메소드의 문제점

위의 2 가지 싱글턴 구현방법에는 문제점이 있다. 사실 일반적인 Web 개발에서 이 문제점들을 직접적으로 경험할일이 없을 수도 있지만 어쨌든 문제이긴 하므로 소개하고자 한다.

 

우선 첫째로 클라이언트는 AccessibleObject.setAccessible 메소드로 권한을 획득하고 리플렉션을 통해 private 생성자를 호출할 수 있다.

 

public class SingletonTest {

	public static void main(String[] args) throws InstantiationException, IllegalAccessException, IllegalArgumentException, InvocationTargetException {
		
		Constructor<?> con = ElvisStaticMember.class.getDeclaredConstructors()[0];
		con.setAccessible(true);
		
		ElvisStaticMember newInstance = (ElvisStaticMember) con.newInstance();
		
		System.out.println("origin instance: " + ElvisStaticMember.INSTANCE);
		System.out.println("new instance: " + newInstance);
	}
}

 

위의 코드를 실제로 수행해보면 인스턴스 주소가 다르게 나오는것을 확인할 수 있다.

 

두번째로는 Serializable 에 관한 문제가 있다. 직렬화를 구현하려면 Serializable 인터페이스를 구현하면 된다. 문제는 역직렬화시 싱글톤이 깨진다는것이다.

 

이를 방지하려면 모든 필드를 transient 로 선언하고 readResolve 메소드를 추가해야 한다.


- enum

enum 은 상수형을 선언할 때 사용되지만 기본적으로 싱글턴이기 때문에 enum 을 이용해서도 구현할 수 있다.

 

public enum ElvisSingleton {

	INSTANCE;
	
	public void someMethod() {
		System.out.println("someMethod");
	}
}

 

특별히 접근제어자나 정적 여부 final 로 세세하게 컨트롤 하지 않아도 간결하고 깔끔하게 구현할 수 있다. 단순히 깔끔하기만 할 뿐 아니라 앞에서 언급한 2 가지 문제점도 자동으로 해결된다. 원소가 하나인 enum 자료형은 싱글턴 구현을 위한 최고의 방법이다.

댓글