오브젝트


객체지향의 사실과 오해 도서 1독 이후 개념적으로나마 객체지향 설계에 대한 맥락을 짚어보았다.

파편화된 개념들을 이제 코드를 예제로 학습하기 위해 동일 저자 조영호 님의 두 번째 객체지향 도서 오브젝트 를 선택했다.

챕터 1. 객체, 설계 시작해보자.

실무가 먼저다.

로버트 L. 글래스는 <소프트웨어 크리에이티비티 2.0> 에서 ‘이론 대 실무’라는 흥미로운 주제에 관한 개인적인 견해를 밝히고 있다. 글래스가 그 글에서 우리이게 던진 질문을 한마디로 요약하면 다음과 같다. “이론이 먼저일까 실무가 먼저일까?”

대부분의 사람들은 이론이 먼저 정립된 후에 실무가 그 뒤를 따라 발전한다고 생각한다. 글래스는 그 반대라고 주장한다. 글래스에 따르면 어떤 분야를 막론하고 이론을 정립할 수 없는 초기에는 실무가 먼저 급속한 발전을 이룬다고 한다.

소프트웨어 개발에서 실무가 이론보다 앞서 있는 대표적인 분야로 소프트웨어 설계소프트웨어 유지보수 를 들 수 있다. 대부분의 설계 원칙과 개념은 이론에서 출발해서 실무에 스며들었다기 보다는 실무에서 반복적으로 적용되던 기법들을 이론화 한 것들이 대부분이라는 것이다.

결론적으로 소프트웨어 설계와 유지보수에 중점을 두려면 이론이 아닌 실무에 초점 을 맞추는 것이 효과적이다.

그러니 우리는 이론과 개념은 잠시 뒤로 미루고 간단한 프로그램을 하나 살펴보는 것으로 시작하자.


01 티켓 판매 어플리케이션 구현하기

상황

어느 소극장은 홍보도 할 겸 관람객들의 발길이 이어지도록 작은 이벤트를 기획하기로 했다.
이벤트의 내용은 간단한데 추첨을 통해 선정된 관람객에게 공연을 무료로 관람할 수 있는 초대장을 발송하는 것이다.

공연 당일, 관람객들이 입장하고 있다.
당연한 이야기겠지만 이벤트에 당첨된 관람객과 그렇지 못한 관람객은 다른 방식으로 입장시켜야 한다.

이벤트에 당첨된 관람객은 초대장을 티켓으로 교환한 후에 입장할 수 있다.
이벤트에 당첨되지 않은 관람객은 티켓을 구매해야만 입장할 수 있다.

따라서 관람객을 입장시키기 전에 이벤트 당첨 여부를 확인해야 하고 이벤트 당처맞가 아닌 경우에는 티켓을 판매한 후에 입장시켜야 한다.

위의 코드는 정상적으로 작동한다.

코드의 흐름을 살펴보자.

  1. 공연장 은 먼저 관람객가방 안에 초대장 이 들어있는지 확인한다.

  2. 만약 초대장 이 들어 있다면 이벤트에 당첨된 관람객이므로 판매원 에게서 받은 티켓관람객가방 안에 넣어준다.

  3. 가방 안에 초대장 이 없다면 티켓 을 판매해야 한다. 이 경우 공연장관람객가방 에서 티켓 금액만큼을 차감한 후에 매표소 에 금액을 증가시킨다.

  4. 마지막으로 공연장관람객가방 안에 티켓 을 넣어줌으로써 관람객 의 입장 절차를 끝낸다.

어떤가? 작성된 프로그램의 로직은 간단하고 예상대로 동작한다. 하지만 안타깝게도 이 작은 프로그램은 몇 가지 문제점을 가지고 있다.


02. 무엇이 문제인가.

모든 소프트웨어 모듈에는 세 가지 목적이 있다.

첫 번째 : 실행중에 제대로 동작 하는 것이다. 이것은 모듈의 존재 이유라고 할 수 있다.

두 번째 : 소프트웨어 모듈은 변경을 위해 존재한다. 대부분의 모듈은 생명주기 동안 변경되기 때문에 간단한 작업만으로도 변경이 가능해야 한다. 변경하기 어려운 모듈은 제대로 동작하더라도 개선해야 한다.

세 번째 : 모듈의 세 번째 목적은 코드를 읽는 사람과 의사소통 하는 것이다. 모듈은 특별한 훈련 없이도 개발자가 쉽게 읽고 이해할 수 있어야 한다. 읽는 사람과 의사소통 할 수 없는 모듈은 개선해야 한다.

위 내용에 따르면 모듈은 쉽게 실행 돼야 하고 변경이 용이 해야 하며, 이해하기 쉬워야 한다. 앞에서 작성한 프로그램은 관람객을 입장시키는 데 필요한 기능을 오류없이 정확하게 수행한다. 따라서 제대로 동작해야 한다는 제약은 만족시킨다.

하지만 불행하게도 변경 용이성읽는 사람과의 의사소통 이라는 목적은 만족시키지 못한다. 그 이유를 살펴보자.

예상을 빗나가는 코드

마지막에 소개한 Theater 클래스의 etner() 메서드가 수행하는 일을 말로 풀어보자.

소극장은 관람객의 가방을 열어 그 안에 초대장이 들어있는지 살펴본다.

