-
오브젝트 1 - 맛보기책책책 책을 읽읍시다/프로그래밍 2023. 2. 7. 22:38
저자 : 조영호
책 이모저모
객체지향 개발의 교과서이다. Java, C# 등 객체지향 언어를 다루는 개발자라면 필독해야 하는 도서이다. '토비의 스프링'을 읽었을 때와 같은 감동을 받았다. 이 책의 핵심은 "퍼블릭 인터페이스를 통해 캡슐화된 자율적인 객체들이 메세지를 주고 받으며 협력하는 코드를 설계하라"이다. 이를 다양한 코드 리팩터링 사례를 보여주면서 유연한 시스템이 어떻게 만들어지는지 설명해주는데 읽으면서 참 잘 와닿았다. 쉽게 전달이 되면서도 그렇다고 가볍지는 않고 고도의 전문 지식을 바탕으로 근거를 제시하니 읽으면서 남는게 많은 느낌이 든다.
저자의 전작인 '객체지향의 사실과 오해'는 말 그대로 객체지향의 해묵은 오해를 깨는 도끼였다면, 이번 저서는 깨진 자리에 새살을 돋게하는 영양제 같은 책이다.
주요 내용 정리
절차지향 vs. 객체지향
한 극장의 티켓 판매 어플리케이션을 각 스타일로 구현해 비교해보자. 초대권을 가지고 있으면 초대권을 티켓으로 바꾸고, 없으면 티켓을 구매해야 한다. 각 객체들의 역할은 아래와 같다.
- Audience : 관람객(구매자)
- Bag : 관람객의 티켓/초대권/현금 관리
- Invitation : 초대권
- Ticket : 티켓
- TicketSeller : 티켓 판매원
- TicketOffice : 티켓 판매소
- Theater : 극장, 관람객의 티켓을 확인하여 입장시킨다.
Audience
public class Audience { private Bag bag; public Audience(Bag bag) { this.bag = bag; } public Bag getBag() { return bag; } }
Bag
public class Bag { private Long amount; private Invitation invitation; private Ticket ticket; public Bag(long amount) { this(null, amount); } public Bag(Invitation invitation, long amount) { this.invitation = invitation; this.amount = amount; } 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; } }
Invitation
import java.time.LocalDateTime; public class Invitation { private LocalDateTime when; }
Ticket
public class Ticket { private Long fee; public Long getFee() { return fee; } }
TicketSeller
public class TicketSeller { private TicketOffice ticketOffice; public TicketSeller(TicketOffice ticketOffice) { this.ticketOffice = ticketOffice; } public TicketOffice getTicketOffice() { return ticketOffice; } }
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; } }
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); } } }
객체들의 관계를 표현하면 아래와 같다.
이 어플리케이션은 변경에 취약한데, 각 객체들의 의존성(dependency)가 지나치게 크기 때문이다. 의존성은 변경과 관련돼 있다. 의존성은 변경에 대한 영향을 암시한다. 의존성이라는 말 속에는 어떤 객체가 변경될 때 그 객체에게 의존하는 다른 객체도 함께 변경될 수 있다는 사실이 내포돼 있다. 객체지향 설계는 서로 의존하면서 협력하는 객체들의 공동체를 구축하는 것이기 때문에 애플리케이션의 기능을 구현하는 데 필요한 최소한의 의존성만 유지하고 불필요한 의존성은 제거해야 한다.
객체 사이의 의존성이 과한 경우를 가리켜 결합도(coupling)가 높다고 말한다. 반대로 객체들이 합리적인 수준으로 의존할 경우에는 결합도가 낮다고 말한다. 결합도는 의존성과 관련돼 있기 때문에 결합도 역시 변경과 관련이 있다. 두 객체 사이의 결합도가 높을수록 함께 변경될 확률도 높아지기 때문에 변경하기 어려워진다. 객체 사이의 결합도를 낮춰 변경이 용이하도록 설계해야 한다.
설계를 변경하기 어려운 이유는 Theater가 Audience와 TicketSeller뿐만 아니라 Audience 소유의 Bag과 TicketSeller가 근무하는 TicketOffice까지 마음대로 접근할 수 있기 때문이다. 해결 방법은 Audience와 TicketSeller가 직접 Bag과 TicketOffice를 처리하는 자율적인 존재가 되도록 설계를 변경하는 것이다.
변경된 코드를 먼저 보고 내용을 파악해보자.
Audience
public class Audience { private Bag bag; public Audience(Bag bag) { this.bag = bag; } public Long buy(Ticket ticket) { return bag.hold(ticket); } }
Bag
public class Bag { private Long amount; private Ticket ticket; private Invitation invitation; public Long hold(Ticket ticket) { if (hasInvitation()) { setTicket(ticket); return 0L; } else { setTicket(ticket); minusAmount(ticket.getFee()); 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; } }
TicketSeller
public class TicketSeller { private TicketOffice ticketOffice; public TicketSeller(TicketOffice ticketOffice) { this.ticketOffice = ticketOffice; } public void sellTo(Audience audience) { ticketOffice.sellTicketTo(audience); } }
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 void sellTicketTo(Audience audience) { plusAmount(audience.buy(getTicket())); } private Ticket getTicket() { return tickets.remove(0); } private void plusAmount(Long amount) { this.amount += amount; } }
Theater
public class Theater { private TicketSeller ticketSeller; public Theater(TicketSeller ticketSeller) { this.ticketSeller = ticketSeller; } public void enter(Audience audience) { ticketSeller.sellTo(audience); } }
Theater의 enter 메서드는 sellTo 메서드를 호출하는 간단한 코드로 바뀌었고, 이전과 같이 거의 모든 객체에 직접 접근하지 않고 ticketSeller와만 통신한다. TicketOffice나 Bag 등에 대한 존재조차 모른다.
밀접하게 연관된 작업만을 수행하고 연관성 없는 작업은 다른 객체에게 위임하는 객체를 가리켜 응집도(cohesion)가 높다고 말한다. 자신의 데이터를 스스로 처리하는 자율적인 객체를 만들면 결합도를 낮출 수 있을 뿐더러 응집도를 높일 수 있다.
객체의 응집도를 높이기 위해서는 객체 스스로 자신의 데이터를 책임져야 한다. 자신이 소유하고 있지 않은 데이터를 이용해 작업을 처리하는 객체에게 어떻게 연관성 높은 작업들을 할당할 수 있겠는가? 객체는 자신의 데이터를 스스로 처리하는 자율적인 존재여야 한다. 그것이 객체의 응집도를 높이는 첫걸음이다. 외부의 간섭을 최대한 배제하고 메세지를 통해서만 협력하는 자율적인 객체들의 공동체를 만드는 것이 훌륭한 객체지향 설계를 얻을 수 있는 지름길인 것이다.
수정 전 Theater의 Audience, TicketSeller, Bag, TicketOffice는 필요한 정보를 제공하고 모든 처리는 enter 메서드에서 하였다. Audience, TicketSeller, Bag, TicketOffice는 데이터(data)이며, enter 메서드는 프로세스(process)이다. 이처럼 프로세스와 데이터를 별도의 모듈에 위치시키는 방식을 절차적 프로그래밍(Procedural Programming)이라고 부른다.
반면 자신의 데이터를 스스로 처리하도록 프로세스의 적절한 단계를 Audience와 TicketSeller로 이동시켰는데, 이와 같이 데이터와 프로세스가 동일한 모듈 내부에 위치하도록 프로그래밍하는 방식을 객체지향 프로그래밍(Object-Oriented Programming)이라고 부른다.
객체지향 설계에서는 Theater와 같은 독재자가 존재하지 않고 각 객체에 책임이 적절하게 분배된다. 각 객체는 자신을 스스로 책임진다. 객체지향 애플리케이션은 스스로 책임을 수행하는 자율적인 객체들의 공동체를 구성함으로써 완성된다. 그리고 단순히 데이터와 프로세스를 하나의 객체 안으로 모으는 것 이상으로 적절한 객체에 적절한 책임을 할당하는 것이 핵심이다.
Audience와 Bag도 인터페이스에만 의존하고 있고, 그외 협력에 직접적으로 참여하지 않는 public 메서드는 모두 private으로 수정하여 캡슐화되었다. TicketSeller와 TicketOffice도 마찬가지로 구현이 아닌 인터페이스에만 의존하도록 수정되었다.
Theater, Bag, TicketOffice는 실세계에서 자율적인 존재가 아니지만 객체지향의 세계에서는 이들을 포함하여 모든 것이 능동적이고 자율적인 존재로 바뀐다. 레베카 워프스브록(Rebecca Wirfs-Brock)은 이처럼 능동적이고 자율적인 존재로 소프트웨어 객체를 설계하는 원칙을 가리켜 의인화(anthropomorphism)라고 부른다.
좋은 설계란 무엇인가?
우리가 짜는 프로그램은 두 가지 요구사항을 만족시켜야 한다. 우리는 오늘 완성해야 하는 기능을 구현하는 코드를 짜야 하는 동시에 내일 쉽게 변경할 수 있는 코드를 짜야 한다(Metz12). 좋은 설계란 오늘 요구하는 기능을 온전히 수행하면서 내일의 변경을 매끄럽게 수용할 수 있는 설계다.
변경을 수용할 수 있는 설계가 중요한 이유는 요구사항이 항상 변경되기 때문이다(Shalloway01). 개발을 시작하는 시점에 구현에 필요한 모든 요구사항을 수집하는 것은 불가능에 가깝다. 모든 요구사항을 수집할 수 있다고 가정하더라도 개발이 진행되는 동안 요구사항은 바뀔 수밖에 없다.
그리고 변경을 수용할 수 있는 설계가 중요한 또 다른 이유는 코드를 변경할 때 버그가 추가될 가능성이 높기 때문이다. 코드를 수정하지 않는다면 버그는 발생하지 않는다. 그렇다. 요구사항 변경은 필연적으로 코드 수정을 초래하고, 코드 수정은 버그 발생 가능성을 높인다. 버그의 가장 큰 문제점은 코드를 수정하려는 의지를 꺾는다는 것이다. 코드 수정을 회피하려는 가장 큰 원인은 두려움이다(Feathers04). 그리고 그 두려움은 요구사항 변경으로 인해 버그를 추가할지도 모른다는 불확실성에 기인한다.
훌륭한 객체지향 설계란 협력하는 객체 사이의 의존성을 적절하게 관리하는 설계다. 세상에 엮인 것이 많은 사람일수록 변하기 어려운 것처럼 객체가 실행되는 주변 환경에 강하게 결하보딜수록 변경하기 어려워진다. 객체 간의 의존성은 애플리케이션을 수정하기 어렵게 만드는 주범이다.
데이터와 프로세스를 하나의 덩어리로 모으는 것은 훌륭한 객체지향 설계로 가는 첫걸음일 뿐이다. 진정한 객체지향 설계로 나아가는 길은 협력하는 객체들 사이의 의존성을 적절하게 조절함으로써 변경에 용이한 설계를 만드는 것이다.
'책책책 책을 읽읍시다 > 프로그래밍' 카테고리의 다른 글
오브젝트 3 - 데이터 중심 설계 vs. 책임 주도 설계 (0) 2023.02.13 오브젝트 2 - 유연한 객체 협력 모델 만들기 (0) 2023.02.08 혼자 공부하는 컴퓨터구조 + 운영체제 - 운영체제 (0) 2023.01.17 혼자 공부하는 컴퓨터구조 + 운영체제 - 컴퓨터 4대 핵심 부품 (1) 2023.01.16 혼자 공부하는 컴퓨터구조 + 운영체제 - 컴퓨터 구조 기본 (1) 2023.01.15