IBM Korea Skip to main content
       IBM 홈    |  제품 & 서비스  |  고객지원 & 다운로드  |  회원가입  

다중 스레드 애플리케이션 프로그래밍을 쉽게
생산자-소비자 행위를 간단하게 구현할 수 있도록 하는 Consumer 클래스

Joseph Hartal, 소프트웨어 개발자, GlobaLoop LTD
Ze'ev Bubis, 소프트웨어 개발팀장, GlobaLoop LTD

2002년 2월

생산자-소비자 시나리오는 다중 스레드된 애플리케이션 개발에서 가장 많이 사용되는 구성 개념 중 하나이고, 여기에 문제가 있다. 생산자-소비자 행위는 한 애플리케이션에서 여러 번 되풀이될 수 있기 때문에 코드도 그렇게 될 수 있다. 소프트웨어 개발자인 Ze'ev Bubis와 Saffi Hartal는 Consumer클래스를 만들었는데, 이 클래스는 일부 다중 스레드된 애플리케이션에서 코드 재사용을 용이하게 하고 코드 디버깅과 유지보수를 간편하게 하여 이 문제를 해결하려 한다.

다중 스레드된 애플리케이션은 종종 생산자-소비자 프로그래밍 시나리오를 사용한다. 이 시나리오에서는 생산자 스레드가 반복적인 작업을 생성하여 작업 큐에 보내고 소비자 스레드가 이를 처한다. 이 프로그래밍 메소드는 매우 유용하지만 종종 중복된 코드를 만들어 내기도 하는데, 이는 디버깅과 유지보수에 실질적인 문제가 될 수 있다.

이 문제를 해결하고 코드 재사용을 원할하게 하기 위해 우리는 Consumer 클래스를 만들었다. Consumer 클래스는 작업 큐와 소비자 스레드를 위한 모든 코드뿐 아니라 이들을 연결시키는 논리적 접착제를 포함하고 있다. 이는 우리가 중복적인 코드의 행들을 작성하는 대신 비즈니스 로직 - 작업이 어떻게 처리되어야 하는지를 지정하는 것-에 포커스를 맞출 수 있도록 해 준다. 또한 다중 스레드된 애플리케이션의 디버깅 작업을 훨씬 쉽게 해준다.

이 글에서 우리는 다중 스레드된 애플리케이션 개발에서 일반적인 스레드 사용에 관해 간단히 살펴보고 생산자-소비자 프로그래밍 시나리오에 관해 설명하고 Consumer 클래스가 어떻게 작동하는지를 보여주는 실제 세계의 예를 검토하겠다. 이 글은 다중 스레드된 애플리케이션 개발이나 생산자-소비자 시나리오에 대한 심도 깊은 소개는 하지 않는다. 이 주제들에 대한 글 목록은 참고 자료 를 참조한다.

다중 스레딩의 기본

다중 스레딩은 애플리케이션이 동시에 한 개 이상의 애플리케이션을 처리하도록 하는 프로그래밍 기법이다. 스레드는 보통 두 개의 다른 유형의 다중 스레드 작업에 채택된다.:

  • 적시 이벤, 특정 시간에, 혹은 특정 간격으로 작업이 발생하도록 스케쥴링되어야 할 때
  • 백그라운드 처리, 현재의 실행 흐름과 병행하여 백그라운드 이벤트를 처리하거나 실행해야 할 때

적시 이벤트의 예로는 프로그램 reminder, 타임 아웃 이벤트, 폴링이나 refresh 같은 반복 작업을 들 수 있다. 백그라운드 처리의 예에는 전송을 기다리는 패킷이나 처리를 기다리는 수신 메시지등이 있다.

생산자-소비자 관계

생산자-소비자 시나리오는 백그라운드 처리 범주에 속하는 경우에 적합하다. 이 상황들은 일반적으로 작업 "생산자"측과 작업 "소비자"측을 중심으로 순환한다. 병행 처리되는 작업에 관한 다른 고려 사항도 있다. 대부분의 경우 동일한 자원을 사용하는 작업들은 먼저 들어온 작업이 먼저 서비스 받는 방식으로 순차적으로 처리되어야 하는데, 이 방식은 단일 스레드된 소비자를 사용하여 쉽게 수행될 수 있다. 이 방식을 사용하여 우리는 한 자원에 접근하는 여러 스레드 대신, 하나의 자원에 접근하는 하나의 스레드를 다룬다.

