티스토리 뷰

반응형

저자는 조용호 님이다.  초판은 2019년이지만 지금(2022년 1월에 5쇄 발행) 계속 발행되고 있는 것을 보면, 인기가 많은 책인듯하다. 

 

예전에 도서관에서 빌려 읽었던, 같은 저자의 '객체지향의 사실과 오해'가 쉽가 설명이 잘 되어있어서 마음에 들었었다. 그 책이 이번에 읽은 책의 프리퀄 같은 책이라는 저자의 글에 따라 다음 시리즈인 이 책을 읽게 되었다.

 

확실히 나에게 도움이 된다. 큰 시각에서 객체지향 프로그램의 설계를 어떻게 해야하는지 감을 잡을 수 있었다. 아마도 나와같은 비전공자라면 꼭 한번 읽어보라고 추천해주고 싶다. 

 

 

 

기억하고 싶은 부분

 

설계란 코드를 배치하는 것이다. 

설계는 코드를 작성하는 매 순간 코드를 어떻게 배치할 것인지를 결정하는 과정에서 나온다.

설계는 코드 작성의 일부이며 코드를 작성하지 않고서는 검증할 수 없다. 

 

좋은 설계란?

오늘 요구하는 기능을 온전히 수행하면서 내일의 변경을 매끄럽게 수용할 수 있는 설계다. 

개발을 시작하는 시점에서 구현에 필요한 모든 요구사항을 수집하는 것은 불가능에 가깝다. 

코드를 변경할 때 버그가 추가될 가능성이 높다. 요구사항 변경은 필연적으로 코드 수정을 초래하고, 코드 수정은 버그가 발생할 가능성을 높인다. 따라서 변경에 유연하게 대응할 수 있는 코드는 중요하다. 

변경 가능한 코드란 이해하기 쉬운 코드다. 

 

훌륭한 객체 지향 설계란 협력하는 객체 사이의 의존성을 적절하게 관리하는 설계다. 

 

소프트웨어 모듈의 세가지 목적

1. 실행 중에 제대로 동작하는 것

2. 생명주기 동안 변경되기 때문에 간단한 작업만으로도 변경이 가능한 것

3. 코드를 읽는 사람과 의사소통하는 것

 

의존성(dependency), 결합도(coupling),

 

캡슐화(encapsulation): 변경 가능성이 높은 부분을 객체 내부로 숨기는 추상화 기법이다. 객체 내부에 무엇을 캡슐화 해야하는가? 변경될 수 있는 어떤 것이라도 캡슐화 해야한다.

 

응집도: 모듈에 포함된 내부 요소들이 연관돼 있는 정도를 나타낸다. 모듈 내의 요소들이 하나의 목적을 위해 긴밀하게 협력한다면 그 모듈은 높은 응집도를 가진다.  변경이 발생할 때 모듈 내부에서 발생하는 변경의 정도로 측정할 수 있다. 

 

결합도: 의존성의 정도를 나타내며 다른 모듈에 대해 얼마나 많은 지식을 갖고 있는지를 나타내는 척도다. 어떤 모듈이 다른 모듈에 대해 너무 자세한 부분까지 알고 있다면 두 모듈은 높은 결합도를 가진다. 한 모듈이 변경되기 위해서 다른 모듈의 변경을 요구하는 정도로 측정할 수 있다. 

 

절차적 프로그래밍(Procedural Programming) : 프로세스와 데이터를 별도의 모듈에 위치시키는 방식

객체지향 프로그래밍(Object-Oriented Programming): 데이터와 프로세스가 동일한 모듈 내부에 위치하도록 프로그래밍하는 방식

 

도메인(domain) :  영화 예매 시스템의 목적은 영화를 쉽고 빠르게 예매하려는 사용자의 문제를 해결하는 것이다. 이처럼 문제를 해결하기 위해 사용자가 프로그램을 사용하는 분야를 도메인이라 부른다. 

 

객체지향이 강력한 이유는 요구사항을 분석하는 초기 단계부터 프로그램을 구현하는 마지막 단계까지 객체라는 동일한 추상화 기법을 사용할 수 있기 때문이다. 요구사항과 프로그램을 객체라는 동일한 관점에서 도메인을 구성하는 개념들이 프로그램의 객체와 클래스로 매끄럽게 연결될 수 있다. 

 

상속을 구현 상속과 인터페이스 상속으로 분류할 수 있다. 흔히 구현 상속을 subclassing이라고 부르고 인터페이스 상속을 subtyping이라 부른다. 상속은 구현 상속이 아니라 인터페이스 상속을 위해 사용해야 한다. 

 

