본문 바로가기
IT 공부/객체지향 설계 공부

오브젝트(조영호) - 1장 객체, 설계 정리 및 리뷰

by exdus3156 2023. 12. 6.
※ 1장은 아주 간단한 극장 티켓 예매 코드를 통해 객체지향이 궁극적으로 무엇을 지향하는지 논의한다.
※ 코드는 필요하다 싶은 부분만 적었다. 코드는 책에 있으니 책을 보자.
※ 여기서는 그냥 내가 깨달은 사실, 새롭게 알게 된 사실, 정리할 만한 가치가 있는 지식만을 정리했다.
※ 책을 쉽게 읽을 수 있도록 보조 해석 자료를 만든다는 관점으로 포스팅했다. 따라서 책을 같이 봐야 한다.
※ 보조 자료에 가깝기 때문에 포스팅 제목에 "리뷰"라는 말을 붙였다.

 

1. 티켓 판매 애플리케이션 구현의 특징과 문제점

책에서 다소 복잡하고 장황하게 티켓 판매 애플리케이션을 구현하고 있다. 그러나 자세히 그 과정을 뜯어보면 구현 기법은 아래와 같은 단순한 규칙에 따르고 있다는 것을 알 수 있다.

  1. 도메인의 개념에서 클래스를 따온다. (초대장, 티켓, 관람객, 가방, 극장, 판매원, ..)
  2. 각 클래스의 데이터 구조를 구현한다.
  3. getter, setter, plus, minus, has~, is~ 와 같이 내부 데이터에 대한 단순한 연산을 구현한다.
  4. 최종 기능을 구현한다. 적당한 클래스(ex. 극장)를 찾아 메소드를 코딩한다.

 

티켓 판매 메소드의 최종 구현은 아래와 같다.

public class Theater {
    private TicketSeller ticketSeller;

    public void enter(Audience audience) {
        Bag bag = audience.getBag();
        if ( bag.hasInvitation() ) {
            TicketOffice office = ticketSeller.getTicketOffice();
            Ticket newTicket = office.getTicket();
            bag.setTicket(newTicket);
        } else {
            TicketOffice office = ticketSeller.getTicketOffice();
            Ticket newTicket = office.getTicket();
            Long fee = newTicket.getFee();
            
            bag.setTicket(newTicket);
            bag.minusAmount(fee);
            office.plusAmount(fee);
        }
        
    }

 

위 코드는 자바 언어를 통해 작성되었고, 클래스를 통해 객체를 분할했으므로 언뜻 보면 객체지향 코드처럼 보인다.

그러나 이 코드는 그저 도메인 개념에서 클래스 분할의 아이디어를 얻은 수준에 그칠 뿐이다. 클래스를 분할하고 각각 구현했으므로 객체를 만들었다고 착각하기 쉽다. 위 코드의 클래스(Audience, Bag, TicketSellter, 등)들은 객체가 아니라 데이터를 묶은 박스에 지나지 않는다.

Theater 클래스가 최종 기능을 구현하는 로직 전부를 홀로 담당하고 있다. 즉, 다른 클래스들은 그 어떤 책임도 지지 않는다. 그저 자신이 품고 있는 데이터를 다른 객체(Theater)에게 제공해줄 뿐이다.

Getter, Setter 뿐만 아니라 pulsAmount, minusAmount 도 있으니 괜찮지 않냐고 따질 수 있다. 하지만 맥락적으로 볼 때 이것들 또한 상태를 직접 다루는 연산에 불과하다. 왜냐하면 데이터를 private으로 제어한들, 접근자와 수정자 메소드는 public으로 데이터를 제어하는 것과 다를 것이 하나도 없기 때문이다.

이 부분을 이해하기 위해 직접 아래의 코드를 작성해보았다. 각각의 클래스를 구조체라고 생각해보자.

public void enter(Audience audience) {
    if ( hasInvitation(audience.bag) ) {
        Ticket newTicket = getTicket(ticketSeller.office);
        bag.ticket = newTicket;
    } else {
        Ticket newTicket = getTicket(ticketSeller.office);
        Long fee = newTicket.fee;

        bag.ticket = newTicket;
        bag.amount -= fee;
        office.amount += fee;
    }
}

 

객체지향 향기가 나는 것을 최대한 제거해보았다. C언어 코드라고 해도 믿을 것이다. 원래의 코드와 별로 다를 것이 없다..!

위 코드에서 Theater의 enter 메소드는 프로세스(process)이며, Audience, TicketSeller, TicketOffice, Bag, .. 등은 데이터(Data)다. 프로세스와 데이터가 서로 별도의 모듈에 위치하고 있다. 이것은 전통적인 절차적 프로그래밍 패러다임에 가깝다.

여기서 절차적 프로그래밍에 대한 심도 깊은 논의는 잠시 넘어가자.

핵심은 위 코드가 나쁜 원인을 한 마디로 요약하자면, "책임이 아니라 데이터 자체에 의존하고 있다"는 점이다.

 

2. 설계 개선의 핵심

책이 제안하는 설계 개선의 핵심은 아주 단순하다. "책임을 이동시켜라!"이다. 책임을 적절하게 이동시키면 결과적으로 각 객체가 자신의 일을 적극적으로 수행하게 된다.

public class Theater {
    private TicketSeller ticketSeller;

