객체, 설계

2023. 9. 29. 15:20북리뷰/오브젝트

728x90

# 들어가며

 

이론이 먼저일까 실무가 먼저일까?

 

글래스의 주장에 따르면 초기에는 실무가 먼저 급속할 발전을 이룬다고한다. 이론이 정립할 수 없기 때문이다.

실무가 어느 정도 발전하고 나서야 실용성을 입증하는 이론이 모습을 갖춰가고,

해당 분야가 충분히 성숙해지는 시점에 이르러서야 이론이 실무를 추월하게 된다고 한다.

즉, 이론보다 실무가 먼저다.

 

SW 분야도 마찬가지이다. 아직까지 다른 공학에 비해 역사도 짧다.

컴퓨터가 세상에 나온 이후 수많은 SW가 설계되고 개발되었으나, 이론은 1970년대 돼서야 슬슬 모습을 비추기 시작했다.

이 이론들 역시 실무에서 반복적으로 적용되던 기법들을 이론화한 것을디 대부분이다(디자인패턴, 리팩터링 등)

유지보수에 대한 이론은 거의 전무하다.

 

따라서 SW에 대한 설계/유지보수를 논할 때는 이론을 중심에 두기는 적절하지 않다.

그렇기에 "오브젝트" 라는 책은 "코드" 그 자체를 중점으로 설명한다.

 

추상적인 개념과 이론은 훌륭한 코드를 작성하는데 필요한 도구일 뿐이다.

프로그래밍을 통해 이론을 배우는 것이 개념과 이론을 통해 프로그래밍을 배우는 것보다 더 훌륭한 방법이다.  

 

# 티켓 판매 애플리게이션 구현하기

소극장 운영 서비스를 만들 것이다.

관람객들은 현금 또는 초대장을 티켓으로 교환한 후 입장한다.

따라서 관람객을 입장시키기 전에 초대장 소지 여부를 확인한 후, 초대장이 없으면 티켓을 판매한 다음 입장시켜야한다.

 

다음은 위 요구사항을 만족시키기 위한 코드이다.

### Inviattion

public class Invitation {
    private LocalDateTime when;
}

### Ticket

public class Ticket {
    private Long fee;

    public Long getFee() {
        return fee;
    }
}

### Bag

public class Bag {
    private Long amount;
    private Invitation invitation;
    private Ticket ticket;

    public Bag(Long amount) {
        this(amount, null);
    }

    public Bag(Long amount, Invitation invitation) {
        this.amount = amount;
        this.invitation = invitation;
    }

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

    public boolean hasTicket() {
        return ticket != null;
    }

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

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

    public void plusAmount(Long amount) {
        this.amount += amount;
    }

}

### Audience

public class Audience {
    private Bag bag;

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

    public Bag getBag() {
        return bag;
    }
}

### TicketOffice

public class TicketOffice {
    private Long amount;
    private List<Ticket> tickets = new ArrayList<>();

    public TicketOffice(Long amount, Ticket ...tickets) {
        this.amount = amount;
        this.tickets.addAll(Arrays.asList(tickets));

    }
    public Ticket getTicket() {
        return tickets.remove(0);
    }

    public void minusAmount(Long amount) {
        this.amount -= amount;
    }
    public void plusAmount(Long amount) {
        this.amount += amount;
    }
}

### TicketSeller

public class TicketSeller {
    private TicketOffice ticketOffice;

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

    public TicketOffice getTicketOffice() {
        return ticketOffice;
    }
}

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

코드의 전체적인 구조는 다음과 같다

다음은 소극장을 구현하는 클래스이다

 

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

위 작성한 프로그램은 예상대로 동작한다.

허나, 많은 문제점을 가지고 있다.

 

# 무엇이 문제인가

 

로버트 마틴은 SW모듈이 3가지 기능을 가져야 한다고 설명한다.

모듈 : 클래스, 패키지, 라이브러리 같이 프로그램을 구성하는 임의 요소

1. 실행 중 제대로 동작하는 것

2. 변경을 위해 존재하는 것

3. 코드를 읽는 사람과 의사소통하는 것

 

그럼 위 코드들은 모듈이 가져야한 기능을 전부 충족할까?

아쉽게도 1번 외에는 충족을 못하고 있다.

 

