[Java] 자바의 싱글톤 패턴

 

Singleton(싱글톤 패턴)

 

어떤 클래스가 최초 한번만 메모리를 할당하고, 그 메모리에 객체를 만들어 재사용하는 디자인 패턴을 의미한다. 즉, 생성자의 호출이 반복적으로 호출되었을 때에도 실제로 하나의 객체만이 생성되어 사용되는 패턴으로, 특히 스프링에서 스프링 빈의 관리를 싱글톤으로 하여 객체 인스턴스를 하나만 생성하여 DI(Dependency Injection)와 OCP(개방 폐쇄 원칙) 위반없이도 자유롭게 싱글톤을 사용하게 한다. 이는 메모리의 낭비를 방지할 뿐만 아니라, 객체가 전역성을 띄기에 다른 객체와 공유 또한 용이하다.

 

- 싱글톤 인스턴스는 전역 인스턴스로 데이터를 공유하기 쉽다.

- 공통된 객체를 여러개 생성해서 사용해야 하는 상황에서 많이 사용한다.

- 두 번째 이용시부터는 객체 로딩 시간이 줄어서 성능이 좋아진다.

 

 

 

다양한 싱글톤의 구현

 

1. eager initialization

 

싱글톤으로 생성하기 원하는 클래스 객체를 클래스 내부에 static으로 선언한 뒤에, 생성자를 private으로 설정하여 외부에서 생성자를 통해서 객체를 생성하는 것을 막고, getInstance()라는 static method만을 이용하여 해당 클래스 객체에 접근하게 하는 대표적인 방법이 있다. 

public class MyClass {
    private static MyClass instance = new MyClass();
    
    private MyClass() {}
    
    public static Myclass getInstance() {
    	return instance;
    }
}

 

> 하지만 이 방법은 프로그램의 크기가 커져서 수많은 클래스가 이 방법으로 싱글톤 패턴을 생성할 경우, 클래스 로딩 시점에 인스턴스를 생성하기 때문에 부담을 주고 어떠한 에러처리조차 할 수 없다. (JVM에서 클래스 로더라는것이 클래스 파일을 참조하는 순간 동적으로 읽어서 메모리에 로딩하게 되는데, 이때마다 인스턴스를 생성해주어야 하기 때문에 부담이 간다고 한다.)

 

2. static block을 통해 초기화 로직 담기

public class MyClass {
    private static ExampleClass instance;
    
    private MyClass() {}
    
    // 클래스 로딩, 메모리 할당 후 순차 실행
    static {
    	try {instance = new MyClass()}
        catch(Exception e) {throw new RuntimeException()}
    }
    public static MyClass getInstance() {
    	return instance;
    }
}

> static 블록을 사용하여 클래스가 로딩될 경우 한 번만 실행되게 하는 특성을 이용한다. 1번 방법에서 불가능했던 초기화시의 로직을 담을 수 있는 장점이 있긴 하지만 첫번째 방법에서 문제가 되었던 클래스 로딩 시점에 미리 인스턴스가 생성되게 되는 문제는 해결하지 못했다. (클래스 로딩후에 스태틱 블록이 실행되게 되므로..)

 

3. lazy init을 통해 JVM메모리 초기화 부담 줄이기

public class MyClass {
    private static MyClass instance;
    
    private MyClass() {}
    
    public static MyClass getInstance() {
    	if(instance==null) {instance = new MyClass();}
        return instance;
    }
}

> getInstance메소드에 instance의 생성이 이루어졌는지를 검사하는 로직을 추가하여 getInstance의 호출이 있을 때 객체가 생성되도록 하였다. 따라서 최초 사용시에만 instance가 생성되기 때문에 프로그램이 메모리에 적재되는 시점의 부담이 줄어들게 된다(1,2번의 문제). 하지만 프로그램이 멀티 쓰레드 환경에서 동작할 경우 동일 시점에 getInstance()가 최초 호출되어 1개 이상의 인스턴스가 생성될 수 있는 위험성이 있다.

 

4. Synchronized로 Thread safe 보장

public class MyClass {
    private static MyClass instance;
    
    private MyClass() {}
    
