본문 바로가기

기술 서적

[오브젝트] ch1. 객체, 설계

토마스 쿤이 제안한 패러다임이 과학혁명의 구조를 바라보는 시각을 바꾸었듯이

프로그래밍에도 프로그래밍 패러다임이 있다.

 

프로그래밍 패러다임은 다음을 의미한다.

- 우리가 해결할 문제를 바라보는 방식

- 프로그램을 작성하는 방법

=> 개발자 공동체가 공유하는 프로그래밍 스타일과 모델

 

- 프로그래밍 패러다임은 과거의 시각을 폐기하기보다 단점을 보완하는 발전적 행보를 걸어왔다.

 


1) 티켓 판매 애플리케이션 구현하기

 

- 티켓 판매 애플리케이션을 구현한다

- 소극장은 관객을 입장시킬 수 있다

    - 티켓이 있어야만 입장이 가능하다

    - 무료 이벤트 초대장에 당첨된 관객은 티켓 교환 후, 입장이 가능하다

 

 

초기 설계 다이어그램

 

 

소극장 클래스의 코드

package org.eternity.theater.step01;

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);
        }
    }
}

 

- 관람객의 가방 안에 초대장이 있는지 확인한다

- 있으면 => 이벤트 당첨 관람객에게 티켓을 가방안에 넣어준다

- 없으면 => 가방 에서 티켓 금액만큼 차감한 후 매표소에 금액을 증가시킨 후, 티켓을 넣어준다

 


2. 무엇이 문제인가?

 

로버트 마틴의 <클린 소프트웨어, 애자일 원칙과 패턴, 그리고 실천방법>에서는

소프트 웨어 모듈이 가져야 하는 세가지 기능에 관해 설명한다.

 

여기서 모듈이란 클래스, 패키지, 라이브러리와 같이 프로그램을 구성하는 임의의 요소를 말한다.

 

모듈의 3기능

- 첫째, 제대로 동작할 것
- 둘째, 변경을 위해 존재할 것
- 셋째, 코드 사용자가 이해가 쉬울 것

 

그러나, 위의 예시는 변경가능성과 이해가능성에 위배된다.

 

2-1) 이해가 힘든 이유

 

문제1. 관람객과 판매원이 소극장의 통제를 받는 수동적 존재이다.

- 관람객 입장 : Theater라는 객체가 관람객의 가방을 마음대로 열어본다.

- TicketSeller 입장 : Theater라 티켓과 현금을 마음대로 접근한다

public void enter(Audience audience) {
        if (audience.getBag().hasInvitation()) { // Theater가 getBag으로 가방을 뒤짐
            Ticket ticket = ticketSeller.getTicketOffice().getTicket(); // 매표소내 티켓을 직접 꺼내옴
            audience.getBag().setTicket(ticket);
        } else { 
           ... 중략 ...
        }
    }
}

 

=> 우리의 직관에는 관람객이 직접 자신의 가방에서 돈을 꺼내며, 판매원이 직접 티켓을 꺼내고 돈을 받는다

 

문제2. 세부 구현이 드러나있다.

- Theater의 enter 메서드를 이해하기 위해서는

1) audience가 bag을 가지고 있다는 사실

2) ticketSeller가 ticketOffice에서 티켓을 판매하고 있다는 사실

3) ticketOffice에서 돈과 티켓이 보관되어 있다는 모든 사실을 동시에 기억해야 한다

=> 코드를 읽고 해석하는 사람에게 큰 부담을 준다

 

2-1) 변경에 취약한 이유

간접적 결합도를 표현한 다이어그램

- 가정한 사실

1) 관람객이 항상 가방을 들고 다닌다

2) 판매원이 매표소에서만 티켓을 판매한다

=> 이 가정이 변경되면 코드가 크게 흔들린다

 

결론 : 다른 클래스가 특정 클래스 내부에 대해 많이 알면 알수록 특정 클래스를 변경하기 힘들어진다.

=> 최소한의 의존성만을 남겨두는 것이 좋다

 

+) 의존성의 정도는 결합도로 표현된다

=> 결합도가 높을 수록 변경에 취약하다

=> 결합도가 낮을 수록 변경에 용이하다


 

3. 설계 개선하기

 

Audience와 TicektSeeler의 결합도를 낮추어보자!

= 객체를 자율적인 존재로 만들자

= Theater가 각 객체의 세부적인 정보를 모르게 해보자!

 

theater의 enter 메서드를 ticketSeller의 audience로 이동

// ticketSeller 클래스
public void sellTo(Audience audience) {
        Bag audienceBag = audience.getBag();
        Ticket ticket = ticketOffice.getTicket();

        if (audienceBag.hasInvitation()) {  // 초대장이 있는 경우
            audienceBag.setTicket(ticket);
        } else { // 초대장이 없는 경우 => 티켓 구매
            audienceBag.minusAmount(ticket.getFee());
            ticketOffice.plusAmount(ticket.getFee());
            audienceBag.setTicket(ticket);
        }

}

//theater 클래스
public void enter(Audience audience) {
        ticketSeller.sellTo(audience);
}

 

- theater가 ticketSeller안에 ticketOffice가 있다는 것을 모름 '

