UML 실전에서는 이것만 쓴다 - JAVA 프로그래머를 위한 UML

출처

로버트 C 마틴. UML 실전에서는 이것만 쓴다. 이용원, 정지호(역). 서울:인사이트, 2011.

1. 소프트웨어 모델을 만드는 이유

모델을 만드는 비용이 실제 물건을 만드는 비용보다 훨씬 적을 경우에 모델을 만들어서 설계를 검사해본다.
코드를 작성하는 것보다 UML 다이어그램을 그리는 것이 훨씬 비용이 적은지는 명확하지 않다.

2. UML을 효과적으로 사용하기

  • 다른 사람들과 의사소통하기 : UML은 설계 아이디어에 초점을 맞추어 의사 소통하기에 매우 좋다. 반면 알고리즘의 세부 내용을 전달하는 목적에는 UML이 그다지 유용하지 않다.
  • 로드맵 : UML은 대규모 소프트웨어 구조의 로드맵(road map)을 만들 때 유용하다. 이런 로드맵은 어떤 클래스가 다른 클래스에 의존하는지 개발자가 빨리 파악할 수 있게 해주고 전체 시스템에 대한 참조 도표로도 사용된다.
  • 백엔드(back-end) 문서 : 문서 작성을 프로젝트 막바지에 팀의 마지막 작업으로 하는 것이 가장 좋다.
  • 무엇을 보관하고 무엇을 버려야 하는가 : 칠판이나 종이 조각에 다이어그램을 그려라. 칠판을 자주 지워 버릇하고, 종이 조각은 던져 버려라. 다이어그램을 오랫동안 기록되는 매체에 기록하지 않는 매체에 기록하지 않는 습관을 기르는 것이 좋다.
  • 반복을 통해 다듬기
  • 구조를 점검하기
  • 미니멀리즘 : 다이어그램이 가장 유용한 때는 다른 사람과 의사 소통을 할 때와, 여러분이 설계에 관한 문제점을 푸는 일에 도움이 될 때다. 목적을 달성하기에 꼭 필요한 분량만큼 세부사항을 사용하는 것이 중요하다.

3. 클래스 다이어그램

표기할 수 있는 것

  • 클래스 내부의 정적인 내용이나 클래스 사이의 관계
  • 멤버 변수와 멤버 함수
  • 다른 클래스에서 상속되었는지, 다른 클래스를 참조하는지
  • 소스 코드에 나타나는 클래스 사이의 모든 의존 관계

장점

  • 소스코드에 비해서 구성요소들의 상호 의존 구조를 평가하기가 쉽다.

4. 시퀀스 다이어그램

특징

  • UML 사용자가 가장 많이 그린다.
  • 어떤 객체들이 어떻게 협력하는지 설명해야 할 때
  • 또는 그 협력을 시각화해서 보고 싶을 떄

주의사항

  1. 코드만으로도 이해할 수 있다면 다이어그램은 그리지 않는다.
  2. 여러 시나리오로 쪼개어 그리면 좋다.
  3. 시스템의 전체 흐름에 대한 고차원의 개괄이 잘어울린다. 저차원 연산에 대한 세부사항 등은 어울리지 않는다.
  4. 네트워크, 메시지 전송 등 시간이 걸리는 경우를 표현하기에 좋다.

5. 유스케이스

정의

  • 시스템의 동작 하나를 기술한 것
  • 방금 시스템에 특정한 일을 시킨 사용자의 관점에서 작성
  • 사용자가 보낸 자극 ‘하나’에 대한 반응으로 시스템이 진행하는 ‘눈에 보이는’ 이벤트들의 흐름을 포착

특징

  • 유스케이스를 단순하게 유지하는 것이 유스케이스를 사용하는 비결이다.
  • 유스케이스는 ‘내일이면 다 바뀐다’

6. 객체지향 개발의 원칙

설계의 품질

잘 설계되었다는 말은 무슨 뜻일까? 잘 설계한 시스템은 이해하기도 쉽고, 바꾸기도 쉽고, 재사용하기도 쉽다. 개발하는 데 특별히 어렵지도 않고, 단순하고 간결하며 경제적이다. 잘 설계한 시스템을 개발하는 일은 즐겁다. 반면, 잘못된 설계에서는 마치 썩는 고기처럼 역한 냄새가 난다.

