싱글톤 패턴을 현명하게 사용하라.#

<div class="note"> 저자소개 J.B. Rainsberger (jbr@diasparsoftware.com), Software Developer, IBM Toronto

원문 위치 http://www-128.ibm.com/developerworks/webservices/library/co-single.html </div>

<div class="note" style="background: white;"> 싱글톤을 너무 많이 사용하지 않는가.? 오랜 경험을 가진 프로그래머인 J. B. Rainsberger는 싱글톤을 사용하는 시기와 좀더 유연한 방법을 찾아야 하는 시점을 위한 팁을 제공하고 왜 그렇게 해야 하는지 설명한다. </div>

프로그래밍관련 커뮤니티는 전역 데이터와 객체를 사용하는 것에 찬성하지 않는다. 애플리케이션이 주어진 클래스의 단 하나의 인스턴스를 필요로 하고 그 클래스에 대한 전역 접근이 필요한 상황은 많다. 일반적인 해결법은 싱글톤이라고 알려진 디자인 패턴을 사용하는 것이다. 어쨌든, 싱글톤은 테스트가 다소 불필요할 정도로 어렵고 그것들을 사용할 애플리케이션에 대한 큰 추측을 하게 한다. 이 글에서, 나는 대부분의 적당하지 않은 상황에 싱글톤 사용을 피하기 위한 전략을 언급한다. 나는 또한 싱글톤을 사용해야 하는 몇몇 클래스의 특성들도 언급한다.

자동화된 단위 테스팅은 다음의 상황에서 가장 효과적이다.

  • 클래스간의 결합도(Coupling)는 오직 필요한 수준만큼만이어야 한다.
  • 상품(production) 구현물 대신에 협조(collaborating) 클래스의 모의(mock)구현물을 사용하는 것이 간단하다.

클래스들이 느슨하게 결합되어 있을때, 이것은 하나의 클래스에 독립적으로 테스팅하는데 집중하는 것이 가능하다. 클래스들이 강력하게 결합되어 있을때는, 오직 클래스들의 묶음으로만 테스트가 가능하다. 이것은 많은 버그를 만들고 격리하기가 더욱 어렵게 된다. 일반적으로 말하면, 테스팅은 클래스가 개별적으로 어떠한 연관도 없을 때 가장 쉽다.

단위 테스트는 각각의 클래스가 그것들이 주장하는 대로(시스템의 나머지에 독립적인) 작동하는 것을 확신한다는 것을 의미한다. 단위 테스트를 좀더 효과적이고 좀더 빠르게 수행하도록 만들기 위한 공통적인 방법은 협조객체의 상품 구현물 대신에 모의객체를 사용하는 것이다. 예를 들면, 클래스 B가 예외를 던질때 클래스 A가 응답하는 방법을 테스트하기 위해, 이것은 다음 코드같은 것을 작성하는 것으로 충분하다.

목록 1. 모의객체를 사용한 예제 코드

public class MyTestCase extends TestCase {
     ... 
     public void testBThrowsException() {
         MockB b = new MockB()
         b.throwExceptionFromMethodC(NoSuchElementException.class)

         A a = new A(b);  // Pass in the mock version
         try {
             a.doSomethingThatCallsMethodC()
         }
         catch (NoSuchElementException success) {
             // Check exception parameters? 
         }
     }
     ...  
}

이것은 그 행위를 실제로 다시 수행하는 것보다 행위를 가상으로 수행하는 것이 좀더 간단하다. 여기서, 우리는 B클래스의 상품 구현물이 수행하는 시나리오는 다시 수행하는 것보다 NoSuchElementException을 던지는 c()메소드를 가상으로 수행해본다. 이 가상수행은 시나리오를 재생성하는 것보다 B클래스의 특별한 정보를 다소 덜 요구한다.

싱글톤은 너무 많은 것을 안다.#