가방 안에 초대장이 들어있으면 판매원은 매표소에 보관돼 있는 티켓을 관람객의 가방 안으로 옮긴다.

가방 안에 초대장이 들어 있지 않다면 관람객의 가방에서 티켓 금액 만큼의 현금을 꺼내 매표소에 적립한 후에 매표소에 보관돼 있는 티켓을 관람객의 가방 안으로 옮긴다.

무엇이 문제인지 눈치 챘다. 문제는 관람객과 판매원이 소극장의 통제를 받는 수동적인 존재 라는 점이다.

관람객 입장에서 소극장이라는 제3자가 초대장을 확인하기 위해 관람객의 가방을 마음대로 열어본다? 판매원의 입장에서 소극장이 판매원의 허락도 없이 매표소에 보관 중인 티켓과 현금에 마음대로 접근한다?

확실히 뭔가 이상하다.

이해 가능한 코드란 그 동작이 우리의 예상에서 크게 벗어나지 않는 코드다. 안타깝게도 앞에서 살펴본 예제는 우리의 예상을 빗나간다.

현실에서는 관람객이 직접 자신의 가방에서 초대장을 꺼내 판매원에게 건넨다. 티켓을 구매하는 관람객은 가방 안에서 돈을 직접 꺼내 판매원에게 지불한다. 판매원은 매표소에 있는 티켓을 직접 꺼내 관람객에게 건네고 관람객에게서 직접 돈을 받아 매표소에 보관한다. 하지만 코드 안의 관람객, 판매원은 그렇게 하지 않는다. 현재의 코드는 우리의 상식과는 너무나도 다르게 동작하기 때문에 코드를 읽는 사람과 제대로 의사소통 하지 못한다

코드를 이해하기 어렵게 만드는 또 다른 이유가 있다. 이 코드를 이해하기 위해서는 여러가지 세부적인 내용 들을 한꺼번에 기억하고 있어야 한다는 점이다.

앞의 Theateretner() 메서드를 다시 살펴보자.


public void enter(Audience audience) {
    if (audience.getBag().hasInvitation()) {
       Ticket ticket = ticketSeller.getTicketOffice().getTicket();
       audience.getBag().setTicket(ticket);
    } else {
        Ticket ticket = ticketSeller.getTicketOffice().getTicket();
        audience.getBag().minusAmount(ticket.getFee());
        ticketSeller.getTicketOffice().plusAmount(ticket.getFee());
        audience.getBag().setTicket(ticket);
    }
}

제3자가 위 코드를 보고 이해하기 위해서는 AudienceBag 을 가지고 있고, Bag 안에는 현금과 티켓이 들어있으며 TicketSellerTicketOffice 에서 티켓을 판매하고, TicketOffice 안에 돈과 티켓이 보관돼 있다는 모든 사실을 동시에 기억하고 있어야 한다.

이 코드는 하나의 클래스나 메서드에서 너무 많은 세부사항 을 다루기 때문에 코드를 작성하는 사람 뿐 아니라 코드를 읽고 이해해야 하는 사람 모두에게 큰 부담을 준다.

하지만 가장 심각한 문제는 이것이 아니다. 그것은 AudienceTicketSeller 를 변경할 경우 Theater 도 함께 변경해야 한다는 사실이다.


변경에 취약한 코드

그렇다. 더 큰 문제는 변경에 취약 하다는 것이다. 이 코드는 관람객이 현금과 초대장을 보관하기 위해 항상 가방을 들고 다닌다고 가정한다. 또한 판매원이 매표소에서만 티켓을 판매한다고 가정한다. 관람객이 가방을 들고 있지 않다면 어떻게 해야 할까? 관람객이 현금이 아니라 신용카드를 이용해서 결제해야 한다면 어떻게 해야 할까? 판매원이 매표소 밖에서 티켓을 판매해야 한다면 어떻게 해야 할까? 이런 가정이 꺠지는 순간 모든 코드가 일시에 흔들리게 된다.

관람객이 가방을 들고 있다는 가정이 바뀌었다고 상상해 보자 Audience 클래스에서 Bag 을 제거해야 할 뿐만 아니라 AudienceBag 에 직접 접근하는 Theateretner 메서드 역시 수정해야 한다.

Theater 는 관람객이 가방을 들고 있고 판매원이 매표소에서만 티켓을 판매한다는 지나치게 세부적인 사실에 의존해서 동작한다. 이러한 세부적인 사실 중 한 가지라도 바뀌면 해당 클래스 뿐만 아니라 이 클래스에 의존하는 Theater도 함꼐 변경 해야 한다. 이처럼 다른 클래스가 Audience 내부에 대해 더 많이 알면 알수록 Audience 를 변경하기 어려워진다.

이것은 객체 사이의 의존성(Dependency) 와 관련된 문제다. 문제는 의존성이 변경과 관련돼 있다는 점 이다.

의존성은 변경에 대한 영향을 암시 한다. 의존성이라는 말 속에는 어떤 객체가 변경 될 때 그 객체에게 의존하는 다른 객체도 함께 변경될 수 있다는 사실이 내포돼 있다.

그렇다고 해서 객체 사이의 의존성을 완전히 없애는 것이 정답은 아니다. 객체지향 설계는 서로 의존하면서 협력하는 객체들의 공동체를 구축하는 것이다. 따라서 우리의 목표는 애플리케이션의 기능을 구현하는데 필요한 최소한의 의존성만 유지하고 불필요한 의존성을 제거 하는 것이다.

