001. (The Essence of Object-Orientation) 1. 협력과 역할 그리고 책임

1. 협력과 역할 그리고 책임

1.1. 절차적 프로그래밍과 구조적 프로그래밍 그리고 객체지향 프로그래밍

초기의 프로그래밍 방식은 값을 입력받아 주어진 순서대로 정해진 연산을 처리한 뒤 그 결과를 반환해주는 방식이었다.

이는 절차적 프로그래밍(Procedural Programming) 라고 불리며, 프로그램은 명령어의 집합체라는 정의에 충실하였다.

허나 절차적 프로그래밍은 간단한 알고리즘까지는 문제없이 표현할 수 있으나, 복잡도가 조금만 올라가면 순서도로 표현하기 어려운 형태가 되어버린다.

이후 에츠허르 다익스트라(Edsger Wybe Dijkstra) 가 프로그램을 프로시저 단위로 분류하고, 프로시저끼리 호출하는 구조적 프로그래밍 방식을 제안하면서 발전이 시작되었다.

참고 에츠허르 다익스트라는 당신이 지금 머릿속에 떠올린 최단 경로 알고리즘을 고안한 그 사람이 맞다.
위에서 발표한 논문은 GOTO 문의 해로움(Go To Statement Considered Harmful) 이 라는 논문이다.
논문을 읽고 싶다면 이 링크 를 참고하면 된다.

이렇게 구조적 프로그래밍이 도입되고, 커다란 문제를 작은 문제로 나누어 해결하는 탑다운 방식이 정착되었다.

다만 구조적 프로그래밍도 데이터의 처리 방식을 구조화했을 뿐 데이터를 구조화한 것이 아니기 때문에 문제점은 여전히 남아있었다.

이를 극복하기 위한 방법으로 등장한 것이 바로 객체지향 프로그래밍(OOP : Object-Oriented Programming) 이다.

객체지향은 구조적 프로그래밍의 탑다운 방식과 정 반대로, 작은 문제들을 해결할 수 있는 객체들을 만든 뒤 이를 조합하여 큰 문제를 해결하는 바텀업 방식으로 프로그램을 디자인한다.

여기서 객체지향 프로그래밍은 현실 세계에 존재하는 사물을 인지적 관점에서 최대한 모방하여 프로그램으로 구현하는 것이 필수적으로 요구된다.

따라서 객체지향 프로그래밍으로 작성된 소프트웨어는 현실 세계를 투영한 것이며, 결국 객체란 현실 세계에 존재하는 사물에 대한 추상화임을 의미한다.

1.2. 현실 세계의 사물과 객체간의 괴리

현실 세계를 아무리 잘 모방하더라도, 이는 객체지향의 철학적인 개념만을 잘 설명해줄 수 있을 뿐

유연하고 실용적인 관점에서의 객체지향의 분석 및 설계를 설명하기엔 적잡하지 않다.

실제로 애플리케이션을 개발하면서 객체과 직접적으로 대응을 현실 세계의 사물을 찾기란 쉽지 않다는 것을 우리는 경험적으로 알고 있다.

그럼 객체지향의 목표는 무엇일까?

객체지향의 목표는 현실 세계를 모방하는 것이 아닌 새로운 세계를 창조하는 것이다.

즉 단순히 현실 세계를 그대로 옮기는 것이 아니라, 고객과 사용자를 만족시킬 수 있는 소프트웨어를 개발하는 것이 목표인 것이다.

그럼 현실 세계의 모방이라는 애매모호한 개념을 왜 배우는 걸까?

이는 현실 세계에 대한 비유를 통해 객체지향의 다양한 측면을 이해하고 학습하는 데 도움을 주기 때문이다.

예시를 한 번 들어보자.

스스로 결정하는 현실 세계의 생명체가 있을 때, 이를 모방하여 생명체의 상태와 행위를 캡슐화(encapsulation) 하는 것은 객체의 자율성(autonomous) 를 설명하기 용이하다.

참고 객체의 자율성은 객체의 내부와 외부를 명확하게 구분하여 책임을 명시하고 외부에서의 간섭을 배제하는 것을 말한다.

사람들이 암묵적인 약속과 명시적인 계약을 기반으로 협력하며 목표를 달성해나가는 과정 또한