어느 부분이 위험한가.? 클래스간의 결합도는 클래스들이 의존하는 다른 클래스의 인스턴스를 얻는 위치를 알게되면 급격히 증가한다. 첫번째, 제공자 클래스가 인스턴스화되는 방법내 변경이 클라이언트 클래스로 나아간다. 이것은 어느 애플리케이션이 제공자의 하위클래스와 협력하는 것을 클라이언트 클래스에게 자유롭게 알리도록 허용하는 것을 말하는 Liskov Substitution 원리에 위배된다. 이 위배사항은 단위 테스트를 할때 느껴진다. 하지만 좀더 중요한것은, 이전버전에 대해 호완되는 방법으로 제공자를 향상시키는 것을 어렵게 만든다. 단위 테스트는 위에서 언급된것처럼 제공자의 행위를 가상으로 수행하는 목적을 위해 클라이언트 클래스에 모의 제공자를 전달할수 없다. 그 다음, 제공자의 소스에 대한 접근을 요구하는 클라이언트의 코드 변경없이 제공자를 향상시킬수 있는 것은 없다. 제공자의 소스에 접근할때, 당신은 제공자의 178개의 클라이언트를 변경하길 원하는가.? 멋진 계획이 없다면 그냥 주말을 쉬어라.

협력 클래스는 애플리케이션이 그것들 모두를 묶을수 있도록 빌드되어야만 한다. 이것은 애플리케이션의 유연성을 향상시키고 단위테스트를 좀더 간단하게, 좀더 빠르게, 좀더 효과적으로 만든다. 이것이 클래스를 테스트하는 것을 좀더 쉽게 한다는 것을 기억하라.


싱글톤으로 부터 옮기기#

싱글톤은 당신이 믿는 만큼 바람직하지 않기 때문에, 나는 클라이언트 코드를 어떻게 효과적으로 변경할지를 언급할것이다.

나는 이 문제를 상세하게 설명할 것이다. 클라이언트가 싱글톤 인스턴스를 가져올때마다, 클라이언트는 제공자가 싱글톤이라는 사실에 불필요하게 결합된다. 예제처럼, Deployer와 Deployment를 고려하라. 애플리케이션은 오직 하나의 Deployer만이 필요하다. 그래서 우리는 이것을 싱글톤으로 만든다. 지금 우리는 이 메소드를 아래와 같이 코딩할수 있다.

목록 2. Deployer 코딩하기

public class Deployment {
     ... 
     public void deploy(File targetFile) {
         Deployer.getInstance().deploy(this, targetFile)
     }
     ...  
}

이것은 클라이언트가 자기 자신을 Deployment하도록 Deployment를 요청할수 있기 때문에 좋은 방법(shortcut)처럼 보인다. 클라이언트는 Deployer에 대해서 알지 못한다. 비록 이것이 정말 좋은 방법이라고 하더라도, 이것으로 인한 장점은 우리가 다른 종류의 Deployer를 사용하거나 사용하길 원할때 결론적으로 곧 사라져버리게된다. Deployment작업은 Deployer를 명확한 클래스로 안다. 그래서 우리는 Deployment를 위한 소스의 변경없이 Deployer의 하위클래스를 대신 사용할수 없다.

Deployment는 Deployer가 싱글톤임을 아는 것보다 클라이언트는 다음처럼 Deployment 생성자에 Deployer의 인스턴스를 전달해야만 한다.

목록 3. Deployment 생성자로 Deployer 전달하기

public class Deployment {
     private Deployer deployer; 

     public Deployment(Deployer aDeployer) {
         deployer = aDeployer; 
     }

     public void deploy(File targetFile) {
         deployer.deploy(this, targetFile)
     }
     ...  
}

두개의 클래스는 다소 덜 결합되었다. 지금은 Deployers가 생성되는 방법에 의지하는 것보다 Deployment에서 Deployer로 간단하게 관련된다. 당신은 Deployers에 대한 결정을 애플리케이션에 남겨두게 된다.