위 코드의 Thread.enter 메서드를 자세히 보면, 소극장은 관람객의 가방을 열서 초대장 여부를 살펴본다. 그리고 판매원은 티켓을 관람객 가방 안으로 옮긴다.

해당 부분에서 문제가 있다. 바로 관람객과 판매원이 소극장의 통제를 받는 수동적인 존재라는 점이다. 

제 3자가 가방을 마음대로 열어도 되나, 티켓을 함부로 꺼내도 되나?

관람객과 판매원이 직접 해야하는 일을 소극장이 수행하고 있다.

 

이해 가능한 코드란 그 동작이 우리 예상에서 크게 벗어나지 않는 코드이다.

허나, 위 코드들은 우리의 예상을 벗어난다.

현실에서는 관람객이 직접 자신의 가방에서 초대장을 꺼내 판매원에게 건낸다.

하지만, 코드 안에서는 그렇게 하지 않는다.

그렇기에 코드가 우리의 상식과 다르게 동작하기에 코드를 읽는 사람과 제대로 의사소통이 불가능하다.

 

또 다른 문제가 있다. 이 코드를 이해하기 위해서는 여러 세부 내용을 한꺼번에 기억하고 있어야 한다.

Theater.enter 를 이해하기 위해서는 Audience의 Bag, TicketeSeller의 TicketOffice. 또 그 안에 보관되어 있는 돈과 티켓...

하나의 클레스나 메서드에 너무 많은 세부사항을 다루기 때문에 코드 작성자/리뷰어 둘 다에게 큰 부담을 준다.

 

가장 심각한 문제는 변경에 취약하다는 점이다.

Audience나 TicketSeller을 변경하면 Theater도 변경해야한다. 

만약 Audience가 가방에 초대장을 넣고 다니지 않는다면?

Theater에서는 항상 가방을 들고 다닌다고 가정하였기에, 그 가정이 깨지는 순간 모든 코드가 흔들리게 된다.

지나치게 세부적인 사실에 의존하여 동작하기에 변경이 어려운 것이다. 

 

위 문제는 의존성과 관련한 문제이다. 의존성은 변경과 관련이 되어있다.

즉, 어떤 객체가 변경될 때 그 객체에 의존하는 다른 객체도 함께 변경될 수 있다.

 

그렇다고 의존성을 완전히 없애는 것은 정답이 아니다.

OOP는 서로 의존하면서 협력하는 객체들의 공동체를 구축하는 것이기에,

최소한의 필요한 의존성만 유지하고, 불필요한 의존성을 제거해야 하는 것이다.

즉, 설계 시에 객체 사의에 의존성을 낮춰 변경에 용이한 설계를 만들어야 한다.

객체 간 의존성이 높다는 말은 결합도(coupling)이 높다고도 말한다.

 

# 설계 개선하기 

어떻게 하면 의존성을 낮출 수 있을까?

해결 방법은 간단하다. 바로 너무 세세한 정보를 알지 못하도록 정보를 차단하면 된다.

 

위 처럼 Theater의 enter 메서드에 접근하는 모든 코드를 TicketSeller 내부로 숨겼다.

그리고 해당 로직을 sellTo 메서드로 옮겼다.

이제는 Theater에서 TicketOffice로 직접 접근할 수 없게 되었다.

즉,티켓을 꺼내거나, 판매 요금을 적립하는 일을 Theater가 하는게 아닌, TicketSeller 스스로 수행할 수 밖에 없는 것이다.

 

이처럼 개념적/물리적으로 객체 내부의 세부 사항을 감추는 것을 캡슐화 라고 부른다.

캡슐화를 통해 접근을 제한하여 객체 간 결합도를 낮춰 변경을 더 용이하게 할 수 있다.

 

이제 Theater은 TicketSeller의 인터페이스에만 의존하고,

TicketSeller 내부 로직은 구현 영역에 속한다.

객체를 인터페이스와 구현으로 나누고 인터페이스만 공개하는 것은 객체 사이 결합도를 낮추고 변경에 용이하게 하는 가장 기본적인 설계 유원칙이다.

즉, 객체 내부의 상태를 캡슐화하고, 객체 간에는 오직 메시지를 통해서만 상호작용하도록 만들어야 한다. 

