이전에 오브젝트 책 읽은 거 복습하면서 정리를 하는데 1장 부분의 코드나 설명들이 책의 전반적인 내용이 담겨있어서 정리해 봤다.
개념보다는 실무가 더 중요하다. 유지보수와, 설계에 관한 한 지금까지는 그러하다.
그리고 소프트웨어 규모가 커지면 커질수록 소프트웨어 설계 분야에서 이론이 실무를 추월할 가능성은 희박해 보임. 아무튼 저자는 이 책에서 좀 더 '코드'에 대해서 더 다룰 거임
01. 티겟 판매 애플리케이션 구현하기
전체적인 첫 구조는 다음과 같음

예제 코드 따로 자바로 올려져 있어서 나는 파이썬으로 작성함. 필요하다면 나중에 자바로 적긴 할 거임.
class Audience:
def __init__(self, bag):
self.bag = bag
def getBag(self):
return self.bag
class Invitation:
def __init__(self, when):
self.when = when
class Bag:
def __init__(self, amount):
self.amount = amount
self.invitation = None
self.ticket = None
def hasInvitation(self):
return self.invitation is not None
def hasTicket(self):
return self.ticket is not None
def setTicket(self, ticket):
self.ticket = ticket
def minusAmount(self, amount):
self.amount -= amount
def plusAmount(self, amount):
self.amount += amount
class Ticket:
def __init__(self, fee):
self.fee = fee
def getFee(self):
return self.fee
class TicketOffice:
def __init__(self, amount, tickets):
self.amount = amount
self.tickets = tickets
def getTicket(self):
return self.tickets.pop(0)
def minusAmount(self, amount):
self.amount -= amount
def plusAmount(self, amount):
self.amount += amount
class TicketSeller:
def __init__(self, ticketOffice):
self.ticketOffice = ticketOffice
def getTicketOffice(self):
return self.ticketOffice
class Theater:
def __init__(self, ticketSeller):
self.ticketSeller = ticketSeller
def enter(self, audience):
if audience.getBag().hasInvitation():
ticket = self.ticketSeller.getTicketOffice().getTicket()
audience.getBag().setTicket(ticket)
else:
ticket = self.ticketSeller.getTicketOffice().getTicket()
audience.getBag().minusAmount(ticket.getFee())
self.ticketSeller.getTicketOffice().plusAmount(ticket.getFee())
audience.getBag().setTicket(ticket)
02. 무엇이 문제인가
로버트 마틴은 클린 소프트웨어에서 다음과 같은 말을 함
모든 소프트웨어 모듈에서는 3가지 목적이 있다. 첫 번째 목적은 실행 중에 제대로 동작하는 것이다. 이것은 모듈의 존재 이유라고 할 수 있다. 두 번째 목적은 변경을 위해 존재하는 것이다. 대부분의 모듈은 생명주기 동안 변경되기 때문에 간단한 작업만으로도 변경이 가능해야 한다. 변경하기 어려운 모듈은 제대로 동작하더라도 개선해야 한다. 모듈의 세 번째 목적은 코드를 읽는 사람과 의사소통하는 것이다. 모듈은 특별한 훈련 없이도 개발자가 쉽게 읽고 이해할 수 있어야 한다. 읽는 사람과 의사소통할 수 없는 모듈은 개선해야 함.
예상을 빗나가는 코드
일단 코드 자체를 해석하기만 해도 어색한 부분이 있다. 나머지 객체들이 수동적임, TicketSeller가 너무 다 처리하고 너무 많은 걸 알고 있음. 이러한 로직은 이해하기도 어려움.
변경에 취약한 코드
가장 큰 문제는 변경에 취약한 거임. 예를 들어 다른 요구 사항들이 들어오게 된다면? 의존성 있는 모든 코드를 수정해야 함. 의존성이란 말속에는 어떤 객체가 변경될 때 그 객체에 의존하는 다른 객체도 함께 변경될 수 있다는 사실이 내포돼 있음. 그렇다고 의존성을 완전히 없앨 순 없음. 최소한만 유지하고 불필요한 걸 제거해야 함

