코드의 여백

[오브젝트] 1장 '객체'지향 '설계', 무엇이 중요한가?

by rowing0328

※ 책 내용을 바탕으로 제 관점에서 풀어쓴 글입니다. 일부 내용이 다를 수 있습니다.

 

소프트웨어를 개발할 때 좋은 설계란 무엇일까?

 

프로그램이 잘 돌아가는 것은 기본이고,

나중에 요구사항이 바뀌어도 수정이 쉽고,
코드 읽기도 편해야 좋은 설계라고들 한다.

 

객체 지향 프로그래밍(OOP)은 바로 이러한 유연하고 이해하기 쉬운 설계를 만드는 방법 중 하나이다.

 

조영호 님의 저서 『오브젝트』 1장은 간단한 예제를 통해 절차적인 코드객체지향적으로 개선하면서,

객체 지향 설계의 핵심 개념을 설명한다.

 

이번 포스트에서는 1장의 내용을 쉽게 이해할 수 있도록 풀어서 요약해 보겠다.

 

 

티켓 판매 애플리케이션 예제 (절차지향 방식)

 

1장에서는 작은 극장의 티켓 판매 애플리케이션 예시로 이야기를 시작한다.

극장에는 관람객(Audience), 매표소(TicketOffice), 매표원(TicketSeller), 극장(Theater) 등이 등장한다.

 

관람객 중 일부는 초대장(Invitation)이 있어 무료입장이 가능하고,

초대장이 없는 관람객은 티켓을 구매해야 한다.

 

이 시나리오를 처음에는 절차지향적 방식으로 구현한 코드가 소개된다.

예를 들어, 극장의 enter() 메서드가 관람객 입장을 처리하는 절차는 다음과 같다.

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);                        // 티켓을 가방에 넣어줌
    }
    
}

 

이 코드에서는 Theater 객체가 거의 모든 일을 직접 처리하고 있다.

관람객의 가방(Bag) 안에 초대장이 있는지 확인하고, 티켓을 건네주고,
가방에서 돈을 꺼내오고, 매표소 금액을 증가시키는 동작까지 모두 Theater가 관장한다.

 

한눈에 봐도 Theater가 관람객과 매표소 속속들이 들여다보며 조종하고 있어,

객체지향적이라기보다 절차지향적인 흐름이 느껴진다.

 

 

무엇이 문제였을까? - 높은 결합도와 낮은 응집도

 

이 초기 구현은 동작 자체는 문제가 없지만, 설계 품질 측면에서 여러 문제가 드러난다.

 

캡슐화 위반

Theater가 관람객의 내부 상태(가방 속 돈, 초대장)에 직접 접근하고,

매표소의 티켓과 현금에도 직접 손을 뻗친다.

 

남의 내부 정보를 지나치게 알고 간섭하고 있는 셈이다...

 

객체 내부 구현이 외부로 노출되면, 그 객체를 수정할 때,
이를 사용하던 다른 코드도 함께 변경해야 하므로 유지보수가 어렵다.

 

이상한 책임 분배

원래 현실 세계에서 돈을 주고 티켓을 건네주는 일은 매표원이 할 일인데,

코드에서는 극장이 그 일을 전부 처리한다.

 

정작 TicketSeller 객체는 하는 일이 없어서 허수아비처럼 되고 말았다.

 

역할이 뒤섞여 있어 코드를 이해하기에도 혼란스러워진다...

 

높은 결합도

Theater가 Audience의 가방 구조, TicketSeller의 매표소 구조구체적인 세부 사항에 강하게 의존하고 있다.

예를 들어 관람객이 항상 가방을 가지고 있다는 전제를 Theater 코드가 짊어지고 있는 셈이다.

 

만약 관람객이 가방 대신 모바일 앱으로 티켓을 보여주는 식으로 요구사항이 바뀐다면,

Theater 코드를 비롯해 연쇄적으로 많은 부분을 수정해야 할 것이다.

 