나쁜 설계의 냄새

  1. 경직성 : 무엇이든 하나를 바꿀 때마다 반드시 다른 것도 바꿔야 하며, 그러고 나면 또 다른 것도 바꿔야 하는 변화의 사슬이 끊이지 않기 때문에 시스템을 변경하기 힘들다.
  2. 부서지기 쉬움 : 시스템에서 한 부분을 변경하면 그것과 전혀 상관없는 다른 부분이 작동을 멈춘다.
  3. 부동성 : 시스템을 여러 컴포넌트로 분해해서 다른 시스템에 재사용하기 힘들다.
  4. 끈끈함 : 개발 환경이 배관용 테이프나 풀로 붙인 것처럼 꽉 달라붙은 상태다. 편집 - 컴파일 - 테스트 순환을 한 번 도는 시간이 엄청나게 길다.
  5. 쓸데없이 복잡함 : 괜히 머리를 굴려서 짠 코드 구조가 굉장히 많다. 이것들은 대개 지금 당장 하나도 필요 없지만 언젠가는 굉장히 유용할지도 모른다고 기대하며 만든 것이다.
  6. 필요 없는 반복 : 코드를 작성한 프로그래머 이름이 마치 ‘복사’와 ‘붙여넣기’같다.
  7. 불투명함 : 코드를 만든 의도에 대한 설명을 볼 때 그 설명에 ‘표현이 꼬인다’라는 말이 잘 어울린다.

의존 관계 관리하기

잘못 관리한 의존 관계가 많은 냄새의 원인이다. 잘못 관리한 의존 관계는 서로 단단하게 결합(coupling)하여 얽히고설킨 코드로 나타난다.
객체지향 언어(Object Oriented Programming Language, OOPL)는 의존관계를 관리하는 데 도움이 되는 도구를 제공한다. 인터페이스를 만들어 의존 관계를 끊거나 의존의 방향을 바꿀 수도 있다. 다형성을 사용하면 어떤 함수를 포함한 모듈에 의존하지 않고도 그 함수를 호출할 수 있다. 정말로 객체지향 언어는 의존 관계를 우리가 원하는 모양대로 만들 수 있는 강력한 힘을 준다.

단 하나의 책임 원칙 (The Single Responsibility Principle, SRP)

어떤 클래스를 변경해야 하는 이유는 오직 하나뿐이어야 한다.

클래스는 오직 하나만 알아야 한다. 오직 하나의 책임만 져야 한다. 결합도가 높은 모든 개념을 각기 다른 클래스로 분리하여 클래스마다 변경해야 하는 이유가 오직 하나만 있도록 만드는 것이 바람직하다.

개방-폐쇄 원칙 (The Open - Closed Principle, OCP)

소프트웨어 엔티티(클래스, 모듈, 함수 등)는 확장에 대해서는 개방되어야 하지만, 변경에 대해서는 폐쇄되어야 한다.

모듈 자체를 변경하지 않고도 그 모듈을 둘러싼 환경을 바꿀 수 있어야 한다.
OCP를 지키지 않고 구현하면 사용자의 모든 행동을 GUI API를 호출하는 클래스에 넣어 놓을 것이다. 반면 OCP를 지키는 시스템이라면 GUI를 조작하는 부분과 데이터를 조작하는 부분을 구분해놓는다.
실제 코드를 작성하기 전에 단위 테스트를 먼저 작성함으로써 OCP를 지키는 경우가 가장 많다.

리스코프 교체 원칙 (Liskov Substitution Principle, LSP)

서브타입은 언제나 자신의 기반 타입(base type)으로 교체할 수 있어야 한다.

LSP에 따르면, 기반 클래스(base class)의 사용자는 그 기반 클래스에서 유도된 클래스를 기반 클래스로써 사용할 때, 특별한 것을 할 필요 없이 원래 기반 클래스를 사용하는 양 그대로 사용할 수 있어야 한다. 더 자세히 말하자면, instanceof나 다운캐스트(downcase)를 할 필요가 없어야 한다. 사용자는 파생 클래스에 대해서 아무것도 알 필요가 없어야 한다.

의존 관계 역전 원칙 (Dependency Inversion Principle, DIP)

A. 고차원 모듈은 저차원 모듈에 의존하면 안 된다. 이 두 모듈 모두 다른 추상화된 것에 의존해야 한다.

B. 추상화된 것은 구체적인 것에 의존하면 안 된다. 구체적인 것이 추상화된 것에 의존해야 한다.

