본문 바로가기

Architecture & Design Pattern

[오브젝트] 오브젝트를 읽고, 1~2장

 지인 중 오브젝트를 읽고 티스토리에 정리하는 분이 계셔서, 우연히 오브젝트란 책을 접하게 되었다.

 

 핵심을 정리한 내용을 살짝 훑어보니, 내가 요즘 지속적으로 고민하고 있는 좋은 구조, 좋은 프로그래밍, 좋은 설계에 대한 실마리를 얻을 수 있을 것 같았다.

 

 Tuist를 이용해 모듈화를 진행하고, 여러 디자인 패턴 및 아키텍쳐를 실용적으로 사용하는 과정에서 늘 최선이 무엇인지에 대해 끝없이 재고중이었다. 모듈화를 공부하며 느낀 점은, 모듈 수준의 분리, 즉 컴포넌트를 이루는 클래스 간의 설계 이전에 클래스 자체의 설계를 먼저 깊게 고민해야겠다는 것이다. 지금까지 기본적인 '객체지향'에 대해서는 공부하고 익히려 노력했지만, deep dive한 경험은 없었기에 더욱 오브젝트라는 책이 반가웠다.

프롤로그 : 프로그래밍 패러다임

 책은 패러다임에 대한 이야기로 시작한다. 패러다임은 각 분야에서 사용될 수 있는 말이다. 그 중에서도 '프로그래밍 패러다임'이란 특정 시대의 성숙한 개발자 공동체에 의해 수용된 프로그래밍 방법과 문제 해결 방법, 프로그래밍 스타일을 말한다.

 

 '과학 혁명의 구조'를 저술한 토머스 쿤은 '패러다임에 대한 공부는 과학도가 훗날 과학 활동을 수행할 특정 과학자 공동체의 구성원이 될 수 있도록 준비시키는 것이다.'라는 말을 했다. 이 개념은 프로그래밍에도 동일하게 적용할 수 있다. 프로그래밍 패러다임은 동일한 프로그래밍 스타일과 모델을 공유하여 불필요한 의견 충돌을 방지하는, 즉 커뮤니케이션 비용을 줄이는 구조적 역할을 제공한다. 이는 개발자 공동체 전체의 발전으로 연결된다. 범위와 속성은 다르지만, 협업에서 개발 컨벤션과 비슷한 역할을 하는 것 같다.

 

 오늘날에는 절차지향, 객체지향, 함수형 등의 패러다임을 하나 또는 동시에 여럿 채택한 언어들이 공존하고 있다. 따라서 프로그래밍 패러다임은 과학 혁명에서 '혁명'과는 다르게 상호 보완하고 변화하는 '발전'적인 성질을 가진다.

 

 이처럼 저자는 패러다임에 대한 이야기로 시작하여, 패러다임이 제공하는 역할과 패러다임을 공부해야 할 필요성, 그리고 책의 방향성에 대한 이야기를 이어가고 있다. 지금까지 객체지향에 대한 '깊은' 공부는 한 적이 없기 때문에 뒤의 이야기가 기대되었다.

1장 : 객체 설계

 개발 경력 1년 6개월에 불과하지만 느낀 점을 이야기하자면, 개발이란 다분히 실전적인 성격을 가진다. '이론이 먼저냐, 실무가 먼저이냐'라는 질문을 던졌을 때, 실무를 통해 경험이 쌓이면 해당 분야에서 효율을 입증할 수 있는 이론이 발전할 수 있다고 생각한다. 저자는 프로그래밍은 다른 분야보다도 역사가 짧기 때문에, 실무가 더욱 이론에 앞선다고 주장한다. 따라서 이론의 발전사와 같은 부분을 설명하기 보다는, 당장 실무에 적용할 수 있는 '코드'를 중심으로 객체지향을 설명하고자 한다.

 