이처럼 한 객체가 많은 다른 객체의 내부에 의존하면 결합도가 높다고 부른다.

 

낮은 응집도

한 클래스가 자신의 고유한 역할 이상으로 이것저것 다 떠맡으면 응집도가 떨어진다.

 

위 예에서 Theater는 본연의 책임(공연 관리나 장소 제공 등?) 뿐만 아니라 매표 업무, 검표 업무까지 하고 있다.

한 곳에 과도한 기능이 몰리면, 코드를 이해하고 수정하기가 더욱 어려워진다.

 

 

정리하면, 이 절차적인 코드는 필요한 기능은 수행하지만 변경에 유연하지 않고,

이해하기 어려운 구조이다.

 

좋은 객체 지향 설계의 목표가 변경에 유연한 코드를 만드는 것이라면,

현재 설계는 그 반대 방향으로 가고 있는 셈이다.

 

 

객체지향적으로 설계 개선하기

 

그렇다면 이 코드를 객체지향적으로 리팩터링 하면 어떻게 개선될까?

핵심 아이디어는 간단하다.

 

"자신과 관련된 일은 자기 자신이 처리하도록 하자!"

 

즉, Theater가 혼자 다 하던 일을 적절히 관람객과 판매원에게 나눠 맡기는 것이다.

 

매표원에게 티켓 판매 책임을 돌려주기

Theater의 enter() 메서드는 더 이상 직접 티켓을 꺼내거나 돈거래를 하지 않고,

대신 TicketSeller의 메서드를 호출해서 처리하도록 바꿔보자.

 

예를 들어 TicketSeller.sellTo(Audience audience)라는 메서드를 만들어,

Theater는 단지 ticketSeller.sellTo(audience)를 호출하는 식이다.

그러면 TicketSeller 객체 내부에서 티켓을 꺼내고 요금을 처리하도록 코드를 옮길 수 있다.

 

예제 코드

public class TicketSeller {
    
    private TicketOffice ticketOffice;
    
    // ... 생성자 생략 ...

    public void sellTo(Audience audience) {
        Ticket ticket = ticketOffice.getTicket();           // 매표소에서 티켓 꺼내기
        if (audience.getBag().hasInvitation()) {
            audience.getBag().setTicket(ticket);            // 초대장이 있으면 무료로 티켓만 지급
        } else {
            audience.getBag().minusAmount(ticket.getFee()); // 가방에서 티켓 요금 받기
            ticketOffice.plusAmount(ticket.getFee());       // 매표소 현금 증가
            audience.getBag().setTicket(ticket);            // 티켓 지급
        }
    }
    
}

 

이렇게 하면 매표소 (TicketOffice)에 접근하는 코드는 모두 TicketSeller 안으로 감춰진다.

이제 Theater는 ticketOffice가 있는지조차 모른다.

 

Theater는 오직 "매표원에게 관람객을 입장시켜 달라고 메시지를 보내는" 역할만 한다.

그 결과 Theater와 TicketOffice 사이의 불필요한 의존성이 제거되어 결합도가 낮아진다.

 

관람객(Audience)도 자기 일은 스스로 하도록

위 sellTo() 메서드를 보면, 여전히 audience.getBag()으로 관람객의 가방
직접 접근해서 돈을 빼고 티켓을 넣어주는 부분이 있다.

 

이 로직을 Audience 내부로 옮기면 어떨까?


예를 들어 Audience에 buy(Ticket ticket) 메서드를 추가하여,
관람객이 스스로 티켓을 구매하게 만든다.

 

Audience의 buy()는 초대장의 유무를 스스로 판단하여 티켓을 가방에 넣고,
지불 금액을 리턴하도록 구현한다.

 

예제 코드

public class Audience {
    
    private Bag bag;
    
    // ... 생성자 생략 ...

