5장: 구현 설계

5장: 구현 설계

  • 규모가 크든 작든, 그리고 그걸 “구현”이라 부르든 아니든, 프로그래머는 결국 프로그램의 일부를 직접 설계하게 됨 — 코딩이 기계적일 만큼 설계가 완전한 경우는 드물기 때문임
  • 그러니 핵심은 규모나 “구현이냐”는 분류가 아니라, 설계를 우연에 맡기지 말고 명시적 활동으로 의식하고 다루는 것임 (그래야 작은 프로젝트도 신중한 설계의 이득을 봄)

5.1 설계의 어려움

  • 소프트웨어 설계는 명세를 동작하는 소프트웨어로 바꾸는 방안을 구상하는 일로, 요구사항과 코딩·디버깅을 잇는 활동임
  • 좋은 상위 설계는 여러 하위 설계를 안전하게 담는 구조를 제공함
  • 그런 설계는 본질적으로 까다로우며, 다음과 같은 성격을 지님:
    • 사악한(wicked) 문제: 한 번 풀어 봐야 비로소 문제가 명확해짐, 그래서 정의하려 한 번 풀고 제대로 풀려고 다시 푸는 과정이 됨 — 예: Tacoma Narrows 다리는 무너지고 나서야 공기역학까지 고려해야 함을 알게 됨
    • 지저분한 과정: 결과는 깔끔해도 과정엔 헛디딤과 실수가 많음, 코딩 후보다 설계에서 고치는 편이 싸기에 실수 자체가 설계의 일부임
    • 절충과 우선순위: 모든 특성을 동시에 만족할 순 없으므로, 경쟁하는 특성(속도·공간·개발 시간 등) 중 무엇을 우선할지 정하고 균형점을 고르는 것이 설계자의 핵심 일임
      • 같은 문제라도 응답 속도를 우선하면 한 설계가, 개발 시간을 우선하면 다른 설계가 나옴
    • 제약 수반: 설계는 가능성을 넓히는 동시에 일부러 좁히는 일이기도 함, 제한이 없으면 통제 없이 비대해지지만 제한이 단순화를 강제하고 그 단순화가 오히려 해결책을 개선함
      • 시간·자원이 무한하면 신발 한 켤레마다 방을 하나씩 두는 식으로 끝없이 뻗어 나감
    • 비결정적: 같은 문제를 세 사람에게 맡기면 셋 다 다르면서도 수용 가능한 설계가 나올 수 있음
    • 발견적(heuristic): 결과를 보장하는 절차가 아니라 경험 법칙과 시행착오임, 한 번 통한 기법이 다음에도 통하리란 보장은 없음
    • 창발적: 설계는 머리에서 완성된 채 나오지 않고 리뷰·논의·코드 작성 경험을 통해 진화함

5.2 핵심 설계 개념

복잡성 관리 — 소프트웨어의 일차 기술 명령

  • 복잡성 관리는 소프트웨어 개발에서 가장 중요한 기술 주제이며, 이 장 전체의 핵심 메시지임
  • 어려움은 두 종류임 (Brooks): 본질적(그것이 그것이기 위해 꼭 필요한) 어려움과 우발적(우연히 가진) 어려움
    • 우발적 어려움 상당수는 고급 언어와 통합 개발 환경으로 이미 해소됨
    • 본질적 어려움은 복잡하고 무질서한 현실과 정확히 맞물려야 해서 진전이 더딤
  • 프로젝트가 기술적 이유로 실패할 때 원인은 대개 통제되지 않은 복잡성임, 아무도 변경의 영향을 이해하지 못하는 지점에 이르면 진전이 멈춤
  • 한 사람의 머리로 프로그램 전체를 담을 수 없으므로, 한 번에 일부에만 집중하도록 시스템을 조직해야 함
  • 그래서 대응은 두 갈래임: 한 번에 다룰 본질적 복잡성을 최소화하고, 우발적 복잡성이 불필요하게 번지지 않게 함
  • 복잡성 관리를 으뜸 목표로 받아들이면 나머지 설계 판단 다수가 한결 단순해짐

바람직한 설계의 특성

  • 좋은 설계는 여러 특성을 갖되 일부 목표는 서로 충돌하므로, 경쟁하는 목표 사이에서 좋은 절충을 만드는 것이 설계의 도전임
  • 그중 으뜸은 최소 복잡성임, 영리한 설계보다 단순하고 이해하기 쉬운 설계를 택해 한 부분에 몰두할 때 나머지를 안전히 무시할 수 있게 함
  • 그 밖에 유지보수 용이성, 느슨한 결합, 확장성, 재사용성, 간결성, 계층화, 표준 기법 사용, 높은 팬인·낮은 팬아웃, 이식성이 좋은 설계의 지표임