03. 설계 개선하기
요약하면 기존에 Theater에서 audience와 ticketSeller 내부의 bag, ticketOffice까지 전부 알고 있음. 그래서 audience와 ticketSeller는 수동적으로 소통함. 그 뜻은 결국 Theater에서 모든 정보를 알고 있는 거임 그럼 변경에 취약함. 그래서 각가의 로직 자체를 audience와 ticketSeller 내부로 넣어서 캡슐화함
자율성을 높이자
구체적인 코드는 따로 정리함. 위에 작성한 거처럼. 구체적인 로직을 직접 Audience와 TicketSeller 가 책임지게 함. Theater는 그 메서드를 호출함 변경한 부분은 아래와 같음.
class TicketSeller:
def __init__(self, ticketOffice):
self._ticket_office: TicketOffice = ticketOffice
def getTicketOffice(self) -> TicketOffice:
return self._ticket_office
def sellTo(self, audience):
if audience.getBag().hasInvitation():
ticket = self._ticket_office.getTicket()
audience.getBag().setTicket(ticket)
else:
ticket = self._ticket_office.getTicket()
audience.getBag().minusAmount(ticket.getFee())
self._ticket_office.plusAmount(ticket.getFee())
audience.getBag().setTicket(ticket)
class Theater:
def __init__(self, ticketSeller: TicketSeller):
self.ticket_seller = ticketSeller
def enter(self, audience):
self.ticket_seller.sellTo(audience)

다음은 Audience 부분 캡슐화
class Audience:
def __init__(self, bag):
self.bag = bag
def getBag(self):
return self.bag
def buy(self, ticket):
if self.bag.hasInvitation():
self.bag.setTicket(ticket)
return 0
else:
self.bag.setTicket(ticket)
self.bag.minusAmount(ticket.getFee())
return ticket.getFee()
class TicketSeller:
def __init__(self, ticketOffice):
self._ticket_office: TicketOffice = ticketOffice
def getTicketOffice(self) -> TicketOffice:
return self._ticket_office
def sellTo(self, audience):
self._ticket_office.plusAmount(\
audience.buy(self._ticket_office.getTicket()))

무엇이 개선되었는가
예를 들어 Audience가 가방이 아니라 작은 지갑을 소지하도록 변경해야 한다면 Audience 내부만 변경하면 됨. 비슷하게 TicketSeller가 매표소가 아니라 은행에 돈을 보관하려면 내부 메서드만 수정하면 됨. 그걸 호출하는 Theater는 아무것도 몰라도 됨.
어떻게 한 것인가.
수정 후의 Theater는 Audience나 TicketSeller의 내부에 직접 접근 안 함. 그니까 내부의 내용물을 직접 처리함.
객체의 자율성을 높이는 방향으로 설계를 개선함. 그 결과 이해하기 쉽고 유연한 설계를 얻음.
캡슐화와 응집도
핵심은 객체 내부의 상태를 캡슐화하고 객체 간에 오직 메시지를 통해서만 상호작용하도록 만드는 것임. 밀접하게 연관된 작업만을 수행하고 연관성 없는 작업은 다른 객체에게 위임하는 객체를 가리켜 응집도가 높다고 말함.
객체의 응집도를 높이기 위해서는 객체 스스로 자신의 데이터를 책임져야 함. 객체는 자신의 데이터를 스스로 처리하는 자율적인 존재여야 함. 그것이 객체의 응집도를 높이는 첫걸음임.
절차지향과 객체지향
수정전 코드는 Theater의 enter메서드를 프로세스라 볼 수 있고 나머지 클래스들은 데이터로 볼 수 있음 이렇게 프로세스와 데이터를 별도의 모듈에 위치시키는 방식을 절차적 프로그래밍이라고 부름.
절차적 프로그래밍에서 가장 큰 문제는 데이터의 변경으로 인한 영향을 지역적으로 고립시키기 어렵다는 것임.
변경하기 쉬운 설계는 한 번에 하나의 클래스만 변경할 수 있는 설계다. 절차적 프로그래밍은 프로세스가 필요한 모든 데이터에 의존해야 한다는 근본적인 문제점 때문에 변경에 취약할 수밖에 없음.
객체 지향은 데이터와 프로세스가 동일한 모듈 내부에 위치하도록 함. 객체지향 설계의 핵심은 캡슐화를 이용해 의존성을 적절히 관리함으로써 객체 사이의 결합도를 낮추는 것임.
책임의 이동
두 방식의 근본적인 차이를 만드는 것은 책임의 이동임. 여기서 책임은 기능을 가리킴(보통 객체지향에서 그렇게 씀). 두 방식의 차이 아래와 같음

