싱글톤 패턴이란?

인스턴스를 오직 하나만 제공하는 패턴을 말한다.

시스템 런타임, 환경 세팅에 관한 정보 등 인스턴스가 여러개일때 문제가 생길 수 있는 경우 인스턴스를 하나만 제공한다.

  • 마우스 포인터가 2개일경우 생기는 문제상황
  • 회원 정보를 관리하는 리포지토리
  • 주문 서비스 도우미 등

이럴 때 유의해야 할 사항은 어떤게 있을까?

  • new 연산자를 사용하지 못하게끔 Access Modifier를 private로 설정해 둔다.
  • Global Access를 하려면 getter, setter, 그리고 getInstance를 메서드로 둔다.
  • getInstance 메서드 안에도 new가 함부로 들어가면 안 되니 null check를 꼭 해둔다.

예제

영준이네 동아리 '프로토스' 는 서로 '칼라'라는 중앙 소통 장치로 소통을 한다.

그런 입장에서 칼라가 2개일 경우 서로 소통이 되지 않아 큰 문제가 발생한다. 그래서 칼라는 꼭 단일 개체로 유지해주어야 한다.

public class Protoss{
	public static void main(String[] args){
		Khala khala = Khala.getInstance()
		Khala khala1 = Khala.getInstance()
		System.out.println(khala==khala1)
	}
}

칼라와 칼라1이 같은 인스턴스임을 확인하려면 어떻게 해야 할까?

class Khala{
	private static Khala khala
	public static Khala getInstance(){
		khala = new Khala(); // 매번 new 연산자 돌기때문에 다중분신술됨
	}
}

먼저 이렇게 클래스를 설계해봤다. 여기서 문제가 발생한다.

=> getInstance를 실행할때마다 새로운 칼라 객체가 등장한다.

그래서 칼라 클래스를 다음과 같이 바꿨다.

class Khala{
	private static Khala khala
	public static Khala getInstance(){
		If (khala == null){
			khala = new Khala();
		}
		return khala;
	}
}

칼라 객체가 없을 때만 새로운 객체를 생성하고, 칼라 객체가 있을때는 기존 칼라 객체를 반환했다.

그런데 여기서 또 문제가 발생한다.

=> 우리는 보통 싱글쓰레드가 아닌 멀티쓰레드 환경에서 프로그램을 실행하기 때문에 쓰레드가 2개일 경우 다음과 같은 상황이 발생한다. 거의 속도차이가 없는 쓰레드1과 쓰레드2가 동시에 getInstance() 안으로 들어왔다고 생각해보자.

  • 쓰레드1 : khala == null을 확인하고 새로운 칼라 객체를 반환한다.
  • 쓰레드2 : khala == null을 확인하고 새로운 칼라 객체를 반환한다.

이 상황일경우, 칼라 객체는 2개가 되어버린다. 

 

문제 해결

그러면 이 문제를 어떻게 해결할까?

1. Synchronized

-> getInstance 앞에 Synchronized을 붙여준다.

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

이렇게 되면 위의 멀티쓰레드 문제는 해결했지만, 동기화 과정에서 성능 이슈가 생길 수 있다.

성능을 신경쓰고 싶고, 객체를 만드는 비용이 비싸지 않으면 다음과 같은 과정도 시도해볼 수 있다.

2. 이른 초기화(Eager Initialization)

class Khala{
	private static Khala khala = new Khala()
	public static Khala getInstance(){
		return khala;
	}
}

이렇게 되면 동기화 성능이슈에서도 자유로워질 뿐 아니라 멀티쓰레드 환경에서도 자유롭고, 코드를 조금 더 깔끔하게 쓸 수 있다.

 

3. 이중 체크 잠금(Double Check Locking)

이중 체크 잠금은 1, 2번의 문제를 보완한 방법이다. Synchronized 블럭을 밑에서 사용함으로써 비용을 절감하고, 인스턴스 생성도 필요할 때 해줄 수 있다.

class Khala{
	private static volatile Khala khala;
	
    Khala(){}
	