이전 코드에서, 클라이언트는 필요에 따라 필수로 변경작업이 이루어져야 한다. 인터페이스에는 정적 메소드가 없을수도 있다. 방금의 새로운 코드에서, Deployment는 변경작업이 필요없다. 대신 Deployment의 클라이언트인 애플리케이션은 애플리케이션이 인식할수 있는 방법으로 변경된다. 게다가 애플리케이션이 제대로 디자인되었다면, 하나의 변경은 애플리케이션내 모든 Deployment인스턴스의 행위를 야기할수 있을것이다. 그래서 당신이 인터페이스가 될 필요가 있는 Deployer를 찾을때가 언제든 문제가 없다.

단위테스트에서도 이득을 가지게 된다. 다중 테스트 케이스를 수행할 시점에 왔을때, 각각의 테스트 케이스는 다소 다른 종류의 Deployer를 필요로 할것이다. 그 행위를 특별히 가상으로 수행하기 위해 언제나 Deployer는 특정상태내 있을 필요가 있다. 우리가 이미 본것처럼, 이것을 달성하기 위한 가장 쉬운 방법은 모의 Deployer구현물을 생성하는 것이다. 다음의 테스트 케이스는 이 기법을 잘 보여준다.

목록 4. 모의 Deployer 구현물

public class DeploymentTestCase extends TestCase {
     ... 
     public void testTargetFileDoesNotExist() {
         MockDeployer deployer = new MockDeployer()
         deployer.doNotFindAnyFiles()

         try {
             Deployment deployment = new Deployment(deployer)
             deployment.deploy(new File("validLocation"))
         }
         catch (FileNotFoundException success) {
         }
     }
     ...  
}

여기서, 우리는 우리의 모의 Deployer를 말한다. "어떠한 파일도 찾지 않았나.? 내가 당신에게 전달하는 파일 객체가 무엇인지는 문제가 되지 않는다." 우리는 클라이언트가 존재하지 않는 폴더내 파일로 배치를 시도할 경우를 가상으로 수행하기 위해 이 기법을 사용한다. 예를 들면, 당신은 이것을 가상으로 수행하는 것보다 실질적으로 예외상태를 생성하는 파일명을 명시하지 않은 이유를 궁금해할지도 모른다. 우리는 단지 예외상태를 가상으로 수행하는 것에만 관심을 가지고 이것을 다시 생성하지는 않는다. 다시 생성하는 것은 혼동으로 쉽게 이끈다. 당신의 팀내 신출내기 개발자는 테스트 케이스의 유효하지 않은 파일위치로 d:/doesNotExist를 선택한다. 그는 그의 컴퓨터에서 테스트를 전달하고 그의 변경사항을 통합한다. 지금 당신은 파일시스템에서 d:/doesNotExist 를 가지는 당신의 컴퓨터에서 테스트를 수행한다. 코드가 틀렸을뿐 아니라 테스트가 환경에 많은 부분을 의존하기 때문에 테스트는 실패한다.

이것은 시간을 낭비한다. 당신은 이 문제때문에 30분동안 고립되고 d:/doesNotExist가 위험한 선택인 이유를 신출내기 개발자에게 설명하는데 15분이 소요된다. 그리고 20분동안 팀내 나머지 사람들에게 이러한 코딩에 대해 경고해야 한다. 물론 팀내 새로운 인원이 추가된다면, 이것은 다시 소요된다. doNotFindAnyFiles로 불리는 메소드를 가진 하나의 모의 deployer를 작성하는 것은 당신에게 골치거리를 피하도록 해준다.


싱글톤들을 모으기: 도구상자#

싱글톤 남용은 다른 각도에서 문제를 보는것으로 피할수 있다. 애플리케이션이 오직 하나의 인스턴스를 필요로 한다고 가정하고 애플리케이션은 시작되는 시점에 클래스를 설정한다. 왜 클래스 자체는 싱글톤이 되어야만 하는가.? 애플리케이션은 이러한 종류의 행위를 요구하기 때문에 이것은 이러한 형태(responsibility)를 가지는 애플리케이션을 위해 꽤 논리적인것처럼 보인다. 컴포넌트가 아닌 애플리케이션은 싱글톤이어야만 한다. 애플리케이션은 사용하기 위한 어떤 애플리케이션 특유의 코드를 위해 사용가능한 컴포넌트의 인스턴스를 만든다. 애플리케이션이 이러한 다양한 컴포넌트를 사용할때, 이것은 우리가 도구상자라고 불리는 것들로 그것들을 모을수 있다.