모듈의 세 가지 목적

 모듈이란 클래스, 패키지, 라이브러리와 같이 프로그램을 구성하는 임의의 요소를 뜻한다. 그리고 '클린 소프트웨어'의 저자 로버트 마틴은 모듈의 세 가지 목적을 아래와 같이 설명한다.

 

- 제대로 동작하는 것

- 변경을 위해 존재하는 것

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

 

 책의 첫 번째 장인 만큼, 위의 세 가지 목적 중에서 '제대로 동작하는 것' 이외에 다른 목적이 달성되지 않는 예시를 제시한다. 우선 '의사소통'의 문제가 발생하는 이유가 등장한다.

 

현실 세계를 반영하는 설계

 프로그래머는 사람이고, 현실 세계에서 살아가고 있다. 저자는 이해 가능한 코드란 우리의 예상에서 크게 벗어나지 않는 코드라고 말한다. 따라서 우리는 객체의 관계를 설계할 때, 현실 세계와 반영하여 이해 가능한 코드를 작성할 수 있다. 객체지향의 당위성을 설명하는 신선한 관점이었다. 예시의 코드에서는 '소극장' 클래스가 '관람객' 및 '판매원'의 상태를 직접 제어하고 있었다. 이는 현실 세계의 이치에 맞지 않기에 이해가 어려울 수 있다. '소극장'은 여러 클래스를 직접 통제하고, 너무 많은 세부사항을 다루고 있다.

 

 다음으로 '변경에 취약한 코드'의 근거가 제시된다. '소극장'은 너무 많은 클래스와 세부사항을 알고 있어야 하기 때문에, 다른 클래스에서 변경이 생길 경우 소극장 클래스도 같이 변경해야 할 확률이 증가한다. 이는 '의존성'의 문제이며, 최소한의 의존성만 유지하고 불필요한 의존성을 제거할 것을 권장한다. 또한 객체 사이의 의존성이 높은 것을 '결합도'가 높다고 표현한다. 결합도가 낮을 수록 변경에 용이하다.

 

자신의 문제는 스스로 해결하자

 위의 문제를 해결하기 위해, '자율성'을 높이는 것으로 변경이 쉬운 설계를 구현할 수 있다. 예시의 코드에서 변경이 어려운 이유는, '소극장' 클래스가 다른 클래스에 마음대로 접근할 수 있다는 것에서 시작된다.

  따라서 소극장 클래스로부터 '숨기기'가 필요하다. 이는 익히 알고 있는 '캡슐화'의 개념이며, 개념적이나 물리적으로 객체 내부의 세부적인 사항을 감추는 것을 뜻한다. 소극장 클래스는 관람객 클래스가 특정 '행동'을 할 수 있다는 사실만 알고 있으면 된다. 이는 소극장 클래스가 Interface에 의존하는 것으로 실행이 가능하다. 이후 구현은 관람객 클래스에 맡기고, 숨길 수 있다. 관람객은 자율성을 보장받게 된다. '자신의 문제를 스스로 해결하는 것'이 핵심이다.

 

 

응집도 : 객체 스스로 상태를 관리하고, 처리한다

 객체 내부의 상태를 캡슐화하고 객체 간에 '메시지'를 통해서만 상호작용하도록 만든다. 응집도가 높은 객체는 자신과 깊이 연관된 작업만 수행하고, 그렇지 않은 일들은 다른 전문적인 객체에게 위임하는 객체이다. 그리고 객체의 응집도를 높이기 위해서는, 객체 스스로 자신의 데이터를 책임지고, 자신의 데이터를 스스로 처리하게 만들어야 한다.

 

절차지향과 객체지향

 절차지향은 초기의 소극장 클래스에서 확인할 수 있다. 모든 처리가 하나의 클래스 안에 위치하고, 나머지 클래스는 단지 데이터의 역할만 수행한다. 나머지 클래스의 행동이 존재하지 않으며, 자율성이 없다. 이런 것들이 우리의 직관에 위배된다. 동시에 의존성 구조도 복잡해지며, 변경하기 어렵게 된다.

 

 반대로 객체지향이란, 데이터와 프로세스가 동일한 모듈 내부에 위치하도록 프로그래밍하는 방식이다. 따라서 문제가 생겼을 때 특정 문제는 특정 클래스만 수정하는 것으로 해결할 수 있다.

 