표준 소비자를 구동하려면, 들어오는 모든 작업을 저장하기 위해 작업 큐가 생성된다. 생산자 스레드는 처리되어야 하는 새로운 객체를 가져와 소비자의 큐에 추가한다. 그러면 소비자 스레드는 큐에서 각 객체를 꺼내어 차례로 처리한다. 큐가 비워지면 소비자는 휴면 상태로 간다. 새로운 객체가 빈 큐에 추가되면 소비자는 깨어나서 객체를 처리한다. 대부분의 애플리케이션이 순차 처리를 선호하기 대문에 소비자는 보통 단일 스레드형이다.

문제 : 코드 중복

생산자-소비자 시나리오는 매우 일반적이기 때문에 애플리케이션을 구축할 때 여러 번 등장하고, 그 결과 코드 중복이 발생한다. 우리는 생산자-소비자 시나리오를 여러 번 채택한 애플리케이션 개발 프로세스에서 이것이 근심거리가 된다는 사실을 알게 되었다.

생산자-소비자 행동이 처음 필요할 때 우리는 하나의 스레드와 하나의 클래스를 채택한 클래스를 작성하여 이를 구현하였다. 두번째로 필요할 때 우리는 처음부터 다시 이것을 구현하려 했지만 이전에 구현했다는 사실을 알게 되었다. 그래서 코드를 복사하여 객체가 처리되는 방식을 수정하였다. 생산자-소비자 행동을 애플리케이션에 세번째 구현했을 때는 우리가 너무 많은 코드를 중복시키고 있다는 것이 분명해졌다. 우리는 우리의 모든 생산자-소비자 시나리오를 처리할 포괄적인 Consumer 클래스가 필요하다고 결정하였다.

우리의 솔루션 : Consumer 클래스

Consumer 클래스를 만들 때 우리의 목적은 애플리케이션 내의 모든 생산자-소비자 인스턴스에 대해 새로운 작업 큐와 소비자 스레드를 작성할 때 수반되는 코드 중복 문제를 없애는 것이었다. 적절한 Consumer 클래스가 있으면 우리는 작업 처리에 국한된 코드만 작성하면 된다 (비즈니스 로직). 그러면 코드가 깔끔해지고 유지 보수가 쉬워지며 변경에 더 유연해진다.

Consumer 클래스에 대한 우리의 요구 사항은 아래와 같다.:

  • 재사용 : 우리는 모든 것을 가진 클래스를 원하였다. 스레드, 큐 및 이들을 연결시키는 모든 로직이 그것이다. 그러면 우리는 큐의 특정 작업을 처리하는 코드만 작성하면 될 것이다. (예를 들어, onConsume(ObjectjobToBeConsumed)이라는 메소드는 Consumer 클래스를 사용하는 프로그래머에 의해 오버로드될 것이다.)
  • 큐 선택 : 우리는 큐 구현을 Consumer 객체가 사용하도록 설정할 수 있기를 원하였다. 그러나 이것은 우리가 큐가 스레드에 안전함을 보증해야 하거나 소비자 작업과 상충되지 않을 단일 스레드된 생산자를 사용해야 함을 의미한다. 양 경우 모두, 다른 프로세스들이 자신의 메소드에 접근하는 것을 허가하도록 큐를 설계해야 한다.
  • 소비자 스레드 우선 순위 설정 : 우리는 Consumer의 스레드가 실행될 우선 순위를 설정할 수 있기를 원하였다.
  • 소비자 스레드 명명하기 : 스레드가 의미 있는 이름을 가지는 것이 편리하고, 분명히 디버깅에도 도움이 된다. 예를 들어, 여러분이 자바 가상 머신에 신호를 보내면 가상 머신은 모든 스레드와 그에 상응하는 스택 트레이스의 스냅샷인 전체 스레드 덤프를 만들어낼 것이다. 이 스레드 덤프를 윈도우 플랫폼에 생성하려면 여러분은 자바 프로그램이 실행되고 있는 윈도우에서 <ctrl><break>를 누르거나 윈도우의 Close 버튼을 클릭해야 한다. 전체 스레드 덤프를 사용해 자바 소프트웨어 문제를 진단하는 방법에 관한 상세 정보는 참고 자료를 참조한다.

클래스 코드

Listing 1에서와 같이, 위는 getThread()메소드에 Consumer의 스레드를 생성하기 위해 "lazy creation"을 사용하였다.:

