본문 바로가기

도서/Object

[Object] Chapter 01 - 객체, 설계

반응형

객체지향 패러다임을 설명하기 위해 추상적인 개념이나 이론을 앞세우지 않고, 가능하면 개발자인 우리가 가장 잘 이해할 수 있고 가장 능숙하게 다룰 수 있는 코드를 이용해 객체지향의 다양한 측면을 설명하려고 노력할 것이다.

 

소프트웨어 모듈은 세 가지 목적이 있다.

 

1. 실행 중에 제대로 동작

2. 변경을 위해 존재

3. 코드를 읽는 사람과 의사소통하는 것 (이해하기 쉬움)

 

public class Theater {
    private var ticketSeller: TicketSeller

    init(ticketSeller: TicketSeller) {
        self.ticketSeller = ticketSeller
    }

    public func enter(audience: Audience) {
        if audience.bag.hasInvitation {
            let ticket = ticketSeller.ticketOffice.ticket
            audience.bag.setTicket(ticket)
        } else {
            let ticket = ticketSeller.ticketOffice.ticket
            audience.bag.minusAmount(amount: ticket.fee)
            audience.bag.setTicket(ticket)
        }
    }
}

 

Theater 클래스의 enter 메서드가 수행하는 일을 말로 풀면 다음과 같다.

 

극장은 관람객의 가방을 열어 그 안에 초대장이 들어 있는지 살펴본다.

가방 안에 초대장이 들어 있으면 판매원은 매표소에 보관돼 있는 티켓을 관람객의 가방 안으로 옮긴다.

가방 안에 초대장이 들어 있지 않다면 관람객의 가방에서 티켓 금액만큼의 현금을 꺼내 매표소에 적립 후, 매표소에 보관돼 있는 티켓을 관람객의 가방 안으로 옮긴다.

 

01  예상을 빗나가는 코드 - 이해하기 어려움

내가 관람객이라고 가정 - 극장이 내 가방을 마음대로 열어 본다.

내가 판매원이라고 가정 - 극장이 매표소에 보관 중인 티켓과 현금에 마음대로 접근한다.

 

현실에서는 관람객이 직접 자신의 가방에서 돈을 직접 꺼내 판매원에게 지불한다.

판매원은 매표소에 있는 티켓을 직접 꺼내 관람객에게 건네고 관람객에게서 직접 돈을 받아 매표소에 보관한다.

 

상식과 벗어나기 때문에 코드를 읽는 사람과 제대로 의사소통하지 못한다.

 

02 변경에 취약한 코드

위 코드는 관람객이 가방을 들고 다닌다고, 현금을 통한 결제 한다고 가정한다.

또한 판매원이 매표소에서만 티켓을 판매한다고 가정한다.

 

관람객이 가방을 들고 있지 않다면? 신용카드를 이용해서 결제한다면?

판매원이 매표소 밖에서 티켓을 판매해야하는 상황이 온다면?

 

관람객이 가방을 들고 있다는 가정이 바뀐다면, Audience 클래스에서 bag를 제거할 뿐만 아니라 Theater의 enter 메서드 역시 수정해야 한다.

 

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

의존도가 높은 만큼, 위 세부적인 사실 중 한 가지라도 변경된다면 해당 클래스뿐만 아니라, 이 클래스에 의존하는 Theater도 함께 변경해야 한다.

 

이와 같이 변경에 어려움이 따르는 것은 객체 사이의 의존성(dependency)과 관련되어 있다.

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

하지만 모든 의존성을 제거하는 것은 불가능하다. 객체지향 설계는 서로 의존하면서 협렵하는 객체들의 공동체를 구축하는 것이다.

따라서 필요한 최소한의 의존성만을 유지하고 불필요한 의존성을 제거해야 한다.

 

03 설계 개선하기

변경과 의사소통이라는 문제는 서로 엮여 있다. 코드를 이해하기 어려운 이유는 Theater가 관람객의 가방과 판매원의 매표소에 직접 접근하기 때문이다. 이것은 관람객과 판매원이 자신의 일을 스스로 처리한다는 우리의 직관에서 벗어난다.

해결 방법은 간단하다. Theater가 Audience와 TicketSeller에 관해 너무 세세한 부분까지 알지 못하도록 정보를 차단하면 된다!

관람객과 판매원을 자신의 일을 스스로 처리할 수 있는 자율적인 존재로 만들면 된다.

 

1. Theater의 enter 메서드에서 TicketOffice에 접근하는 모든 코드를 TicketSeller 내부로 이동

 

public class Theater {
    private var ticketSeller: TicketSeller

    init(ticketSeller: TicketSeller) {
        self.ticketSeller = ticketSeller
    }

    public func enter(audience: Audience) {
    	ticketSeller.sellTo(audience)
    }
}

public class TicketSeller {

    private let ticketOffice: TicketOffice
    
    init(ticketOffice: TicketOffice) {
    	self.ticketOffice = ticketOffice
    }
    
    public func sellTo(audience: Audience) {
        if audience.bag.hasInvitation {
            let ticket = ticketOffice.ticket
            audience.bag.setTicket(ticket)
        } else {
            let ticket = ticketOffice.ticket
            audience.bag.minusAmount(amount: ticket.fee)
            ticketOffice.plusAmount(ticket.fee)
            audience.bag.setTicket(ticket)
        }
    }
}

 

TicketOffice에 접근하는 코드를 모두 TicketSeller 내부로 옮겼으므로, 외부에서 접근이 필요 없다.