자주 변경되는 콘크리트 클래스(concrete class)에 의존하지 마라.
만약 어떤 클래스에서 상속받아야 한다면, 기반 클래스를 추상 클래스로 만들어라.
어떤 클래스의 참조(reference)를 가져야 한다면, 참조 대상이 되는 클래스를 추상 클래스로 만들어라.
만약 어떤 함수를 호출해야 한다면, 호출되는 함수를 추상 함수로 만들어라.

인터페이스 격리 원칙 (Interface Segregation Principle, ISP)

클라이언트는 자신이 사용하지 않는 메서드에 의존 관계를 맺으면 안 된다.

비대한 클래스(fat class)란 메서드를 몇 십 몇백 개 가지는 클래스를 가리키는 말이다. 비대한 클래스가 거대하고 보기 흉하다는 사실 말고도, 한 사용자가 이 비대한 클래스의 메서드를 다 사용하는 일이 매우 적다는 것도 문제다. 즉, 메서드를 몇십개 선언한 클래스에서 사용자는 단지 두세 개만 호출할지도 모른다. 불행하게도 이 사용자들은 ‘호출하지도 않는’ 메서드에 생긴 변화에서도 영향을 받는다.

객체지향 설계 결론

  1. SRP - 어떤 클래스를 변경해야 할 이유는 오직 하나뿐이어야 한다.
  2. OCP - 클래스를 변경하지 않고도 그 클래스의 환경을 바꿀 수 있어야 한다.
  3. LSP - 유도된 클래스의 메서드를 퇴화시키거나 불법으로 만드는 일을 피하라. 기반 클래스의 사용자는 그 기반 클래스에서 유도된 클래스에 대해 아무것도 알 필요가 없어야 한다.
  4. DIP - 자주 변경하는 콘크리트 클래스 대신 인터페이스나 추상 클래스에 의존하라.
  5. ISP - 어떤 객체의 사용자에게 그 사용자한테 필요한 메서드만 있는 인터페이스를 제공하라.

이 원칙들을 적용하는 가장 좋은 방법은 능동적으로 적극 적용하는 것이 아니라, 문제가 생겼을 때 그에 대한 반응으로써 적용하는 것이다. 코드의 구조적인 문제를 처음 발견했거나, 어떤 모듈이 다른 모듈에서 생긴 변화에 영향을 받음을 처음 깨달았을 때 그때 비로소 원칙 가운데 하나 또는 여러 개를 써서 이 문제를 해결할 수 있는지 알아보아야 한다.
물론, 이렇게 문제가 생겨야 비로소 반응하는 접근 방법을 쓰기로 한다면, 초기에 고통을 느낄 수 있도록 시스템에 ‘적극적으로’ 압력을 가해야 한다.
단위 테스트를 엄청나게 작성해 보는 것이 아픈 지점을 찾는 가장 좋은 방법 가운데 하나다. 테스트 대상 코드보다 테스트를 먼저 작성하면 더 좋다.

7. dX (eXtreme Programming, XP)

dX의 실천 방법 가운데 핵심은 ‘모든 것’을 ‘짧은’ 주기로 반복하는 것이다. 요구사항, 분석, 설계, 구현, 테스팅, 문서화 등 모든 것이 포함된다.
짧은 주기는 한 주 또는 두 주를 의미한다. 최초의 탐사 작업 이후에는 아무리 초기 주기라도 실제 작동하는 코드를 주요 결과물로 내야 한다.

8. 패키지

자바의 패키지는 본질적으로 이름 공간(namespace)이다. 패키지를 사용하면 프로그래머가 작은 개인 공간을 만들어서 그 안에서 클래스를 선언할 수 있다. 이 공간에서 만든 클래스들은 다른 패키지에 있는 똑같은 이름의 클래스와 이름 충돌(name collision)을 일으키지 않는다.

패키지 설계의 원칙

이 원칙 모음은 소프트웨어 어플리케이션에서 규모가 큰 구조를 조직하는 일을 도와준다. 기능을 기준으로 시스템을 나누지 않게 된다. 이 원칙에 따라 나뉜 패키지의 목적은 자주 변경하는 클래스를 따로 모으고, 변경할 이유가 다른 클래스를 갈라놓는 것이다.

패키지 릴리스/재사용 등가 원칙 (Release/Reuse Equivalency principle, REP)