책임의 이동

 책임의 관점에서 살펴볼 수도 있다. 절차지향에서는 책임이 소극장 클래스에 집중되어 있지만, 객체지향에서는 책임이 여러 클래스에 분산되어 있다. 각 객체는 자신을 스스로 책임진다. 이는 단일 책임 원칙을 반영한다.

 

객체지향도 모든 것이 현실과 같을 수는 없다. 다르다.

 객체지향에서는 각 클래스가 책임을 가지고, 현실에서는 수동적인 존재가 능동적인 존재로 변모한다. 이를 의인화라고도 한다. 저자는 훌륭한 객체지향 설계란 소프트웨어를 구성하는 모든 객체들이 자율적인 존재로 행동하는 설계라고 말한다.

 

좋은 설계란

 좋은 설계란 무엇일까? 클린 아키텍쳐에서도 나오는 내용이지만, 소프트웨어는 '가치'와 '구조'를 제공해야 한다. 이 책에서는 오늘 요구하는 기능을 온전히 수행하면서, 내일의 변경을 매끄럽게 수용하는 것이 좋은 설계라고 말한다. 요구사항은 항상 변화할 수밖에 없다. 그렇기에 설계에서 변경에 대한 수용성은 필수적이다.

 

 조금은 다른 관점에서, 변경에 대한 수용성이 중요한 이유가 제시된다. 코드를 변경할 때에는 버그가 추가될 가능성이 높다. 이는 개발자에게 두려운 상황이며, 변경을 회피하게 만든다. 그러나 요구사항은 반영되어야 하기 때문에, 애초에 좋은 설계가 필요한 것이다.

2장 : 객체지향 프로그래밍

클래스와 객체

 객체지향 패러다임으로의 전환은 클래스 이전에 객체에 집중함으로써 수행할 수 있다.

1. 객체가 어떤 상태와 행동을 가지는지 결정하라 : 클래스는 객체를 추상화한 것이기 때문에, 구체적인 객체를 고민하여 클래스의 윤곽을 잡을 수 있다.

2. 객체의 공동체를 상상하라 : 객체는 단일은 의미가 없다. 각자의 책임을 가지고 협력하는 공동체의 일원이다.

객체의 공동체를 생각하고, 비슷한 객체를 타입(인터페이스)로 묶고, 이 타입을 기반으로 클래스를 설계할 수 있다.

 

도메인

 도메인이란 문제를 해결하기 위해 사용자가 프로그램을 사용하는 분야이다. 객체지향의 장점 중 하나는 객체라는 추상화 개념을 이용하여 도메인을 구성할 수 있다는 것이다. 클래스의 구조는 자연스레 도메인의 구조와 유사한 형태가 된다.

 

경계 짓기

 접근제한자를 적절히 이용하여 경계를 명확하게 구분 지으면, 객체의 자율성을 보장할 수 있다.

- 캡슐화를 통해 상태와 행동을 객체 내부로 묶는다.

- 객체 내부로의 접근을 통제한다.

이렇게 하면 객체는 스스로의 상태를 관리하며 행동할 수 있는 자율적인 존재가 된다.

 

구현 은닉

 내부 구현을 은닉하고 퍼블릭 인터페이스를 통해 필요한 기능만 제공하면, 다른 프로그래머로부터의 영향에서 객체를 보호할 수 있다. 의도치 않은 버그를 방지하며, 프로그래머가 알아야 하는 지식을 제한할 수도 있다. 동시에 작성자는 내부의 구현을 자유롭게 수정할 수 있게 된다. 결국 변경에 자유로운 설계로 이어지는 것이다.

 