또한 객체는 스스로 자신의 데이터를 책임져야 한다.

 Audience와 Bag도 다음과 같이 수정할 수 있다.

 

### Audience 캡슐화

### Bag 캡슐화

 

## 절차지향과 객체지향

수정하기 전 코드를 기준으로,

Theater의 enter 메서드는 수행 절차가 진행되기에 프로세스(process) 라 할 수 있고,

Audience, TicketSeller, Bag, TicketOffice는 수행에 필요한 정보를 제공하기에 데이터(Data)로 볼 수 있다.

절차지향적 프로그래밍을 프로세스와 데이터가 별도 모듈에 위치하고 있다.

절차적 프로그래밍 방식은 모든 처리가 하나의 클래스 안에 위치하고, 나머지 클래스는 단지 데이터의 역할만 수행하기에 의존성이 높다.

 

허나 수정한 코드에서는 프로세스가 데이터를 소유한 Audience와 TicketSeller로 위치했다.

이처럼 객체지향적 프로그래밍 방식은 데이터와 프로세스가 동일한 모듈에 위치해있다.

 

훌륭한 객체지향 설계의 핵심은 캡슐화를 통해 의존성을 적절히 관리함으로써 객체 결합도를 낮추는 것이다.

 

또한 수정 전 코드는 주로 Theater에 의해 작업 흐름이 제어된다. 즉, 책임이 Theater에 집중돼있다.

허나 책임을 개별 객체로 이동/분배하면서 자신이 스스로 책임지게 된다.

객체지향 설계의 핵심은 적절한 객체에 적절한 책임을 할당하는 것이다.

 

# 더 개선할 수 있을까?

TicketSeller는 TicketOffice의 자율권을 침해한다.

따라서 위 방식 처럼 TicketOffice도 캡슐화를 진행하자

이제 ticketSeller는 TicketOffice의 데이터를 직접 참조하지 않고, sellTicketTo 메시지를 호출하여 원하는 목적을 달성할 수 있다!

허나 위 리펙터링이 만족스러운가? 이전 케이스보다는 만족스럽지 않다.

왜나하면 TicketOffice와 Audience 사이에 새로운 의존성이 추가되었으니까!

TicketOffice의 자율성은 올라갔으나, 전체 설계 관점에서는 결합도가 상승했다.

과연 이러한 리펙터링이 좋은건가? 이전 것이 좋은건가?

 

위 케이스를 통해 두 가지를 알 수 있다.

1. 어떤 기능을 설계하던, 방법은 한 가지 이상일 수 있다.

2. 결국 설계는 트레이오프의 산물이다.

설계는 균형의 예술이다. 훌륭한 설계는 적절한 트레이드오프의 결과물이다. 어떤 경우이던 장단점이 있기 마련이다. 

 

## 자율적인 객체

Audience와 TicketSeller는 객체이기에 스스로를 책임져야한다.

그러면 Bag은? Theater은? 이들은 실제로 자율적인 존재는 아니다.

허나, 객체지향 세계에서 만큼은 무생물 역시 스스로 행동하고 자신을 책임져야 한다.

현실에서 수동적인 존재라고 해도, 객체지향 세계에서는 모든 것이 능동적이고 자율적인 존재로 바뀌어야 한다.

즉, 사물에 의인화가 필요하다.

 

## 객체지향 설계

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

설계를 구현과 분리해서 말하는 것은 불가하다.

그럼 좋은 설계는 무엇일까?

오늘 완성해야 하는 기능을 구현하는 코드를 짜는 동시에, 내일 쉽게 변경할 수 있는 코드를 짜야 한다.

즉, 좋은 설계는 요구사항을 완전히 수행하면서, 변경에 용이해야 한다.

그러한 이유는 요구사항이 항상 변경되기 때문이다. 

객체지향 설계는 위 좋은 설계에 적합한 설계법이다.

훌륭한 객체지향 설계는 협력하는 객체 사이의 의존성을 적절히 관리하는 설계이다.

 

728x90

'북리뷰 > 오브젝트' 카테고리의 다른 글

설계 품질과 트레이드오프  (0) 2023.10.01
역할, 책임, 협력  (1) 2023.09.30
객체지향 프로그래밍  (0) 2023.09.30