객체 사이의 의존성이 과한 경우를 가리켜 결합도(coupling) 가 높다고 말한다. 반대로 객체들이 합리적인 수준으로 의존할 경우에는 결합도가 낮다고 말한다.

결합도는 의존성과 관련 돼 있기 때문에 결합도 역시 변경과 관련이 있다. 두 객체 사이의 결합도가 높을수록 함께 변경될 확률도 높아지기 때문에 변경하기 어려워진다. 따라서 설계의 목표는 객체 사이의 결합도를 낮춰 변경이 용이한 설계를 만드는 것이어야 한다.


03. 설계 개선하기

예제 코드는 세 가지 목적 중 한 가지는 만족시키지만 다른 두 조건은 만족시키지 못한다. 이 코드는 기능은 제대로 수행하지만 이해하기 어렵고 변경하기가 쉽지 않다.

여기서 변경과 의사소통이라는 문제가 서로 엮여있음 에 주목하라. 코드를 이해하기 어려운 이유는 Theater 가 관람객의 가방과 판매원의 매표소에 직접 접근하기 때문이다. 이것은 관람객과 판매원이 자신의 일을 스스로 처리해야 한다 는 우리의 직관을 벗어난다. 다시 말해서 의도를 정확하게 의사소통 하지 못하기 때문에 코드가 이해하기 어려워진 것이다.

Theater 가 관람객의 가방과 판매원의 매표소에 직접 접근한다는 것은 TheaterAudienceTicketSeller결합 다는 것을 의미한다. 따라서 AudienceTicketSeller 를 변경할 때 Theater 도 함꼐 변경해야 하기 때문에 전체적으로 코드를 변경하기도 어려워진다.

해결 방법은 간단하다. TheaterAudienceTicketSeller 에 관해 너무 세세한 부분까지 알지 못하도록 정보를 차단하면 된다. 사실 관람객이 가방을 가지고 있다는 사실과 판매원이 매표소에서 티켓을 파냄한다는 사실을 Theater 가 알아야 할 필요가 없다. Theater 가 원하는 것은 관람객이 소극장에 입장하는 것 뿐이다. 따라서 관람객이 스스로 가방 안의 현금과 초대장을 처리하고 판매원이 스스로 매표소의 티켓과 판매 요금을 다루게 한다면 이 모든 문제를 한 번에 해결할 수 있을 것이다.

다시말해서 관람객과 판매원을 자율적인 존재 로 만들면 되는 것이다.

자율성을 높이자.

설계를 변경하기 어려운 이유는 TheaterAudienceTicketSeller 뿐 아니라 Audience 소유의 BagTicketSeller 가 근무하는 TicketOffice 까지 마음대로 접근할 수 있기 때문이다.

해결 방법은 AudienceTicketSeller 가 직접 BagTicketOffice 를 처리하는 자율적인 존재 가 되도록 설계를 변경하는 것이다.

첫 번째 단계는 Theaterenter() 메서드에서 TicketOffice 에 접근하는 모든 코드를 TicketSeller 내부로 숨기는 것이다. TicketSellersellTo() 메서드를 추가하고 Theater 에 있던 로직을 이 메서드로 옮기자.

TicketSeller 클래스의 변경

TicketSeller 에서 getTicketOffice() 메서드가 제거 됐다는 사실에 주목하자. ticketOffice 의 가시성이 private 이고 접근 가능한 퍼블릭 메서드가 더 이상 존재하지 않기 때문에 외부에서는 ticketOffice 에 직접 접근할 수 없다. 결과적으로 ticketOffice 에 대한 접근은 오직 TicketSeller 안에만 존재하게 된다. 따라서 TicketSellerticketOffice 에서 티켓을 꺼내거나 판매 요금을 적립하는 일을 스스로 수행할 수 밖에 없다.

이처럼 개념적이나 물리적으로 객체 내부의 세부적인 사항을 감추는 것캡슐화(encapsulation) 이라고 부른다. 캡슐화의 목적은 변경하기 쉬운 객체를 만드는 것 이다. 캡슐화를 통해 객체 내부로의 접근을 제한 하면 객체와 객체 사이에 결합도 를 낮출 수 있게 때문에 설계를 좀 더 쉽게 변경할 수 있게 된다.

Theater 클래스의 변경

위와 같이 이제 Theaterenter() 메서드는 sellTo() 메서드를 호출하는 간단한 코드로 바뀐다. 수정된 Theater 클래스 어디에도 ticketOffice 에 접근하지 않는다는 사실에 주목하자. TheaterticketOfficeTicketSeller 내부에 존재한다는 사실을 알지 못한다. Theater 는 단지 TicketSellersellTo() 메시지를 이해하고 응답할 수 있다는 사실만 알고있을 뿐이다.

Theater 는 오직 TicketSeller인터페이스(Interface) 에만 의존한다. (TicketSeller의 sellTo() 인터페이스) TicketSeller 가 내부에 TicketOffice 인스턴스를 포함하고 있다는 사실은 구현(Implementation) 의 영역에 속한다.

객체를 인터페이스와 구현으로 나누고 인터페이스만을 공개 하는 것은 객체 사이의 결합도를 낮추고 변경하기 쉬운 코드를 작성하기 위해 따라야 하는 가장 기본적인 설계 원칙이다.