도메인 모델은 불완전한 사람들이 세상을 바라보는 모델에 기반하기 때문에 그 역시 동일한 불완전성을 가질 수 밖에 없다. 

 

 단일 책임 원칙(Sigle Responsibility Prinsible, SRP): 클래스는 단 한가지의 변경 이유만 가져야 한다는 것. 

 

올바른 객체지향 설계의 무게 중심은 항상 객체의 내부가 아니라 외부에 맞춰져 있어야 한다. 객체가 내부에 어떤 상태를 가지고 그 상태를 어떻게 관리하는가는 부가적인 문제다. 중요한 것은 객체가 다른 객체와 협력하는 방법이다. 객체의 구현이 이미 결정된 상태에서 다른 객체와의 협력 방법을 고민하기 때문에 이미 구현된 객체의 인터페이스를 억지로 끼워 맞출 수 밖에 없다. 

 

책임은 객체의 입장이 아니라 객체가 참여하는 협력에 적합해야 한다. 협력에 적합한 책임이란 메시지 수신자가 아니라 메시지 송신자에게 적합한 책임을 의미한다. 다시 말해서 메시지를 전송하는 클라이언트의 의도에 적합한 책임을 할당해야 한다는 것이다. 메시지를 결정한 후에 객체를 선택해야 한다. 

 

책임 주도 설계 

- 시스템이 사용자에게 제공해야 하는 기능인 시스템 책임을 파악한다 

- 시스템 책임을 더 작은 책임으로 분할 한다

- 분할된 책임을 수행할 수 있는 적절한 객체 또는 역할을 찾아 책임을 할당한다

- 객체가 책임을 수행하는 도중 다른 객체의 도움이 필요한 경우 이를 책임질 적절한 객체 또는 역할을 찾는다

- 해당 객체 또는 역할에게 책임을 할다함으로써 두 객체가 협력하게 한다

 

책임을 할당할 수 있는 다양한 대안들이 존재한다면 응집도와 결합도의 측면에서 더 나은 대안을 선택하는 것이 좋다. 

 

응집도가 높은 클래스는 인스턴스를 생성할 때 모든 속성을 함께 초기화 한다. 반면 응집도가 낮은 클래스는 객체의 속성 중 일부만 초기화하고 일부는 초기화되지 않은 상태로 남겨진다. 

메서드가 객체의 모든 속성을 사용한다면 클래스의 응집도가 높다고 볼 수 있다. 반면 메스드들이 사용하는 속성에 따라 그룹이 나뉜다면 클래스의 응집도가 낮다고 볼 수 있다. 

 

디미터 법칙(Law of Demeter): 객체의 내부 구조에 강하게 결합되지 않도록 협력 경로를 제한하는 것 . "낯선 자에게 말하지 마라", "오직 인접한 이웃하고만 말하라", "오직 하나의 도트만 사용하라"

아래에 조건을 만족하는 인스턴스에만 메시지를 전송하도록 프로그래밍해야한다. 

- this 객체

- 메서드의 매개변수

- this 의 속성

- this의 속성인 컬렉션의 요소

- 메서드 내에서 생성된 지역 객체

 

오퍼레이션은 클라이언트가 객체에게 무엇을 원하는지를 표현해야 한다. 다시 말해 객체 자신이 아닌 클라이언트의 의도를 표현하는 이름을 가져야 한다. 

 

어떤 절차를 묶어 호출 가능하도록 이름을 부여한 기능 모듈을 루틴(routine)이라고 부른다. 루틴은 다시 프로시져(procedure)와 함수(function)로 구분할 수 있다. 프로시저는 정해진 절차에 따라 내부의 상태를 변경하는 루틴의 한 종류다. 이에 반해 함수는 어떤 절차에 따라 필요한 값을 계산해서 반환하는 루틴의 한 종류다. 

프로시저는 부수효과를 발생시킬 수 있지만 값을 반환할 수 없다. 

함수는 값을 반환할 수 있지만 부수효과를 발생시킬 수 없다. 

명령(command)과 쿼리(query)는 객체의 인터페이스 측면에서 프로시저와 함수를 부르는 또다른 이름이다. 

명령과 쿼리를 뒤 섞으면 실행 결과를 예측하기 어려워 질 수 있다. 명령과 쿼리를 분리하면 코드는 예측 가능하고 이해하기 쉬우며 디버깅이 용이한 동시에 유지보수가 수월해질 것이다. 

 

