Chapter 5

5 minute read

5.1 설계의 어려움

설계는 불명확한 문제다

호스트 리텔과 멜빈 웨버는 “불명확한” 문제란 전체 고흔 일부를 해결해야만 정의할 수 있는 문제라고 정의했다(Horst and Melvin 1973).
본질적으로 이 역설적인 말은 문제를 명확히 정의하려면 문제를 한 번 “해결”해야 하며, 작동하는 솔루션을 만들기 위해 다시 문제를 해결해야 한다는 의미를 담고 있다.
이러한 프로세스는 소프트웨어 개발에서 수십 년 동안 내려온 오랜 관습이다(Peters and Tripp 1976).

설계는 엉성한 프로세스다(결과는 정돈되었을지라도)

완성된 소프트웨어 설계는 정돈되어 있고 깔끔해 보여야 하지만, 설계 과정은 최종 결과물만큼은 깔끔하지 않다.
사실 실수를 하는 것이 설계의 핵심이다.
같은 문제를 코드 작성 후 발견하는 것보다 설계 단계에서 발견하고 수정하는 것이 비용이 적게 든다.

설계는 “충분”한 상태를 알기 어렵기 때문에 엉성해 보이기도 한다. 설계에 있어서 충분한 상태를 묻는다면 가장 흔한 대답은 “더 이상 시간이 없을 정도이다.

설계는 절충과 우선순위의 문제다

현실 세계에서는 설계자의 주요 업무 중 하나가 서로 상충하는 설계 특징을 비교하여 그 특성들 사이의 균형을 맞추는 일이다.
더 중요한 것을 생각하여 그에 맞는 설계를 선택해야 한다.

설계에는 제약이 따른다

설계의 핵심은 어느 정도는 가능성을 만들고 어느 정도는 가능성을 제한한다는 데 있다.
제약 사항 덕분에 좀 더 간결한 해결방안을 고안할 수밖에 없고, 그 결과 더 나은 해결책이 나온다.

설계는 비결정적이다

어떤 목적을 달성하는 방법은 한 가지 이상일 수 있지만 컴퓨터 프로그래밍을 설계하는 방법은 보통 수십 가지에 이른다.

설계는 발견적 학습과정이다

설계는 비결정적이기때문에 설계 기법은 예상된 결과를 만들어 내는 반복적 처리과정이라기보다 발견적 학습이라고 할 수 있다.
설계에는 시행착오가 따른다. 만능인 도구는 없다.

설계는 창발적이다

설계는 누군가의 머릿속에서 완전한 형태로 갑자기 솟아나오지는 않는다.
설계는 설계 검토와 격식 없는 토론, 코드 작성 경험 자체, 코드 수정 경험을 통해 진화하고 발전한다.

5.2 핵심 설계 개념

소프트웨어의 주요 기술적 의무: 복잡성 관리

  • 본질적 어려움과 비본질적 어려움
    본질적 속성은 어떠한 사물이 그러한 사물이 되기 위해 반드시 가져야 하는 속성이다.
    비본질적 속성은 부수적이며 추가적이고 우연한 것으로 생각할 수도 있다.

    소프트웨어에서의 비본질적 어려움은 오래 전에 해결되었다.

    본질적인 어려움은 느리게 해결될 수밖에 없다.
    소프트웨어 개발이 본질적으로 매우 복잡하고 서로 연관된 개념들에 대한 세부 사항들을 해결하는 작업이기 때문이다.
    소프트웨어의 본질적 어려움의 근원은 본질적이고 비본질적인 복잡성 때문이다.
  • 복잡성 관리의 중요성
    프로젝트가 기술적 이유로 실패한 경우에는 그 원인을 복잡성 관리 부족에서 찾을 수 있다.
    복잡성 관리는 소프트웨어 개발에서 가장 중요한 기술적 주제이다.
    저자는 “소프트웨어의 주요 기술적 의무는 복잡성을 관리하는 것”이라고 말한다.

    현대 컴퓨터 프로그램을 보관할 수 있을 정도의 큰 두뇌를 가진 사람은 아무도 없다(Dijkstra 1972).
    한 번에 한 부분을 제대로 집중할 수 있게 프로그램을 구상해야 한다.
    한 번에 생각해야 하는 프로그램의 크기를 최소화하는 것이다.
  • 복잡성을 해결하는 방법
    1. 두뇌가 한 번에 처리해야 하는 본질적인 복잡성의 양을 최소화한다.
    2. 비본질적인 복잡성이 불필요하게 증가하지 않도록 한다.

      일단 소프트웨어에서 복잡성을 관리하는 것이 다른 기술적 목표를 달성하는 것보다 중요하다는 것을 이해한다면 무엇을 고려할지 분명해진다.

바람직한 설계의 특징