Theater의 결합도를 낮춘 설계

위 그림은 수정 후 클래스 사이의 의존성 을 나타낸 것이다. Theater 의 로직을 TicketSeller 로 이동시킨 결과, Theater 에서 TicketSeller 로의 의존성이 제거됐다는 사실을 알 수 있다. TicketOffice 와 협력하는 TicketSeller내부 구현이 성공적으로 캡슐화 된 것이다.



이제 다음으로 Audience 캡슐화를 개선하자. TicketSellerAudiencegetBag() 메서드를 호출해서 Audience 의 내부의 Bag 인스턴스에 직접 접근한다. Bag 인스턴스에 접근하는 객체가 Theater 에서 TicketSeller 로 바뀌었을 뿐 Audience 는 여전히 자율적인 존재가 아닌 것이다.

TicketSeller 와 동일한 방법으로 Audience 의 캡슐화를 개선할 수 있다. Bag 에 접근하는 모든 로직을 Audience 내부로 감추기 위해 Audiencebuy() 메서드를 추가하고 TicketSellersellTo 메서드에서 getBag 메서드에 접근하는 부분을 buy() 메서드로 옮기자.

Audience 클래스의 변경

buy() 메서드는 인자로 전달된 TicketBag 에 넣은 후 지불된 금액을 반환한다. 변경된 코드에서 Audience 는 자신의 가방 안에 초대장이 들어있는지를 스스로 확인 한다. 외부의 제 3자가 자신의 가방을 열어보도록 허용하지 않는다. AudienceBag 을 직접 처리하기 때문에 외부에서는 더이상 AudienceBag 을 소유하고 있다는 사실을 알 필요가 없다.

이제 Audience 클래스에서 getBag() 메서드를 제거할 수 있고 결과적으로 Bag 의 존재를 내부로 캡슐화 할 수 있게되었다.

TicketSeller 클래스의 변경

이제 TicketSellerAudience 의 인터페이스에만 의존하도록 수정되었다. 코드를 수정한 결과, TicketSellerAudience 사이의 결합도가 낮아졌다. 또한 내부 구현이 캡슐화 됐으므로 Audience 의 구현을 수정하더라도 TicketSeller 에는 영향을 미치지 않는다.

모든 수정이 끝났다. 캡슐화를 개선한 후에 가장 크게 달라진 점은 AudienceTicketSeller 가 내부 구현을 외부에 노출하지 않고 자신의 문제를 스스로 책임지고 해결 한다는 것이다. 다시 말해서 자율적인 존재 가 된 것이다.


무엇이 개선됐는가.

수정된 예제 역시 첫 번째 에제와 마찬가지로 관람객들을 입장시키는 데 필요한 기능을 오류 없이 수행 한다. 따라서 동작을 수행해야 한다는 첫 번째 목적을 만족시킨다. 그렇다면 변경 용이성과 의사소통은 어떨까?

수정된 AudienceTicketSeller 는 자신이 가지고 있는 소지품을 스스로 관리 한다. 이것은 우리의 예상과도 정확하게 일치한다. 따라서 코드를 읽는 사람과의 의사소통이라는 관점에서 이 코드는 확실히 개선된 것으로 보인다.

더 중요한 점은 AudienceTicketSeller 의 내부 구현을 변경하더라도 Theater 를 함께 변경할 필요가 없어졌다는 것이다.

Audience 가 가방이 아니라 작은 지갑을 소지하도록 코드를 변경하고 싶은가? Audience 내부만 변경하면 된다. TicketSeller 가 매표소가 아니라 은행에 돈을 보관하도록 만들고 싶은가? TicketSeller 내부만 변경하면 된다.

두 경우 모두 변경은 AudienceTicketSeller 내부만으로 제한된다. 따라서 수정된 코드는 변경 용이성 측면에서도 확실히 개선됐다고 말할 수 있다.


어떻게 한 것인가.

간단하다. 판매자가 티켓을 판매하기 위해 TicketOffice 를 사용하는 모든 부분을 TicketSeller 내부로 옮기고, 관람객이 티켓을 구매하기 위해 Bag 을 사용하는 모든 부분을 Audience 내부로 옮긴 것이다. 다시 말해 자기 자신의 문제를 스스로 해결하도록 코드를 변경 한 것이다. 우리는 우리의 직관을 따랐고 그 결과로 코드는 변경이 용이하고 이해 가능하도록 수정됐다.

수정하기 전의 코드와 수정한 후의 코드를 다시한번 비교해보자. 수정하기 전에는 TheaterAudienceTicketSeller 의 상세한 내부 구현까지 알고 있어야 했다. 따라서 TheaterAudienceTicketSeller 에 강하게 결합돼 있었고, 그 결과 AudienceTicketSeller 의 사소한 변경에도 Theater 가 영향을 받을 수 밖에 없었다.

수정한 후의 TheaterAudienceTicketSeller 의 내부에 직접 접근하지 않는다. AudienceBag 내부의 내용물을 확인하거나, 추가하거나, 제거하는 작업을 스스로 처리하며 외부의 누군가에게 자신의 가방을 열어보도록 허용하지 않는다.

TicketSeller 역시 매표소에 보관된 티켓을 직접 판매하도록 바뀌었다.

우리는 객체의 자율성을 높이는 방향 으로 설계를 개선했다. 그 결과, 이해하기 쉽고 유연한 설계를 얻을 수 있었다.