Listing 1. Consumer의 스레드 생성하기

     /**
       * Lazy creation of the Consumer's thread.
       *
       * @return  the Consumer's thread
       */
      private Thread getThread()
      {
         if (_thread==null)
         {
            _thread = new Thread()
            {
               public void run()
               {
                  Consumer.this.run();
               }
            };
         }
         return _thread;

Listing 2에서와 같이, 스레드의 run()메소드는 Consumerrun() 메소드를 실행시키는데, 이것이 주 소비자 루프이다. :

Listing 2. The run() method is the main Consumer loop

     /**
       *  Main Consumer's thread method.
       */
      private void run()
      {
         while (!_isTerminated)
         {
            // job handling loop
            while (true)
            {
               Object o;
               synchronized (_queue)
               {
                  if (_queue.isEmpty())
                     break;
                  o = _queue.remove();
               }
               if (o == null)
                  break;
               onConsume(o);
            }

            // if we are not terminated and the queue is still empty
            // then wait until new jobs arrive.

            synchronized(_waitForJobsMonitor)
            {
               if (_isTerminated)
                  break;
               if(_queue.isEmpty())
               {
                  try
                  {
                     _waitForJobsMonitor.wait();
                  }
                  catch (InterruptedException ex)
                  {
                  }
               }
            }
         }
}// run()

기본적으로, Consumer의 스레드는 더 이상 큐에서 대기하는 작업이 없을 때까지 실행된다. 그 후 휴면 상태로 가서 add(Object)에 대한 첫번째 호출이 있을 때 깨어난다. add(Object)는 큐에 신규 작업을 추가하고 스레드를 "차서" 깨운다.

"휴면"과 "차기"는 wait()notify() 메커니즘을 사용해 수행된다. 실제 소비자 작업은 Listing 3에 나와 있는 것과 같이 OnConsume(Object) 메소드에 의해 처리된다.:

Listing 3. 소비자 깨우기 및 통지하기

     /**
      * Add an object to the Consumer.
      * This is the entry point for the producer.
      * After the item is added, the Consumer's thread
      * will be notified.
      *
      * @param  the object to be 'consumed' by this consumer
      */
      public void add(Object o)
      {
         _queue.add(o);
         kickThread();
      }

      /**
       * Wake up the thread (without adding new stuff to consume)
       *
       */
      public void kickThread()
      {
         if (!this._thread.isInterrupted())
         {
            synchronized(_waitForJobsMonitor)
            {
               _waitForJobsMonitor.notify();
            }
         }
      }

예제 : MessagesProcessor

Consumer 클래스가 어떻게 작동하는지를 보여 주기 위해 간단한 예를 들겠다. MessagesProcessor 클래스는 도착하는 메시지를 비동기적으로 (즉 호출 스레드를 방해하지 않고) 처리한다 . 이 클래스의 작업은 들어오는 각 메시지를 인쇄하는 것이다. MessagesProcessor는 도착하는 메시지 작업을 다루는 내부 Consumer를 포함한다. Listing 3에서와 같이, 새로운 작업이 빈 큐에 들어 오면 ConsumerprocessMessage(String) 메소드를 호출하여 이를 처리하도록 한다.:

Listing 4. MessagesProcessor 클래스

      class MessagesProcessor
      {
         String _name;
         // anonymous inner class that supplies the consumer
         // capabilities for the MessagesProcessor
         private Consumer _consumer = new Consumer()
         {
            // that method is called on each event retrieved
            protected void onConsume(Object o)
            {
               if (!(o instanceof String))
               {
                  System.out.println("illegal use, ignoring");
                  return;
               }
               MessagesProcesser.this.processMessage((String)o);
            }
         }.setName("MessagesProcessor").init();

         public void gotMessageEvent(String s)
         {
            _consumer.add(s);
         }
         private void processMessage(String s)
         {
            System.out.println(_name+" processed message: "+s);
         }

         private void terminate()
         {
           _consumer.terminateWait();
           _name = null;
         }

         MessagesProcessor()
         {
            _name = "Example Consumer";
         }
      }

위의 코드에서 볼 수 있듯이, Consumer를 커스터마이징하는 것은 매우 간단하다. 익명의 내부 클래스를 택해 Consumer 클래스를 확장하고 추상 메소드인 onConsume()를 오버로드하면 된다. 이리하여 우리 예제에서 간단히 processMessage를 호출한다.

클래스의 고급 기능들

우리가 출발시 원했던 기본 요구사항에 덧붙여 우리는 Consumer 클래스에 몇 가지 유용한 고급 기능을 제공하였다.

이벤트 통지

  • onThreadTerminate(): 이 메소드는 Consumer가 종료되기 직전에 호출된다. 우리는 이 메소드를 디버깅 목적으로 오버라이드한다.

  • goingToRest(): 이 메소드는 Consumer 스레드가 휴면 상태로 가기 직전에 호출된다 (즉 _waitForJobsMonitor.wait() 호출 직전). Consumer가 휴면 상태로 가기 직전에 일단의 처리된 작업을 핸들링해야 하는 복잡한 경우에 이러한 통지가 필요할 수 있다.

종료

  • terminate(): Consumer 스레드의 비동기적 종료

  • terminateWait(): Consumer 스레드가 실제로 종료될 때까지 호출 스레드를 대기 상태로 설정한다. .

우리 예제에서 만약 terminateWait() 대신 terminate()를 사용했다면 문제가 발생할 것이다. _name 이 null 값으로 설정된 후 onConsume() 메소드가 호출될 수 있기 때문이다. 그러면 processMessage를 실행시키는 스레드가 NullPointerException을 보내게 될 것이다.

결론 : Consumer 클래스의 장점

Consumer 클래스의 소스를 참고 자료 섹션에서 다운로드받을 수 있다. 이 소스를 자유롭게 사용하고 여러분이 필요한대로 확장시키기 바란다. 다중 스레드된 애플리케이션 개발에서 이 클래스를 사용할 때의 이점은 다음과 같다.:

  • 코드 재사용/중복 코드 제거 : Consumer 클래스를 가지고 있다면 여러분 애플리케이션의 모든 인스턴스에 대해 새로운 소비자를 작성하지 않아도 된다. 생산자-소비자 시나리오가 애플리케이션 개발에 얼마나 자주 등장하는지를 생각하면 이 클래스는 엄청난 시간을 절약해 준다. 또한 중복된 코드는 버그에게는 비옥한 땅과도 같다는 것을 기억하기 바란다. 그리고 중복 코드는 기본적인 코드 유지보수를 더욱 어렵게 한다.

  • 버그 감소 : 검증된 코드의 사용은 버그 방지에 좋은데, 특히 다중 스레드된 애플리케이션을 다룰 때 그러하다. Consumer 클래스는 이미 디버깅되었기 때문에 더 안전하다. Consumer 클래스는 또한 스레드와 자원간에 안전 중개자 역할을 함으로써 스레드 관련 버그를 방지한다. Consumer는 다른 스레드들을 대신해서 자원들에 순차적으로 접근한다.

  • 훌륭하고 깔끔한 코드 : Consumer 클래스를 사용하면 이해와 유지보수가 더욱 쉬운, 보다 간단한 코드를 작성하도록 해준다. Consumer 클래스를 사용하지 않을 경우 우리는 두 개의 다른 기능을 처리하는 코드를 작성해야 한다. : 소비 로직 (큐와 스레드 관리, 동기화등)과 소비자의 용도나 기능을 지정해 놓은 코드가 그것이다.

이 글의 작성에 도움을 주신 Allot Communications의 Jonathan Lifton과 Dov Trietsch에게 감사를 표합니다.

참고 자료

목 차:
다중 스레딩의 기본
생산자 - 소비자 관계
문제점
우리의 솔루션
클래스 코드
예제
고급 기능들
결론
참고 자료
필자 소개
기사에 대한 평가
관련 dW 링크:
다중 스레드된 자바 애플리케이션 작성하기
동기화는 적이 아니다
경쟁 줄이기
때로는 공유하지 않는 것이 최선
Subscribe to the developerWorks newsletter
US 원문 읽기
Also in the Java zone:
Tutorials
Tools and products
Code and components
Articles
필자 소개
Photo of Joseph Hartal Joseph (Saffi) Hartal는 GlobaLoop LTD의 소프트웨어 개발자이다. Tel-Aviv 대학에서 컴퓨터 과학 및 수학 학사 학위와 MBA를 받았다. 지난 10년간 소프트웨어 개발자로 일해 오면서 C++에 실시간으로 임베디드되는 코드와 자바 클라이언트 및 서버 애플리케이션을 작성하였다. Saffi는 대부분의 시간을 인프라 코드 작성과 수수께끼 풀기로 보낸다.


Photo of Zeev Bubis Ze'ev Bubis는 GlobaLoop LTD의 소프트웨어 개발자이다. Tel-Aviv 대학에서 컴퓨터 과학 및 수학 학사 학위를 받았다. 지난 10년간 소프트웨어 개발자로 일해 오면서 다양한 플랫폼과 언어를 대상으로 하는 소프트웨어 애플리케이션을 작성하였다. 최근 3년간 Ze'ev는 자바에서 클라이언트 애플리케이션과 서버 애플리케이션을 개발하는데 포커스를 맞추어 왔다.

이 기사에 대하여 어떻게 생각하십니까?

정말 좋다 (5) 좋다 (4) 그저그렇다 (3) 수정보완이 필요하다(2) 형편없다 (1)

  회사소개  |  개인정보 보호정책  |  법률  |  문의