뛰어난 설계는 몇 가지 공통적인 특징이 있다.
다음의 목표들을 충족한다면 훌륭한 설계라고 할 수 있다.

  • 복잡성 최소화: “간단”하고 “이해하기 쉬운” 설계를 만들어라.
  • 유지보수의 편리함: 유지보수 개발자를 청중이라고 생각하고 시스템을 쉽게 이해할 수 있게 설계하라.
  • 느슨한 결합: 프로그램의 각 부분 사이의 연결을 최소화하면 통합, 테스트, 유지보수 시 작업이 최소화된다.
  • 확장성: 내부 구조를 해치지 않게 기능을 개선할 수 있도록 예측 가능한 변경 사항을 미리 고민하라.
  • 재사용성: 현재 시스템의 일부를 다른 시스템에 적용할 수 있도록 설계하라.
  • 높은 팬인(fan-in): 시스템이 유틸리티 클래스를 잘 활용하도록 설계하라.
  • 낮은 팬아웃(fan-out): 특정 클래스가 다른 클래스를 적게 사용하게 하라(많아도 7개보다 적게).
  • 이식성: 시스템을 다른 환경으로 쉽게 이동시킬 수 있도록 설계하라.
  • 간결성: 불필요한 부분이 없게 시스템을 설계하라.
  • 계층화: 시스템을 다른 계층을 보지 않고도 특정 계층에서 볼 수 있도록 설계하라.
  • 표준 기법들: 표준적이고 일반적 접근 방법을 사용해 설계하라.

설계 수준

하나의 시스템에도 여러 상세 수준이 있는데, 각 상세 수준마다 설계가 필요하다.

  • 수준 1: 소프트웨어 시스템
    시스템 수준은 서브시스템이나 패키지 같이 상위 수준의 클래스 조합을 충분히 생각하는 데 도움을 준다.
  • 수준 2: 서브시스템이나 패키지로 분할
    이 수준에서는 프로그램을 주요 서브시스템으로 어떻게 나눌 것인지, 각 서브시스템이 다른 서브시스템을 어떻게 사용하게 할 것인지를 결정한다.
    이 수준에서는 서로 다른 서브시스템이 서로 어떻게 소통할 것인지에 대한 규칙을 정하는 것이 중요하다.
    모든 서브시스템이 다른 서브시스템과 소통할 수 있다면 그것들을 나눔으로써 얻는 장점이 사라질 것이다.
    커뮤니케이션을 제한하여 각 서브시스템을 의미 있게 만들어라.
    특히 시스템 수준의 다이어그램이 순환 구조를 가지도록 설계하는 것은 절대 안된다.
    규모가 큰 프로그램이나 프로그램군에서는 서브시스템 수준의 설계가 중요한 차이를 만든다.
    서브시스템 수준의 설계를 건너뛰어도 좋을 정도로 프로그램이 작다고 생각한다면 적어도 그 수준의 설계를 건너뛴다는 결정을 명확히 인지해야 한다.
  • 수준 3: 클래스로 분할
    이 수준에서는 시스템에 필요한 모든 클래스를 구체화하는 작업이 들어간다.
    각 클래스가 시스템의 나머지 부분과 상효작용하는 방법에 대한 세부적인 사항이 클래스에 명시되어야 한다(특히 클래스 인터페이스).
    또한, 객체지향 설계에서 가장 핵심적 개념은 객체와 클래스의 차이를 인지하는 데에 있다.
    클래스: 프로그램 코드로 정적
    객체: 프로그램의 실행할 때 생성되는 특정한 값과 속성을 갖는 동적인 것
  • 수준 4: 루틴으로 분할
    이 단계에서는 클래스를 루틴으로 나눈다.
    수준 3에서 정의한 클래스 인터페이스가 몇 가지 루틴을 정의할 것이다.
    수준 4에서는 클래스의 private 루틴을 상세히 설계한다.
    클래스의 루틴을 완전하게 정의하는 작업을 통해 클래스의 인터페이스에 대해 더 잘 이해하게 된다면 인터페이스도 그에 맞게 수정해야 하는 경우가 생긴다.
    이 수준의 분할과 설계는 어느 프로젝트에서든 꼭 해야 한다.

5.3 설계 빌딩 블록: 발견적 학습

설계에는 정답이 없으므로 좋은 소프트웨어 설계를 위해 발견적 학습을 효과적으로 적용해야 한다.

현실 세계의 객체를 찾아라

설계의 대안을 규명하는 최초이자 가장 널리 알려진 접근 방법은 “정석대로의” 객체지향적 접근 방법으로, 현실 세계의 객체와 가상의 객체를 찾는 것에 초점을 맞춘다.
객체를 설계하는 단계는 다음과 같다(꼭 순서대로 진행되진 않는다).

  1. 객체와 객체의 속성(메서드와 데이터)을 식별한다.
  2. 각 객체에 무엇을 할 수 있는지 결정한다.
  3. 각 객체가 다른 객체에 무엇을 할 수 있는지 결정한다.
  4. 각 객체에서 다른 객체에 보일 부분을 결정한다. 즉, 공개/비공개 부분을 결정한다.
  5. 각 객체의 공개 인터페이스를 정의한다.

일관성 있게 추상화하라

