BackEnd/OOP

[오브젝트] 객체, 설계 (1장)

샤아이인 2022. 1. 31.

조영호님의 오브젝트 라는 책을 읽으며, 나중에 다시 상기하는 용도로 간략하게 정리해 봅니다.

 

오브젝트: 코드로 이해하는 객체지향 설계

역할, 책임, 협력을 향해 객체지향적으로 프로그래밍하라! 객체지향으로 향하는 첫걸음은 클래스가 아니라 객체를 바라보는 것에서부터 시작한다. 객체지향으로 향하는 두 번째 걸음은 객체를

wikibook.co.kr

 

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

책에서 나온 티켓 판매 어플리케이션은 절차지향적이다. 이를 개선해 나가야 한다.

 

구현한 클래스 다이어그램은 다음과 같다.

출처 - 오브젝트 17p

우선 절차 지향방식의 Theater는 다음과 같다.

public class Theater {
    private TicketSeller ticketSeller;

    public Theater(TicketSeller ticketSeller) {
        this.ticketSeller = ticketSeller;
    }

    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가지 기능에 관해 다음과 같이 설명한다.

1) 모듈은 정상적으로 실행되어야 한다

2) 변경에 용이해야 한다

3) 이해하기 쉬워야 한다.

 

위 코드는 필요한 기능을 오류없이 정확하게 수행하고 있다. 제대로 동작해야 한다는 제약을 만족한다.

하지만, 변경 용이성과 이해하기 쉬워야 한다는 제약은 만족시키지 못하고 있다.

 

위 코드는 여러가지 문제가 있다.

1) 소극장이 관람객의 가방에서 직접 돈을 가져간다. 관람객의 허락과는 상관없이 소극장이 관람객의 가방에 접근하고 있다.

2) 소극장이 직접 매표소의 티켓과 현금에 접근한후, 관람객에게 티켓을 전달하고, 받은 돈을 관람객의 매표소에 적립한다.

=> 티켓판매원이 하는 일이 없다.

 

따라서 위 코드는 우리의 상식과는 너무 다르게 작동하고 있다.

또한 코드를 이해하기 위해 세부적인 내용을 한꺼번에 기억하고 있어야 한다.

 

가장 큰 문제는 Audience 와 TicketSeller를 변경할 경우 Theater를 함께 변경해야한다는 사실이다.

 

예를 들어 관람객이 가방을 들고있다는 가정이 변경된다고 해보자.

Theater는 관람객이 가방을 들고 있고 판매원이 매표소에서만 티켓을 판매한다는 지나치게 세부적인 사실에 의존해서 동작한다.

 

이처럼 다른 Class 가 Audience의 내부에 대하여 더 많이 알면 알수록 Audience의 변경이 어려워 진다.

 

이것이 객체 사이의 의존성(dependency)와 관련된 문제이다.

의존성이라는 말 내부에는 어떤 객체가 변경될 때 그 객체에게 의존하는 다른 객체도 함께 변경될 수 있다는 사실이 내포돼 있다.

 

그렇다고 모든 의존성을 제거하자는 것이 아니다!, 최소한의 의존성만 유지하고, 불필요한 의존성을 제거해야 한다.

맨 위 다이어그램에서 살펴봤듯 Theater는 너무 많은 클래스에 의존하고 있다.

이렇게 객체 사이의 의존성이 과한 경우를 결합도(coupling)가 높다고 말한다.

 

2. 설계 개선하기

우리는 변경과 의사소통이라는 문제가 서로 엮여 있음에 주목해야 한다.

 

코드를 이해하기 어려운 이유는 Theater가 관람객의 가방과 판매원의 매표소에 직접적으로 접근하기 때문이다.

이는 Theater가 Audience 와 TicketSeller에 결합된다는 것을 의미한다.

 

이를 해결하기 위해, 정보를 차단해야 한다.

과연 Theater가 관람객이 가방을 갖고 있다는 사실과, 판매원이 매표소에서 티켓을 판매한다는 사실을 알아야 할까??

 

알 필요가 없다!

 

관람객이 스스로 요금을 계산하고, 판매원이 스스로 티켓을 판매한다면 모든 문제가 해결된다.

관람객과 판매원을 자율적인 존재로 만들어야 하는것 이다.

 

1) Theater의 enter 메서드에서 TicketOffice에 접근하는 모든 코드를 TicketSeller 내부로 숨기는 것 이다.

Theater 내부에 있던 enter 코드의 logic을 TicketSeller에게로 옮겨보자!

public class TicketSeller {
    private TicketOffice ticketOffice;