캡슐화와 응집도

핵심은 객체 내부의 상태를 캡슐화 하고 객체 안에 오직 메시지를 통해서만 상호작용 하도록 만드는 것이다. TheaterTicketSeller 의 내부에 대해서는 전혀 알지 못한다. 단지 TicketSellersellTo() 메시지를 이해하고 응답 할 수 있다는 사실만 알고 있을 뿐이다. TicketSeller 역시 Audience 의 내부에 대해서는 전혀 알지 못한다. 단지 Audiencebuy 메시지에 응답할 수 있고 자신이 원하는 결과를 반환할 것이라는 사실만 알고 있을 뿐이다.

밀접하게 연관된 작업만을 수행하고 연관성 없는 작업은 다른 객체에게 위임 하는 객체를 가리켜 응집도 가 높다고 말한다. 자신의 데이터를 스스로 처리하는 자율적인 객체 를 만들면 결합도를 낮출 수 있을 뿐더러 응집도를 높일 수 있다.

객체의 응집도를 높이기 위해서는 객체 스스로 자신의 데이터를 책임 져야 한다. 객체는 자신의 데이터를 스스로 처리 하는 자율적인 존재여야 한다. 그것이 객체의 응집도를 높이는 첫 걸음이다. 외부의 간섭을 최대한 배제 하고 메시지를 통해서만 협력 하는 자율적인 객체들의 공동체를 만드는 것이 훌륭한 객체지향 설계를 얻을 수 있는 지름길 인 것이다.


절차지향과 객체지향

수정하기 전의 코드에서는 Theaterenter() 메서드에서 AudienceTicketSeller 로 부터 BagTicketOffice 를 가져와 관람객을 입장시키는 절차를 구현했다. Audience / TicketSeller / Bag / TicketOffice 는 관람객을 입장시키는데 필요한 정보 를 제공하고 모든 처리는 Theaterenter() 메서드 안에 존재했었다는 점에 주목하자.

이 관점에서 Theaterenter() 메서드는 프로세스(Process) 이며 Audience / TicketSeller / Bag / TicketOffice데이터(Data) 이다. 이처럼 프로세스와 데이터를 별도의 모듈에 위치시키는 방식을 절차적 프로그래밍 이라고 부른다.

절차적 프로그래밍 방식으로 작성된 코드는 전형적인 의존성 구조 를 갖는다. 프로세스를 담당하는 TheaterAudience / TicketSeller / Bag / TicketOffice 모두에 의존하고 있음을 주목하자. 이것은 모든 처리가 하나의 클래스 안에 위치하고 나머지 클래스는 단지 데이터의 역할만 수행하기 때문이다.

이는 Audience / TicketSeller / Bag / TicketOffice 가운데 하나만 변경되더라도 Theater 역시 함께 변경해야 하는 취약점을 보여준다.

변경하기 쉬운 설계는 한 번에 하나의 클래스만 변경 할 수 있는 설계다. 절차적 프로그래밍은 프로세스가 필요한 모든 데이터에 의존해야 한다는 근본적인 문제점 때문에 변경에 취약할 수 밖에 없다.

해결 방법은 자신의 데이터를 스스로 처리 할 수 있도록 프로세스의 적절한 단계를 AudienceTicketSeller 로 이동시키는 것이다.

수정한 후의 코드에서는 데이터를 사용하는 프로세스가 데이터를 소유하고 있는 AudienceTicketSeller 내부로 옮겨졌다. 이처럼 데이터와 프로세스가 동일한 모듈 내부에 위치 하도록 프로그래밍 하는 방식을 객체 지향 프로그래밍 이라고 부른다.

훌륭한 객체지향 설계의 핵심은 캡슐화 를 이용해 의존성을 적절히 관리 함으로써 객체 사이의 결합도를 낮추는 것 이다. 예제 코드에서 살펴본 것 처럼 객체지향 코드는 자신의 문제를 스스로 처리해야 한다는 우리의 예상을 만족시켜 주기 때문에 이해하기 쉽고, 객체 내부의 변경이 객체 외부에 파급되지 않도록 제어 할 수 있기 때문에 변경하기가 수월하다.


책임의 이동

두 방식 사이에 근본적인 차이를 만드는 것은 책임의 이동 이다. 여기서 책임기능 을 가리키는 객체지향 세계의 용어로 생각해도 무방하다.

두 방식의 차이점을 가장 쉽게 이해할 수 있는 방법은 기능을 처리하는 방법 을 살펴보는 것이다.

첫 번째 예제에서는 작업 흐름이 주로 Theater 에 의해 제어된다는 사실을 알 수 있다. 객체지향 세계의 용어를 사용해서 표현하면 책임이 Theater에 집중돼 있는 것 이다.

책임 중앙 집중된 절차적 프로그래밍

책임이 분산된 객체지향 프로그래밍

객체지향 설계에서는 제어 흐름이 각 객체에게 적절하게 분산돼 있음을 알 수 있다. 다시말해 하나의 기능을 완성하는 데 필요한 책임이 여러 객체에 걸쳐 분산돼 있는 것이다.

변경 전의 절차적 설계에서는 Theater 가 전체적인 작업을 도맡아 처리했다. 변경 후의 객체지향 설계에서는 각 객체가 자신이 맡은 일을 스스로 처리 했다. 다시 말해 Theater 에 몰려있던 책임이 개별 객체로 이동한 것이다. 이것이 바로 책임의 이동 이 의미하는 것이다.