애플리케이션의 도구상자는 자체적인 설정을 책임지거나 이것을 설정하기 위해 애플리케이션 구동 기법을 허용하는 싱글톤이다. 도구상자의 일반적인 패턴은 아래와 비슷한 유형으로 보인다.

목록 5. 도구상자 싱글톤의 일반적인 패턴

public class MyApplicationToolbox {
     private static MyApplicationToolbox instance; 

     public static MyApplicationToolbox getInstance() {
         if (instance == null) {
             instance = new MyApplicationToolbox()
         }
         return instance; 
     }

     protected MyApplicationToolbox() {
         initialize()
     }

     protected void initialize() {
         // Your code here
     }

     private AnyComponent anyComponent; 

     public AnyComponent getAnyComponent() {
         return anyComponent()
     }
     ... 

     // Optional: standard extension allowing
     // runtime registration of global objects. 
     private Map components; 

     public Object getComponent(String componentName) {
         return components.get(componentName)
     }

     public void registerComponent(String componentName, Object component
{
         components.put(componentName, component)
     }

     public void deregisterComponent(String componentName) {
         components.remove(componentName)
     }

}

도구상자는 자체적으로 싱글톤이고 이것은 다양한 컴포넌트 인스턴스의 생명시간(lifetime)을 관리한다. 애플리케이션이 이것을 설정하거나 이것이 메소드 초기화내 정보를 설정하기 위해 애플리케이션을 요청한다. 지금 애플리케이션은 이것이 요구하는 클래스의 수를 결정할수 있다. 이러한 결정내 변경은 아마도 애플리케이션 특유의 코드에 영향을 끼칠것이지만, 하부구조 레벨로 재사용가능하지는 않다. 게다가, 이러한 클래스는 그것들을 사용하는 것을 선택하는 방법에 의존하지 않기 때문에 하부구조를 테스트하는 것은 좀더 쉽다.


이것이 정말 싱글톤이어야 하는 시점.#

클래스가 진실로 싱글톤인지 아닌지를 결정하기 위해, 당신은 스스로 몇가지 질문을 해봐야만 한다.

  • 모든 애플리케이션이 정확하게(<- 이 말이 중요하다) 같은 방법으로 이 클래스를 사용하는가.?
  • 모든 애플리케이션이 이 클래스의 인스턴스를 오직 하나만(<- 이 말이 중요하다) 필요로 하는가.?
  • 이 클래스의 클라이언트는 그것들이 담당하는 애플리케이션을 알아채지 못해야만 하는가.?

만약 당신이 이 세가지의 질문에 모두 "예" 라고 답변을 한다면, 당신은 싱글톤으로 결정해야 할것이다. 여기에서 핵심사항은 모든 애플리케이션이 이것을 정확하게 같은것으로 처리하고 클라이언트는 애플리케이션 컨텍스트없이 클래스를 사용할수 있을때에만 클래스가 싱글톤이다.

싱글톤에서 오래 사용된 예제는 로깅서비스이다. 우리가 이벤트-기반 로깅 서비스를 사용한다고 가정하자. 클라이언트 객체는 텍스트가 로깅 서비스로 메시지를 보내서 로깅되도록 요청한다. 다른 객체는 실제로 로깅 요청과 그것들을 다루기 위해 로깅 서비스에 의해 귀를 귀울임(listening)으로써 어딘가(콘솔, 파일, 또는 다른 어떤것)에 텍스트를 쓴다. 첫째, 로깅 서비스는 싱글톤이 되기 위해 고전적인 테스트로 전달하는 것을 알라.