다른 수준에 있는 서로 다른 세부 사항을 다루지만, 세부 사항을 무시해도 문제가 없는 개념과 맞물리는 능력을 추상화라고 한다.
집합체(aggregate)라는 개념을 도입해 작업할 때 추상화를 통해 작업하고 있다고 말할 수 있다.

훌륭한 개발자는 루틴 인터페이스 수준, 클래스 인터페이스 수준, 패키지 인터페이스 수준의 단계적 추상화를 제공함으로써 더 빠르고 안전하게 프로그래밍할 수 있게 해준다.

구현 세부 사항을 캡슐화하라

추상화는 객체를 높은 수준에서 볼 수 있도록 하고
캡슐화는 다른 수준에서 해당 객체를 특정 수준 이상으로 볼 수 없도록 하는 것이다.
캡슐화는 복잡한 부분을 보지 못하게 함으로써 복잡성을 관리하는 데 도움을 준다.

상속이 설계를 단순화할 수 있을 때 상속하라

객체지향 프로그래밍에서는 일반적인 타입의 클래스를 정의하고, 구체적인 타입의 클래스를 정의할 때에 일반적 타입의 클래스를 상속함으로써 객체 사이의 유사성과 차이점을 정의할 수 있다.
상속은 추상화와 시너지 효과가 있고, 프로그래밍을 단순화하는 등의 장점을 지닌 가장 강력한 도구의 하나이지만 제대로 사용하지 못한다면 큰 손해를 입을 수 있다.
자세한 사항은 6.3절의 “상속(“is a”관계)”에서 설명한다.

비밀을 숨겨라(정보 은닉)

정보 은닉은 복잡성을 감추는 데 중점을 두고 있기 때문에 소프트웨어의 주요 기술적 의무(복잡성 관리)에 특히 강력한 발견적 기법이다.

  • 비밀과 프라이버시
    클래스의 역할은 정보들을 숨기고 프라이버시를 보호하는 것이다.
    시스템에서 사소하게 변경된 사항이 클래스 인터페이스를 벗어나는 범위까지 영향을 미쳐서는 안 된다.
    클래스 설계에서 가장 중요한 작업 중 하나는 어떤 기능을 클래스 외부로 알리고 어떤 기능을 비밀로 남길지 결정하는 것이다.
    클래스에 대한 인터페이스는 가능한 한 내부 작업을 드러내지 않아야 한다.
    좋은 클래스는 빙산의 일각과 같아서 클래스 대부분을 노출하지 않는다.
    정보 은닉은 숫자 상수 대신 이름 상수(named constant)를 사용하는 것부터 데이터 타입의 생성을 비롯해 클래스 설계, 루틴 설계, 서브시스템 설계에 이르기까지 모든 설계 수준에서 유용하다.
  • 은닉의 두 가지 부류
    1. 특별하게 관심이 없는 경우에 고민할 필요가 없도록 복잡성을 감추는 것
    2. 변경 발생 시 그 효과가 일부에만 영향을 미치도록 변경 원인을 감추는 것
  • 정보 은닉의 장애물
    1. 정보의 지나친 배분: 시스템 전체에 정보를 지나치게 배분하는 것
    2. 순환 의존성: ex) A가 B의 루틴 호출, B가 A의 루틴 호출
    3. 전역 데이터로 오해받는 클래스 데이터: 클래스 데이터에 대한 직접적 접근은 단일 클래스로 구성된 일부 루틴으로 제한된다.
    4. 성능 손해: 아키텍처 수준에서는 정보 은닉을 위한 시스템 설계가 성능을 위한 설계와 충돌하지 않기 때문에 걱정할 필요가 없다.
  • 정보 은닉의 가치
    정보 은닉을 사용한 큰 프로그램이 그렇지 않은 프로그램보다 4배나 수정하기 쉽다.
    “이 클래스에서 무엇을 숨겨야 하는가?”라고 묻는 것이 인터페이스 설계 문제를 해결하는 데 가장 중요하다.
    클래스의 비밀을 훼손하지 않고 함수나 데이터를 공개 인터페이스에 놓을 수 있다면 그렇게 하라. 그렇지 않다면 하지 말라.

변경될 것 같은 영역을 찾아라

훌륭한 설계자에 대한 연구에서 그들이 변경을 예측할 수 있는 능력을 공통으로 갖고 있다는 사실을 발견했다(Glass 1995). 변경 사항을 수용하는 것은 좋은 프로그램 설계에서 가장 어려운 부분이다.
변경의 효과가 한 루틴이나 클래스, 패키지에 제한되도록 불안정한 영역을 고립시켜야 한다.
그를 위해서는 다음의 단계를 따라야 한다.

  1. 변경될 것 같은 항목을 찾는다.
  2. 변경될 것 같은 항목을 분류한다.
  3. 변경될 것처럼 보이는 항목을 고립시킨다.

다음은 변경될 가능성이 큰 영역이다.

  • 비즈니스 규칙
  • 하드웨어 의존성
  • 입/출력
  • 표준을 따르지 않는 언어 기능
  • 어려운 설계 및 구현 부분
  • 상태 변수
  • 데이터 크기 제약

Leave a comment