티스토리 뷰
728x90
반응형
서론
- 로버트L. 글래스는 어떠한 책에서 "이론 대 실무" 라는 흥미로운 주제에 관해 개인적인 견해를 밝힌적이 있습니다. 그 글에서는
"이론이 먼저일까?, 실무가 먼저일까?" 라는 주제 였는데 요약하자면 많은 사람들은 이론이 먼저 정립된 후에 실무가 그 뒤를 따라 발전한다고 생각한다고 합니다. 허나 글래스의 입장은 반대였고 이론을 정립할 수 없는 초기에는 실무가 먼저 급속한 발전을 이루고 어느정도 발전된 뒤에 비로소 실무의 실용성을 입증할 수 있는 이론이 서서히 그 모습을 갖춘다고 하였습니다. - 글래스의 결론은 이론보다 실무가 먼저라는 것입니다. 따라서 어떤 분야든 초기 단계에서는 아무것도 없는 상태에서 이론을 정립하기보다는 실무를 관찰한 결과를 바탕으로 이론을 정립하는게 최선이라 합니다.
티켓 판매 애플리케이션 구현하기
// 초대장 클래스
public class Invitation {
private LocalDateTime when; // 초대 일자
}
// 티켓 클래스
@Getter
public class Ticket {
private Long fee; // 가격
}
// 관람객의 가방 클래스
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 this.invitation != null;
}
public boolean hasTicket() {
return this.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;
}
}
// 관람객 클래스
@Getter
@AllArgsConstructor
public class Audience {
private Bag bag; // 가방
}
// 매표소 클래스
public class TicketOffice {
private Long amount; // 매표소가 가지고 있는 금액
private List<Ticket> tickets = new ArrayList<>(); // 매표소가 가지고 있는 티켓들
public TicketOffice(Long amount, List<Ticket> tickets) {
this.amount = amount;
this.tickets.addAll(tickets);
}
public void minusAmount(Long amount) {
this.amount -= amount;
}
public void plusAmount(Long amount) {
this.amount += amount;
}
public Ticket getTicket() {
return tickets.get(0);
}
}
// 매표소에서 일하는 판매원
@Getter
@AllArgsConstructor
public class TicketSeller {
private TicketOffice ticketOffice;
}
// 극장 클래스
@AllArgsConstructor
public class Theater {
private 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 클래스의 enter 메서드를 살펴보자
- 1. 관람객의 가방을 가져와 초대권이 있는지 살펴봅니다.
- 1. 만약 초대권이 있다면 판매원은 매표소에서 티켓을 가져옵니다.
- 2. 관람객의 가방에 티켓을 보관합니다.
- 2. 관람객의 가방을 가져와 초대권이 있나 살펴보았지만 초대권이 없습니다.
- 1. 판매원은 매표소에서 티켓을 가져옵니다.
- 2. 관람객의 가방을 가져와 티켓만큼의 금액을 가져갑니다.
- 3. 판매원은 매표소에가서 티켓만큼의 금액을 보관합니다.
- 4. 관람객은 가방에 티켓을 보관합니다.
무엇이 문제인가
- 로버트 마틴은 "클린 소프트웨어: 애자일 원칙과 패턴 그리고 실천 방법"에서 소프트 웨어 모듈이 가져야 하는 세가지 기능에 설명하고 있습니다. 여기서 모듈이란 프로그램을 구성하는 임의의 요소를 의미합니다.
- 목적 1. 실행중에 제대로 동작하는 것입니다.
- 목적 2. 변경을 위해 존재하는 것입니다. 대부분의 모듈은 생명주기 동안 변경되기 때문에 간단한 작업만으로도 변경이 가능해야 합니다.
- 목적 3. 코드를 읽는 사람과 의사소통하는 것입니다. 모듈은 특별한 훈련이 없어도 개발자가 쉽게 읽고 이해할 수 있어야 합니다.
💡 예상을 빗나가는 코드
- 위의 코드는 무엇이 문제일까? 문제는 관람객과 판매원이 극장의 통제를 받는 수동적인 존재라는 점입니다.
- 만약 우리가 관람객이라면 극장 입장과 동시에 나의 가방을 열어보고 돈을 빼가거나 티켓을 가방에 넣는다면 기부니가 좋을까?
또한 우리가 판매원이라는 관점에서도 극장의 허락도 없이 매표소에 가서 티켓과 현금에 마음대로 접근할 수 있다면 이 또한 맞을까? - 코드를 이해하기 어렵게 만드는 또 한가지 이유가 있습니다. 이 코드를 이해하기 위해서는 극장 입장에서는 너무 많은 것들을 기억하고 있어야 합니다. 이 극장은 관람객에는 가방이 있음을, 그 가방 안에는 현금과 티켓이 있음을, 판매원이 매표소에서 티켓을 판매하고 맾소에 현금과 티켓이 보관되어 있음을 등 많은 책임감을 가지고 극장을 운영하게 됩니다.
💡 변경에 취약한 코드
- 하지만 더 큰 문제는 변경에 취약하다는 점입니다. 이 코드는 관람객이 항상 가방안에 현금과 티켓을 보관하고 가방을 들고다닌다고 가정하고 있습니다. 만약 가방을 가지고 다니지 않고 휴대폰만 가지고 다닌다면 어떻게 해야할까요?
그럼 극장(Theater), 관람객(Audience)의 클래스를 수정해야 합니다. - 이것은 객체 사이의 의존성과 관련된 문제입니다. 의존성이라는 말 속에는 어떤 객체가 변경될 때 그 객체에 의존하고 있는 다른 객체도 변경될 수 있다는 사실이 내포되어 있습니다.
- 객체 사이의 의존성이 강한 경우 결합도(coupling)가 높다고 말합니다. 반대로 객체들이 합리적인 수준으로 의존하고 있는 경우 결합도가 낮다고 표현합니다. 결합도가 높을수록 다른 코드가 함께 변경될 확률도 높아지므로 변경이 어려워집니다.
설계 개선하기
💡 자율성을 높이자.
- 현재는 극장이 많은 책임을 가지고 있습니다. 이 책임을 각자에게 돌려 각자의 일은 각자가 처리하도록 하는것입니다.
- 아래 코드를 보면 극장은 관람객이 입장하면 판매원이 알아서 하도록 책임을 위임합니다.
-> 해당 판매원은 관람객에게 티켓 여부를 살피게 하고 매표소에 현금을 보관합니다.
-> 관람객은 자신의 가방에서 티켓 여부를 확인합니다. 티켓이 없다면 티켓 금액만큼 가방에서 현금을 제외합니다. - 우리는 아래 코드를 통해 자신의 일은 자신이 하도록 세부사항을 감추었으며 이를 캡슐화라고 합니다.
// 극장 클래스
@AllArgsConstructor
public class Theater {
private TicketSeller ticketSeller;
public void enter(Audience audience) {
// 관람객이 입장하면 판매원에게 책임 위임
ticketSeller.sellTo(audience);
}
}
// 매표소에서 일하는 판매원
@AllArgsConstructor
public class TicketSeller {
private TicketOffice ticketOffice;
public void sellTo(Audience audience) {
ticketOffice.plusAmount(audience.buy(ticketOffice.getTicket()));
}
}
// 관람객 클래스
@AllArgsConstructor
public class Audience {
private Bag bag; // 가방
public Long buy(Ticket ticket) {
if (bag.hasInvitation()) {
bag.setTicket(ticket);
return 0L;
} else {
bag.setTicket(ticket);
bag.minusAmount(ticket.getFee());
return ticket.getFee();
}
}
}
💡 그림으로 살펴보기
💡 무엇이 개선되었는가
- 수정된 코드에서는 관람객(Audience)과 판매원(TicketSeller)는 스스로 소지품을 관리합니다. 여기서 중요한 점은 관람객이나 판매원의 내부 코드를 수정하더라도 극장(Theater)는 변경할 필요가 없어지게 됩니다. 또한 관람객이 가방이 아닌 휴대폰을 소지해 스마트 뱅킹을 이용하거나 판매원이 매표소가 아닌 은행에 돈을 보관하도록 만들고 싶으면 해당 클래스의 내부 코드만 수정하면 됩니다.
💡 캡슐화와 응집도
- 여기서 핵심은 객체의 내부 상태를 캡슐화하고 객체 간에 오직 메시지를 통해서만 상호작용 하도록 만드는 것입니다.
코드에서 극장(Theater)은 판매원의 내부에 대해서는 전혀 알지 못합니다. 단지 판매원의 sellTo 메서드를 통해 메시지를 전달하고 응답을 받고 있을 뿐입니다. - 밀접하게 연관된 작업만을 수행하고 연관성이 없는 작업은 다른 객체에게 위임하는 객체를 가리켜 응집도가 높다고 말합니다. 자신의 데이터를 스스로 처리하는 자율적인 객체를 만들면 결합도를 낮출 수 있을뿐더러 응집도를 높일 수 있습니다.
객체의 응집도를 높이기 위해서는 객체 스스로 본인의 데이터를 처리하고 가공해야 합니다.
💡절차지향과 객체지향
- 절차지향 프로그래밍이란 물이 위에서 아래로 흐르는 것처럼 순차적인 처리가 중요시 되며, 프로그램 전체가 유기적으로 연결되도록 만드는 기법입니다.
- 아래는 우리가 처음 만든 극장(Theater) 클래스입니다. 절차지향 프로그래밍처럼 위에서 아래로 흐르며 극장이 관람객, 매표소, 가방 등에 의존하고 있습니다.
- 객체지향 프로그래밍이란 판매원(TicketSeller)과 관람객(Audience)에게 자신의 데이터를 스스로 처리하도록 적절하게 단계를 나누어 이동시키고 자신의 일은 자신이 할 수 있도록 코드를 캡슐화했습니다. 이처럼 데이터와 프로세스가 동일한 모듈 내부에 위치하도록 프로그래밍하는 방식을 객체지향 프로그래밍이라 합니다.
💡 책임의 이동
- 절차지향, 객체지향 두 방식 사이에 근본적인 차이를 만드는 것은 책임의 이동입니다. 두 방식의 차이점을 가장 쉽게 이해할 수 있는 방법은 기능을 처리하는 방법을 살펴보는 것입니다. 절차지향은 작업의 흐름이 주로 극장(Theater)에 의해 제어됩니다. 이를 객체지향 용어를 사용해 표현하면 책임이 극장에 집중되어 있다고 표현할 수 있습니다.
- 변경전 절차지향에서는 극장이 전체적인 작업을 처리했었습니다. 변경 후에는 각 객체가 자신이 맡은 일을 스스로 처리하고 있습니다.
다시 말해 극장에 몰려 있던 책임이 개별로 나누어 진것을 알 수 있습니다. - 객체지향 설계의 핵심은 적절한 객체에 적절한 책임을 할당하는 것입니다. 객체는 다른 객체와의 협력이라는 문맥안에서 특정한 역할을 수행하는데 필요한 적절한 책임을 수행해야 합니다. 따라서 객체가 어떤 데이터를 가지고 있느냐보다는 객체에게 어떤 책임을 부여할것인지에 대해 초점을 맞춰야 합니다.
- 우리는 지금까지의 과정을 토대로 불필요한 세부사항을 객체 내부로 숨기고 캡슐화하는 것은 객체의 자율성을 높이고 응집도 높은 객체들의 공동체를 만들 수 있도록 하였습니다.
💡 더 개선할 수 있다.
- 기존 판매원에서는 매표소에 직접 접근하여 현금을 수정할 수 있었습니다. 하지만 매표소 일은 매표소가 알아서 할 수 있도록 수정하였습니다. 그리고 관람객에서는 관람객이 직접 가방을 열고 티켓을 행방을 알아봤으나 해당 로직을 가방에게 위임하여 알아서 처리할 수 있도록 수정하였습니다.
- 하지만 여기서 매표소(TicketOffice)가 sellTicketTo 메서드 내부에서 티켓(Ticket)에 의존하고 있습니다. 객체에게 자율성을 높이고 응집도 높은 공동체를 만들려했으나 의존성이 추가되었습니다. 트레이드오프의 시점이 왔는데 해당 문제는 상황에 맞게 유연하게 대처해야 합니다.
// 매표소에서 일하는 판매원
@AllArgsConstructor
public class TicketSeller {
private TicketOffice ticketOffice;
public void sellTo(Audience audience) {
ticketOffice.sellTicketTo(audience);
}
}
// 매표소 클래스
public class TicketOffice {
private Long amount; // 매표소가 가지고 있는 금액
private List<Ticket> tickets = new ArrayList<>(); // 매표소가 가지고 있는 티켓들
public TicketOffice(Long amount, List<Ticket> tickets) {
this.amount = amount;
this.tickets.addAll(tickets);
}
public void sellTicketTo(Audience audience) {
plusAmount(audience.buy(getTicket()));
}
private void minusAmount(Long amount) {
this.amount -= amount;
}
private void plusAmount(Long amount) {
this.amount += amount;
}
private Ticket getTicket() {
return tickets.get(0);
}
}
// 관람객 클래스
@AllArgsConstructor
public class Audience {
private Bag bag; // 가방
public Long buy(Ticket ticket) {
return bag.hold(ticket);
}
}
// 관람객의 가방 클래스
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 Long hold(Ticket ticket) {
if (hasInvitation()) {
setTicket(ticket);
return 0L;
} else {
setTicket(ticket);
minusAmount(ticket.getFee());
return ticket.getFee();
}
}
private boolean hasInvitation() {
return this.invitation != null;
}
private boolean hasTicket() {
return this.ticket != null;
}
private void setTicket(Ticket ticket) {
this.ticket = ticket;
}
private void minusAmount(Long amount) {
this.amount -= amount;
}
private void plusAmount(Long amount) {
this.amount += amount;
}
}
객체지향 설계
💡 설계가 왜 필요한가
- 책에서 개인적인 정의가 나오는데 "설계란 코드를 배치하는 것이다[Metz12]" 좋은것 같다.
- 좋은 설계란 요구하는 기능을 온전히 수행하면서 내일의 변경을 매끄럽게 수용할 수 있는 설계하고 말해주고 있습니다.
💡 객체지향 설계
- 지금까지 우리는 데이터와 프로세스를 객체라는 한 덩어리 안으로 밀어 넣었다고 해서 변경하기 쉬운 설계을 얻었다! 라고 할 수 있는 것은 아닙니다. 객체지향 세계에서 애플리케이션은 객체들로 구성되며 애플리케이션의 기능은 객체들 간의 상호작용을 통해 구현되며, 객체들 사이의 상호작용은 객체 사이에 주고 받는 메시지로 표현됩니다.
✔️ 정리
- 휼륭한 객체지향 설계란 협력하는 객체 사이의 의존성을 적절하게 관리하는 설계입니다.
- 객체지향 설계의 핵심은 적절한 객체에 적절한 책임을 할당하는 것입니다.
- 객체는 다른 객체와의 협력이라는 문맥안에서 특정한 역할을 수행하는데 필요한 적절한 책임을 수행해야 합니다. 따라서 객체가 어떠한 데이터를 가지느냐보다 객체에 어떤 책음을 할당할 것인가에 초점을 맞춰야 합니다.
- 불필요한 세부사항을 객체 내부로 캡슐화하는 것은 객체의 자율성을 높이고 응집도 높은 객체들간의 공동체를 만들 수 있습니다.
728x90
반응형
'스터디 > 오브젝트' 카테고리의 다른 글
오브젝트 - 6장 메시지와 인터페이스 (0) | 2022.10.09 |
---|---|
오브젝트 - 5장 책임 할당하기 (0) | 2022.10.07 |
오브젝트 - 4장 설계 품질과 트레이드오프 (0) | 2022.10.02 |
오브젝트 - 3장 역할, 책임, 협력 (1) | 2022.09.30 |
오브젝트 - 2장 객체지향 프로그래밍 (0) | 2022.09.28 |
공지사항
최근에 올라온 글
최근에 달린 댓글
- Total
- Today
- Yesterday
TAG
- @ControllerAdvice
- space based architecture
- transactional outbox pattern spring boot
- spring boot redis 대기열 구현
- pipe and filter architecture
- 레이어드 아키텍처란
- polling publisher spring boot
- redis sorted set으로 대기열 구현
- 람다 표현식
- spring boot 엑셀 다운로드
- java userThread와 DaemonThread
- spring boot excel download paging
- JDK Dynamic Proxy와 CGLIB의 차이
- redis sorted set
- 트랜잭셔널 아웃박스 패턴 스프링부트
- spring boot redisson destributed lock
- spring boot excel download oom
- spring boot redisson 분산락 구현
- pipeline architecture
- spring boot redisson sorted set
- spring boot poi excel download
- microkernel architecture
- transactional outbox pattern
- redis 대기열 구현
- 자바 백엔드 개발자 추천 도서
- service based architecture
- java ThreadLocal
- 트랜잭셔널 아웃박스 패턴 스프링 부트 예제
- 공간 기반 아키텍처
- 서비스 기반 아키텍처
일 | 월 | 화 | 수 | 목 | 금 | 토 |
---|---|---|---|---|---|---|
1 | 2 | |||||
3 | 4 | 5 | 6 | 7 | 8 | 9 |
10 | 11 | 12 | 13 | 14 | 15 | 16 |
17 | 18 | 19 | 20 | 21 | 22 | 23 |
24 | 25 | 26 | 27 | 28 | 29 | 30 |
글 보관함