객체들이 서로 메시지(message) 를 주고 받으면 공동의 목표를 달성하기 위한 협력(collaboration) 관계를 설명하기 용이하다.

이처럼 현실 세계의 사물을 기반으로 소프트웨어 객체를 식별하고 구현까지 이어가는 개념은 객체지향 설계의 핵심인 완전연결성(seamlessness) 를 설명하는 데 적합하다.

참고 객체지향에서 완전연결성이란 소프트웨어의 구성을 하나의 지속적인 프로세스로 전환할 수 있는 성질 을 의미한다.

지금까지 객체지향에 대한 기본적인 내용을 다루기 위해 현실 세계의 모방이라는 개념에 대해서 살펴보았다.

이제부터 본격적으로 객체지향에 대해 알아보도록 하자.

1.3. 협력

전통적인 3-Tier Architecture를 생각해보자.

3-Tier Architecture는 사용자와 직접적으로 상호작용하는 클라이언트, 클라이언트의 요청을 수행할 서버, 수행에 필요한 데이터를 가진 데이터베이스 로 구성된 패턴을 의미한다.

sequenceDiagram

participant C as Client
participant S as Server
participant D as Database

C ->> S : request
S ->> D : request

물론 클라이언트 혼자 독자적을 처리할 수 있는 요청도 있겠지만,

일반적으로는 위 그림처럼 클라이언트는 요청(request)을 처리하기 위해 서버와 데이터베이스의 힘을 빌려야한다.

sequenceDiagram

participant C as Client
participant S as Server
participant D as Database

C ->> S : request
S ->> D : request
D ->> S : response
S ->> C : response

그리고 서버와 데이터베이스는 요청에 순차적으로 응답(response)해주어 클라이언트까지 전달된다.

이처럼 요청과 응답을 통해 협력(collaboration) 하는 것은 거대하고 복잡한 문제를 해결할 수 있는 초석이 된다.

1.4. 역할

위에서 언급했던 3-Tier Architecture를 다시 가져와서 클라이언트, 서버, 데이터베이스의 이름을 제거해보자.

sequenceDiagram

participant Component1
participant Component2
participant Component3

당연하겠지만 이름이 없으면 각 컴포넌트가 어떤 역할을 하는지 알 수가 없다.

이름을 다시 부여해보자.

sequenceDiagram

participant C as Client
participant S as Server
participant D as Database

이제 이름을 통해 컴포넌트의 대략적인 역할 을 추측할 수 있게 되었다.

이처럼 특정한 역할은 특정한 책임을 지니고 있음을 맡은 역할을 통해 협력에 참여하며 적절한 책임을 수행함을 의미한다.

역할에 대한 주요 개념들을 정의해보자.

1. 여러 객체가 동일한 역할을 수행할 수 있다
클라이언트 입장에서 요청을 한다고 가정해보자.

적절한 요청이라면 수많은 서버 중 어떤 서버가 해당 요청을 수행할지는 신경쓰지 않아도 된다.

서버는 그저 요청에 대한 응답만 책임져주면 된다.

2. 역할은 대체 가능성을 나타낸다
어떠한 역할이 명백하게 정의되어있고, 이에 대한 책임을 수행할 수 있다면 어떤 객체가 해당 역할을 하더라도 문제가 되지 않는다.

3. 책임의 수행을 자율적으로 수행한다
동일한 요청에 대해 어떻게 수행할지는 자율적으로 수행할 수 있다.

1부터 10까지 더해달라는 요청에 대해 어떤 객체는 1부터 10까지 순차적으로 더한 뒤 응답해줄 수도 있고,

가우스 공식을 활용해 응답해줄 수도 있다.

이처럼 동일한 요청에 대해 서로 다른 방식으로 응답할 수 있는 특징을 객체의 다형성(polymorphism)이라고 한다.

4. 하나의 객체가 여러 역할을 수행할 수 있다
하나의 객체가 꼭 하나의 역할만을 수행할 필요는 없다.

아래 그림을 보자.

sequenceDiagram

participant C as Client
participant SA as Server A
participant SB as Server B

C ->> SA : request
SA ->> SB : request
SB ->> SA : response
SA ->> C : response

클라이언트의 요청에 대해 두 개의 서버가 각자 필요한 동작을 수행한다고 가정해보면