    public void enter(Audience audience) {
        ticketSeller.sellTicket(audience);
    }
}

위 코드는 개선된 Theater.enter() 메소드다. 예전에 자신이 모든 로직을 홀로 담당했던 것을 생각해보면 엄청난 혁신이다. enter()는 그저 ticketSeller에게 기능을 수행해달라고 요청할 뿐이다.

class TicketSeller {
    private TicketOffice office;
    
    public void sellTicket(Audience audience) {
        office.giveTicketTo(audience);
    }
}
class TicketOffice {
    private List<Ticket> tickets;
    private Long profit;
    
    public void giveTicketTo(Audience audience) {
        Ticket newTicket = tickets.remove(0);
        Long fee = audience.buy(newTicket);
        increaseProfit(fee);
    }
    
    private Ticket getNewTicket() {
        return tickets.remove(0);
    }
    
    private void incresseProfit(fee) {
        this.profit += fee;
    }
}

(※ 오류 처리 등 필요한 로직 일부분은 제외했다.)

하나의 이야기가 그려진다. Theater는 TicketSeller에게 관람객에게 티켓을 판매하라고 요청한다. TicketSeller는 TicketOffice에게 티켓을 전달하라고 요청한다. TicketOffice는 관람객에게 티켓을 주고, 적절한 요금을 받아 매출을 올린다.

 

3. 절차적 프로그래밍에 대한 논의

나쁜 예제로 제시된 Theater 객체는 자신이 모든 로직을 담당하고 있다. 책에 나온 enter 메소드 로직은 (약간 과하긴 하지만) 절차적 프로그래밍의 향기가 난다. 아마 저자가 의도했을 것이다.

나는 개인적으로 절차적 프로그래밍에 대해 심도 있게 논의하고 싶진 않았다. 그건 내가 절차적 프로그래밍의 단점이라고 '말해지는 것들'에 대해 납득시킬 수 있을 만큼 내 언어로 설명이 불가능하기 때문이다.

다만 Theater.enter() 메소드가 티켓 구매와 관련된 모든 프로세스를 담당하는 것은 명백하다. 이를 통해 우리는 해당 Theater 클래스가 사실상 하나의 거대한 프로그램이라는 사실을 알 수 있다. 

객체지향으로서 개선된 코드는 객체가 각자의 일을 담당하고 있다. 즉, 각각의 객체는 아주 작은 프로그램이다. 결론적으로 객체지향은 작은 프로그램들을 만들고, 나중에 서로 같이 일하도록 만드는 기술이다.

이를 토대로 절차적 프로그래밍에 대한 아주 작은 결론 하나를 내려볼 수 있을 것 같다. 바로 절차적 프로그래밍이란, 모든 것을 수행하는 하나의 프로그램을 만드는 것이라고...

 

 

절차적 프로그래밍이 구조적으로 데이터(data)와 프로세스(process)를 분리했다는 말은, 달리 말하면 모듈 외부에서 데이터와 프로세스를 들고 있어야 한다는 의미가 된다. 

위 enter() 메소드 코드를 보면, TicketSeller라고 하는 데이터 구조에 의존하고 있다는 것을 알 수 있다. 따라서 TicketSeller 데이터 구조가 변경되면 enter() 메소드를 담고 있는 Theater 클래스도 변경해야 한다. 

그런데 TicketSeller 데이터 구조는 내부적으로 복잡한 계층적인 데이터들을 가지고 있다. TicketSeller는 TicketOffice, Ticket, List, Long 등에 의존하고 있다. 따라서 이들 중 하나라도 데이터 구조가 변경되면 역시나 TicketSeller도 변경의 압박을 받는다