    public static Khala getInstance(){
		if (khala==null){
			(synchronized(Khala.class){ // 쓰레드 두개가 한번에 들어와도 여기서 걸리게됨
				if(khala==null){
					khala = new Khala()
				}
			}
		}
		return khala;
	}
}

 

 

3번의 이중 체크에서 눈여겨 볼 곳은 바로 volatile이라는 키워드다.

더보기

Volatile을 쓰면 ?
멀티 코어 프로세서에서는 각 코어마다 Cache가 있어서 RAM의 값을 복사해온다.

그러다 보니 메모리의 값이 변경되었는데도 캐시에 저장된 값이 갱신되지 않아

각 Cache 안의 변수값이 Cache마다, 그리고 원본과도 달라질 수가 있다.

따라서 volatile을 붙여 코어의 Cache에서 RAM의 값을 복사하지 말고,

코어가 RAM의 원본을 매번 복사하게 만든다.

나중에 포스팅 하겠으나, 더욱 자세한 설명은

https://www.charlezz.com/?p=45959 을 참조하면 된다.

 

그러나 1,2,3번에도 문제가 있었으니 바로 자바 1.4 이상에서만 동작한다는 것이다.

자바의 핵심은 호환성에 있어서, 정말 크리티컬한 기술이 아니면 자바에서는 호환성이 좋은 코드를 추천한다.

그래서 자바의 추천은 다음과 같다.

 

4. 스태틱 이너 클래스(Static Inner class) 

동기화에서도 안전하고, 자바 전 구간에서 호환되는 방법이다.

public class Khala {
    private Khala(){}

    private static class KhalaHolder{
        private static final Khala khala = new Khala();
    }

    public static Khala getInstance(){
        return Khala.khala;
    }

}

클래스홀더라는 이너클래스를 만들고, 이너클래스에서 객체를 생성해 반환한다.

 

1,2,3,4의 문제점

완벽할 것 같았던 이 싱글톤 방식에도 허점이 있었으니, 바로 자바의 reflection과 serialization/deserialization이다.

Reflection은 쉽게 말하자면 class가 어떻게 구성되어있는지를 몰라도, 클래스의 객체를 생성해버릴수가 있다.

Serialization또한 바이트코드를 건드려 응용 프로그램에서 쓰는 데이터를 네트워크를 통해 전송하거나 DB 또는 파일에 저장 가능한 형식으로 바꾼다.

Deserialization은 외부 소스에서 데이터를 읽고 이를 런타임 객체로 바꾸는 반대 프로세스다.

 

그걸 또 막는 방법?

이 무슨 강퇴->강퇴반사->슈퍼방장같은 flow인가 싶지만, 다 방법이 있다.

1. enum의 사용

enum은 reflection을 못하게 만들기 때문에 reflection에 대해 안전하다.

또한 Serialize를 상속하고 있기 때문에 이또한 방어할 수 있다.

enum Khala{
	khala;
}

enum은 이렇게만 만들어 주면 된다. 그러나 enum에도 문제점이 있으니, 바로 '상속 불가능'하다는 점이다.

또한 클래스가 이미 만들어져 있어서 리소스 낭비가 있을 수 있다. 그러나 이정도 쯤이야...

 

Spring에서의 싱글톤 방식?

스프링에서 ApplicationContext등의 Stateless해야한 객체들은 늘 싱글톤 방식으로 구현해야 한다.

싱글톤 방식으로 구현할때에는 위의 reflection등을 비롯한 여러 문제가 발생하는데, 대표적으로

  • 의존관계상 클라이언트가 구체 클래스에 의존한다 ⇒ DIP 위반
  • 따라서 OCP 위반의 가능성이 높다.

등이 있다. 그러나 스프링 컨테이너가 이와 같은 문제들을 대부분 해결해준다.

@Conflguration과 @Autowired가 그 예이다.

그래서 객체 생성시 getBean으로 내용물을 꺼내오면 언제나 같은 객체가 반환된다. 이는 추후에 스프링 포스팅에서 다시 작성하도록 하겠다.

 

+ Recent posts