멀티 스레딩 환경에서의 Singleton 사용 및 견고한 Singleton 구현 방법 (Enum Singleton)


Singleton?

Singleton pattern은 특정 인스턴스에 대하여 단일 인스턴스만 만들어질 수 있도록 제한하는 패턴이다. 스레드 풀이나 캐시, 네트워크 모듈 등등에서 같은 객체를 여러개 만드는 것은 불필요한 자원을 할당하며 예기치 않은 오류를 불러올 수 있는데, 이런 상황에서 singleton을 사용하여 하나의 인스턴스만을 생성하여 사용하도록 관리를 해주면 불필요한 메모리 낭비를 줄일 수 있고 사용성 측면에서도 효율적이라 자주 쓰이는 패턴중에 하나이다.

자주 사용하던 방식은 이런 방식이다.

이렇게 되면 생성자가 private이라 무조건 getInstance()를 통해서만 인스턴스에 접근할 수 있기 때문에 mInstance == null인 최초 상황에서만 초기화가 되며 그 이후에는 생성된 기존의 인스턴스를 사용하게 된다.




그러나 과연 'Singleton'을 보장할 수 있을까?

일단 기본적으로 Android는 멀티 스레딩 환경이다. 만약 A thread와 B thread에서 동시에 이 인스턴스에 접근하려고할 때 마침 mInstance가 null이였다면, A thread에서도 mInstance == null를 타기 때문에 mInstance를 새로 할당할거고 B thread에서도 mInstance == null를 타며 새로 할당하게 될 것이다.
그렇게 되면 예상했던 것과는 달리 mInstance가 2개 이상의 중복 생성이 되며 예상하지 못한 오류를 만들어낼 수도 있다.

일단 해결책으로, Main thread에서만 접근하도록 제어할 수도 있을 것이다. 다른 스레드에서 접근하면 Exception을 날린다던가 하는 식으로 예외처리를 할 수 있지만, 만약 이 singleton 객체가 꼭 멀티 스레딩 환경에서도 사용가능하게끔 해야 한다면??

그래서 나온 해결책이 getInstance()를 synchronized 시키는 것이다. mInstance를 체크하고 할당하는 부분을 동기적으로만 실행하게끔해서 구멍을 막는 것이다.





함수 동기화는 약 100배 정도의 성능 저하를 발생시킨다.


사실 getInstance()를 접근하는 시점이 멀티 스레딩 환경일 경우는 정말 극 소수일 수 있다. 아니 무엇보다 mInstance가 할당되는 시점은 가장 최초 사용시 1회 뿐이기 때문에 최초 한 번 이후의 synchronized는 불필요한 구문이다. 과연 그 최초 1회 시점을 위해 함수 전체에 synchronized를 걸어서 함수동기화하는 방법이 옳은 방법일까?

그렇다면 이러한 성능저하 없이, 정말 '딱 1회만' 인스턴스를 생성하는 방법을 고민해 볼 필요가 있다.

위 방법처럼 아예 클래스를 로딩할 때 인스턴스를 생성하고, 별 다른 고민 없이 인스턴스를 가져다 쓸 수도 있다. 클래스 로더에 의해 클래스가 처음 로딩될 때만 생성 되기 때문에 Thread safe하다.

이 인스턴스를 앱 실행 동안 필수로 사용하거나 생성비용이 적다고하면 이는 좋은 해결책이 될 수도 있다. 그러나 만약 필수로 사용하게 되는 인스턴스가 아니라면 필요하지 않은 인스턴스가 항상 static 영역에 위치하고 있게 되며, 심지어 인스턴스 생성 비용이 큰 경우라면 되려 비효율적일 수도 있다는 점을 감안해야 한다.




필요한 시점에서만 Synchronized 할 수 없을까?

사실 다중 스레드 환경에서의 중복 생성 이슈를 막기 위해서라면, 함수 전체가 아니라 생성하는 부분에만 synchronized를 걸어주면 된다. (이런 개념을 DCL이라고 한다.)


이렇게 되면 mInstance == null인 최초 1회에만 Thread safe하게 인스턴스를 생성할 것이다. 그리고 여러 스레드에서도 올바른 mInstance를 사용하기 위해 volatile 키워드를 달아준다. volatile는 객체가 Thread cache에 의해 다른 스레드에서 기존의 캐시된 인스턴스를 들고 있는 일이 발생하지 않도록 해준다. 즉 synchronized 내의 코드에서 mInstance가 할당되면 mInstance를 바라보는 다른 곳에서도 이 자원을 공유할 수 있게끔 해준다는 것이다. (volatile의 사용성은 32비트 환경에서의 long/double 사용 등, 본문에서 언급한 내용 외에도 여러가지 용도로 사용할 수 있는데 일단은 thread 관련된 점만 언급했다.)