또한 TicketSeller는 ticketOffice에서 티켓을 꺼내거나 판매 요금을 적립하는 일을 스스로 수행할 수밖에 없도록, 가시성을 private으로 지정한다.

이처럼 개념적이나 물리적으로 객체 내부의 세부적인 사항을 감추는 것을 캡슐화(encapsulation)라고 한다.

 

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

TicketSeller가 내부에 TicketOffice 인스턴스를 포함하고 있다는 사실을 구현(implementation)의 영역에 속한다. 

객체의 인터페이스와 구현으로 나누고 인터페이스만을 공개하는 것은 객체 사이의 결합도를 낮추고 변경하기 쉬운 코드를 작성하기 위해 따라야 하는 가장 기본적인 설계 원칙이다.

 

 

2. TicketSeller와 동일한 방법으로 Audience의 bag를 내부로 감추어 캡슐화

 

public class TicketSeller {

    private let ticketOffice: TicketOffice
    
    init(ticketOffice: TicketOffice) {
    	self.ticketOffice = ticketOffice
    }
    
    public func sellTo(audience: Audience) {
        ticketOffice.plusAmount(
            audience.buy(ticket: ticketOffice.ticket)
        )
    }
}

public class Audience {
    private let bag: Bag

    init(bag: Bag) {
        self.bag = bag
    }
    
    public func buy(ticket: Ticket) -> Int {
    	if bag.hasInvitation {
            bag.setTicket(ticket)
            return .zero
        } else {
            bag.setTicket(ticket)
            bag.minusAmount(ticket.fee)
            return ticket.fee
        }
    }
}

 

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

Audience가 Bag를 직접 처리하기 때문에 외부에서는 더 이상 Audience가 Bag를 소유하고 있다는 사실을 알 필요가 없다.

접근 제한자를 private로 변경하여 캡슐화할 수 있게 됐다.

 

 

3. Audience와 마찬가지로 Bag를 자율적인 존재로 바꿔보자

 

public class Audience {
    var bag: Bag

    init(bag: Bag) {
        self.bag = bag
    }
    
    public func buy(ticket: Ticket) -> Int {
        return bag.hold(ticket: ticket)
    }
}

public class Bag {
    private var amount: Int
    private let inviation: Invitation?
    private var ticket: Ticket?

    init(
        inviation: Invitation? = nil,
        amount: Int
    ) {
        self.inviation = inviation
        self.amount = amount
    }
    
    public func hold(ticket: Ticket) -> Int {
        if hasInvitation {
            self.ticket = ticket
            return .zero
        } else {
            self.ticket = ticket
            minusAmount(ticket.fee)
            return ticket.fee
        }
    }

    private var hasInvitation: Bool {
        inviation != nil
    }

    private func minusAmount(_ amount: Int) {
        self.amount -= amount
    }

    private func plusAmount(_ amount: Int) {
        self.amount += amount
    }

}

 

public이었던 hasInvitation, minusAmount, plusAmount 메서드들은 더 이상 외부에서 사용되지 않고 내부에서만 사용되기 때문에 가시성을 private으로 변경했다. setTicket은 단순 대입으로, 외부 접근이 없으므로 메서드를 제거했다.

 

 

4. TicketSeller도 살펴보자

 

public class TicketSeller {    
    public func sellTo(audience: Audience) {
        ticketOffice.plusAmount(
            audience.buy(ticket: ticketOffice.ticket)
        )
    }
}

 

TicketSeller 역시 TicketOffice의 자율권을 침해한다. 잃어버린 TicketOffice의 자율권을 찾아주자.

 

public class TicketOffice {
    public func sellTicketTo(audience: Audience) {
        plusAmount(audience.buy(ticket: ticket))
    }

    private var ticket: Ticket {
        tickets.removeFirst()
    }

    private func plusAmount(_ amount: Int) {
        self.amount += amount
    }
}

public class TicketSeller {
    public func sellTo(audience: Audience) {
        ticketOffice.sellTicketTo(audience: audience)
    }
}

 

이 변경은 이전 변경들 만큼 만족스럽지 않다. 그 이유는 TicketOffice와 Audience 사이 의존성이 추가되었기 때문이다.

변경 전에는 TicketOffice가 Audience에 대해 몰랐지만, 변경 후에는 TicketOffice가 Audience에게 직접 티켓을 판매하기 때문에, Audience에 대해 알아야 한다.

새로운 의존성이 추가된 것이며, 의존성 추가는 높은 결합도를 의미하고, 높은 결합도는 변경하기 어려운 설계를 의미한다.

TicketOffice의 자율성은 높아졌지만, 전체 설계 관점에서는 결합도가 상승했다.

 

 

설계의 자율성과 결합도 측면에서 모두를 만족할 마땅한 방법이 없다. 트레이드오프 시점이 왔다.

 

설계의 트레이드오프

  • 어떤 기능을 설계하는 방법은 한가지 이상일 수 있다.
  • 동일한 기능을 한 가지 이상의 방법으로 설계할 수 있기 때문에 결국 설계는 트레이트 오프의 산물이다. 어떤 경우에도 모든 상황을 만족시킬 수 있는 설계를 만들 수는 없다.

구현과 관련된 모든 것들이 트레이드 오프의 대상이 될 수 있다. 작성하는 모든 코드에는 합당한 이유가 있어야 한다. 비록 아주 사소한 결정이더라도 트레이드오프를 통해 얻어진 결론과 그렇지 않은 결론 사이의 차이는 크다.

반응형