    public Long buy(Ticket ticket) {
        if (bag.hasInvitation()) {
            bag.setTicket(ticket);
            return 0L;                         // 초대장이 있으면 돈을 내지 않음
        } else {
            bag.minusAmount(ticket.getFee());
            bag.setTicket(ticket);
            return ticket.getFee();            // 초대장이 없으면 티켓 요금 지불
        }
    }
    
}

 

이제 관람객이 알아서 자신의 가방을 뒤져 초대장이 있는지 확인하고, 티켓을 챙기며, 필요한 경우 돈을 지불한다.

이렇게 하면 외부에서 audience.getBag() 같은 내부 접근은 필요 없어지고,

Audience 내부에 Bag을 캡슐화할 수 있게 된다.

 

마지막으로 TicketSeller의 sellTo() 메서드도 Audience의 새로운 buy()를 활용하도록 수정한다.

이제 매표원은 관람객에게 티켓을 주고 돈을 돌려받는지 여부만 확인하면 된다.

 

예제 코드

public void sellTo(Audience audience) {
    Ticket ticket = ticketOffice.getTicket();
    Long fee = audience.buy(ticket);    // 관람객에게 티켓을 주고, 지불된 요금을 받음
    ticketOffice.plusAmount(fee);      // 받은 금액만큼 매표소 현금 증가
}

 

이처럼 관람객과 판매원을 보다 "자율적인 객체"로 바꾸자,

Theater는 관람객과 매표원의 구체적 속사정을 몰라도 된다.

 

극장은 그저 "입장 처리" 메시지를 매표원에게 보낼 뿐이고,

관람객과 매표원이 알아서 티켓 교환 과정을 수행한다.

 

각 객체가 자신의 상태와 행동을 스스로 책임지는 구조로 재편된 것이다.

 

그 결과 얻을 수 있는 이점들은 다음과 같다.

 

캡슐화

객체 내부 구현 세부사항(가방 안의 내용물, 매표소 내부 등)을 감춰서 외부 간섭을 줄인다.

TicketSeller만이 TicketOffice를 다루고, Audience만이 Bag을 다루게끔 변경했다.


이렇게 정보를 차단하면 객체 간 상호의존이 줄어들어 변경에 강한 코드가 된다.

한 객체의 내부 구조가 바뀌어도 다른 객체에는 영향이 최소화된다.

 

낮은 결합도

불필요한 의존성을 없애 결합도를 낮추었다.

이제 Theater는 Audience의 가방 유무를 몰라도 되고, TicketSeller의 내부 TicketOffice 존재도 알 필요가 없다.

 

오직 "sellTo 메시지를 이해하는 TicketSeller"와 "buy 메시지를 이해하는 Audience"와 협력할 뿐이다.

시스템이 보다 유연하게 변경될 수 있게 된 거다.

 

높은 응집도

각 객체가 자기 역할에 충실하니 응집도가 올라갔다.


Theater는 이제 본연의 역할(입장 처리 지휘)만 하고, 금전 거래 로직은 TicketSeller로,

가방 속 처리 로직은 Audience로 책임이 적절히 분산되었다.

 

객체지향 설계에서는 이렇게 책임을 적절히 나누고 협력시키는 것이 핵심이다.

 

 

리팩터링 후의 클래스 구조를 그림으로 그려보면,

초기의 뒤엉킨 의존 관계가 많이 단순해졌음을 알 수 있다.

 

각 객체는 메시지(메서드 호출)를 통해서만 상호작용하고,

내부 구현은 서로 숨긴 채 자율적으로 동작한다.

 

이것이 바로 객체지향다운 설계의 모습이다.

 

 

객체를 자율적인 존재로 바라보기 (의인화의 비유)

 

지금까지 본 개선 과정에서,

우리는 사물에 역할을 부여하여 자율적으로 행동하도록 만들었다.

 

현실에서는 가방이나 매표소 같은 것은 그저 수동적인 물걸일 뿐이지만,

