
[객체지향의 사실과 오해] 라는 책의 내용을 내가 이해한대로 정리해 보려고 한다.
좋은 라인전이 완성되기 까지의 과정을 상상해보면
대충 위의 과정을 생각해볼 수 있다. 이 과정은 역할이 다른 세명의 사람이 각자에게 부여된 책임을 수행하는 협력의 과정이다.
여기까지 이해했다면 이 책을 한줄로 정리할 수 있다.
객체지향의 핵심은 객체가 협력 안에서 어떤 책임과 역할을 수행할 것인지를 결정하는 것이다.
객체 지향은 말그래도 객체를 지향한다. 객체의 객체에 의한 객체를 위한 방법론이 객체 지향이다. 객체 지향은 class가 아닌 객체를 지향한다. 반드시 기억해야한다.
객체란 식별 가능한 개체 또는 사물이다. 객체는 챔피언처럼 만질 수 있는 구체적인 사물일 수도 있고, 턴처럼 추상적인 개념일 수도 있다. 객체는 구별 가능한 식별자, 특징적인 행동, 변경 가능한 상태를 가진다. 소프트웨어 안에서 객체는 저장된 상태와 실행 가능한 코드를 통해 구현된다.
한마디로 정리하면 식별자, 행동, 상태의 삼위일체가 바로 객체라는 것이다. 그렇다면 이들 각각은 무엇일까? 책에서는 이들에 대해서 이렇게 설명한다.
상태는 특정 시점에 객체가 가지고 있는 정보의 집합으로 객체의 구조적 특징을 표현한다. 객체의 상태는 객체에 존재하는 정적인 프로퍼티와 동적인 프로퍼티 값으로 구성된다. 객체의 프로퍼티는 단순한 값과 다른 객체를 참조하는 링크로 구분할 수 있다.
행동은 외부의 요청 또는 수신한 메시지에 응답하기 위해 동작하고 반응하는 활동이다. 행동의 결과로 객체는 자신의 상태를 변경하거나 다른 객체에게 메시지를 전달할 수 있다. 객체는 행동을 통해 다른 객체와의 협력에 참여하므로 행동은 외부에 가시적이어야 한다.
식별자란 어떤 객체를 다른 객체와 구분하는 데 사용하는 객체의 프로퍼티다. 값은 식별자를 가지지 않기 때문에 상태를 이용한 동등성 검사를 통해 두 인스턴스를 비교해야 한다. 객체는 상태가 변경될 수 있기 때문에 식별자를 이용한 동일성 검사를 통해 두 인스턴스를 비교할 수 있다.
한마디로 정리하면
상태는 정보 집합이고,
행동은 다른 객체와 협력하기 위한 활동이며,
식별자는 객체의 고유성을 위한 프로퍼티다.
"내가 킬을 먹으면 승급 성공"라는 한줄의 문장에서 우리는 객체의 행동은 상태를 변경시킨다는 것을 알 수 있다. 킬을 먹으면 내 티어가 바뀌기 때문이다. 그리고 행동은 상태에 영향을 받음을 알 수 있다. 승급전이 아니라면 승급을 할 수 없기 때문이다.
객체를 구성하는 상태, 행동, 식별자에 대한 개념을 알았다면 이제 객체를 더 깊게 이해해보자
리그오브레전드, 객체지향 세계 모두 협력이 필수적이다.
우리는 정글을 했으면 적절한 타이밍에 알아서 갱을 오고, 갱킹 루트 정도는 알아서 짜는 정글러와 게임을 하고 싶다. 그렇지 않은가? 객체도 마찬가지다.
우리는 모든 객체를 한명의 사람으로 바라봐야 한다.
객체를 생명을 가진 한명의 사람으로 바라보는 것이 좋은 객체를 만드는 첫걸음이다. 앞서 좋은 객체는 같이 일하고 싶은 사람이라고 하면서 협력의 중요성을 계속해서 강조했다.
한타를 한다고 생각해보자. 필요한 객체들을 떠올려보면 탑 객체, 정글 객체, 미드 객체, 원딜 객체, 서폿 객체가 있다. 이들은 한타라는 목표를 위해 서로 협력한다. 더 정확히 말하면 협력을 위해서 존재한다. 협력이 전제되지 않았다면 이러한 객체들의 분류는 아무런 필요가 없기 때문이다. 이처럼 객체는 다른 객체화 협력하기 위해서 존재한다.
서폿 객체는 한타를 위해 원딜 객체가 포탑 골드 겸상 금지라는 요청에 응답한다. 응답한다는 말에서 우리는 자연스럽게 서폿 객체에 생명을 불어넣었다. 현실에서의 서포터는 추상적인 개념이다. 서포터는 서포터라는 직업일 뿐 우리에게 응답을 주지 않는다. 서폿 객체는 소환사의 협곡에서 태어나고, cs를 양보하고, 대신 죽는다. 바로 이것에 우리는 주목해야 한다. 객체지향의 세계는 현실에 대한 모방이 아닌 새로운 세계이기 때문이다. 객체지향이라는 세계에서 각 객체들은 삶을 영위한다.
결국 하고 싶은 말은 객체를 한 명의 자율적이고 협력 가능한 사람으로 바라보는 것이 더 좋은 객체를 만들기 위한 첫걸음이라는 것이다.
언제나 행동이 우선이다.
앞서 객체를 한명의 사람으로 보자는 이야기를 했다. 객체는 다른 객체와 협력하기 위해 존재한다. 객체의 행동은 객체가 협력에 참여하는 유일한 방법이다. 우리가 어플리케이션 안에서 어떤 행동을 원하느냐가 어떤 객체가 적합한지를 결정하기 때문에 객체지향 설계는 어플리케이션에 필요한 협력을 생각하고 협력에 참여하는 데 핗요한 행동을 생각한 후 행동을 수행한 객체를 선택하는 방식으로 수행해야 한다.
객체가 어떤 행동을 하느냐에 따라 객체의 종류, 즉 타입이 결정된다. 어떤 객체들이 동일하게 행동한다면 객체는 동일한 타입에 속한다.
이즈리얼과 자야는 다르지만 원딜이라는 개념으로 분류할 수 있고, 유미와 룰루는 다르지만 서포터라는 개념으로 분류할 수 있다. 이 개념을 객체지향 세계에서 타입이라 부른다.
책에서는 추상화, 행동에 따른분류, 개념, 타입으로 여러 예시와 함께 논리를 전개하지만 쓰다보니 책 내용을 옮겨 적는 느낌이 들어서 생략하였다.
다시 돌아와서 객체의 타입을 결정하는 것은 객체의 행동뿐이다. 객체가 어떤 데이터를 갖고 있는지는 타입을 결정하는 데 아무런 영향도 미치지 않는다. 당연하지 않은가? 이즈리얼과 자야하는 포지셔닝, 좋아하는 서폿이 제각각이지만 둘다 원딜이기 때문이다.
행동에 따라 객체를 분류하기 위해서는 객체가 내부적으로 관리하는 데이터가 아닌, 객체가 외부에 제공해야 하는 행동을 먼저 생각해야 한다. 이를 위해서는 객체가 외부에 제공하는 행동, 즉 책임을 먼저 결정하고 그 책임을 수행하는 데 적합한 데이터를 나중에 결정한 후 데이터를 책임을 수행하는 데 필요한 외부 인터페이스 뒤로 캡슐화해야 한다. 이러한 방법론을 책임 주도 설계라 부른다.
같은 타입에 속한 객체는 동일한 행동을 하지만 행동을 수행하는 방식은 서로 다를 수 있다. 서로의 데이터 표현 방식이 다를 수 있기 때문이다. 이것을 동일한 요청에 대해 서로 다른 방식으로 응답할 수 있는 능력인 다형성과 연관지을 수 있다.
결국 하고자 하는 말은 이것이다. 객체를 창조할 때 가장 중요하게 고려해야 하는 것은 객체가 이웃하는 개체와 협력하기 위해 어떤 행동을 해야 할지를 결정하는 것이다. 즉, 객체가 협력을 위해 어떤 책임을 지녀야 하는지를 결정하는 것이 객체지향 설계의 핵심이라는 것이다.
리그오브레전드는 팀게임이다.
앞에서 언제나 행동이 우선이라고 강조했다. 그러나 그 행동은 협력이라는 문맥을 반드시 고려해야 한다. 바로 앞에서 강조했듯 객체가 협력을 위해 어떤 책임(행동)을 지녀야 하는지를 결정하는 것이 객체지향 설계의 핵심이기 때문이다.
리그오브레전드와 객체지향 세계 모두에서 협력은 수많은 요청과 응답으로 이루어져 있다. 요청과 응답은 협력에 참여하는 객체가 수행할 책임을 정의한다. 어떤 객체가 요청에 대해 대답해줄 수 있거나, 적절한 행동을 할 의무가 있는 경우 해당 객체가 책임을 가진다고 말한다.
정글은 '갱와라'라는 요청에 응답해야 하므로 '갱을 갈'책임을 지게 되는 것처럼 말이다.
이러한 협력 안에서 객체는 다른 객체로부터 요청이 전송됐을 경우에만 자신에게 주어진 책임을 수행한다. 객체지향 세계에서 요청은 메시지를 통해 전송된다. 메시지는 협력에 참여하는 두 객체 사이의 관계를 강조한 것이다. 메시지는 협력을 위해 한 객체가 다른 객체로 접근할 수 있는 유일한 방법이다.
객체지향 설계는 협력에 참여하기 위해 어떤 객체가 어떤 책임을 수행해야 하고 어떤 객체로부터 메시지를 수신할 것인지를 결정하는 것으로 부터 시작된다. 어떤 클래스가 필요하고 어떤 메서드를 포함해야 하는지를 결정하는 것은 책임과 메시지에 대한 대략적인 윤곽을 잡은 후에 시작해도 늦지 않다.
'cs를 양보한다'와 '포탑골드를 먹지 않는다'라는 책임을 가진다는 것은 무엇을 의미하는가? 그것은 라인전이라는 협력에 참여하기 위해 유미는 '서포터'라는 역할을 수행하고 있고 케이틀린은 '원딜'이라는 역할을 수행하고 있음을 의미한다.
결론적으로 어떤 객체가 수행하는 책임의 집합은 객체가 협력안에서 수행하는 역할을 암시한다. 이 역할은 유미가 수행하든, 룰루가 수행하든 해당 역할을 수행할 수 있는 어떤 객체라도 대신할 수 있다. 역할을 이용해 협력을 추상화했기 때문에 '유미'나 '룰루'의 역할을 수행할 수 있는 어떤 객체라도 협력에 참여할 수 있게 되었다.
그렇다고 '그레이브즈'가 서포터의 역할을 대체할 수 있는 것은 아니다. '유미'와 '룰루'를 서포터라 부를 수 있는 이유는 'cs를 양보한다.'와 '포탑골드를 먹지 않는다.'라는 메시지를 이해할 수 있기 때문이다. 따라서 역할을 대체할 수 있는 객체는 동일한 메시지를 이해할 수 있는 객체로 한정된다.
결국 동일한 역할을 수행할 수 있다는 것은 해당 객체들이 협력 내에서 동일한 책임의 집합을 수행할 수 있다는 것을 의미한다. 즉, 행동이 호환된다면 객체가 역할을 대체할 수 있다. 어떤 객체가 서포터라는 역할을 대체할 수 있는 이유는 그 객체가 cs를 양보할 수 있고 포탑골드를 겸상하지 않을 수 있기 때문이다. 따라서 본질적으로 역할은 다른 객체에 의해 대체 가능함을 의미한다.
결국 하고 싶은 말은 언제나 협력 속에서 책임과 역할을 정의하라는 것이다.
객체가 존재하는 이유는 행동을 통해 협력에 참여하기 위해서다. 따라서 중요한 것은 객체의 행동, 즉 책임이다.
누군가 나에게 객체지향에 대해서 설명해달라고 했을 때 가장 먼저 나오는 말은 클래스였다. '객체지향은 클래스와 클래스 간의 관계를 표현하는 시스템이다.'가 내 설명의 요약이였다. 하지만 객체 지향에서 중요한 것은 클래스가 아닌, 협력에 참여하는 동적인 객체이며, 클래스는 단지 시스템에 필요한 객체를 표현하고 생성하기 위해 프로그래밍 언어가 제공하는 구현 메커니즘이다.
내 설명이 클래스로 시작했던 것은 협력이라는 문맥을 고려하지 않고 각 객체를 독립적으로 바라보기 때문이었다. '저격수'의 인스턴스를 모델링할 경우 대부분은 긴 총을 들고 있고, 위장에 뛰어나며, 얼굴에 위장 크림을 바르고 있는 모습을 떠올릴 것이다. 그러고는 머릿속에 떠오른 저격수의 모습을 기반으로 클래스를 개발하기 시작할 것이다.
하지만 이 모습은 리그오브레전드에서 저격수 케이틀린과는 어울리지 않는다. 중요한 것은 저격수의 겉모습이 아니다. 리그오브레전드에서 케이틀린이 중요한 이유는 승리라는 협력에 '원딜러'라는 역할로 참여해서 캐리라는 책임을 수행할 수 있기 때문이다.
그렇다면 올바른 객체를 설계하기 위해서는 어떻게 해야 하는가? 먼저 견고하고 깔끔한 협력을 설계해야 한다. 협력을 설계한다는 것은 설계에 참여하는 객체들이 주고받을 요청과 응답의 흐름을 결정한다는 것을 의미한다. 이렇게 결정된 요청과 응답의 흐름은 객체가 협력에 참여하기 위해 수행될 책임이 된다.
일단 객체에게 책임을 할당하고 나면 책임은 객체가 외부에 제공하게 될 행동이 된다. 협력이라는 문맥에서 객체가 수행하게 될 적절한 책임, 즉 행동을 결정한 후에 그 행동을 수행하는 데 필요한 데이터를 고민해야 한다. 그리고 객체가 협력에 참여하기 위해 필요한 데이터와 행동이 어느 정도 결정된 후에 클래스의 구현 방법을 결정해야 한다. 결과적으로 클래스와 데이터는 협력과 책임의 집합이 결정된 후에야 무대 위에 등장할 수 있다.
리그오브레전드에서 누군가는 앞라인에 서야 하고 누군가는 뒤에서 딜을 해야하며 누군가는 딜하는 그를 지켜야 한다.
이처럼 협력을 구성하는 데 필요한 일련의 책임을 먼저 고안하고 나면 그 책임을 수행하는 데 필요한 객체를 선택하게 된다. 어떤 책임은 오른에게, 어떤 책임은 케이틀린에게, 어떤 책임은 룰루에게 할당하면서 책임을 각 객체에게 할당해 나간다.
그리고 이렇게 할당된 책임은 오른, 케이틀린, 룰루라는 객체들이 외부에 제공하게 될 행동을 정의하게 된다.
이제 행동이 결정됐으니 각 객체가 필요로 하는 데이터를 정의할 수 있다. 그리고 이렇게 데이터와 행동이 결정된 후에야 오른, 케이틀린, 룰루를 구현하는 클래스를 개발할 수 있을 것이다.
객체지향 시스템에서 가장 중요한 것은 충분히 자율적인 동시에 충분히 협력적인 객체를 창조하는 것이다. 이 목표를 달성할 수 있는 가장 쉬운 방법은 객체를 충분히 협력적으로 만든 후에 협력이라는 문맥 안에서 객체를 충분히 자율적으로 만드는 것이다.
이 글은 제게 많은 도움이 되었습니다.