객체지향 설계에서는 독재자가 존재하지 않고 각 객체에게 책임이 적절하게 분배된다. 따라서 각 객체는 자신을 스스로 책임 진다. 객체지향 어플리케이션은 스스로 책임을 수행하는 자율적인 객체들의 공동체를 구성함으로써 완성된다.

우리가 작성한 코드에서 데이터와 데이터를 사용하는 프로세스가 별도의 객체에 위치한다면 절차적 프로그래밍 방식 을 따르고 있을 확률이 높다. 반면 데이터와 데이터를 사용하는 프로세스가 동일한 객체 안에 위치한다면 객체지향 프로그래밍 방식을 따르고 있을 확률이 높다.

사실 객체지향 설계의 핵심은 적절한 책임을 적절한 객체에 할당 하는 것이다. 객체는 다른 객체와의 협력이라는 문맥 안에서 특정한 역할을 수행하는데 필요한 적절한 책임을 수행해야 한다. 따라서 객체가 어떤 데이터를 가지느냐 보다는 객체에 어떤 책임을 할당할 것이냐에 초점 을 맞춰야 한다.

설계를 어렵게 만드는 것은 의존성 이라는 것을 기억하라. 해결 방법은 불필요한 의존성을 제거함으로써 객체 사이의 결합도를 낮추는 것 이다. 예제에서 결합도를 낮추기 위해 선택한 방법은 Theater 가 몰라도 되는 세부사항을 AudienceTicketSeller 내부로 감춰 캡슐화 하는 것이다.

결과적으로 불필요한 세부사항을 객체 내부로 캡슐화 하는 것은 객체의 자율성 을 높이고 응집도 높은 객체들의 공동체를 창조할 수 있게 한다.

불필요한 세부사항을 캡슐화 하는 것은 자율적인 객체들이 낮은 결합도와 높은 응집도를 가지고 협력하도록 최소한의 의존성만을 남기는 훌륭한 객체지향 설계다.


더 개선할 수 있다.

현재의 설계는 이전의 설계보다는 분명히 좋아졌지만 아직도 개선의 여지가 있다. Audience 클래스를 살펴보자.

분명 Audience 는 자율적인 존재다 스스로 티켓을 구매하고 가방 안의 내용물을 직접 관리한다. 하지만 Bag 은 어떤가? Bag 은 과거의 Audience 처럼 자기 자신을 스스로 책임지지 않고 Audience 에 의해 끌려다니는 수동적인 존재 이다. 이전 예제를 통해 우리가 객체지향에 조금이라도 눈을 떴다면 Bag 에 문제가 있다는 사실을 이해할 수 있다.

Bag 을 자율적인 존재로 바꿔보자. 방법은 이전과 동일하다. Bag 의 내부 상태에 접근하는 모든 로직을 Bag 안으로 캡슐화 해서 결합도를 낮추면 된다. Baghold() 메서드를 추가하자.

변경된 Bag 클래스

이제 Bag 은 관련된 상태와 행위를 함꼐 가지는 응집도 높은 클래스 가 되었다. public 메서드였던 hasInvitation() / minusAmount() / setTicket() 메서드들은 더이상 외부에서 사용되지 않고 내부에서만 사용되기 때문에 가시성을 private 으로 변경했다. 이 작은 메서드들을 제거하지 않고 그대로 유지한 이유는 코드의 중복을 제거하고 표현력을 높이기 위해서다. 이렇게 작은 메서드로 코드를 작게 분리하는 것이 얼마나 유용한지 계속 공부하면서 실감해보자.

Bag 의 구현을 캡슐화 시켰으니 이제 AudienceBag 의 구현이 아닌 인터페이스 에만 의존하도록 수정하자.

TicketSeller 역시 TicketOffice 의 자율권을 침해한다. 아래 코드에서 알 수 있듯 현재의 TicketSellerTicketOffice 에 있는 Ticket 을 마음대로 꺼내서는 자기 멋대로 Audience 에게 팔고 Audience 에게 받은 돈을 마음대로 TicketOffice 에 넣어버린다.

이제 Bag 처럼 TicketOffice 에게도 잃어버린 자율권을 찾아주자. TicketOfficesellTicketTo 메서드를 추가하고 TicketSellersellTo() 메서드의 내부 코드를 이 메서드로 옮기자.

이제 getTicket() 메서드와 plusAmount 메서드는 TicketOffice 내부에서만 사용되기 때문에 가시성을 private 으로 변경할 수 있다. TicketSellerTicketOfficesellTicketTo() 메서드를 호출함으로써 원하는 목적을 달성할 수 있다. 좋은 소식은 이제 TicketSellerTicketOffice 의 구현이 아닌 인터페이스에만 의존하게 됐다는 점이다.

안타깝게도 이 변경은 처음에 생각했던 것 만큼 만족스럽지 않다. 그 이유는 TicketOfficeAudience 사이에 의존성 이 추가되었기 때문이다. 변경 전에는 TicketOfficeAudience 에 대해 알지 못했었다. 변경 후에는 TicketOfficeAudience 에게 직접 티켓을 판매하기 때문에 Audience 에 대해 알고 있어야 한다.