    public TicketSeller(TicketOffice ticketOffice) {
        this.ticketOffice = ticketOffice;
    }

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

이제 ticketOffice에 대한 접근은 오직 TicketSeller에서만 가능하다.

TicketSeller는 ticketOffice에서 티켓을 꺼내거나 판매 요금을 적립하는 일을 스스로  수행할수밖에 없다.

=> 이를 캡슐화 라고 부른다. 캡슐화를 통해 변경하기 쉬운 객체를 만들게 된다.

 

Theater의 enter 메서드는 다음과 같이 간단하게 변경된다.

public class Theater {
    private TicketSeller ticketSeller;

    public Theater(TicketSeller ticketSeller) {
        this.ticketSeller = ticketSeller;
    }

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

이제 Theater는 ticketOffice에 대하여 전혀 알지 못한다.

Theater는 단지 ticketSeller의 sellTo 메서드를 통해 메시지를 이해하고 응답할수 있을 뿐이다.

 

Theater는 오직 TicketSeller의 인터페이스에 의존한다.

TicketSeller가 내부에 TickerOffice 인스턴스를 포함하고 있다는 사실을 구현의 역영게 해당된다.

 

2) Audience 를 캡슐화 하자. TicketSeller는 Audience는 getBag 메서드를 호출해서 Audience 내부의 Bag 인스턴스에 직접 접근한다.

캡슐화 하는 방식은 이전과 같다.

 

Audience에서 buy() 라는 메서드를 만들어 줬다.

public class Audience {
    private Bag bag;

    public Audience(Bag bag) {
        this.bag = bag;
    }

    public Long buy(Ticket ticket){
        if(bag.hasInvitation()){
            bag.setTicket(ticket);
            return 0L;
        }else{
            bag.setTicket(ticket);
            bag.minusAmount(ticket.getFee());
            return ticket.getFee();
        }
    }
}

변경 된 코드에서는 Audience가 자신의 가방안에 초대장이 들어있는지 스스로 확인한다.

Audience가 가방을 직접 다루기 때문에 외부에 더이상 getter를 노출시킬 필요가 없다.

getBag()을 제거하고 결과적으로 Bag의 존재를 내부로 캡슐화할 수 있게되었다.

 

Audience 와 TicketSeller를 자율적인 객체로 만들었다!

출처 - 오브젝트 24p

 

캡슐화와 응집도

핵심은 객체 내부의 상태를 캡슐화 하고, 객체 간에 오직 메시지를 통해서만 상호작용 하도록 만드는 것 이다.

 

Theater는 

단지 TicketSeller 가 sellTo 메서드를 통해 응답할수 있다는 사실만 알고 있을 뿐 이다.

단지 Audience 가 buy 메시지에 응답할 수 있고 자신이 원하는 결과를 반환할 것 이라는 사실만 알고있다.

 

객체의 응집도를 높이기 위해서는 객체 스스로 자신의 데이터를 책임지고, 외부의 간섭을 최대한 배제하고 메시지를 통해서만 협력해야 한다.

 

절차지향 vs 객체지향

▶ 절차지향

절차지향 적 관점에서는 Theater의 enter 메서드는 Process 이며, audience, ticketSeller, bag, ticketOffice 는 데이터 이다.

이처럼 프로세스와 데이터를 별도의 모듈에 위치시키는 방식을 절차지향적 프로그래밍 이라 부른다.

 

▶ 객체지향

데이터를 스스로 처리하도로고 하여 프로세스가 데이터를 소유하고 있는 Audience, TicketSeller 내부로 옮기는 방식을

객체지향 프로그래밍 이라 부른다.

 

즉, 객체지향은 책임을 분산시킨다. 이전의 절차지향 모델은 Theater에 책임이 몰려있다.

하지만 객체지향은 책임을 적절하게 객체들에게 분산시키게 된다.

출처 - 오브젝트

 

그래 거짓말이다!

이전까지 객체는 스스로의 처리를 위해 Audience와 TicketSeller 역시 스스로 자신을 책임져야 한다 했었다.

하지만 Theater는? Bag? TicketOffice는? 이들은 실세계 에서는 자율적인 존재가 아니다.

가방에서 돈을 꺼내는것은 관람객이지 가방이 아니다.

 

하지만 이들을 생물처럼 스스로 행동하고, 자신을 책임지는 자율적인 존재로 취급했다.

 

이처럼 능동적이고 자율적인 존재로 소프트웨어 객체를 설계하는 원칙을 가리켜 의인화(anthropomorphism)이라 부른다.

실세계 에서는 생명이 없는 수동적인 존재라고 하더라고, 객체지향 세계로 넘어오는 순간 그들은 생명과 지능을 가진

싱싱한 존재로 다시 태어난다.

 

댓글