    public static synchronized MyClass getInstance() {
        if(instance==null) {instance = new MyClass();}
        return instance;
    }

> lazy init과 동일하지만 synchronized 키워드를 이용하여 쓰레드의 동시접근 문제를 해결하였다. 하지만 수많은 쓰레드들이 getInstance()메소드를 호출하게 되면 높은 cost로 인해서 성능 저하가 발생할 수 있다.

 

5. Lazy Holder 패턴

public class MyClass {
    private MyClass() {}
    
    private static class InnerInstanceClass {
        private static final MyClass instance = new MyClass();
    }
    
    public static MyClass getInstance() {
        return InnerInstanceClass.instance;
    }
}

> JVM의 클래스 로더 매커니즘과 클래스의 로드 시점을 이용하여 내부 클래스를 통해 생성시킴으로서 쓰레드 간의 동기화 문제도 해결한다. 위에서 문제되었던 (1)클래스 인스턴스 생성시점에 생성, (2) 초기화 로직 (3) 멀티쓰레드 환경에서의 동시접근 (4) 멀티쓰레드 cost 등등의 문제점이 모두 해결되었다.

 

처음 getInstance를 호출해야 inner class에 접근하게 되면서 생성지점을 조절하는 방법으로, 멀티쓰레드 환경에서 동시에 getInstance()가 호출되더라도 instance가 final변수이므로 재 할당되지 않는다. 따라서 성능이 좋지 않는 synchronized키워드를 사용하지 않고도 동시성 문제를 해결할 수 있는 좋은 방법이라고 할 수 있다.

 

 

 

참고) https://elfinlas.github.io/2019/09/23/java-singleton/

 

 

 

싱글톤이 안티 패턴?

하지만 싱글톤도 자바 코드로만 구현할 경우 여러가지 문제점을 안고 있기 때문에 안티패턴으로 불리기도 한다.

 

- 멀티 쓰레드 환경에서 동기화 처리 문제와 같은 문제점이 발생할 수 있다.

- 싱글톤으로 만든 객체의 역할이 복잡하다면, 싱글톤 객체를 사용하는 다른 객체와의 결합도가 높아져서 OOP의 OCP원칙(개방폐쇄원칙-확장에 대해선 열려있고, 수정에 대해서는 닫혀있어야 한다)을 위반할 수도 있다.

- 싱글톤 객체 자체의 수정이 이루어질 경우 예기치 못한 오류(side effect)가 발생할 수도 있다.

 

- 싱글톤은 자신만이 객체를 생성할 수 있도록 private 생성자를 가진다. 따라서 외부에서 생성자를 호출할 수 없어 상속이라는 OOP의 장점중 하나를 사용할 수 없게 되는 문제가 생긴다.

- 싱글톤은 생성 방식이 제한적이고, 동적으로 객체를 주입하기도 힘들기 때문에 테스트 코드 작성에 어려움이 있다.

- 서버 환경에서는 싱글톤이 1개만 생성됨을 보장하지 못한다.

- OOP에서 전역 상태로만 사용되는 객체의 사용은 바람직하지 않다.

 

따라서 바이트 코드의 조작을 통해서 위의 단점들을 해결해주는 스프링과 같은 프레임워크를 이용한 싱글톤이 아닌, 자바 코드만 사용해서 싱글톤을 사용할 때는 위와 같은 단점들을 감수해야 한다.

 

- 스프링에서는 별도의 private생성자를 필요로 하지 않기 때문에 상속이 가능하다

- 스프링 자체의 테스트 툴을 통해서 테스트가 용이하다

- 또한 프레임워크를 사용하면 여러 환경에서도 원래 목적인 1개의 객체 생성과 동기화 등을 보장받을 수 있다.

 

 

스프링 프레임워크의 싱글톤 참고

https://eckrin.tistory.com/entry/Spring-%EC%8B%B1%EA%B8%80%ED%86%A4-%EC%BB%A8%ED%85%8C%EC%9D%B4%EB%84%88

 

'[ Languages ] > Java' 카테고리의 다른 글

[Java] stream  (0) 2022.08.07
[Java] 자바의 동작 원리와 특징  (0) 2022.07.08
[JAVA] BufferedReader, BufferedWriter  (0) 2022.02.13
[Java] equals와 hashCode  (0) 2022.02.01
[Java] static에 관한 생각  (0) 2022.01.24