소프트웨어 객체 세계에서는 이들을 한다.

 

마치 각각의 객체에 "생명"이나 "의지"가 있는 것처럼 생각하며 설계하는 것이다.

이러한 원칙을 의인화라고 부른다.

 

예를 들어 현실에서는 사람이 가방에서 돈을 꺼내준다고 표현하지만,
객체지향적으로 가방이 스스로 돈을 내어준다는 식으로 발상을 전환한다.

 

사람이 가방 속 내용을 일일이 아는 대신, 그냥 "가방아, 돈 좀 내줘" 하고 메시지를 보내는 것이다.

그러면 가방 객체는 내부에 현금이 있든 카드가 있든 알아서 처리해서 필요한 금액을 돌려줄 것이다.

 

이렇게 하면 사람 객체(혹은 Theater)는 가방 내부가 현금인지 카드인지 몰라도 되니,

나중에 가방 속 구현이 바뀌어도 사람 쪽 코드를 바꿀 이유가 없다.

 

객체지향에서는 이처럼 현실 세계의 존재들을 소프트웨어 안에서 재창조하여 각자 자율적인 역할을 부여한다.

현실을 그대로 모방하기보다는, 문제를 해결하기 좋도록 개념을 재구성하는 것이다.

 

1장의 티켓 판매 예제에서도, 우리는 관람객, 매표원, 가방, 매표소 모두를
자기 스스로 움직이는 개체처럼 다루었다.


현실에서는 사람이 가방을 뒤져 돈을 내지만,
소프트웨어 객체 세계에서는 Bag 객체가 자율적으로 동작하도록 한 것이다.


이런 비유를 통해 캡슐화와 자율성이라는 객체지향 개념을 더욱 직관적으로 이해할 수 있다.

 

 

객체들의 협력이 이끄는 유연한 설계

 

『오브젝트』 1장에서는 저자가 전달하는 핵심 메시지는 분명하다.

 

"객체지향 설계의 본질은 적절한 객체들에게 책임을 분산하고,
그 객체들이 협력하도록 만드는 것"

 

객체 하나하나는 자신의 상태와 행동을 관리하고,
외부와는 메시지를 통해서만 소통한다.

 

그렇게 함으로써 시스템은 유연하고 변경에 강한 구조를 갖추게 된다.

 

 

마무리 - '미녀와 야수' 소품들이 떠오르다

 

프로그램이 잘 돌아가는 것은 출발점에 불과하다.

진짜 좋은 설계는 새로운 요구사항이 나와도 쉽게 변경할 수 있고,

다른 사람이 코드를 읽어도 이해하기 쉽게 구성된 설계이다.

 

객체지향은 바로 그러한 설계를 실현하기 위한 사고방식이라고 1장은 이야기한다.

절차 중심으로 짠 코드에 객체 지향의 옷을 입혀보니, 훨씬 깔끔하고 사람답게 일하는 시스템으로 변했다.

 

특히 이번 장을 읽으면서 디즈니의 '미녀와 야수'가 떠올랐다.

영화 속에서 주전자나 촛대, 시계 같은 소품들이 저마다의 생명을 가지고 움직이고 대화하며,

필요할 때 알아서 역할을 해내는 모습이 인상적이다.

 

객체지향 세계도 비슷하다.

평소라면 수동적인 도구나 데이터에 불과했을 존재들을,

소프트웨어 안에서는 자율적이고 능동적으로 행동하는 '객체'로 만들어,
각자 책임을 맡도록 설계한다.

 

그러면 시스템 내 객체들이 마치 생명을 가진 존재들처럼 유기적으로 협력하며,
자연스럽게 문제를 해결할 수 있게 된다.

 

이렇듯 1장에서 티켓 판매 예제를 통해 맛본 객체지향 세계의 단편은,
앞으로 남은 장들에서 더욱 구체적으로 펼쳐질 것이다.

블로그의 정보

코드의 여백

rowing0328

활동하기