토마스 쿤이 제안한 패러다임이 과학혁명의 구조를 바라보는 시각을 바꾸었듯이
프로그래밍에도 프로그래밍 패러다임이 있다.
프로그래밍 패러다임은 다음을 의미한다.
- 우리가 해결할 문제를 바라보는 방식
- 프로그램을 작성하는 방법
=> 개발자 공동체가 공유하는 프로그래밍 스타일과 모델
- 프로그래밍 패러다임은 과거의 시각을 폐기하기보다 단점을 보완하는 발전적 행보를 걸어왔다.
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. 객체 지향 설계
설계가 코드를 작성하는 것보다 높은 차원의 창조적인 행위라 생각하는 사람이 많다.
그러나, 설계와 구현을 떨어뜨려 이야기하는 것은 불가하다. 설계는 곧 코드의 배치이며, 코드 작성의 일부이다
그렇다면 좋은 설계란?
오늘 요구하는 기능을 온전히 설계하면서, 내일의 변경을 매끄럽게 수용할 수 있는 설계이다.
객체 사이의 의존성을 적절하게 관리하는 설계다.
'기술 서적' 카테고리의 다른 글
[오브젝트] ch4. 설계 품질과 트레이드 오프 / 데이터 중심 설계의 문제점 (0) | 2024.04.11 |
---|---|
[오브젝트] ch3. 역할, 책임, 협력 (0) | 2024.04.11 |
[오브젝트] ch2. 객체지향 프로그래밍 (0) | 2024.04.09 |
[스터디] 이펙티브 자바 - item4. 인스턴스화를 막으려거든 private 생성자를 사용하라 (0) | 2024.03.12 |
[좋은 코드, 나쁜 코드] ch1-ch2 (0) | 2024.03.05 |