보통 클래스들을 따로따로 재사용하지 않고, 대개 몇몇 클래스들을 한 그룹으로 묶어 함께 재사용한다. 이런 재사용 그룹의 클래스들은 한 패키지 안에 들어가야 한다. 그리고 이 패키지는 그것을 재사용할 사람들이 편하게 사용할 수 있는 방향으로 릴리스되고 계속 유지보수되어야 한다.
이 원칙에 따르면, 다른 사람들이 편하게 재사용할 수 있는 패키지를 만드는 것도 클래스를 패키지 안에 배치할 떄 고려할 기준이다.

공통 폐쇄 원칙 (Common Closure Principle, CCP)

SRP에 따르면 모든 클래스는 그 클래스를 변경할 이유가 오직 하나뿐이어야 한다. CCP는 이 원칙을 패키지까지 확장한 것이다. 우리는 패키지의 모든 클래스가 똑같은 종류의 변화에는 똑같이 폐쇄되기를 원한다. 만약 어떤 것을 볍ㄴ경해야 한다면, 그것 때문에 바꾸어야 할 클래스들이 단 한 패키지에만 몰려 있기를 원한다.
CCP의 목표는 변경 가능성이 비슷한 클래스들을 하나로 묶는 것이다. 이렇게 하면 무엇을 바꿔야 할 경우 의존 관계 구조 안의 패키지들 가운데 매우 적은 수만 변경하면 된다.

공통 재사용 법칙 (Common Reuse Principle, CRP)

ISP에 따르면 클래스의 클라이언트마다 따로따로 인터페이스를 만드는 것이 좋다. CRP는 이 원칙을 패키지까지 확장한 것이다. 많은 패키지를 클라이언트로 가지는 패키지는 이것을 모두 책임지므로 그 책임이 막중하다. 이 패키지에 변화가 생기면 여기에 의존하는 모든 패키지에 큰 영향을 줄 수도 있다. 그러므로 한 클라이언트가 사용하는 클래스들과 다른 클라이언트가 사용하는 클래스들은 최대한 분리해야 한다.

의존 관계 비순환 원칙 (Acyclic Dependencies Principle, ADP)

패지키 의존 관계 그래프에 순환이 있다면 빌드할 때나 개발할 때 문제가 생길 수도 있다. 순환이 있으면 어떤 클래스와 패키지 들을 먼저 빌드하고 어떤 것을 다음에 할지 결정하지 못한다.
의존관계는 추이적(transitive)이다. 해결 방법은 패키지 의존 관계 그래프에서 순환을 제거하는 것이다.

안정된 의존 관계 원칙 (Stable Dependencies Principle, SDP)

어떤 패키지들은 바꾸기 쉽다. 반면 어떤 것들은 바꾸기 어려운데, 다른 많은 패키지들이 의존하기 때문이다.
SDP에 따르면 패키지는 바뀌기 쉬워서 자신보다 불안정한 패키지들에 의존하면 안 된다. 모든 패키지 의존 관계 화살표는 언제나 화살표가 출발하는 패키지(의존하는 패키지)보다 변경하기 어려운 패키지를 가리켜야 한다.

안정된 추상화 원칙 (Stable Abstractions Principle, SAP)

안정된 패키지는 바꾸기 어렵다. 그래도 이 패키지를 유연하게 유지하는 방법이 필요할 떄도 있다. OCP에 따르면 모듈을 변경하지 않고도 확장할 수 있는 방법이 있다. 안정된 패키지가 변경하기 어렵다고 해서 반드시 확장하기도 어려운 것은 아니다. 따라서 SAP에 따르면 안정된 패키지를 쉽게 확장할 수 있도록 유지하기 위해, 안정된 패키지는 추상적이어야 한다. 패키지가 안정적일수록 더 추상적이어야 한다.

패키지 설계 결론

ADP는 패키지나 컴포넌트의 의존 관계 순환에 문제가 많으므로 해결해야 함을 보여준다. 현재 구조를 그림으로 그려보면 이런 순환을 해결하는 데 도움이 되는 경우가 많다. 이런 다이어그램을 그리는 가장 좋은 방법은 당연히 코드에서 생성하는 것이다.

9. 객체 다이어그램

특정 순간의 시스템 상태를 보이는 것이 유용한 경우도 있다. 시스템의 스냅샷 사진처럼, UML 객체 다이어그램은 어떤 순간의 객체들과 그 객체 사이의 관계 그리고 그 객체들의 속성 값을 보여준다.

10. 상태 다이어그램

FSM(Finite State Machine)은 어떤 소프트웨어를 작성하든 굉장히 유용한 도구다. GUI나 통신 프로토콜, 그 밖에도 이벤트 기반 시스템이라면 어떤 종류에서든 사용한다.