Server A는 요청을 받는 역할도, 요청을 하는 역할도 동시에 수행중임을 알 수 있다.

1.5. 책임

위에서 역할에 대해서 설명하면서 책임이라는 단어가 등장하였다.

이렇듯 특정한 역할은 특정한 책임을 암시하며, 특정한 역할을 위해 적합한 책임을 수행하게 된다.

이 책임을 객체지향입장에서 풀이해본다면 객체가 수행하는 행동 이라고 볼 수 있다.

책임이란 객체에 정의되는 잉집도 있는 행위의 집합이며, 객체가 유지해야하는 정보와 수행할 수 있는 행동에 대한 서술이라고 볼 수 있다.

객체의 책임은 크게 두 가지로 분류할 수 있다.

1. 무엇을 알고 있는가?

  • 사적인 정보에 관한 것
  • 관련된 객체에 관한 것
  • 자신이 유도하거나 계산할 수 있는 것

2. 무엇을 할 수 있는가?

  • 객체의 생성
  • 계산 수행
  • 다른 객체의 행동을 시작
  • 다른 객체의 활동을 제어하고 조절

1.6. 객체의 세계와 객체지향 설계

어떤 객체도 섬이 아니다. (No object is an island) - A Laboratory For Teaching Object-Oriented Thinking(1989) link

객체의 세계는 인간의 세계와 유사하다.

객체 공동체 안에 존재하는 수많은 객체들은 각자에게 주어진 역할책임 을 다하는 동시에 시스템의 더 큰 목적을 이루기 위해 다른 객체와도 적극적으로 협력 한다.

애플리케이션의 목적이 결국 사용자의 요청을 적절하게 수행하여 응답을 해준다는 것을 상기해보면 객체들은 협력을 통해 애플리케이션의 기능을 구현하고 있음을 추측할 수 있다.

애플리케이션의 기능은 더 작은 책임으로 분할되고, 이 책임을 적절하게 수행할 수 있는 역할을 가진 객체에 의해 수행된다.

또한 객체는 자신의 책임을 수행하는 도중에 다른 객체와 협력하기도할 것이다.

결론적으로 기능이라는 것은 객체 간의 연쇄적인 요청과 응답의 흐름으로 구성된 협력으로 구현되는 것이다.

객체지향 설계라는 것은 적절한 책임을 적절한 객체에게 할당하는 것부터 시작된다.

여기서 책임은 객체지향 설계의 품질을 결정짓는 가장 중요한 요소로, 책임이 불분명한 객체는 애플리케이션의 미래 역시 불분명하게 만든다.

즉 얼마나 적절한 책임을 객체에 할당하느냐가 애플리케이션의 완성도를 결정한다.

1.7. 단일 책임 원칙과 God Object

상술했듯 객체지향 애플리케이션의 윤곽은 역할, 책임, 협력을 통해 그려진다.

이 중 실제 협력에 참여하는 주체는 객체(object) 로 객체지향이라는 이름에서 느껴지듯이 이 객체야말로 객체지향의 중심이다.

객체는 애플리케이션의 기능을 구현하기 위해 존재하며, 우리가 생각하기에 아주 작은 기능조차 하나의 객체가 책임지기에는 그 복잡도가 높고 거대하다.

따라서 아주 작은 기능이더라도 여러 객체가 서로 협력하여 구현하게 되며, 이 협력의 품질을 객체의 품질이 결정한다.

여기서 기능을 동작시키기 위한 책임들을 나열하고 하나의 객체가 하나의 책임을 가지도록 하며, 객체가 수행하는 모든 것은 이 하나의 책임을 위해 존재하도록 설계해야 한다.

참고 이를 우리는 단일 책임 원칙(Single Responsibility Principle) 이라고 부른다.

정리해보자면

하나의 기능을 위해서 여러 객체가 필요하고, 각 객체는 하나의 책임만을 성실히 수행하는 것이 이상적이라고 볼 수 있다.

이 객체는 크게 두 가지 속성을 지녀야 한다.

첫 번째, 객체는 협력성(cooperation) 을 가져야 한다.

객체는 다른 객체의 요청에 반응하여 적극적으로 도움을 주어야 한다.