  • 요청자는 로그로 요청을 보내기 위한 잘 알려진 객체를 필요로 한다. 이것은 전역적인 접근 지점(global point of access)을 의미한다.
  • 로깅 서비스는 다중 리스너가 등록할수 있는 하나의 이벤트 소스가 되기 때문에, 오직 하나의 인스턴스가 될 필요가 있다.

고전적인 싱글톤 디자인 패턴의 요구사항은 여기에 언급되었지만 사실 더 있다.

  • 비록 다른 애플리케이션이 다른 출력 장치로 로깅을 하더라도, 그 리스너를 등록하는 방법은 언제나 같다. 모든 사용자 정의는 리스너를 통해 이루어진다. 클라이언트는 텍스트가 로깅될 방법과 위치에 대해서 알 필요 없이 로깅을 요청할수 있다. 모든 애플리케이션은 정확하게 같은 방법으로 로깅 서비스를 사용할 것이다.
  • 어떤 애플리케이션은 로깅 서비스의 오직 하나의 인스턴스를 가지고 작동될수 있다.
  • 재사용가능한 컴포넌트를 포함해서 어떤 객체도 로깅 요청자가 될수 있다. 그래서 그것들은 어떤 특정 애플리케이션으로 결합되지 않아야 한다.

고전적인 요구사항에 추가적으로, 위에서 언급한 요구사항은 로깅 서비스에 의해 맞아 떨어진다. 우리는 우리가 나중에 우리의 선택을 후회할 걱정없이 싱글톤으로 로깅 서비스를 안전하게 구현할수 있다.

이러한 규칙에도 불구하고, 당신은 클래스가 싱글톤이 되어야만 할때 당신에게 알릴 코드를 두는것을 검토해야만 한다. 당신이 객체를 인식하면, 당신이 다룰 방법을 생각해볼수 없다. 당신은 스스로에게 요청해야 할것이다.

  • 내가 이 클래스의 인스턴스를 어디서 가지게 되는가.?
  • 이 객체는 애플리케이션이나 내가 작성한 컴포넌트에 속하는가.?
  • 내가 사용자정의를 이 클라이언트로 미뤄서 이 클래스를 작성할수 있는가.?

만약 당신이 이것을 가진다면, 아마도 이것은 싱글톤일것이다. 하지만 코드가 당신에게 이것은 여기서 하위클래스를 사용하길 원한다고 말하고 여기에 없다면, 당신은 당신의 결정을 다시 생각해볼 필요가 있다.

걱정하지 말라: 코드는 무엇을 하는지 당신에게 언제나 알려줄것이다. 집중해서 한번 잘 들어보라.


Resources#

  • A discussion on the Singleton Design Pattern. As always, don't be lured by hard-and-fast rules that use words like "always" and "never." On the same site, you'll find a discussion of the OnceAndOnlyOnce design principle, which takes as its starting point Kent Beck's suggestion that "code wants to be simple."
  • To learn more about unit testing, see the essential paper on the subject, "Unit Testing with Mock Objects," first presented by Tim MacKinnon, Steve Freeman, and Philip Craig at the Extreme Programming 2000 conference.
  • Yahoo's Extreme Programming Group is full of discussions on how unit testing techniques affect design, usually for the better. This was my primary motivation for moving away from singletons where possible.
  • The test cases in this article were implemented using the JUnit test framework. JUnit is lightweight, simple, and powerful, making it the ideal test framework for both small and big products.
  • To learn more about JUnit, see Incremental development with Ant and JUnit on developerWorks.
  • IBM's WebSphere Commerce Suite site. Inside you can find several references to agility. It is this need to respond to constant change that has led me to look for ways to make change less difficult.
  • This IBM Research paper on subject-oriented programming and design patterns is still under construction, but will be of interest to anyone interested in design patterns.
  • Another IBM Research link: This one lists dozens of excellent resources on design patterns.

Add new attachment

Only authorized users are allowed to upload new attachments.
« This page (revision-7) was last changed on 06-Apr-2006 09:45 by 이동국