Skip to main content

5. 구부러지거나 부러지거나

  • 빠른 변화 속도에 맞추기 위해서는 가능한 느슨하고 유연한 코드를 작성하는 것이 좋습니다.
  • 되돌릴 수 있는 의사 결정을 내릴 수 있는 구체적인 방법에 대해 아래에서 설명합니다. 이를 잘 사용하면 유연성과 적응성을 잃지 않게 됩니다.
  • 모듈간의 의존 정도를 의미하는 결합도를 줄이는 방법에 대해서도 이야기합니다.
  • 유연함을 유지하는 한 가지 좋은 방법은 가능한 적은 양의 코드를 작성하는 것입니다.

Item 26. 결합도 줄이기와 디미터 법칙#

좋은 울타리는 좋은 이웃을 만든다.

  • 이전 챕터의 직교성과 계약에 의한 설계에서 샤이 코드를 작성하는 것이 이롭습니다.
  • 코드를 모듈로 구성하고 이들 간의 상호작용을 제한합니다. 이러한 모듈이 변경되거나 교체된다 하더라도 다른 모듈은 변경 없이 수행될 수 있을 것입니다.

결합도 줄이기#

  • 소프트웨어에서도객체에게 요청을 했을 때 다른 객체로 넘겨 처리하는 것은 좋지 않습니다.
  • 의존의 증가는 시스템의 변화가 코드에 영향을 미칠 수 있는 위험이 커집니다.
  • 직접 객체간의 관계가 깨지고 의존 관계가 종합적으로 되어 있는 경우 다양한 방식으로 나타납니다.
    • 단위 테스트를 링크하기 위한 명령어가 테스트 프로그램 자체보다 긴 대규모 C, C++ 프로젝트
    • 한 모듈의 '간단한' 수정이 이와 관계없는 모듈을 통해 시스템 전역에 퍼져나가는 경우
      • 개발자가 수정한 부분이 시스템에 어떤 영향을 미칠지 몰라 코드의 수정을 두려워 하는 경우

불필요한 의존이 많은 시스템은 유지보수하기 어렵고, 비용이 많이 들며 시스템 자체가 매우 불안정한 경향이 있습니다. 이러한 의존도를 최소화하기 위해 디미터 법칙을 사용하여 메서드, 함수를 설계합니다.

디미터 함수 법칙#

  • 디미터 함수 법칙은 프로그램에서 모듈간 결합도를 최소화하려 시도합니다.
  • 해당 법칙은 한 객체가 제공하는 메서드에 접근하기 위해 또 다른 객체들을 통하는 것을 허용하지 않습니다.

Tip 36. 모듈간의 결합도를 최소화하라

확실히 차이를 낳는가?#

  • 응답 집합(response set)이 큰 클래스는 작은 클래스보다 에러를 발생시키기 쉽습니다.
  • 응답집합은 클래스의 메서드가 직접 호출하는 함수의 수를 의미합니다.
class Demeter {
private:
A *a;
int func();
public:
// ...
void example(B& b);
}
void Demeter::example(B& b) {
C c;
int f = func(); // 자신
b.invert(); // 메서드로 넘어온 인사
a = new A();
a->setActive(); // 자신이 생성한 객체
c.print(); // 직접 포함하고 있는 객체
}
  • 디미터 함수를 따르면 함수를 호출하는 클래스의 응답집합 크기를 줄일 수 있기 때문에 좀 더 에러가 적은 클래스들을 만들 수 있습니다.
  • 디미터 법칙은 코드를 더 적응성 있고 강하게 만들어 주지만 주계약자로서의 대가를 치러야합니다.
  • 주 계약자는 모든 하부 계약자를 직접 관리하고 이들에게 일을 위임해주어야 합니다.
  • 다만 이는 언어에 따라서 달라질 수도 금지해야 할 수도 있습니다.
  • 디미터 법칙 역시 만들려는 애플리케이션에 맞게 장점과 단점을 잘 고려해야 합니다.
  • 디미터 법칙과 반대로 여러 모듈의 결합도를 높힘으로써 중요한 성능향상을 꾀하는 경우도 존재합니다.

그러나, 이러한 경우가 아니라면 깨지기 쉽고 유연하지 않은 미래를 향해 가고 있는 것입니다.


Item 27. 메타 프로그래밍#