불필요한 정보를 제거하고 현재의 문제 해결에 필요한 핵심만 남기는 작업을 추상화라 부른다. 가장 일반적인 추상화 방법은 한 번에 다뤄야 하는 문제의 크기를 줄이는 것이다. 큰 문제를 해결 가능한 작은 문제로 나누는 작업을 분해라고 부른다. 

 

의존성은 명시적으로 표현돼야 한다. 의존성을 구현 내부에 숨겨두지 마라. 

 

인스턴스를 생성하는 로직과 생성된 인스턴스를 사용하는 로직을 분리하라. (법칙은 아니고, 원칙 정도)

유연하고 재사용 가능한 설계를 원한다면 객체와 관련된 두가지 책임을 서로 다른 객체로 분리해야 한다. 하나는 객체를 생성하는 것이고, 다른 하나는 객체를 사용하는 것이다. 

 

유연한 설계를 원한다면 컴파일 타임 의존성을 고정시키고 런타임 의존성을 변경하라 

 

도메인 모델은 설계를 위한 중요한 출발점이지만 단지 출발점이라는 사실을 명심해야 한다. 실제로 동작하는 애플리케이션은 데이터베이스 접근을 위한 객체와 같이 도메인 개념들을 초월하는 기계적인 걔념들을 필요로 할 수 있다. 

어떠한 행동을 추가하려고 하는데 이 행동을 책임질 마땅한 도메인 개념이 존재하지 않는다면 pure fabrication을 추가하고 이 객체에게 책임을 할당하라. 

 

의존성 주입: 사용하는 객체가 아닌 외부의 독립적인 객체가 인스턴스를 생성한 후 이를 전달해서 의존성을 해결하는 방법

- 생성자 주입: 객체를 생성하는 시점에 생성자를 통해 의존성 해결

- setter 주입: 객체 생성 후 setter 메서드를 통한 의존성 해결

- 메서드 주입: 메서드 실행시 인자를 이용한 의존성 해결

 

어떤 협력에서 중요한 정책이나 의사결정, 비즈니스의 본질을 담고 있느 것은 상위 수준의 클래스다. 그러나 이런 상위 클래스가 하위 수준의 클래스에 의존한다면 하위 수준의 변경에 의해 상위 수준 클래스가 영향을 받게 될 것이다. 상위 수준의 클래스는 어떤 식으로든 하위 수준의 클래스에 의존해서는 안된다. 이 경우 해결사는 추상화다. 추상화에 의존하도록 수정하면 하위 수준 클래스의 변경으로 인해 상위 수준의 클래스가 영향을 받는것을 방지할 수 있다. 모든 의존성의 방향이 추상 클래스나 인터페이스와 같은 추상화를 따라야 한다. 

 

합성(composition): 새로운 클래스의 인스턴스 안에 기존 클래스의 인스턴스를 포함시키는 방법. 구현이 아닌 퍼블릭 인터페이스에 대해서만 의존할 수 있기 때문에 런타임에 객체의 관계를 변경할 수 있다. 

 

믹스인(mixin): 객체를 생성할 때 코드 일부를 클래스 안에 섞어 넣어 재사용하는 기법. 합성이 실행 시점에 객체를 조합하는 재사용방법이라면 믹스인은 컴파일 시점에 필요한 코드 조각을 조합하는 방법이다.  추상 서브클래스(abstract subclass), 쌓을 수 있는 변경(stackable modification)이라고 부르기도 한다. 

 

중복 코드 제거

- 상속

- 합성 (composition)

- 믹스인(mixin)

 

훅 메서드(hook method): 추상 메서드와 동일하게 자식 클래스에서 오버라이딩할 의도로 메서드를 추가했지만 편의를 위해 기본(default) 구현을 제공하는 메서드

 

메서드 오버라이딩( overriding): 자식 클래스 안에 상속받은 메서드와 동일한 시그니처의 메서드를 재정의해서 부모 클래스의 구현을 새로운 구현으로 대체하는 것

 

메서드 오버로딩(overloading): 부모 클래스에서 정의한 메서드와 이름은 동일하지만 시그니처는 다른 메서드를 자식 클래스에 추가하는 것

 

업캐스팅: 부모 클래스 타입으로 선언된 변수에 자식 클래스의 인스턴스를 할당하는 것

동적 바인딩: 실행될 메서드를 컴파일 시점이 아니라 실행시점(run time)에 결정하는 것

 

this(self) 참조 : 현재 객체. 동일한 메시지를 수신하더라도 객체의 타입에 따라 적합한 메서드를 동적으로 선택 가능