설계 수준

  • 설계는 여러 수준에서 이뤄짐: ① 시스템 전체 ② 서브시스템/패키지 ③ 클래스 ④ 루틴 ⑤ 루틴 내부
  • 특히 서브시스템 수준에서는 통신 규칙이 중요함, 모든 서브시스템이 서로 통신하면 분리해 둔 이점이 사라지기 때문임
    • “알 필요 있을 때만” 통신을 허용하고, 의존이 A→B→C→A처럼 순환하지 않는 비순환 그래프를 지향함

5.3 설계 빌딩 블록: 발견법

  • 설계는 비결정적이므로, 효과적인 발견법을 능숙히 적용하는 것이 좋은 설계의 핵심 활동임
  • 아래 발견법은 모두 복잡성 관리라는 한 가지 목적으로 수렴함

주요 발견법

  • 실세계 객체 찾기: 가장 흔한 출발점, 객체와 속성을 식별하고 각 객체가 무엇을 할 수 있고 무엇을 공개/비공개할지 반복적으로 정함 — 예: 청구 시스템을 직원·고객·타임카드·청구서 객체로 모델링
  • 일관된 추상화 형성: 무관한 세부를 안전하게 무시하고 개념을 다루게 해 줌, 루틴·클래스·패키지 인터페이스 각 수준에서 추상화를 만들면 더 빠르고 안전하게 작업할 수 있음 — 예: 유리·나무·못의 조합을 그냥 “집”이라 부름
  • 구현 세부 캡슐화: 추상화가 “높은 수준에서 봐도 된다”면 캡슐화는 “그 외 수준은 볼 수 없다”임, 복잡성을 아예 보지 못하게 막아 관리함 — 예: 집 바깥은 보되 문이 무슨 재질인지는 못 봄
  • 비밀 숨기기(정보 은닉): 변할 결정과 복잡성을 한곳에 숨겨 변경이 인터페이스 밖으로 번지지 않게 함, “무엇을 숨겨야 하는가?”라는 질문이 많은 설계 난제를 풀어 주어 이 장이 특히 강조함 — 예: 리터럴 100을 흩뿌리지 않고 MAX_EMPLOYEES 상수 뒤에 숨김
  • 상속 — 설계를 단순화할 때: 비슷하면서 일부만 다른 객체를 공통 타입으로 묶어 중복을 줄임, 강력하지만 잘못 쓰면 큰 해가 되므로 단순해질 때만 씀 — 예: 정규직·시간제를 공통 ‘직원’ 타입으로 묶고 다른 부분만 따로 둠
  • 변할 가능성이 큰 영역 식별: 업무 규칙·하드웨어 의존성·입출력처럼 불안정한 영역을 미리 격리해, 변경 영향을 한 곳에 가둠 — 예: 세율이 바뀌어도 그 영향이 한 모듈에만 머물게 함
  • 결합 느슨하게 유지: 모듈 사이 연결을 작고·가시적이고·유연하게 두어 한 모듈을 쉽게 가져다 쓰게 함, 내부 동작 지식에 의존하는 의미적 결합은 컴파일러가 못 잡는 방식으로 깨져 가장 위험함 — 예: 각도 하나만 받는 sin()은 느슨, 변수 여러 개를 넘기는 InitVars(...)는 빡빡함
  • 흔한 설계 패턴 찾기: 검증된 기성 해결책으로 복잡성과 오류를 줄이고 논의를 더 높은 수준으로 끌어올림, 단 억지로 끼워 맞추면 오히려 복잡해짐 — 예: Factory Method, Observer, Singleton, Strategy 등

그 밖의 발견법

  • 이 밖에도 강한 응집, 계층 구조, 클래스 계약 정형화, 책임 할당, 테스트를 고려한 설계, 실패 회피, 의식적 바인딩 시점 선택, 중앙 통제 지점, 무차별 대입, 다이어그램, 모듈성 유지 같은 발견법이 있음
  • 공통 지침은 한 접근에 갇히지 않는 것임, 막히면 다른 방식을 시도하고 설계 전체를 한 번에 풀려 하지 말 것 (정보가 부족하면 미결로 둬도 됨)