아무리 뛰어난 천재라도 세부사항에 집착하면 그 재능이 발휘되지 않는 법입니다.

  • 세부 사항은 우리의 깔끔한 코드를 어질러 놓습니다.
  • 세부 사항을 코드에서 몰아내면 우리의 코드는 매우 설정 가능(comfigurable)하게 되고 소프트해집니다.
  • 즉, 변화에 쉽게 적응할 수 있게 됩니다.

동적 설정#

  • 시스템을 되도록 설정가능하게 만드는 것이 중요합니다.

Tip 37. 통합하지 말고 설정하라.

  • 메타데이터(metadata)를 이용해서 반환 매개 변수, 사용자 선호사항, 설치 디렉터리와 같은 애플리케이션 설정 옵션을 기술합니다.
  • 메타데이터란 데이터에 관한 데이터입니다.
    • 데이터베이스 스키마, 데이터 디렉터리 등을 대표적인 예로 들 수 있습니다.
  • 넓은 의미의 메타데이터는 애플리케이션을 기술하는 모든 데이터입니다.

메타데이터 주도 애플리케이션#

  • 가능한 많은 메타데이터를 써서 애플리케이션을 설정하고 실행시킵니다.
  • 동적이고 적응가능한 프로그램을 만드는 것입니다.
  • 일반적인 경우에 대해서 프로그램을 만들고, 특별한 것들은 컴파일된 코드 밖 어딘가에 내놓는 것이 중요합니다.

Tip 38. 코드에는 추상화를, 메타데이터에는 세부 내용을

이와 같은 접근은 다음의 이점을 가집니다.

  • 설계의 결합도를 줄여 좀 더 유연하고 적응성 있는 프로그램을 만들 수 있습니다.
  • 세부사항을 코드 밖으로 몰아냄으로써 보다 강하고 추상적인 디자인을 만들 수 있습니다.
  • 애플리케이션을 커스터마이징하기 위해 다시 컴파일할 필요가 없습니다.
  • 메타데이터는 범용 프로그래밍 언어보다 문제 도메인에 가까운 방식으로 표현될 수 있습니다.
  • 동일한 애플리케이션 엔진과 상이한 메타데이터를 이용해 여러 다른 프로젝트를 진행할 수 있게 됩니다.

비지니스 로직#

  • 비지니스 정책이나 룰은 프로젝트의 다른 부분보다 변화하기 쉽기 때문에 이를 유연한 포맷을 통해 유지보수 하는 것이 좋습니다.
  • 끔직하게 복잡한 작업 흐름 요구사항을 준수해야 하는 경우, 비지니스 룰에 따라 액션을 시작하고 멈추어야 합니다.

예제: 엔터프라이즈 자바빈즈#

  • 엔터프라이즈 자바빈즈(EJB)는 분산, 트랜잭션 기반 환경의 프로그래밍을 단순화해 주는 프레임워크입니다.
  • EJB는 애플리케이션을 설정하고 코드 작성의 복잡도를 줄이기 위해 어떻게 메타데이터를 사용하면 되는지에 대한 좋은 사례입니다.
  • 특정 규약을 따르는 자족적(self-contained) 객체인 빈을 작성하고, 여러 하위(low-level) 세부사항을 관리해주는 빈 컨테이너(bean container)에 넣어주면 됩니다.
  • 빈 컨테이너는 빈이 여러 방식 중 하나를 선택해 트랜잭션을 관리할 수 있도록 해줍니다.
  • 빈의 행동에 영향을 미치는 모든 매개 변수는 빈의 디플로이먼트 디스크립터(deployment descriptor)에서 지정합니다.

협동적 설정#

  • 애플리케이션들이 서로를 설정하게 만든다면 소프트웨어가 스스로 환경에 적응하도록 만들면, 매우 강력할 것입니다.

도도 코드를 작성하지 말라#

  • 메타데이터를 사용하지 않는다면 코드는 최대한의 적응성이나 유연성을 얻을 수 없습니다.

Item 28. 시간적 결합#

  • 시간에는 우리에게 의미 있는 두가지 측면으로 나눠지며, 동시성(같은 시각에 일어나는 일들)순서(시간 속에서 일들의 상대적인 위치)입니다.
  • 일반적으로 사람은 직선적 사고(먼저 이것, 그다음 저것 순)를 하고 이는 시간적 결합(temporal coupling)을 만들게 됩니다.
    • 즉, 메서드 A는 메서드 B보다 반드시 먼저 호출해야하는 경우가 나옵니다.
    • 이는 유연하지도 않고, 현실적이지도 않습니다.
  • 우리는 동시성을 허용할 필요가 있고, 시간이나 순서에 따른 의존성의 결합을 끊는 방법을 생각할 필요가 있습니다.
  • 이는 유연성을 얻고, 작업 흐름 분석, 아키텍처, 설계, 배치(deploy)와 같은 개발의 여러 측면에서 시간과 관련된 모든 의존성도 줄일 수 있습니다.