협력 : 메시지를 통해 소통한다

 지금까지의 구조로 객체를 디자인하면, 객체는 다른 객체의 퍼블릭 인터페이스를 통해 행위를 요청할 수 있다. 요청을 받은 객체는 자신의 구현에 따라 맡은 일을 처리하고 응답한다.

 

 저자는 이러한 과정을 메시지를 전송하고, 수신한다고 표현한다. 객체 A가 객체 B의 메서드 C를 호출하는 것을 A가 B에게 메시지를 전송하는 행위라고 볼 수 있다. 객체 A는 B의 내부에 메서드 C가 존재하는지도 알 수 없고, 단지 메시지를 이해할 수 있다는 것만 알고 있다. 실제 메서드의 실행은 B의 책임인 것이다.

 

컴파일 시간 의존성과 실행 시간 의존성

 특정 클래스가 다른 클래스에 접근할 수 있는 경로를 가지거나, 해당 클래스의 객체의 메서드를 호출한다면 두 클래스 사이에 의존성이 존재한다고 말한다.

 

 인터페이스와 추상 메서드를 사용한다면 코드의 의존성과 실행 시점의 의존성이 다를 수 있다. 이는 클래스 의존성과 객체 의존성이 다르다는 말과 동일하다. 이러한 구조는 설계를 유연하게 해주며, 재사용을 쉽게 한다.

 

 장점만 있는 것은 아니다. 의존성이 상이할수록 코드 이해가 어려워진다. 디버깅도 마찬가지다. 유연성과 가독성의트레이드오프를 고려하여 설계할 필요가 있다.

 

인터페이스를 공유하기

 인터페이스를 공유하는 방식은 2가지이다. 첫째는 상속이고, 둘째는 인터페이스를 이용하는 것이다.

 

 일반적으로 상속을 생각할 때, 코드의 재사용에 포커스를 둘 수 있다. 그러나 상속은 어떤 측면에서는 변경에 불리한 구조를 만들 수 있다. 정말 중요한 점은 상속을 통해 부모 클래스의 인터페이스를 그대로 물려받을 수 있다는 것이다.

 

 인터페이스는 객체가 이해할 수 있는 메시지의 목록을 정의하는 것이다. 다른 객체들은 이 인터페이스를 채택하는 객체들이 해당 메시지를 이해할 수 있다는 사실에 집중하게 된다. 행동의 주체가 누구인지는 중요하지 않다. 그것이 유연성을 부여한다.

 

다형성

 다형성이란 동일한 메시지에 객체의 타입에 따라 다르게 응답할 수 있는 능력을 말한다. 동일한 인터페이스를 가지더라도, 행동의 주체는 실행 시점에 결정된다.

 

추상화의 힘

 추상화는 요구사항의 정책을 높은 수준에서 서술하게 해준다. 이는 세부 사항에 신경쓰지 않고 설계에 집중하게 해준다. 어플리케이션의 거대한 흐름을 조금 더 효율적으로 작성하고 바라볼 수 있게 되는 것이다.

추상화는 또한 유연한 설계를 가능하게 한다. 유연성이란 말 그대로 수정에 대해 열려 있음을 뜻한다.

 

책임의 위치와 설계

 만약 조건문에 의해 책임의 계층이 달라진다면 좋지 않은 설계이다. 일관된 흐름이 깨지고 소통이 무너질 수 있기 때문이다. 분기가 필요했다면, 책임을 지는 객체를 추가하는 것으로 해결할 수 있다. 수정이 아닌 추가만으로도 기능을 확장하는 것이 가능하다.

 

코드의 재사용과 상속

 상속은 캡슐화를 위반하고, 설계가 유연하지 못하게 만든다. 자식 클래스는 부모 클래스의 모든 것을 알고 있다. 따라서 부모와 자식 간의 유연한 변경이 불가능해지는 것이다. 또한 컴파일 타임에 상속 관계가 결정되기 때문에, 실행 시점에 객체의 종류를 변경할 수 없다. 따라서 상속보다는 합성을 권장한다.