변경 전에는 존재하지 않았던 새로운 의존성이 추가된 것이다. 의존성의 추가는 높은 결합도를 의미하고 높은 결합도는 변경하기 어려운 설계를 의미한다. TicketOffice 의 자율성은 높였지만 전체 설계의 관점에서는 결합도가 상승했다. 어떻게 할 것인가?

현재로서는 Audience 에 대한 결합도와 TicketOffice 의 자율성 모두를 만족시키는 방법이 잘 떠오르지 않는다. 트레이드 오프 시점이 왔다. 어떤 것을 우선해야 하는가? 토론 끝에 개발팀은 TicketOffice 의 자율성 보다는 Audience 의 결합도를 낮추는 것이 더 중요하다는 결론에 도달했다.

이 작은 예제를 통해 우리는 두 가지 사실을 알게 됐다. 첫째. 어떤 기능을 설계하는 방법은 한 가지 이상 일 수 있다. 둘째. 동일한 기능을 한 가지 이상의 방법으로 설계할 수 있기 때문에 결국 설계는 트레이드 오프의 산물 이다. 어떤 경우에도 모든 사람들을 만족시키는 설계를 만들 수는 없다.

설계는 균형의 예술 이다. 훌륭한 설계는 적절한 트레이드 오프의 결과물이라는 사실을 명심하라. 이러한 트레이드 오프 과정이 설계를 어려우면서도 흥미진진한 작업으로 만드는 것이다.


그래, 거짓말이다!

앞에서 실생활의 관람객과 판매자가 스스로 자신의 일을 처리하기 때문에 코드에서의 AudienceTicketSeller 역시 스스로 자신을 책임져야 했다고 말했던 것을 기억하는가? 이것은 우리가 세상을 바라보는 직관과도 일치한다 따라서 이 직관에 따르는 코드는 이해하기가 더 쉬운 경향이 있다.

그러나 Theater 는 어떤가? Bag 은? TicketOffice 는? 이들은 실세계에서는 자율적인 존재가 아니다. 소극장에 관람객이 입장하기 위해서는 누군가가 소극장의 문을 열고 입장을 허가해 줘야 한다. 가방에서 돈을 꺼내는 것은 관람객이지 가방이 아니다. 판매원이 매표소에 없는데도 티켓이 저절로 관람객에게 전달되지는 않을 것이다. 그럼에도 우리는 이들을 관람객이나 판매원과 같은 생물처럼 다뤘다. 무생물 역시 스스로 행동하고 자기 자신을 책임지는 자율적인 존재 로 취급한 것이다.

비록 현실에서는 수동적인 존재라고 하더라도 일단 객체지향 세계에 들어오면 모든 것이 능동적이고 자율적인 존재 로 바뀐다. 이처럼 능동적이고 자율적인 존재로 소프트웨어 객체를 설계하는 원칙을 가리켜 의인화 라고 부른다.

결국 훌륭한 객체지향 설계란 소프트웨어를 구성하는 모든 객체들이 자율적으로 행동하는 설계 를 가리킨다. 그 대상이 비록 실세계에서는 생명이 없는 수동적인 존재라고 하더라도 객체지향의 세계로 넘어오는 순간 그들은 생명과 지능을 가진 싱싱한 존재로 다시 태어난다.

따라서 이해하기 쉽고 변경하기 쉬운 코드를 작성하고 싶다면 차라리 한 편의 애니메이션을 만든다 라고 생각하라. 다른 사람의 코드를 읽고 이해하는 동안에는 애니메이션을 보고 있다고 자신의 뇌를 속여라. 그렇게 하면 코드 안에서 웃고 떠들고 화내는 가방 객체를 만나더라도 당황하지 않을 것이다.


객체지향 설계

설계가 왜 필요한가.

개인적으로 가장 좋아하는 설계의 정의는 다음과 같다.

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

어떤 사람들은 설계가 코드를 작성하는 것보다는 높은 차원의 창조적인 행위라고 생각하는 것 같다. 하지만 설계를 구현과 떨어트려서 이야기 하는것은 불가능 하다. 설계는 코드를 작성하는 매 순간 코드를 어떻게 배치할 것인가를 결정하는 과정 에서 나온다. 설계는 코드 작성의 일부 이며 코드를 작성하지 않고서는 검증할 수 없다.

예제로 돌아가서 변경 전의 코드와 변경 후의 코드를 비교해보자. 두 코드를 실행한 결과는 같다. 두 코드 모두 소극장에 방문한 관람객들을 입장시키는 작업을 성공적으로 수행한다. 하지만 코드를 배치하는 방법은 완전히 다르다.

첫 번째 코드에서는 데이터와 프로세스를 나누어 별도의 클래스에 배치했지만 두 번째 코드에서는 필요한 데이터를 보유한 클래스 안에 프로세스를 함께 배치 했다. 두 프로그램은 서로 다른 설계를 가진 것이다.

그렇다면 좋은 설계란 무엇인가? 우리가 짜는 프로그램은 두 가지 요구사항을 만족시켜야 한다. 우리는 오늘 완성해야 하는 기능을 구현하는 코드를 짜야 하는 동시에 내일 쉽게 변경할 수 있는 코드를 짜야한다. 좋은 설계란 오늘 요구하는 기능을 온전히 수행하면서 내일의 변경을 매끄럽게 수용 할 수 있는 설계다.

변경을 수용할 수 있는 설계가 중요한 이유는 요구사항이 항상 변경 되기 때문이다.