작업 흐름#

  • 많은 프로젝트에서 요구사항 분석의 일부로서 사용자들의 작업 흐름을 모델화하고 분석하는 작업이 필요합니다.
  • 동시에 일어나도 되는 것과 엄격한 순서에 따라 일어나야하는 것은 어떤 것인지 찾아내는 것입니다.
    • UML 활동 다이어그램 등을 사용합니다.

Tip 39. 작업흐름 분석을 통해 동시성을 개선하라

UML 활동 다이어그램

아키텍처#

설계 과정에서는 다양한 제약조건들에 대응해야합니다. 다음의 예시가 있습니다.

  • 데이터베이스 작업은 완료하는 데 상대적으로 긴 시간이 걸립니다.
  • 각 트랜잭션마다, 데이터베이스 트랜잭션이 처리되는 동안에도 통신 서비스를 중단해서는 안됩니다.
  • 동시에 진행되는 세션이 너무 많으면 데이터베이스 성능이 저하됩니다.
  • 데이터 선마다 다수의 트랜잭션들이 동시적으로 진행 중입니다.

OLTP 아키텍처

  • 이러한 예제는 배고픈 여러 소비자(hungry consumer) 모델로 프로세스들이 있을 때 빠르고 간단하게 부하를 분산할 수 있는 방법입니다.
  • 중앙 일정 관리자 대신 여러 개의 독립적인 소비자 작업(consumer task)들과 중앙집중식 작업 큐를 사용합니다.
  • 이러한 구조는 자기 속도에 맞추어 일을 할 수 있기 때문에 컴포넌트들은 서로 시간적으로 결합이 끊깁니다.

Tip 40. 서비스를 사용해서 설계하라

동시성을 고려한 설계#

  • 직선형 코드에서는 엄밀하지 않은 프로그래밍으로 이끌리는 전제들을 남발하기 쉽습니다.
  • 그러나 동시성을 염두에 둔다면 여러 가지 일들을 더 주의 깊게 생각하게 될 수 밖에 없습니다.
    • 먼저 전역 변수나 정적 변수들을 동시 접근으로부터 보호해야합니다.

더 깔끔한 인터페이스#

  • 동시성과 시간 순서에 따른 의존성 고려는 더 깔끔한 인터페이스를 설계하는 방향으로 만들기도 합니다.
  • c의 strtok와 java의 StringTokenizer는 쓰레드에서 사용할 때 큰 차이가 납니다.
// 작동 안되는 코드;
char buf1[BUFSIZE], buf2[BUFSIZE];
char *p, *q;
strcpy(buf1, "this is a test");
strcpy(buf2, "this ain't gonna work");
p = strtok(buf1, " ");
q = strtok(buf2, " ");
while(p && q) {
printf("%s %s\n", p, q);
p = strtok(NULL, " ");
q = strtok(NULL, " ");
}
// 작동 되는 코드
StringTokenizer st1 = new StringTokenizer("this is a test");
StringTokenizer st2 = new StringTokenizer("this test will work");
while(st1.hasMoreTokens() && st2.hasMoreTokens()) {
System.out.println(st1.nextToken());
System.out.println(st2.nextToken());
}

Tip 41. 언제나 동시성을 고려해서 설계하라

배치#

  • 동시성 요소가 포함된 아키텍처를 설계한 다음에는 수많은 동시적 서비스들을 다루는 것에 대해 생각하기도 더 쉬워집니다.
  • 애플리케이션을 어떻게 독립 애플리케이션으로 할지, 클라이언트-서버로 할지, n-티어로 할지 결정하는 문제에 대해서도 유연하게 대응할 수 있습니다.
  • 비동시적 애플리케이션에 동시성을 추가하는 것은 매우 힘듭니다.
  • 동시성을 허용하도록 설계한다면 확장 가능성이나 성능에 대한 요구사항이 들어올 때 더 쉽게 그것에 맞춰줄 수 있으며 깔끔한 설계의 이점을 얻을 수 있습니다.