이제 완벽히 'Singleton'이 보장될 수 있을까?

코드 상으로 여전히 남아있긴 하지만 synchronized 범위도 줄였고, DCL 개념을 사용하고 있기 때문에 앞서 나온 멀티 스레딩 이슈들은 다 해결 된 것 같다.

..라고 생각하지만 사실 아니다.

일단 리플렉션을 사용하면 충분히 singleton이 깨질 수 있다.


위와 같이 사용하면 생성자의 접근제어자와 상관없이 접근이 가능하기 때문에 앞서 나온 모든 종류의 singleton이 더 이상 singleton이 아니게 된다. (뭔가 '보스 등장' 같은 느낌)

그리고 직렬화, 역직렬화를 통해서도 singleton을 깰 수 있다.


위 코드를 통해 두 개의 인스턴스를 생성하고 비교해보면 두 인스턴스가 다른 값을 가진다는 것을 알 수 있다.





궁극의 Singleton을 찾아보자.

물론 앞서 언급한 singleton 파괴 방법은 사실 억지일 수 있다. 어떤 개발자가 굳이 자신의 코드 내의 singleton 패턴을 리플렉션으로 생성자를 얻어와서 사용하고, 역직렬화까지 해가며 접근하려고 할까?

또한 다른 개발자가 자신의 singleton을 함부로 사용하는 것을 남발하기 위해서라도, 리플렉션이나 역직렬화에 대해 singleton을 보존하기 위한 해결책도 있다.

리플렉션을 통한 생성자 접근을 방지하기 위해서는, mInstance가 null이 아닌데도 생성자가 호출되는 시점을 체크해서 Exception을 날려주면 된다.


역직렬화를 통한 인스턴스 생성을 방지할 때는 Serializable을 상속받아 readResolve를 구현해주면 된다.


뭐.. 방금 언급한 2가지 해결 방법을 섞어서 singleton을 만든다면 어느 정도 real-singleton을 만들 수는 있다. 그러나 이와 같이 예상할 수 없는 상황들을 고려했을 때, 항상 singleton이라고 보장하기가 힘들다.

그래서 이와 같은 문제점을 해결하고자, Java를 개발한 썬 마이크로시스템사의 Joshua Bloch가 Effective Java에서 Enum Singleton에 대하여 저술했다.




Enum Singleton

모든 enum은 프로그램 내에서 최초 사용시 단 1회만 초기화 된다는 점을 활용하여 고안된 singleton이다. 사용법은 간단하다.



인스턴스를 선언해주고 그 아래로 쭉쭉 필요한 메소드들을 정의 해주면 된다. 생성 관련한 연산이 필요하다면, 어차피 생성자가 1회만 불린다는 것이 보장되어있으니 생성자를 정의해주고 필요한 작업들을 해주면 된다.

장점은 다음과 같다.

  • INSTANCE 가 생성될 때, multi thread 로 부터 안전하다.
  • 단 한번의 인스턴스 생성을 보장한다.
  • 사용이 간편하다.
  • enum value는 자바 프로그램 전역에서 접근이 가능하다.





마치며

여러 방법의 singleton 사용 방법이 있고, 2001년도지만 공식 문서 When is a Singleton not a Singleton? 도 있다.

Enum Singleton 같은 경우엔 흔히 쓰는 singleton 기법이 아니여서 (적어도 우리 회사에선) 처음 접한 다른 개발자들은 당황하거나 이해하지 못할 수 있다. 물론 어느 방법이든 Perfect way는 없으며, 어떻게 보면 기존의 방법이 가독성, 통일성을 이유로 되려 최선의 방법일 수 있다.

'Enum Singleton이 singleton을 사용하는 최고의 방법이다!' 라는 생각보다는 이런 방법도 있고 저런 방법도 있다는 정도로만 생각하고 각종 예외 상황을 유의해서 개발하는 것이 가장 좋은 포인트일 것 같다.

댓글

  1. 소연누님 volidate 가아니라 volatile이에여

    답글삭제
    답글
    1. 어머..ㅎㅎ 지적 감사요 ^^.. 부끄럽네여 (총총)

      삭제

댓글 쓰기

이 블로그의 인기 게시물

5년차의 슬럼프

10대 여고생이 만난 프로그래밍 - 마이크로소프트웨어 31주년 컨퍼런스

About Me