5.4 설계 실천법

  • 반복: 설계는 A에서 B로 갔다가 다시 A로 돌아오는 반복 과정임, 첫 시도가 괜찮아 보여도 멈추지 말 것 — 둘째 시도가 거의 항상 더 낫고 매번 배우는 게 있음 (에디슨이 필라멘트 1000번 실패를 “안 되는 1000가지를 발견했다”고 한 것처럼)
  • 분할 정복: 누구의 머리도 복잡한 프로그램 전체를 담지 못하므로, 관심 영역으로 나눠 각각 다루고 막히면 반복함
  • 하향식·상향식: 하나는 큰 문제를 잘게 나누고 다른 하나는 작은 조각에서 쌓아 올림, 경쟁이 아니라 상호 보완이므로 잘 되는 쪽을 찾을 때까지 오감
  • 실험적 프로토타이핑: 특정 설계 질문에 답할 최소한의 “버릴 코드”만 작성함, 질문이 구체적이어야 하고 코드를 반드시 버린다는 태도를 지켜야 효과가 있음 — 예: DB 처리량이 걱정이면 Table1·Column1 같은 가짜 테이블에 임시 데이터를 넣어 성능만 측정
  • 협업 설계: 두 머리가 한 머리보다 나음, 오류를 찾는 게 목적이면 공식 검사가, 대안을 많이 만드는 게 목적이면 덜 형식적인 방식이 맞음
  • 설계는 얼마나 충분한가: 팀 경험·시스템 수명·신뢰도·규모에 따라 다름, 판단이 서지 않으면 더 상세히 하는 쪽이 나음 — 큰 설계 오류는 쉽다고 보고 아예 설계하지 않은 영역에서 나오기 때문임
  • 설계 기록: 형식 문서만이 답은 아님, 코드 주석·위키·이메일·화이트보드 사진·CRC 카드·가벼운 UML 등 가벼운 방법으로도 충분히 남길 수 있음

5.5 잘 알려진 방법론에 대한 의견

  • 설계의 역사는 “코딩 전에 전부 설계”와 “아예 설계하지 말라” 사이를 오갔지만, 모든 세부를 설계하는 것과 아무것도 설계하지 않는 것 — 이 두 극단만은 늘 틀림
  • BDUF(과도한 사전 설계)의 대안은 “설계 없음”이 아니라 “충분한 만큼의 사전 설계(ENUF)”이며, 얼마가 충분한지는 결국 판단의 문제임
  • 그러니 설계를 사악하고 발견적인 과정으로 다룰 것: 첫 안에 안주하지 말고 협업하고, 단순함을 추구하고, 필요하면 프로토타입하고, 반복하고 또 반복함

참고 자료

  • 소프트웨어 설계 일반: Riel, Object-Oriented Design Heuristics / Plauger, Programming on Purpose / Meyer, Object-Oriented Software Construction / Larman, Applying UML and Patterns
  • 설계 이론: Parnas & Clements, “A Rational Design Process: How and Why to Fake It” / 정보 은닉의 시초인 Parnas의 세 논문(1972·1979·1985)
  • 설계 패턴: Gamma et al., Design Patterns (GoF 정전) / Shalloway & Trott, Design Patterns Explained
  • 설계 일반: Polya, How to Solve It (발견적 문제 해결의 고전) / Petroski, Design Paradigms (실패에서 배우기) / Glass, Software Creativity
  • 표준: IEEE Std 1016-1998(설계 기술서), IEEE Std 1471-2000(아키텍처 명세)

체크리스트: 구현 설계

설계 실천

  • 첫 시도가 아니라 여러 시도 중 가장 나은 것을 골라 반복했는가?
  • 시스템을 여러 방식으로 분해해 봤는가?
  • 하향식과 상향식 양쪽에서 접근했는가?
  • 위험하거나 낯선 부분을 최소한의 버릴 코드로 프로토타입했는가?
  • 설계를 공식·비공식으로 다른 사람이 리뷰했는가?
  • 구현이 명백해 보이는 지점까지 설계를 진행했는가?
  • 적절한 기법으로 설계를 기록했는가?

설계 목표

  • 아키텍처 수준에서 보류된 이슈를 설계가 충분히 다루는가?
  • 설계가 계층으로 나뉘어 있는가?
  • 서브시스템·패키지·클래스, 그리고 루틴으로의 분해에 만족하는가?
  • 클래스들이 서로 최소한으로만 상호작용하도록 설계됐는가?
  • 클래스와 서브시스템을 다른 시스템에서도 쓸 수 있는가?
  • 유지보수하기 쉽고, 간결하며, 표준 기법을 쓰는가?
  • 전체적으로 우발적·본질적 복잡성을 모두 최소화하는가?

요점 정리

  • 소프트웨어의 일차 기술 명령은 복잡성 관리이며, 단순함에 초점을 둔 설계가 이를 크게 돕는다.
  • 단순함은 두 방향으로 달성된다. 한 번에 다뤄야 할 본질적 복잡성을 최소화하고, 우발적 복잡성이 불필요하게 번지지 않게 하는 것이다.
  • 설계는 발견적이다. 단일 방법론에 교조적으로 매달리면 창의성과 프로그램 모두 해친다.
  • 좋은 설계는 반복적이다. 더 많은 가능성을 시도할수록 최종 설계가 좋아진다.
  • 정보 은닉은 특히 가치 있는 개념이다. “무엇을 숨겨야 하는가?”라는 질문이 많은 어려운 설계 문제를 해결한다.
  • 설계에 관한 유용한 정보는 이 책 밖에도 많다. 여기 제시한 관점은 빙산의 일각일 뿐이다.

results matching ""

    No results matching ""