Q. 아침에 일어나서 출근 준비를 할 때 얼마나 많은 작업을 동시에 수행하고 이를 UML 활동 다이어그램으로 표현할 수 있는가?


Item 29. 단지 뷰일 뿐이야#

  • 프로그램을 책임에 따른 여러 모듈에 나누는 것이 좋다고 배우나, 이 또한 새로운 문제를 만나게 됩니다.
    • 런타임에 객체를 어떻게 서로 이야기하고, 모듈 사이의 논리적 의존성을 어떻게 관리해야 하는지에 대해 생각해야합니다.
  • 이러한 개념을 이벤트(event) 라는 개념에서 시작합니다.
  • 이벤트를 통해 어떤 객체의 상태 변화를 이에 관심을 가질 다른 객체들에게 알릴 수 있습니다.

출판/구독#

  • 모든 이벤트를 루틴 하나에 몰아넣는 일은 객체 캡슐화에 위배되며, 결합도를 늘리고, 여러 문제들 만듭니다.
  • 객체는 자기가 필요한 이벤트들만 구독해서 받아보고 필요하지 않은 이벤트들은 받아오지 않도록 해야합니다.
  • 이러한 경우 출판/구독 프로토콜(관찰자 패턴) 을 사용합니다.

모델-뷰-컨트롤러#

  • 데이터인 모델, 모델을 표시하는 뷰, 그리고 뷰를 관리하는 컨트롤러에서 모델을 분리하는 개념이 모델-뷰-컨트롤러(Model-View-Controller, MVC) 이 핵심입니다.

CORBA 이벤트 서비스

  • CORBA 이벤트 서비스에 참여하는 객체들은 이벤트 통보를 이벤트 채널(event channel)이라고 부르는 공유 버스를 통해 이벤트를 전송하고 수신할 수 있습니다.
  • 이벤트 채널은 밀기(push)와 끌기(pull) 두 기본적인 방식으로 작동합니다.
  • 밀기 방식은 이벤트 공급자들이 이벤트 채널에게 이벤트가 일어났다고 통보합니다. 그러면 채널이 관심있다고 등록한 모든 클라이언트 객체들에게 그 이벤트를 배포합니다.
  • 끌기 방식은 클라이언트가 주기적으로 이벤트 채널에 대해 폴링(polling)을 수행하고, 이벤트 채널은 다시 요청에 대응하는 이벤트 데이터를 제공하는 공급자에게 폴링을 합니다.
  • CORBA는 서로 다른 프로그래밍 언어, 멀리있는 공간, 다른 아키텍처에서의 객체들이 서로 의사소통이 가능하도록 만들어줍니다.

Tip 42. 모델에서 뷰를 분리하라

  • 모델과 뷰/컨트롤러를 분리하면 적은 비용으로 큰 유연성을 얻게 됩니다.

자바 트리 뷰#

  • 자바의 트리 위젯이 MVC 설계의 좋은 예입니다.

GUI를 넘어서#

  • MVC는 GUI 개발을 넘어 일반적으로 사용할 수 있는 프로그래밍 기법입니다.
    • 모델 : 대상 객체를 나타내는 추상 데이터 모델, 모델은 어떤 뷰나 컨트롤러에 대해서 직접적인 지식을 지니지 않는다
    • 뷰 : 모델을 해석하는 방법, 뷰는 모델의 변화 그리고 컨트롤러가 보내는 논리적 사건을 구독합니다.
    • 컨트롤러 : 뷰를 제어하고 모델에 새로운 데이터를 제공하는 방법, 모델과 뷰 둘 모두에 이벤트를 보냄
  • 모델-뷰 네트워크도 자주 쓰이는 설계 기법입니다.
    • 각 링크마다 원본 데이터와 그 데이터를 만드는 이벤트 사이의 결합을 끊습니다.
    • 모든 새로운 뷰는 또 다른 추상화입니다.
    • 서로 의 관계가 네트워크 이기 때문에 유연성도 커지고, 모델마다 많은 뷰, 뷰 하나가 여러 모델을 이용할 수도 있습니다.
    • 이러한 복잡한 시스템이라면 디버깅 뷰 를 만드는 것이 좋고, 이는 모델의 상세 내부 정보를 보여주는 특수한 뷰입니다.