super 참조 : '지금 이 클래스의 부모 클래스에서부터 메서드 탐색을 시작하세요'라는 의도. 자식 클래스에서 부모 클래스의 인스턴스 변수나 메서드에 접근하기 위해 사용

 

위임(delegation): 자신이 수신한 메시지를 다른 객체에게 동일하게 전달해서 처리를 하는 것. 위임은 항상 문백을 가리키는 self(or this) 참조를 인자로 전달한다. 이것이 self를 전달하지 않는 포워딩과 위임의 차이점이다. 

 

서브클래싱(subclassing): 다른 클래스의 코드를 재사용할 목적으로 상속을 사용하는 경우

서브타이핑(subtyping): 타입 계층을 구성하기 위해 상속을 사용하는 경우. 인터페이스 상속이라고도 한다

 

대체 가능성을 결정하는 것은 클라이언트다. 

잘 설계된 애플리케이션은 이해하기 쉽고, 수정이 용이하며, 재사용 가능한 협력의 모임이다. 

 

협력을 일관성 있게 만들기

- 변하는 개념을 변하지 않는 개념으로부터 분리하라. 즉, 코드에서 새로운 요구사항이 있을 때마다 바뀌는 부분이 있다면 그 행동을 바뀌지 않는 다른 부분으로부터 골라내서 분리해야 한다. 

- 변하는 개념을 캡슐화하라. 그렇게 하면 나중에 바뀌지 않는 부분에는 영향을 미치지 않은 채로 그 부분만 고치거나 확장할 수 있다.

 

if문과 같이 조건에 따라 분기되는 어떤 로직들이 있다면 이 로직들이 바로 개별적인 변경이라 불 수 있다. 객체지향에서 변경을 다루는 전통적인 방법은 조건 로직을 객체 사이의 이동으로 바꾸는 것이다. 

 

애플리케이션에서 유사한 기능에 대한 변경이 지속적으로 발생하고 있다면 변경을 캡슐화할 수 있는 적절한 추상화를 찾은 후, 이 추상화에 변하지 않는 공통적인 책임을 할당하라. 현재의 구조가 변경을 캡슐화하기에 적합하지 않다면 코드를 수정하지 않고도 원하는 변경을 수용할 수 있도록 협력과 코드를 리팰터링하라. 변경을 수용할 수 있는 적절한 역할과 책임을 찾다보면 협력의 일관성이 서서히 윤곽을 드러낼 것이다. 

 

디자인 패턴의 목적은 설계를 재사용하는 것이다. 다양한 변경을 다루기 위해 반복적으로 재사용할 수 있는 설계의 묶음 이다. 

 

프레임워크는 설계와 코드를 함께 재사용하기 위한 것이다. 프레임워크는 애플리케이션의 아키텍쳐를 구현 코드의 형태로 제공한다. 

 

패턴을 분류하는 가장 일반적인 방법은 패턴의 범위나 적용 단계에 따라 아키텍쳐 패턴, 분석 패턴, 디자인 패턴, 이디엄 의 4가지로 분류하는 것이다. 

 

디자인 패턴의 구성요소가 클래스와 메서드가 아니라 역할과 책임이라는 사실을 이해하는 것이 중요하다. 어떤 구현 코드가 어떤 디자인 패턴을 따른다고 이야기할 때는 역할, 책임, 협력의 관점에서 유사성을 공유한다는 것이지 특정한 구현 방식을 강제하는 것은 아니라는 점을 이해하는 것 역시 중요하다. 

 

대부분의 패턴 입문자가 빠지기 쉬운 함정은 패턴을 적용하는 컨텍스트의 적절성은 무시한 채 패턴의 구조에만 초점을 맞추는 것이다. 패턴을 익힌 후에는 모든 설계 문제를 패턴으로 해결하려고 시도하기 쉽다. 

반응형

'앱 개발 > ' 카테고리의 다른 글

플러터 앱 개발 첫걸음  (0) 2023.09.09
모바일 앱 개발을 위한 다트 & 플러터  (2) 2023.09.09
클린 아키텍처  (0) 2023.02.11
실용주의 프로그래머  (4) 2023.01.28
Refactoring  (0) 2022.02.11
댓글
공지사항
최근에 올라온 글
최근에 달린 댓글
Total
Today
Yesterday
링크
«   2025/02   »
1
2 3 4 5 6 7 8
9 10 11 12 13 14 15
16 17 18 19 20 21 22
23 24 25 26 27 28
글 보관함