- ticketOffice에 대한 접근이 오직 ticketSeller에서만 이루어짐

=> 캡슐화(객체 내부의 세부적인 사항을 감추는 것)

 

- theater는 오직 ticketSeller의 인터페이스(sellTo)에만 의존함

- ticketSeller 안에 ticketOffice가 있다는 것은 구현 영역의 속함

=> 객체를 구현과 인터페이스로 나누고, 인터페이스만 공개하는 것은 결합도를 낮추고 변경하기 쉬운 코드를 작성하기 위해 따라야 하는 기본적인 설계 원칙

 

 

개선2) audience가 직접 bag을 확인하도록 개선

//audience가 초대장이 있는지 스스로 확인
  public Long buy(Ticket ticket){
        if (bag.hasInvitation()){
            bag.setTicket(ticket);
            return 0L;
        }else{
            bag.minusAmount(ticket.getFee());
            bag.setTicket(ticket);
            return ticket.getFee();
        }
}

//ticketSeller => getBag를 audience 내부로 캡슐화
public void sellTo(Audience audience) {
        ticketOffice.plusAmount(audience.buy(ticketOffice.getTicket()));
 }

 

- ticketSeller의 sellTo 메서드에서 audience의 bag에 접근하지 않음 => 캡슐화

- audience가 bag을 직접 처리

- ticketSeller가 audience 내부에 bag이 있는지 없는지 몰라도 됨.

 

=> audience와 ticketSeller가 구현을 외부에 노출하지 않고 자신의 문제를 스스로 책임지고 해결함

 

무엇이 개선되었는가?

- audience와 ticketSeller가 자신이 가진 필드를 직접 관리 => 사용자의 코드 이해도 상승

- audience와  ticketSeller의 내부구현을 변경해도 theater를 변경할 필요가 없음 => 변경 용이성 상승

 

어떻게 한 것인가?

- 자기 자신의 문제를 스스로 해결하도록 코드를 변경 => 객체의 자율성을 높이도록 수정

 

캡슐화와 응집도

- 객체 내부의 상태를 캡슐화 + 객체 간 메시지를 통해서만 상호작용하도록

- 응집도가 높다는 것 = 밀접하게 연관된 작업만을 수행하고, 연관성이 없는 작업은 위임하는 것

 

절차지향과 객체지향

 

절차적 

: 프로세스와 데이터를 별도의 모듈에 위치시킴

: Theater가 모든 프로세스를 처리

: Bag, Audience, TicketSeller, TicketOffice는 모두 데이터의 지위만을 가짐

=> 데이터의 변경으로 인한 영향을 지역적으로 고립시키기 어려움

 

객체지향적

: 데이터와 프로세스가 동일한 모듈 내부에 위치하도록 프로그래밍하는 방식

: 각 객체는 자신을 스스로 책임진다

: 하나의 변경으로 인한 여파를 여러 클래스로 전파되지 않도록 억제

: 책임의 분산 => 변경에 더 유동적으로 반응

 

결론

결합도를 낮추는 것 == 불필요한 의존성을 없애는 것

how? : 캡슐화

so? : 자율성과 응집도를 높이고, 결합도를 낮춤


더 개선할 수 있다

- bag의 내부 상태에 접근하는 모든 로직을 캡슐화

 

public class Bag {
    public Long hold(Ticket ticket) {
        if (hasInvitation()) {
            setTicket(ticket);
            return 0L;
        } else {
            minusAmount(ticket.getFee());
            setTicket(ticket);
            return ticket.getFee();
        }
    }

    private boolean hasInvitation() {
        return invitation != null;
    }

    private void setTicket(Ticket ticket) {
        this.ticket = ticket;
    }

    private void minusAmount(Long amount) {
        this.amount -= amount;
    }
}

public class Audience {

    public Long buy(Ticket ticket) {
        return bag.hold(ticket);
    }
}

 

 

- TicketOffice에게 자율성 부여 => sellTicket과정에서의 돈의 흐름을 캡슐화

  // TicketOffice  
  public void sellTicketTo(Audience audience){
        Ticket ticket = getTicket();
        plusAmount(audience.buy(ticket));
    }
    
 // TicketSeller
  public void sellTo(Audience audience) {
        ticketOffice.sellTicketTo(audience);
  }

 

트레이드 오프

- TicketOffice와 audience 사이에 의존성이 추가되었다

- ticketOffice 자체의 자율성은 높아졌지만 vs 전체 구조의 결합도가 상승함

=> 기능을 설계하는 방식은 한 가지 이상일 수 있음

=> 동일한 기능을 한 가지 이상으로 설계가능하기에 설계는 트레이드 오프의 산물

 

 


4. 객체 지향 설계

 

설계가 코드를 작성하는 것보다 높은 차원의 창조적인 행위라 생각하는 사람이 많다.

그러나, 설계와 구현을 떨어뜨려 이야기하는 것은 불가하다. 설계는 곧 코드의 배치이며, 코드 작성의 일부이다

 

그렇다면 좋은 설계란?

오늘 요구하는 기능을 온전히 설계하면서, 내일의 변경을 매끄럽게 수용할 수 있는 설계이다.

객체 사이의 의존성을 적절하게 관리하는 설계다.