몰려있던 책임이 다른 객체로 이동했음. 각 객체는 자신을 스스로 책임짐
설계를 어렵게 만드는 것은 의존성이라는 것을 기억해야 함. 해결법은 불필요한 의존성을 제거함으로써 객체 사으이 결합도를 낮추는 것임. 여기서는 몰라도 되는 세부사항을 각 클래스로 캡슐화함. 결과적으로 불필요한 세부사항을 캡슐화하는 것은 자율성을 높이고 응집도 높은 객체들의 공동체를 창조할 수 있게 함.
불필요한 세부사항을 캡슐화하는 자율적인 객체들이 낮은 결합도와 높은 응집도를 가지고 협력하도록 최소한의 의존성만 남기는 것이 훌륭한 객체지향 설계임.
더 개선할 수 있다
위의 리팩토링처럼 bag도 자율적으로 처리하게 캡슐화해야 함.
class Bag:
def __init__(
self, amount, invitation: Optional[Invitation] = None, ticket: [Ticket] = None
):
self._amount = amount
self._invitation = invitation
self._ticket = ticket
def hold(self, ticket):
if self.hasInvitation():
self.setTicket(ticket)
return 0
else:
self.setTicket(ticket)
self.minusAmount(ticket.getFee())
return self._ticket.getFee()
그리고 TicketSeller의 ticketOffice를 직접 호출해서 plusAmount 하는 부분도 수정해서 ticketOffice로 옮겨야 하는데 문제가 그러면 ticketOffice와 Audience 간에 의존성이 생김. 책에서는 자율성보다는 Audience자체에 대한 결합도를 낮추는 것이 더 중요하다 생각해서 그냥 기존대로 놔둠. 그니까 밑에 ticketSeller sellTo부분 그대로 놔둠
class TicketSeller:
...
#수정하려면 그냥 ticket_office 내부에 추가함 그러면 ticket_office가 audience에 의존성 생김
def sellTo(self, audience):
self._ticket_office.plusAmount(\
audience.buy(self._ticket_office.getTicket()))
그래, 거짓말이다.
앞에서는 실생활에 맞게 소프트웨어 객체를 설계하는 것이 이해하기 쉬운 것이라고 했는데, 지금 보면 그냥 무생물이든 뭐든 각 객체의 자율성을 보장하는 방식으로 설계를 함. 이런 걸 의인화라고 함. 비현실적이어도 소프트웨어 설계에서는 수동적인 개체도 자율적으로 행동하도록 만들어야 함
04. 객체지향 설계
설계가 왜 필요한가
좋은 설계란 오늘 요구하는 기능을 온전히 수행하면서 내일의 변경을 매끄럽게 수용할 수 있는 설계임.
그리고 변경을 수용할 수 있는 설계가 중요한 또 다른 이유는 코드를 변경할 때 버그가 추가될 가능성이 높기 때문임. 요구사항의 변경은 필연적으로 코드 수정을 초래하고, 코드 수정은 버그가 발생할 가능성을 높임. 버그의 가장 큰 문제는 코드를 수정하려는 의지를 꺾는다는 것임.
객체지향 설계
변경 가능한 코드란 이해하기 쉬운 코드다. 코드가 아무리 유연해도 이해하기 어려우면 수정하려는 마음이 선뜻 들지는 않을 거임.
훌륭한 객체지향 설계란 협력하는 객체 사이의 의존성을 적절하게 관리하는 설계임.
나의 생각 및 정리
기존의 설계가 결합도를 낮추고 응집도 높게 작성이 되었어도, 시간이 지나면서 기존 코드가 새로운 요구 사항에 비해 구체적인 사항을 드러내는 형태로 바뀌어 간다면, 그니까 요구사항이 더 추상화된 요구사항을 원한다면 그건 다시 새롭게 캡슐화되어야 함.
위 챕터에서 초반에 Theater의 enter 메서드에서 구체적으로 작성된 부분을 Audience와 TicketSeller의 buy , sellTicketTo로 메서드를 내부에 만들고 그 인터페이스를 호출하게 바꿈. 이것만 보면 그 로직 자체에 해당하는 부분은 추상화가 된 거임 그니까 의존성이 적어지기도 함 ticket이 직접 호출을 안 하나 끼. 근데 만약에 요구사항이 단순히 티켓만 파는 게 아니라 팝콘이나 굿즈 등이 생긴다고 치면 ticketSeller의 파는 로직을 일반화해서 수정할 필요가 생길 거임. 그렇게 되면 Theater는 TicketSeller의 메서들 호출하는 것보다. 다른 추상화 계층을 통해서 호출하게 될 거임.
참고 및 출처: <오브젝트> (조영호 지음, 위키북스 , 2019)
'Book' 카테고리의 다른 글
커맨드와 커맨드 핸들러 (0) | 2023.10.29 |
---|---|
메시지 버스로 통합하기 (2) | 2023.10.22 |
애그리게이트와 일관성 경계 (0) | 2023.10.08 |
작업 단위 패턴 (0) | 2023.10.05 |
MySQL 퍼포먼스 최적화 (1~3장) (0) | 2023.08.17 |