객체지향 설게

따라서 우리가 진정으로 원하는 것은 변경에 유연하게 대응할 수 있는 코드 이다. 객체지향 프로그래밍은 의존성을 효율적으로 통제할 수 있는 다양한 방법을 제공 함으로써 요구사항 변경에 좀 더 수월하게 대응할 수 있는 가능성을 높여준다.

적어도 앞의 예제를 통해 코드 변경이라는 측면에서는 객체지향이 과거의 다른 방법보다 안정감을 준다는 사실에 공감하길 바란다.

변경 가능한 코드란 이해하기 쉬운 코드 이다. 만약 우리가 어떤 코드를 변경해야 하는데 그 코드를 이해할 수 없다면 어떻겠는가? 그 코드가 변경에 유연하다고 하더라도 아마 코드를 수정하겠다는 마음이 선뜻 들지는 않을 것이다.

객체지향 패러다임은 우리가 세상을 바라보는 방식대로 코드를 작성할 수 있게 돕는다. 세상에 존재하는 모든 자율적인 존재처럼 객체 역시 자신의 데이터를 스스로 책임지는 자율적인 존재 이다. 객체지향은 우리가 세상에 대해 예상하는 방식대로 객체가 행동하리라는 것을 보장함으로써 코드를 좀 더 쉽게 이해할 수 있게 한다.

그러나 단순히 데이터와 프로세스를 객체라는 덩어리 안으로 밀어넣었다고 해서 변경하기 쉬운 설계를 얻을 수 있는 것은 아니다. 객체지향의 세계에서 애플리케이션은 객체들로 구성되며 애플리케이션의 기능은 객체들 간의 상호작용을 통해 구현 된다. 그리고 객체들 사이의 상호작용은 객체 사이에 주고받는 메시지로 표현 된다.

예제 코드에서 관람객을 입장시키는 일련의 과정들이 Audience / TicketSeller / Bag 인스턴스 간의 상호작용을 통해 구현됐다는 사실을 기억하자. 이 과정에서 TicketSellerAudiencebuy 메시지를 전송하고 TicketSellerBagminusAmount 라는 메시지를 전송한다.

이처럼 애플리케이션의 기능을 구현하기 위해 객체들이 협력하는 과정 속에서 객체들은 다른 객체에게 의존 하게 된다. TicketSellerAudience 에게 메시지를 전송하기 위해서는 Audience 에 대해 알고 있어야 한다. 메시지를 전송하기 위한 이런 지식이 두 객체를 결합시키고 이 결합이 객체 사이의 의존성 을 만든다. 훌륭한 객체지향 설계란 협력하는 객체 사이의 의존성을 적절하게 관리 하는 것이다. 세상에 엮인 것이 많은 사람일 수록 변하기 어려운 것처럼 객체가 실행되는 주변 환경에 강하게 결합 될 수록 변경하기 어려워 진다. 객체간의 의존성은 애플리케이션을 수정하기 어렵게 만드는 주범이다.

데이터와 프로세스를 하나의 덩어리로 모으는 것은 훌륭한 객체지향 설계로 가는 첫 걸음일 뿐이다. 진정한 객체지향 설계로 나아가는 길은 협력하는 객체들 사이의 의존성을 적절하게 조절 함으로써 변경에 용이한 설계를 만드는 것이다.


정리

오리엔테이션처럼 가볍게 읽으려면 가볍게 읽을 수 있으면서도 문장 하나하나를 해석해보려면 한 없이 심오하고 알찬 내용들이다.

주된 내용은 다음 세 가지로 요약할 수 있겠다.

  1. 수동적이지 않고 스스로 판단하고 행동하는 자율적인 객체

  2. 인터페이스와 구현의 분리 (명세 관점과 구현 관점)

  3. 캡슐화 를 통한 낮은 결합도와 높은 응집도를 가진 객체지향 설계

나름대로 위 내용들을 좀 더 압축해 보자면 결국 캡슐화 가 아닐까 싶다.

캡슐화 는 객체의 세부적인 구현 내용은 감추고 외부에는 퍼블릭 인터페이스 만을 공개함으로써 인터페이스, 즉 메시지 전송을 이용해서만 접근 가능하게끔 한다.

그리고 그 과정에서 인터페이스와 구현의 분리 가 자연스럽게 진행되고 인터페이스만 외부에 공개함에 따라 세부적인 구현은 객체가 스스로 수행하게끔 구성된다.

이는 구현에 해당하는 세부 내용을 직접 처리 함으로써 자신의 데이터를 직접 처리하게 되고 결과적으로 보다 응집도 높은 설계 로 이어진다.

마찬가지로 메시지를 보내는 객체는 외부 인터페이스에만 의존 하게 됨으로써 더 낮은 수준의 의존성을 갖게 되고 결과적으로 결합도 낮은 설계 로 이어진다.

의식의 흐름을 기억하자.

캡슐화 를 통해 인터페이스와 구현의 분리 가 되고 이에 따라 세부적인 구현 내용은 객체 스스로 처리 함으로써 보다 자율적인 객체로 이어진다.

전체적인 그림은 처음 모든 프로세스를 직접 처리했던 Theater 에서 각 객체들로 책임이 이동함으로써 자율적인 객체들의 협력으로 탈바꿈한 예제를 기억하자.

참고 및 출처

  • 오브젝트