상태가 어디에 있는가? 이 질문은 작지만 문제 도메인이 어떻게 개체로 나누어지는지 확인할 때 유용한 질문입니다. 상태를 포함하는 것은 무엇인가? 변경할 수 있는 값을 포함하는 것은 무엇인가? 언제, 어떻게 변경할 수 있는가? 변경 사항을 관찰할 수 있는가? 상태의 변경 사항은 어떤 사람이 관찰해야 하는가?
모든 관련 상태를 한 곳에 보관하 라는 원칙을 지키고 있는 경우 이러한 질문은 문제 도메인이 어떻게 개체로 나누어지는지 확인할 때 처음 던지는 질문입니다. MVC(Model-View-Controller) 아키텍처의 가장 큰 장점은 상태를 한 곳에 보관하기 편리하다는 점이라고까지 말할 수 있습니다.
상태가 중요한 큰 이유 중 하나는 스레딩입니다. 실제로 스레딩을 나중에
고려하는 코드 디자인을 주로 보게 되며, 보통은 이런 방식으로 작업합니다. 동시성은 인간이 생각하는 자연스러운 방식이 아닙니다.
그러므로 일반적으로 라이브러리는 스레딩에 대한 생각 없이 설계된 다음 문제가 나타나면 멀티스레딩으로 해결할 수 있습니다. 원래의
설계 목적은 아니지만 스레딩은 코드베이스로 전달됩니다. 코드베이스 없이 설계된 코드에 스레딩 모델을 새로 설치하기란 매우 어렵고
때로는 불가능한 경우도 있습니다.
일부 스레딩 모델은 간단합니다. 예를 들어 Swing 애플리케이션을 작성할 경우 오래 실행되는 I/O를 수행할 수 있는 메소드가 있다면 해당 메소드의 첫 줄은 다음과 같아야 합니다.
assert !EventQueue.isDispatchThread();
이는 모든 환경에 똑같이 해당됩니다.
그러나 I/O를 이벤트 스레드 외부에 유지하는 것이 당연합니다. 애플리케이션 내에서 모델의 변경을 관리하면 문제가 더 복잡해지고 불분명해집니다.
위의 질문 중 다른 하나는 상태의 변경 사항은 어떤 사람이 관찰해야 하는가?입니다. 이 질문에서 파생된 몇 가지 질문이 있습니다. 이러한 변경 사항은 어디에 게시해야 하는가? 및 변경 사항을 관찰하는 코드에 변경된 사항에 대한 참조가 필요한가?
잠시, 모델을 포함하는 개념으로 돌아가보겠습니다. 모델은 관련된 모든 상태를 한 곳에 가져옵니다. 이것은 스레딩 문제와 아무런 관련이 없습니다. 지금도 스레딩은 여전히 모델 내에서 어느 정도 처리되어야 합니다. 그렇지 않으면 멀티 스레딩된 환경에서 모델의 적절한 사용법에 대해 설명서를 작성해야 합니다. 이 중요한 시점에 이 작업을 훨씬 쉽게 수행할 수 있는 방법이 있으며, 이로써 모델을 사용하는 코드를 만드는 방법에 관해 모두 알 수 있습니다. API를 만드는 경우 단순한 데이터 모델보다 더 유용한 기능이 있을 것입니다. 또한 모델에 액세스하는 방법도 있습니다.
기본적으로 다음과 같이 수행할 수 있습니다. 나머지 API를 작성할 때 모델에 대한 참조를 가져오는 유일한 방법이 참조를 전달하는 방법 밖에 없도록 작성하는 경우(예: 필요한 경우에만 호출) 코드가 호출될 컨텍스트(특히 어떤 잠금을 가지고 있는 동안 어떤 스레드에 있는지)를 알아야 합니다.
SwingWorker라는 한 클래스를 임의로 선택해보겠습니다. 이것은 유용하지만 다음 두 가지 면에서 볼 때 그리 잘 만들어졌다고 보기 어렵습니다.
- 작업 상태(진행률, 제목)와 작업 수행 및 해당 작업의 결과를 하나의 클래스에 한꺼번에 고려하여 작성됨
- JavaBean이 아닌 코드에 beans 패턴을 사용함
위에서 여러 가지를 한꺼번에 고려한 혼합 문제에 대해 언급했습니다. 이런 혼합 문제를 방지하는 것은 이상적인 아키텍처의 순수성을 강조하는 학문적인 문제라고만 치부할 수 없습니다. API는 사람이 사용하는 것입니다. public 클래스에서 혼합 문제를 방지하면 코드가 더욱 분명해지고 그 결과도 더 이해하기 쉬워집니다. 또한, 대부분의 사람들은 꼭 필요한 경우가 아니면 잘 읽지 않는 설명서를 작성할 필요성도 줄어듭니다.
완벽한 세상에서는 작업을 나타내는 코드에 상태를 포함할 필요가 없습니다(하위 클래스가 상태일 수 있지만 인터페이스/최상위 클래스에 setter나 getter 또는 mutable 상태가 없음). 이 작업을 수행하는 코드가 모델 개체에 참조를 전달하는 경우 작업 상태를 on으로 설정할 수 있습니다. 이 경우 작업 상태 문제가 해결됩니다.
그리고 심리적인 부담도 조금 줄어듭니다. SwingWorker 하위 클래스를 작성하는 사람은 스스로에게 "이제 내 상태가 어떤지 나 자신에게 말할 수 있어야 해"라고 생각해야 합니다. 많은 개발자가 자신이 작성하는 클래스를 의인화하여 자신이 편집하는 클래스나 프로그램 카운터를 1인칭인 나라고 생각하는 것은 아닌지 의심됩니다. 이것을 확실하게 증명할 수는 없지만, 제 자신도 그렇고 또 코드에 대해 토론하던 다른 개발자들도 그런 경우가 많았습니다. 경험적으로 자신은 그렇지 않다는 사람이 있다면 코드를 작성할 때 어떤 식으로 생각하는지 궁금합니다. 꼭 알려 주십시오.
java.util.concurrent.Future를 반환하는 것만큼 간단합니다. 다음과 비슷한 출력이 나타납니다.
public interface Task<T> {
public T runInBackground (TaskStatus status);
public void runInForeground (TaskStatus status, T backgroundResult);
}
public interface TaskStatus {
public void setTitle (String title);
public void setProgress (String msg, long progress, long min, long max);
public void setProgress (String msg); //indeterminate mode
public void done();
public void failed (Exception e);
}
그러면 작업을 실행하는 구체 클래스인 TaskRunner 클래스가 있을 것입니다.
public final class TaskRunner {
public <T> java.util.concurrent.Future<T> launch (Task<T> task, TaskStatus statusUI) { ... }
}
또는
Task가 정적 실행(Task) 메소드 또는 인스턴스 메소드(취향에 따라)가 있는 추상 클래스이고 TaskRunner 클래스는 구현 세부 정보일 수 있습니다. (코드를 더 다듬으면 TaskRunner 및/또는 클래스 경로의 TaskStatus의 팩토리 등의 삽입된 구현을 참조하게 됩니다. TaskStatus를 위임하는 최종 클래스로 만드는 것은 말할 필요도 없고, 또 주제를 벗어난 것이므로......)
이 코드와 SwingWorker의
차이점은 무엇인가? 먼저 모든 문제를 해결하고 분명하게 명시했습니다. 어떤 경우에는 관련 문제를 한꺼번에 해결해주는 것이 좋게
생각될 수도 있지만 정말로 SwingWorker의 문제를 알아보려면 해당 설명서를 읽어보아야 합니다. 반면에 위의 코드 디자인은
더 단순하므로 위의 API에 대한 설명서를 하나도 읽지 않고도 이 API의 사용법을 바로 알아냈을 것입니다.
더욱 중요한 것은 TaskStatus 인스턴스를 전달하여 클라이언트 코드를 건강한 상태로 사용할 수 있다는 점입니다. 실제로 Task를 실행할 때까지 전달된 TaskStatus에 대한 참조를 가져올 수 있는 코드는 없으며, 실제로 클라이언트가 요구할 때까지 전달된 TaskStatus에 대한 참조를 가져올 수 있는 코드도 없습니다. 외부 코드가 firePropertyChange()를 호출할 수 있고, 생성자 내에서 그 상태를 "done" 또는 "started"로 설정할 수 있는 SwingWorker와 비교해보십시오. 우리는 올바른 동작을 보장하지 않습니다. 클라이언트가 TaskStatus에 대한 참조를 일부 개체에 전달할 수 있으며, 이 개체가 다른 스레드에서 악의적인 행위를 시도할 수 있습니다. 구현된 코드가 개체의 악의적인 행위를 방지하는 경우에도 마찬가지입니다. 그러나 실제 사용되는 범위 내에서만 TaskStatus를 사용할 수 있는 방식으로 설계하여 적절하게 사용해야 합니다. 또한 우리가 TaskStatus 인스턴스에 대한 참조를 전달하는 주체이므로 어떤 스레드가 TaskStatus 인스턴스를 호출할지, 그리고 호출 시에 어떤 잠금이 발생할지를 더욱 안전하게 고려할 수 있습니다. 교착 상태가 발생할 가능성이 있으면 우리측에서 그러한 가능성을 최소화하도록 최대한 노력하는 것입니다.
위에서 제기한 질문 중에 변경 사항을 관찰하는 코드에 변경된 사항에 대한 참조가 필요한가?라는 질문이 있습니다. 이 질문에 대한 답은 "그렇지 않다"는 것입니다. Task 개체를 사용 가능하도록 만드는 것은 백해무익할 수 있습니다. Task 자체가 아니라 Task의 상태와 관련된 Task의 run*() 메소드를 코드에 노출할 이유가 없습니다. 상태 보고 코드가 run*() 메소드를 호출할 가능성이 있기 때문이 아니라, Task의 상태에 관심 있는 프로그래머에게 run* 메소드는 불필요하기 때문에 백해무익하다는 것입니다. 대부분의 개발자는 IDE에서 코드 완성을 통해 API를 배웁니다. 혼합 문제는 어떤 것이 관련이 있고 어떤 것이 관련이 없는지 파악하기 어렵게 만듭니다.
여기에서 listener 패턴의 사용을 완전히 제거했다는 사실을 알게 될 것입니다. listener 패턴은 매우 과대 평가되어 있지만, 개발자들이 자바 코어 라이브러리에서 보았기 때문에(아는 것을 사용하게 되므로) 기본으로 사용되는 경우가 많습니다. listener 패턴에는 많은 문제가 있습니다. Windows 고전을 XP 모양으로 바꾸거나 그 반대로 바꾼 뒤에만 나타나는 심각한 버그가 있는 Swing 애플리케이션을 디버깅해야 했던 경험이 있다면 그 고통을 알 것입니다. 저도 압니다. 구체적인 상황은 다음과 같습니다.
- 모양과 분위기가 변경되기 전에 애플리케이션 수신기보다 먼저 구성 요소의 모양 수신기가 호출됩니다.
- 모양과 분위기가 변경된 후에는 구성 요소의 모양 수신기가 애플리케이션의 수신기 다음에 호출됩니다.
특히 수신기를 두 개 이상 포함할 가능성이 매우 낮은 상황에서는 listener 패턴을 사용하지 않는 것이 좋습니다. 상태에서 수신기 모니터가 변경됩니다. 여러 코드가 Task의 상태를 모니터링할 거라 가정하는 것은 좋지 않습니다. 필요한 경우 ProxyTaskStatus를 제공하는 것이 쉽습니다. 이 경우에는 "listeners"가 생성자 인수로 제공됩니다. 모든 명령 문제는 호출자의 문제입니다.
public final class ProxyTaskStatus {
private final TaskStatus[] statii;
ProxyTaskStatus (TaskStatus... statii) {
this.statii = statii;
}
public void setTitle (String title) {
for (TaskStatus status : statii) {
status.setTitle (title);
}
}
//...
}
정말 필요한 경우가 아니라면 다른 사람이
TaskStatus를 구현하도록 맡겨두십시오(use-case만 충분하다면 나중에 언제든지 API에 추가할 수 있습니다.).
listener 패턴 대신 모델 개체에 전달하 는 경우가 있습니다. 여기서 모델 개체는 상황에 대한 상태를 포함하고 있는 개체입니다. 상태를 작업에 포함하여 다른 코드가 상태의 변경 사항을 수신하도록 하는 것과 반대입니다. 대부분의 경우 모델 개체는 UI에 직접 상태를 기록하여 작업 진행 과정을 보여줍니다. 어디에선가 필요한 경우에만 호출하는 방식으로 호출되는 이 패턴을 읽은 적이 있는 것 같습니다(혹시 어디에서 이런 방법이 사용되었는지 아시는 분은 알려주시기 바랍니다.).
잠시, 스레딩으로 돌아가보면 일반적으로 코드에서 스레딩 + 상태가 처리되는 방식은 다음 세 가지입니다.
- 눈 감고 기도하기— 예를 들어 스레딩과 아무 관련이 없고 아무런 문제가 없기를 바라거나, 코드가 단일 스레딩용으로 만들어졌으며 호출자가 올바른 스레딩 동작을 실행해야 한다고 설명서에 명시하는 경우도 있습니다.
- 모두 동기화 — 교착 상태를 다른 사람의 문제로 만듭니다. 성능 및 유지 관리와 별 관계가 없더라도 동기화에 비용을 지불합니다.
- 실제 스레딩 모델 적용
마지막 항목이 바로 제가 주장하는 내용입니다. 모든 관련 상태를 한 곳에 보관하는 방법과 필요한 경우에만 호출하는 방법을 함께 사용하면 복잡한 데이터 모델에서 스레딩 모델을 적용할 수 있는 가능성이 생깁니다. 그러면 설명서에 스레딩에 관한 수많은 규칙을 설명하고 사람들이 이 규칙을 준수하기를 기도할 필요가 없습니다. JDK에서 매주 이 예를 적용하는 것이 Document.render()입니다. 이 코드에서는 Runnable이 종료될 때까지 다른 모든 스레드 쓰기를 차단하는 Runnable을 전달합니다(이 문서의 내용을 이 메소드 밖에서 읽을 수 있고, 그렇게 하는 코드가 많으므로 매주 적용됩니다.).
- SwingWorker 작성자를 비난할 의도는 전혀 없습니다. 자신이 작업하는 라이브러리에 맞게 작성했겠지요. 그렇지만 위와 같은 일반적인 코드로 모든 것을 수행할 수 있으며, 훨씬 간단하고, 더욱 쉽게 사용할 수 있는 결과를 가져옵니다.
- 이 문제와 관련하여 여러 API를 선택할 수 있었지만, 저는 Sun에서 근무하기 때문에 SwingWorker를 선택했습니다. 자기 아기를 못생겼다고 하는 것이 남의 아기를 못생겼다고 하는 것보다 공손해 보이는 것 같아서요. :-)
- 패턴은 안티 패턴보다 추상화하기가 쉽습니다. 안티 패턴은 자신을 포함하는 코드가 프로덕션된 후에만 실제로 표시되는 경향이 있습니다. 10년 전에는 몰랐지만 이제 해서는 안 될 금지 사항이 많아졌습니다.
이 글의 영문 원본은
Where's the state? 에서 보실 수 있습니다.
"Java SE" 카테고리의 다른 글
- STRINGTOKENIZER에서 SCANNER까지 (댓글 2개 / 트랙백 1개) 2005/04/26
- 정적 인스턴스 초기화 블록 사용하기 (댓글 2개 / 트랙백 0개) 2004/04/13
- QUEUE와 DELAYED 프로세싱 (댓글 3개 / 트랙백 0개) 2004/11/04
- Callable을 사용하여 Runnable로부터 결과 반환 (댓글 0개 / 트랙백 0개) 2008/02/20
- 가비지 콜렉션 (댓글 3개 / 트랙백 0개) 2004/06/30
- 스윙에서의 멀티 쓰레딩 (댓글 2개 / 트랙백 0개) 2003/12/12
- SWING COMPONENTS의 저장과 재구성 (댓글 5개 / 트랙백 0개) 2003/08/05
- 스플래시 스크린과 MUSTANG (댓글 1개 / 트랙백 2개) 2005/12/20
- WSIT에서의 지원 토큰과 발급된 토큰 위임 (댓글 20개 / 트랙백 2개) 2007/09/03
- Singleton 패턴에 대한 재고찰 (댓글 4개 / 트랙백 1개) 2006/04/21
댓글을 달아 주세요