두 번째, 객체는 자율성(autonomy) 을 가져야 한다.

다른 객체에 요청에 어떻게 응답할지는 스스로의 원칙에 따라 판단하고 결정해야 한다.

위 두 가지 속성없이 비협조적이고 독단적으로 행동한다면 God Object로 분류될 수 있을 것이다.

참고 God Object란?
객체 혼자 모든 것을 처리하고 판단하는 것을 God Object라고 부른다.
이러한 객체는 당연히 존재할 수 없기에 객체지향에서는 Anti pattern 이며, 정적 분석에서는 code smell 이다.

1.8. 상태 그리고 행위

흔히 객체는 상태(state) 를 가지고 행위(behavior) 한다고 표현한다.

이는 객체가 다른 객체의 요청에 응하여 협력하기 위해선 어떠한 행위을 해야하고, 이 행위에 필요한 상태를 가지고 있어야함을 뜻한다.

상태는 객체의 자율성을 보장하기 위해 판단 기준이 된다.

객체의 자율성은 객체의 내부와 외부를 명확하게 구분하는 것으로부터 야기되며, 객체의 사적인 영역(private)은 외부에서 차단해야 하고

객체의 외부에서는 허락된 수단(public)을 통해서만 의사소통을 수행하도록 디자인해야 한다.

즉 객체는 다른 객체가 무엇을 수행하는 지는 알지만, 어떻게 수행하는지는 알 수 없어야 한다.

1.9. 메시지와 메서드 그리고 캡슐화

객체는 협력을 위해 다른 객체에 요청을 한다.

이때 요청을 위한 수단을 메시지(message) 라 부르며, 요청하는 객체를 송신자(sender), 요청받는 객체를 수신자(receiver 라고 한다.

수신자는 메시지를 이해할 수 있는 판단한 후, 이를 처리하는 데 이 처리를 메서드(method) 라고 부른다.

객체지향 프로그래밍 언어에서 메서드는 클래스 내부에 포함된 함수를 통해 구현되며, 수신된 메시지에 대응하는 시그니쳐를 가진 메서드가 실행된다.

이처럼 외부의 요청을 명세하는 메시지와, 요청을 처리하기 위한 방법인 메서드를 분리하는 것은 객체의 자율성을 높여주는 핵심 매커니즘으로

캡슐화(encapsulation) 개념과 깊은 관련이 있다.

1.10. 객체지향의 본질

지금까지 설명한 내용을 종합해서 객체지향이란 무엇인지 정리해보자.

  • 객체지향이란 시스템을 상호작용하는 자율적인 객체들의 공동체 의 관점으로 바라보며, 객체를 이용해 시스템을 분할하는 방법이다.
  • 자율적인 객체란 상태행위 를 함께 지니며, 스스로 책임지는 객체를 의미한다.
  • 객체는 시스템의 기능을 구현하기 위해 다른 객체와 협력하며, 각 객체는 협력 을 위해 각자 정해진 역할책임 을 가지고 있다.
  • 객체는 메시지 를 이용해 다른 객체와 협력하며, 메시지를 처리하기 적합한 메서드 를 자율적으로 선택한다.

1.11. 마무리

우리는 객체지향하면 클래스를 제일 먼저 떠올린다.

클래스는 객체지향 프로그래밍 언어에서 매우 중요한 구성요소임에는 분명하지만, 중심 개념이라고 하기엔 부적합하다.

지나치게 클래스를 강조하는 프로그래밍 언어적인 관점은 오히려 객체의 캡슐화를 저해하고 클래스간의 강한 커플링을 야기한다.

따라서 객체지향을 위해 우리가 지향해야하는 관점은 코드를 담는 클래스의 관점에서, 메시지를 주고 받는 객체의 관점이 되어야한다.

클래스를 단지 객체들의 협력 관계를 코드로 표현한 도구에 불과한 것이다.

그렇다면 객체지향 프로그래밍의 중심 개념은 무엇일까?

진짜 중심 개념은 적절한 책임을 수행하는 역할 간의 유연하고 견고한 협력 관계를 구축하는 것에 있다.

객체지향의 중심엔 클래스가 아닌 객체가 위치하며, 클래스간의 정적인 관계가 아니라 메시지를 주고받는 동적인 관계가 더 중요하다.