Skills/Java

Java - private 생성자나 열거 타입으로 싱글턴임을 보증하라.

aoaa 2022. 10. 25. 22:16

 Singleton은 애플리케이션이 실행될 때, 어떤 클래스에 최초 한번만 메모리를 할당하고, 그 메모리에 인스턴스를 만들어 사용하는 패턴입니다. 최초로 한번만 할당했기 때문에 여러번 호출되어도 실제로 생성되는 객체는 하나이며, 최초로 생성된 이후에 호출된 생성자는 이미 생성한 객체를 반환하도록 만듭니다.


1. 장점과 단점

 장점이자 사용 이유는 DB에서 커넥션, 스레드풀, 캐시, 로그 등 공통된 객체를 생성해서 사용하는 상황에 최초 한번만 메모리를 할당하기 때문에 메모리 낭비를 방지할 수 있고, 싱글톤으로 구현한 인스턴스는 '전역'이므로 다른 클래스의 인스턴스들이 데이터를 공유하는 것이 가능합니다. 

 다만 이러한 싱글톤 패턴으로 인스턴스를 생성하게 되면, 싱글톤 인스턴스가 너무 많은 역할을 하게되고 이러한 데이터 공유는 다른 클래스들간의 결합도가 높아지게 되는데 객체 지향의 개방-폐쇄 원칙(Open-Close Principle) 위배됩니다. 또한 이런 결합도가 높아지면 테스트하기가 어려워질 수 있는데 타입을 인터페이스로 정의한 뒤 그 인터페이스를 구현해서 만든 싱글톤이 아니라면, 싱글톤 인스턴스를 가짜(mock) 구현으로 대체할 수 없기 때문입니다. 장,단점은 이쯤말하고 설명한 싱글톤 인스턴스를 생성하는 방법에 대해 살펴보겠습니다.


2. public static final 필드 방식의 Singleton

public class Singleton {
	public static final Singleton Instance = new Singleton();
    
    private Singleton() {
    ...
    }
    
    public void Singtletontest() {
    ...
    }
}

 private 생성자인 Singleton은 public static final 필드인 Singleton.Instance를 초기화할 때 딱 한 번 최초로 호출됩니다. 

Access modifier가 public이나 protected인 생성자가 없기 때문에 Singleton 클래스가 초기화될 때 만들어진 인스턴스가 전체 시스템을 통틀어 하나밖에 없음을 보장하게 됩니다. 

 예외적으로 권한이 있는 클라이언트는 리플렉션 API인 AccessibleObject.setAccessible을 사용해 private 생성자를 호출할 수 있는데, 이를 방어하려면 생성자를 수정하여 두 번째 객체가 생성되는 시점에 예외를 던지면 해결됩니다. 

 

 이처럼 public static final를 사용하면 클래스가 싱글턴임을 API에서 잘 드러낼 수 있으며(간결), 절대 다른 객체를  참조할 수 없게 됩니다.


3. 정적 팩토리 방식의 SIngleton 

public class Singleton {
	private static final Singleton Instance = new Singleton();
    private Singleton() {
    }
    
    public static Singleton getInstance() {
    	return Instance;
	}
    
    public void Singletontest() {
    ...
    }
}

 public static Singleton getInstance()와 같이 정적 팩토리 메서드를 public static 멤버로 제공하는 방법입니다. 

생성자를 private으로 설정하여 외부에서 인스턴스를 생성할 수 없게하고, new 키워드로 인스턴스를 하나만 생성합니다. 

 그 인스턴스를 return하는 public static SIngleton getInstance()라는 정적 팩토리 메서드를 생성하여 외부에서는 Singleton.getInstance()를 통해 인스턴스를 접근하도록 합니다. 

 

 이는 1) API를 바꾸지 않고도 싱글톤이 아니게 변경할 수 있습니다. 무슨 의미냐? getInstance()를 호출하는 부분을 수정할필요 없이 

내부에서 private static이 아닌 새 인스턴스를 생성해주면 됩니다. 이는 유일한 인스턴스를 반환하던 팩토리 메서드를 호출하는 스레드 별로 다른 인스턴스를 넘겨주도록 할 수 있게됩니다.

 

2) 정적 팩토리를 Generic SIngleton Factory로 만들수 있습니다.

public class Genericsingleton { 
	public static final Set hash = new HashSet();
    
    	public static final <T> Set<T> hash() {
        	return (Set<T>) hash;
        }
}

 제네릭으로 타입설정을 가능한 인스턴스를 만들고, 반환 시에 제네릭으로 받은 타입을 이용해 타입을 결정할 수 있게됩니다.

 

3) 정적 팩토리의 메서드 참조를 supplier로 사용할 수 있습니다. 

Java 8의 함수적 인터페이스인 Supplier는 인터페이스 매개값은 없고 return 값이 있는 get~~ () 메서드를 가집니다.

이 같은 함수적 인터페이스의 목적은 메서드나 생성자의 매개 타입으로 사용되어 람다식을 대입할 수 있도록 하기 위함입니다.

 위의 코드에 대입하게 되면 SIngleton.getInstance 대신, Supplier<SIngleton>으로 사용할 수 있는 것이죠. 나중에 supplier 파트에서 더 자세히 살펴보겠습니다. 

 

 정적 팩토리 방식의 장점을 활용할 일이 없다면, 대부분 public static final 방식만을 사용합니다.

 

 위의 두 방식으로 만든 싱글톤 클래스를 직렬화하려면 Serializable을 구현하다고 선언하는 것 만으로 부족한데, readResolve 메서드를 제공해야합니다. 

4. 열거 타입 방식의 SIngleton

public enum Singleton {
	Instance;
    
    public void Singletontest() {
    }
}

 enum 타입을 선언하는 방식은 public 필드 방식과 유사하지만 더 간결하고, 추가 노력없이 *직렬화할 수 있고, 복잡한 직렬화 상황이나 리플렉션에서도 제2의 인스턴스가 생기는 일을 막아줍니다. 대부분 생황에서는 element가 하나 뿐인 enum 타입이 싱글톤을 만드는 가장 좋은 방법입니다. 하지만 만들려는 싱글톤이 enum 외의 클래스를 상속해야 한다면 사용할 수 없습니다.

 

*직렬화(Serialization) : Java 시스템 내부에서 사용되는 인스턴스나 데이터를 외부의 Java 시스템에서도 사용할 수 있도록 byte 형태로 변환하는 기술과 byte로 변환된 데이터를 다시 인스턴스로 변환하는 기술(역직렬화)을 말합니다. 시스템적으로는 JVM의 메모리에 상주하고 있는 객체 데이터를 byte형태로 변환하는 기술과 직렬화된 byte 형태의 데이터를 객체로 변환해서 JVM으로 상주시키는 형태를 말합니다. 

 

 

 

 

 

 

 

참조