4. 시퀀스 다이어그램
객체, 생명선, 메시지 등
협력에 참여하는 객체와 클래스는 맨위에 그리고, 객체는 이름 아래 밑줄이 있기 때문에 클래스와 구별된다. 왼쪽의 허수아비(액터)는 익명의 객체다. 이 객체는 협력 과정에 들어오고 나가는 모든 메시지의 시작점이자 마지막점이다.
객체와 액터 아래에 늘어뜨린 점선은 생명선(lifeline)이라고 부른다. 한 객체에서 다른 객체로 보내는 메시지는 두 생명선 사이의 화살표로 그린다. 메시지마다 이름이 붙어 있다. 인자는 이름 뒤 괄호 안에 적거나, 데이터 토큰(반대쪽 끝에 원이 그려진 작은 화살표) 아래에 적는다.
LoginServlet 객체의 생명선에 있는 얇은 사각형은 활성 상자(activation)라고 부른다. 이 상자는 어떤 함수가 실행되는 시간을 나타낸다. EmployeeDB가 객체가 아니라 클래스라는 것에도 주목하라. 클래스는 객체와 비슷하지만 이름에 밑줄이 없다. 그러므로 getEmployee는 정적 메서드일 수밖에 없다.
생성과 소멸
이름이 붙지 않은 메시지 화살표의 끝이 객체의 생명선이 아니라 생성될 객체를 가리킨다.
자바에서 객체는 명시적으로 소멸되지 않으며, 가비지 컬렉터가 우리 대신 객체를 소멸시킨다. 우리가 해제하려고 하는 객체의 생명선이 X자로 일찌감치 끊겨 있다. 이 X자를 가리키는 메시지 화살표는 객체를 해제해서 가비지 컬렉터에 넘기는 행동을 뜻한다.
사례와 시나리오
첫째, 무엇보다 자신에게 시퀀스 다이어그램이 정말 필요한지 물어보아라. 코드만으로 어떤 시스템의 일부분을 설명하는 것이 개발자와 설계자의 목표여야 한다. 개발팀은 표현력이 강하고 가독성 좋은 코드를 작성하기 위해 노력해야 한다. 코드만으로 설명하기 쉬울수록 필요한 다이어그램의 수도 줄어들고, 전체 프로젝트도 나아질 것이다.
둘째, 시퀀스 다이어그램이 필요하다는 생각이 들면 그것을 여러 시나리오로 쪼갤 수 있는지 자신에게 물어보아라.
셋째, 여러분이 무엇을 그리려고 하는지 생각해 보아라. 시스템의 전체 흐름에 대한 고차원의 개괄인가? 대개 고차원 다이어그램이 저차원 다이어그램보다 더 쓸모 있다. 고차원 다이어그램은 다이어그램을 읽는 사람이 마음속에서 시스템의 여러 요소를 하나로 연결해 볼 수 있게 해준다.
반복과 조건
별표(*)로 반복임을 표시하고 대괄호([])로 조건임을 표시 할 수 있다.
비동기 메시지
분산 시스템이나 멀티스레드 시스템에서는 메시지를 보내는 객체가 다른 스레드의 제어 흐름에서 실행될 수도 있다. 이런 메시지 '비동기 메시지(asynchronous message)'라고 부른다.
다중 스레드
비동기 메시지를 쓴다는 말은 곧 제어 흐름에서 다중 스레드를 사용한다는 뜻이다. 메시지 이름 앞에 T1 같은 식별자를 쓰고 콜론(:)을 찍어 놓은 것을 볼 수 있다. 이 식별자는 메시지를 보내는 스레드의 이름이다. 이 다이어그램에서 AsynchronousLogger 객체는 T1 스레드가 생성하고 조작한다. Log 객체 안에서 돌아가는, 실제로 메시지 로그를 수행하는 스레드는 T2라고 이름 붙어 있다.
활동적인 객체
독립된 내부 스레드를 가진 객체를 표현하고 싶은 객체를 표현하고 싶은 경우도 있다. 이런 객체는 활동적인 객체(active object)로 알려져 있다. 자신만의 스레드를 만들고 제어하는 객체는 모두 활동적인 객체다. 그 객체의 메서드에 대해서는 특별한 제한이 없다. 활동적인 객체의 메서드는 그 객체의 스레드에서 돌아가도 되고, 그 메서드를 호출하는 스레드에서 돌아가도 된다.
5. 유스케이스
유스케이스를 '단순하게 유지하는 것'이 유스케이스를 사용하는 진짜 비결이다. 정해진 형식을 맞춰야 하나 걱정하지 말고 그냥 '빈 종이'에 쓰거나, 단순한 워드프로세서로 '빈 페이지'에 작성하거나, '텅 빈 인덱스 카드'에 적어라. 모든 세부사항을 채워야 하는지 걱정할 필요도 없다. 유스케이스를 '그때그때 작성하는 요구사항'이라고 생각하라.
유스케이스란 무엇인가
유스케이스는 시스템의 동작 하나를 기술한 것이다. 유스케이스는 방금 시스템에 특정한 일을 시킨 사용자의 관점에서 작성하며, 사용자가 보낸 자극 '하나'에 대한 반응으로 시스템이 진행하는 '눈에 보이는' 이벤트들의 흐름을 포착한다.
눈에 보이는 이벤트란, 사용자가 볼 수 있는 이벤트를 뜻한다. 유스케이스는 사용자의 눈에 보이지 않는 동작을 전혀 기술하지 않고 시스템 안에 숨겨진 메커니즘도 다루지 않는다. 오직 사용자가 직접 볼 수 있는 것을 적어 놓을 뿐이다.
기본 흐름
유스케이스를 구성하는 항목은 보통 두 개다. 첫째 항목은 기본 흐름(primary course)이다. 예로 판매시점관리(point of sale, POS) 시스템의 전형적인 유스케이스를 보자.
상품 구입하기
- 점원은 상품을 스캐너 위로 통과시킨다. 스캐너가 UPC 코드를 읽는다.
- 상품 가격과 설명이 지금까지 통과시킨 상품 가격의 합계와 함께 고객 쪽 화면에 표시된다. 가격과 설명은 점원의 화면에도 표시된다.
- 가격과 설명이 영수증에 출력된다.
- UPC 코드가 올바르게 읽혔는지 점원이 확인할 수 있도록 시스템이 잘 들리는 '승인' 소리를 낸다.
대체 흐름
유스케이스의 세부사항 가운데 일부는 일이 잘못되는 경우를 고려해야 한다. 이해관계자와 대화할 때 여러분은 실패 시나리오를 이야기해 봐야 한다. 그리고 그 유스케이스를 구현할 시간이 다가올수록 이런 대체 흐름을 더 깊게 생각해야 한다. 대체 흐름은 기본 흐름에 부록처럼 붙게 되며, 다음처럼 작성한다.
<UPC 코드를 읽지 못할 경우>
만약 스캐너가 UPC 코드를 읽는 데 실패하면 시스템은 점원이 다시 시도하도록 '다시 통과시키시오.' 소리를 낸다. 만약 세 번 시도했는데도 스캐너가 UPC 코드를 인식하지 못하면, 점원은 직접 코드를 입력해야 한다.
<UPC 코드가 없을 경우>
상품에 UPC 코드가 없다면, 점원은 가격을 직접 입력해야 한다.
시스템 경계 다이어그램(System Boundary Diagram)
사각형 안에 들어있는 것은 모두 개발 중인 시스템의 일부다. 사각형 바깥을 보면 시스템을 상대로 행동하는 액터를 볼 수 있다. 액터란, 시스템에 자극을 가하며 시스템 바깥에 있는 존재다. 액터는 사용자인데 대개 사람이다. 하지만 다른 시스템이나 심지어 실시간 클럭(realtime clock) 같은 장치가 액터가 될 수도 있다.
경계 사각형 안을 보면 유스케이스들이 들어 있는데, 유스케이스는 타원 안에 그 유스케이스의 이름을 써서 나타낸다. 액터와 그 액터가 자극하는 유스케이스는 선으로 잇는다. 화살표는 그리지 마라. 화살표 방향이 정말로 무엇을 의미하는지 제대로 아는 사람은 아무도 없다.
6. 객체지향 개발의 원칙
잘 설계되었다는 말은 무슨 뜻일까? 잘 설계한 시스템은 이해하기 쉽고, 바꾸기도 쉽고 재사용하기 쉽다. 개발하는 데 특별히 어렵지도 않고 단순하고 간결하며 경제적이다.
나쁜 설계의 냄새
- 경직성(Rigidity): 무엇이든 하나를 바꿀 때마다 반드시 다른 것도 바꿔야 하며, 그러고 나면 또 다른 것도 바꿔야 하는 변화의 사슬이 끊이지 않기 때문에 시스템을 변경하기 힘들다.
- 부서지기 쉬움(Fragility): 시스템에서 한 부분을 변경하면 그것과 전혀 상관없는 다른 부분이 작동을 멈춘다.
- 부동성(Immobility) : 시스템을 여러 컴포넌트로 분해해서 다른 시스템에서 재사용하기 힘들다.
- 끈끈함(Viscosity): 개발 환경이 배관용 테이프나 풀로 붙인 것처럼 꽉 달라붙은 상태다. 편집 - 컴파일 - 테스트 순환을 한 번 도는 시간이 엄청나게 길다.
- 쓸데없이 복잡함(Needless Complexity): 괜히 머리를 굴려서 짠 코드 구조가 굉장히 많다. 이것들은 대개 지금 당장 하나도 필요 없지만 언젠가는 굉장히 유용할지도 모른다고 기대하며 만든 것이다.
- 필요 없는 반복(Needless Repetition): 코드를 작성한 프로그래머 이름이 마치 '복사'와 '붙여넣기' 같다.
- 불투명함(Opacity): 코드를 만든 의도에 대한 설명을 볼 때 그 설명에 '표현이 꼬인다'라는 말이 잘 어울린다.
의존 관계 관리하기
잘못 관리한 의존 관계가 많은 냄새의 원인이다. 잘못 관리한 의존 관계는 서로 단단하게 결합(coupling)하여 얽히고설킨 코드(스파게티 코드)로 나타난다.
객체지향 언어(Object Oriented Programming Language)는 의존 관계를 관리하는 데 도움이 되는 도구를 제공한다. 인터페이스를 만들어 의존 관계를 끊거나 의존의 방향을 바꿀 수도 있다. 다형성을 사용하면 어떤 함수를 포함한 모듈에 의존하지 않고도 그 함수를 호출할 수 있다. 정말로 객체지향 언어는 의존 관계를 우리가 원하는 모양대로 만들 수 있는 강력한 힘을 준다.
단 하나의 책임 원칙(The Single Responsibility Principle, SRP))
어떤 클래스를 변경해야 하는 이유는 오직 하나뿐이어야 한다.
클래스는 오직 하나만 알아야 한다. 오직 하나의 책임만 져야 한다. 더 핵심적인 말로 바꿔 보면, 어떤 클래스를 변경해야 하는 이유는 오직 하나뿐이어야 한다.
아래 Employee 클래스는 너무 많은 것을 알고 있다. 임금과 세금을 계산하는 방법도 알고, 자신을 디스크에 저장하거나 읽어 오는 방법도 안다. 그리고 자신을 XML로 변환하거나 XML에서 읽어 오는 방법도 알고, 다양한 보고서 형식으로 출력하는 방법도 안다.
실제로는 이 모든 개념을 각기 다른 클래스로 분리하여 클래스마다 변경해야 하는 이유가 오직 하나만 있도록 만드는 것이 바람직하다. Employee 클래스는 세금과 임금만 다루고, XML 관련 클래스는 Employee 인스턴스를 XML로 바꾸거나 XML에서 읽어 들인다. 또 EmployeeDatabase 클래스는 Employee 인스턴스를 데이터베이스에 저장하거나 읽어 들이는 역할을 담당하고, 보고서 종류마다 클래스를 하나씩 만들면 좋을 것이다. 간단히 말해서, 우리는 걱정거리를 나누고 싶다(separation of concerns).
UML 다이어그램을 보면 이 원칙을 지키지 않는 예를 무척 발견하기 쉽다. 둘 이상의 주제 영역에 의존 관계인 클래스를 찾아보면 된다. 가장 쉽게 찾을 수 있는 것이 특정 속성을 부여하는 인터페이스를 하나 또는 그 이상으로 구현하는 클래스다. 예를 들어 디스크에 저장하는 능력처럼 어떤 객체에 특정 능력을 부가하는 인터페이스를 생각해 보자. 비즈니스 로직 객체가 조심성 없이 이런 인터페이스까지 구현하면 영속성 문제와 비즈니스 규칙 사이에 필요 없는 결합을 만들기 쉽다.
개방 폐쇄 원칙(The Open Closed Principle, OCP)
소프트웨어 엔티티(Class, Module, Functions, etc.)는 확장에 대해서는 개방되어야 하지만, 변경에 대해서는 폐쇄되어야 한다.
이 원칙의 정의는 거창하지만, 의미는 간단하다. 모듈 자체를 변경하지 않고도 그 모듈을 둘러싼 환경을 바꿀 수 있어야 한다.
아래 EmployeeDB라는 데이터베이스 퍼사드(facade)를 통해 Employee 객체를 다루는 간단한 애플리케이션이 있다. 이 퍼사드는 데이터베이스 API를 직접 다룬다. 바로 이것이 OCP를 위반하는 경우인데, EmployeeDB 클래스의 구현이 변경되면 Employee 클래스도 다시 빌드해야 할지도 모르기 때문이다. Employee는 EmployeeDB를 통해 데이터베이스 API에도 묶인 셈이다. Employee 클래스를 포함하는 시스템은 반드시 TheDatabase API까지 포함해야 한다.
단위 테스트를 할 때는 환경에 생기는 변화를 제어하고 싶은 경우가 자주 생긴다. 예를 들어 Employee를 어떻게 테스트할지 생각해 보자. Employee 객체는 데이터베이스를 변경한다. 하지만 테스트 환경에서 실제 데이터베이스를 바꾸고 싶지 않다. 그렇다고 해서 단위 테스트를 하기 위해 테스트용 더미 데이터베이스를 만들고 싶지도 않다. 그렇자면, 테스트 환경으로 환경을 변경해서 테스트할 때 Employee가 데이터베이스에 하는 모든 호출을 가로챈 다음 이 호출들이 올바른지 검증하면 좋다.
아래처럼 EmployeeDB를 인터페이스로 바꾸면 호출이 올바른지 검증할 수 있다. 이 인터페이스에서 파생한 두 가지 구현을 만들되, 하나는 진짜 데이터베이스를 호출하도록 하고 다른 하나는 우리 테스트를 지원하도록 하면 된다. 이렇게 인터페이스를 만들면 데이터베이스 API와 Employee를 분리할 수 있고, Employee를 손대지 않고도 Employee를 둘러싼 데이터베이스 환경을 변경할 수 있다.
직원 목록을 보여주는 간단한 대화상자를 예로 들어보자. 사용자는 목록에서 어떤 직원을 골라 Terminate(해고) 버튼을 누른다. 이때 직원이 아무도 선택되지 않는다면 버튼이 활성화되지 않아야 한다. 목록에서 직원을 한 명 선택하면 그때 버튼이 다시 활성화되어야 한다. 사용자가 Terminate 버튼을 누르면 해고된 직원은 목록에서 사라지고, 목록은 아무도 선택되지 않은 상태로 되돌아가야 하며 버튼도 다시 비활성화 상태로 돌아가야 한다.
OCP를 지키지 않고 구현하면이 모든 행동을 GUI API를 호출하는 클래스에 넣어 놓을 것이다. 반면 OCP를 지키는 시스템이라면 GUI를 조작하는 부분과 데이터를 조작하는 부분을 구분해 놓는다.
아래는 OCP를 지키는 시스템의 구조다. EmployeeTerminatorModel(직원 해고 모델)은 직원 목록을 관리하며, 사용자가 직원을 선택하거나 해고할 때 통지받는다. EmployeeTerminatorDialog(직원 해고 대화상자)는 GUI를 관리한다. 이 클래스는 표시할 직원 목록을 받아서, 선택된 항목이 바뀌거나 Terminate 버튼이 눌렸을 때 컨트롤러에 통지한다.
EmployeeTerminatorModel은 선택된 직원을 실제로 목록에서 제거하는 책임을 맡는다. 그리고 해고 컨트롤을 활성화할지 말지 결정하는 책임도 맡는다. 이 모델은 이 컨트롤이 버튼으로 구현될지 다른 것으로 구현될지 알지 못하며, 단지 자신과 연결된 뷰에 사용자가 해고 행동을 할 수 있는지 없는지만 말해줄 뿐이다. 마찬가지로, 뷰에서 리스트 박스가 사용된다는 것을 모델이 몰라도, 지금 아무도 선택되지 않도록 하라고 뷰에 말할 수는 있다.
EmployeeTerminatorDialog는 모델이 시키는 대로만 한다. 스스로 아무 결정도 내리지 않고 어떤 데이터도 관리하지 않는다. EmployeeTerminatorModel이 인형을 조작하듯 줄을 당겼다 놓았다 하면 이 대화상자는 그에 따라 움직인다. 사용자가 대화상자와 상호 작용하면 이 대화상자는 EmployeeTerminatorController(직원 해고 컨트롤러) 인터페이스의 메서드를 호출하는 방법으로 자신의 컨트롤러에 어떤 일이 벌어지는지 알리기만 한다. 이 메시지는 모델에 전달되고, 모델은 이 메시지를 해석해서 그에 따라 행동한다.
어떻게 추상화를 해야 OCP를 지키는데 도움이 될까? 나는 실제 코드를 작성하기 전에 단위 테스트를 먼저 작성함으로써 OCP를 지키는 경우가 가장 많다. 각각 테스트 함수를 작성한 다음, 실제 모듈에는 이 테스트 함수를 통과할 수 있을 정도로만 코드를 작성한다.
리스코프 치환 법칙(The Liskov Substitution Principle, LSP)
서브 타입은 언제나 자신의 베이스 타입으로 교체할 수 있어야 한다.
if 문장과 instanceof 표현식이 수없이 많은 코드를 본 적 있는가? 보통 이런 코드는 LSP를 지키지 않아서 생기는데, 이는 곧 OCP도 지키지 않았다는 말이다.
LSP에 따르면 기반 클래스(base class)의 사용자는 그 기반 클래스에서 유도된 클래스를 기반 클래스로써 사용할 때, 특별한 것을 할 필요 없이 마치 원래 기반 클래스를 사용하는 양 그대로 사용할 수 있어야 한다. 더 자세히 말하면, instanceof나 다운캐스트(downcast)를 할 필요가 없어야 한다. 사용자는 파생 클래스에 대해서 아무것도 알 필요가 없어야 한다.
의존 관계 역전 원칙(The Dependency Inversion Principle, DIP)
A. 고차원 모듈은 저차원 모듈에 의존하면 안 된다. 이 두 모듈 모두 다른 추상화된 것에 의존해야 한다. B. 추상화된 것은 구체적인 것에 의존하면 안 된다. 구체적인 것이 추상화된 것에 의존해야 한다.
'자주 변경되는 concrete class에 의존하지 마라.' 만약 어떤 클래스에서 상속받아야 한다면 기반 클래스를 추상 클래스로 만들어라. 어떤 클래스의 참조(reference)를 가져야 한다면, 참조 대상이 되는 클래스를 추상 클래스로 만들어라. 만약 어떤 함수를 호출해야 한다면, 호출되는 함수를 추상 함수로 만들어라.
추상 클래스와 인터페이스는 보통 자신에게서 유도된 구체적인 클래스보다 훨씬 덜 변한다. 그러므로 구체적인 것보다는 이런 추상적인 것에 의존하는 편이 낫다. 이 원칙을 지키면 변화가 일어났을 때 시스템에 미치는 영향을 줄일 수 있다.
그렇다면 컨크리트 클래스인 Vector나 String을 사용하면 안 된다는 말인가? 이것을 사용하면 DIP를 어기게 되는가? 그렇지 않다. 앞으로 변하지 않을 컨크리트 클래스에 의존하는 것은 완벽하게 안전하다. Vector나 String은 다음 10년 동안에도 변하지 않을 가능성이 높다.
우리가 의존하면 안 되는 것은 '자주 변경되는' 컨크리트 클래스다. 활발히 개발중인 컨크리트 클래스나 변할 가능성이 높은 비즈니스 규칙을 담은 클래스가 여기에 속한다. 이런 클래스의 인터페이스를 만든 다음, 이 인터페이스에 의존하는 것이 바람직하다.
UML을 사용하면 이 원칙을 지키는지 매우 쉽게 검사할 수 있다. UML 다이어그램의 화살표마다 따라가서 모두 인터페이스나 추상 클래스를 가리키는지 확인하면 된다. 만약 컨크리트 클래스에 의존하느데 그 클래스가 자주 변경된다면 DIP를 어기는 것이며, 따라서 시스템도 변화에 민감하게 변할 것이다.
인터페이스 격리 원칙(The Interface Segregation Principle, ISP)
클라이언트는 자신이 사용하지 않는 메서드에 의존 관계를 맺으면 안 된다.
이 다이어그램에는 StudentEnrollment라는 클래스를 사용하는 두 클라이언트가 있다. EnrollmentReportGenerator가 prepareInvoice나 postPayment 같은 메서드는 사용하지 않으리라는 것은 명백하다. 마찬가지로 AccountsReceivable은 getName이나 getDate 같은 메서드를 호출하지 않는다고 가정해 보자.
이제 요구사항이 변해서 postPayment 메서드에 새 인자를 추가할 수 밖에 없다고 하면 무슨 일이 일어날까? StudentEnrollment의 선언을 바꾸는 이 변화 때문에 EnrollmentReportGenerator를 다시 컴파일하고 배포해야 할지도 모른다. EnrollmentReportGenerator는 postPayment 메서드와 아무 상관없는데 불행히도 이렇게 해야 한다.
다음처럼 간단한 규칙을 지키면 이렇게 불행한 의존 관계를 막을 수 있다. 사용자에게 딱 필요한 메서드만 있는 인터페이스를 제공해서 필요하지 않는 메서드에서 사용자를 보호하라.
StudentEnrollment 객체를 사용하는 사용자마다 자신이 관심 있는 메서드들만 있는 인터페이스를 제공받는다. 이렇게 하면 사용자가 관심 없는 메서드에서 생긴 변화에서 사용자를 보호할 수 있다. 그리고 사용자가 자신이 사용하는 객체를 너무 많이 알게 되는 일도 막을 수 있다.