(그렇게 세월이 흘러도) 여전히 결합 중#

  • 이러한 결합도 줄이기에도 불구하고, 청취자(listener)와 이벤트 생성자는 아직도 서로에 대한 약간의 지식이 남아 있습니다.
  • 뒤에서는 출판과 구독 형태의 일종이지만 참여하는 누구도 서로에 대해 알거나 서로를 직접 호출할 필요가 없는 형식을 이용해서 결합도를 낮추는 방법에 대해 이야기합니다.

Q. 비행기 개념을 포함한 항공 예약 시스템을 개선해봐라.

// before
public interface Flight {
public boolean addPassenger(Passenger p);
public void addToWaitList(Passenger p);
public int getFlightCapacity();
public int getNumPassengers();
}
// after
public interface Passenger {
public void waitListAvailable();
}
public interface Flight {
public void addWaitListListener(Passenger p);
public void removeWaitListListener(Passenger p);
public void addFullListener(FullListener b);
public void removeFullListener(FullListener b);
}
public interface BigReport extends FullListener {
public void FLightFullAlert(Flight f);
}

Item 30. 칠판#

  • 형사들이 살인사건을 조사하는 경우, 자료가 쌓이면 연결 고리를 찾아내서 자신의 관찰이나 추측을 올리기도 합니다.
  • 이러한 내용들을 칠판에 적어서 모든 교대조에 속한 수많은 사람들과 요원들이 참여해서 사건이 종결될 때까지 계속됩니다.
  • 칠판 시스템을 이용하면, 지식의 소비자와 생산자들이 익명으로 그리고 비동기적으로 데이터를 주고 받는 공간이 생깁니다. 이를 통해 객체들 사이의 결합을 끊고 작성할 코드의 양도 줄어듭니다.

칠판 구현#

  • 컴퓨터 기반의 칠판 시스템은 원래 음성 인식, 지식 기반 추론 시스템 등등 해결해야 할 문제의 규모가 크고 복잡한 인공 지능 애플리케이션에서 사용할 목적으로 발명했습니다.
  • 이런 시스템을 이용하면, 객체의 데이터뿐만 아니라 자바 객체 전체를 칠판에 저장한 후 필드의 부분일치 또는 유형(type)별 검색을 통해 다시 가져올 수 있습니다.
    • 예를 들어 작가는 사람의 하위 유형이고 이러한 유형 정보를 통해 정원사가 아닌 작가 A를 찾을 수 있습니다.
  • JavaSpaces도 그러한 예제입니다.
  • 객체 자체를 저장하는 일을 가능해졌으며 단지 데이터가 아니라 객체의 흐름에 기반한 알고리즘을 설계하는 데 칠판을 이용할 수 있습니다.
  • 칠판에 대한 일관성 있는 인터페이스 하나만 있으면 된다는 점이 이러한 시스템의 가장 큰 장점입니다.
  • 이러한 방식으로 프로그래밍을 하면 이렇게 많은 인터페이스가 필요 없어지기 때문에, 더 우아하고 일관성 있는 시스템을 만들 수 있습니다.

애플리케이션 예제#

칠판 정리하기

  • 사건의 규모가 클 경우, 칠판이 난잡해져서 그 위에서 자료를 찾아보기 힘들어집니다.
  • 칠판을 여러 구획으로 나누고 어떤 방식으로든 자료를 정리하여야 합니다.
  • 소프트웨어 시스템마다 구획을 나누는 방식이 다릅니다. 몇몇 시스템은 상당히 평면적인 지대(zone)나 관심 모임(interest group)을 사용하기도 하는 반면, 다른 시스템들은 트리 구조와 유사한 구조를 채택하기도 합니다.

작업흐름 시스템을 이용해서 가능한 모든 조합과 환경을 포괄하도록 노력할 수도 있습니다. 하지만 이러한 경우 프로그래머의 노력이 많이 들어가며 규정이 바뀌면 작업흐름도 다시 만들어야 합니다.

  • 법적 요구조건을 캡슐화하는 규칙 엔진과 칠판 시스템을 함께 사용하면 이러한 어려움들을 우아하게 해결할 수 있어야합니다.

Tip 43. 칠판을 사용해 작업흐름을 조율하라

  • 참여하는 요소들의 독립성과 심지어는 고립성을 유지하는 동시에 이질적인 사실과 행위자들을 잘 조정하는데 칠판을 사용할 수 있습니다.

Q. 실제 생활에서도 메모판이나 화이트보드를 사용하는가> 그러한 일관성이 중요하는가?

Last updated on