원티드 주관 9월 백엔드 프리온보딩 챌린지 :: 유연한 코드 설계
강사님 : data 엔지니어 유진호님
프로그래밍 패러다임이란 무엇인가?
이걸 배울 때의 목적은 무엇이고 배우면 무엇이 좋을까??
패러다임이란 사전적 의미로, model of somthing, typical example of something의 의미로, 즉 프로토타입, 모델 등의 어떠한 형태의 구조를 나타낼 때라고 볼 수 있겠다.
프로그래밍이란 문제해결을 목적으로 하는 로직 설계이다.
그렇다면 프로그래밍 패러다임이라는 것은 결과적으로 문제해결을 목적으로 하는 로직 설계의 방법론 쯤 된다는 말이다.
강사님께서 초반에 말씀하셨듯, 현대의 개발 프로세스는 요구사항이 수시로 변하기 때문에 코드가 유연해야할 필요성이 있다고 말씀하셨다.
에러, 고객의 요구사항, 빠르게 바뀌는 트렌드와 시장의 흐름, 등등 빠르게 바뀌는 요구사항에 적응하려면, 시스템이 유기적으로 바뀔 수 있어야 하고, 이의 몫은 개발자에게 달려있다.
그렇다면 코드는 변경에 용이하게 설계되어야하고 구현되어야 한다는 말이다.
이 변경 용이성을 맞추기 위해, 프로그래밍 패러다임, 즉 개발 방법론은 여러 종류의 형태를 타게 되었다.
우리는 그저 OOP를 선택했을 뿐이다.
찾아보니 OOP 뿐 아닌, 함수형 프로그래밍, 마이크로 서비스, Event driven 아키텍쳐 등등이 있었다.
코틀린이 유행하는 이유는 타입 강제화에서 벗어나는 것도 있다만, FP를 지원한다는 점에서도 있는 것 같다.
그렇기에 내가 느끼는 이번 강의의 핵심 목표는 “어떻게 하면 변경 용이한 설계를 할 수 있는가???” 였다.
(강사님께서는 “객체지향의 목표; 오늘은 오류 없이 잘 동작하고 내일은 쉽게 변경할 수 있는 코드” 라고 정리해두셨는데, 참 개발자답게 짧고 굵게 설명하셨다 ㅎㅎ)
ttps://incheol-jung.gitbook.io/docs/study/object/2020-03-10-object-chap3
자신의 상태를 직접 관리하고 스스로의 결정에 따라 행동하는 객체를 자율적인 객체라 한다.
그렇다면 여기서 의문점이 여러가지 든다.
🚧 NOTE
여기서부터는 내가 해석한대로이다. 아직 완벽히 이해했다고 생각하지 않으나, 이렇게 알면 되겠구나 짐작스레 정리해본다.
- 여기서 자신의 상태라하면, 객체 자신의 내부 멤버일 것이다.
- 상태를 직접 관리한다는 것은 이를 내부 멤버의 값을 객체만이 접근하겠다는 것이다. 직접, 그러니까 외부의 조작을 허용하지 않겠다는 소리이다. 이는 캡슐화의 내용으로 이어진다!
- 스스로의 결정에 따른 어떤 행동이라하면, 어떤 행동(비즈니스 로직)을 할지는 나에 따라(객체 본인) 달려있다는 소리다.
즉, 상태에 따라 로직A를 실행할지, 로직B를 실행할지는 본인 메서드의 구현부 안에 정해져있다는 소리다.*** 더 나아가면 팩토리 메서드에 따라 정책을 갈아끼우는 상황도 상상할 수 있다. 😊
Theater시스템의 최종 코드 버전에서의 Bag를 보면 알 수 있다.
public class Bag {
private Long amount;
private final 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 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 boolean hasTicket() {
return 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;
}
}
여기서 hold는 Ticket의 상태에 따라 행동A,행동B로 나뉜다.
이 행동을 바꾸는 결정권자는 Bag 본인이다.
Ticket에 따라 로직이 바뀌는 hold는 Bag의 메서드이기 때문이다.
따라서 “스스로 결정한다”라고 말할 수 있을 것이다.
말을 왜,,,애매하게 적어놔가지고,,,
시스템은 어떤 행동 단위의 움직임들이다. 결국 하나의 큰 로직이 작은 로직들로 쪼개져있는 모습일 뿐이다.
이러한 행동들을 책임에 따라 나누어진 객체들에게 나누어주고, 객체들은 본인이 가진 정보들(멤버)들을 기반하여 행동을 하게된다.
(아마 이를 주체적인 객체라고 정의했을 것이다)
이렇게 설계된 것을 OOP라고 할 것이다.
그렇다면 행동과 협력의 차이점이 무엇일까 곰곰히 생각해보았다.
행동은 객체 내부에 정의되어있다. 객체는 본인의 상태에 따라 캡슐화된 내부 구현을 실행한다. 따라서 “캡슐화 되어있는 멤버값에 따라 내부 메서드를 실행한다”라고 봐도 무방비할 것이다.
협력은 행동이랑 다르다. 협력은 어디인가 정의되어 있는 것이 아니다.
협력은 말 그대로 상호작용이다.
객체 A가 객체 B의 행동C를 호출하고,
B의 책임에 따른 행동C에 따라 A가 결정되거나 다른 행동을 하거나 등등이 될 것이다.
이렇게 의존관계를 띄는 것, 이러한 관계들을 모아모아 시스템을 이루는 것을 협력이라고 하는 것 같다.
그리고 여기서 호출하는 메서드나 인자값을 보통 메세지라고도 말하는 것 같다.
각 객체에 주어진 상태(멤버변수)와 행동들(멤버메서드)에 따른 책임이다.
즉, 해당 객체가 무엇을 알고 있고, 무엇을 할 수 있는가를 정의한다.
내가 알기로는 보통 한 객체에 한 책임을 할당한다. (SRP)
여러 책임은 복합적인 의존관계를 띄기 때문에 변경이 어렵기 때문이다.
동일 책임을 수행하는 객체가 여러 개 있는 경우, 이 책임을 하나의 집합체로서(abstract,interface) 뭉치게 한다.
이렇게 하면 유연하고 재사용 가능한 협력을 얻을 수 있다.

행위를 먼저 작성 :: 어떤 기능을 구현해야하는가
이후, 책임을 작성 :: 협력에 참여하기 위해 객체가 맡고 있는 기능 정의서
역할 :: 동일한 책임을 수행하는 객체가 여러 개 있을 경우, 협력을 개별적으로 만들지 않고, 동일한 책임을 수행하는 대표적인 이름(역할)을 부여
Ex)
결제 금액 계산 :: 책임
⇒ 통신사 할인금액 계산 / 멤버십 할인금액 계산 :: 역할
할인금액을 계산하라고 하는 것은 음료이기 때문이다.
즉, 역할로 뭉쳐진 할인정책을 의존해야 할인금액을 알아낼 수 있기 때문이다.
따라서 음료는 정책을 멤버로 가질 수 밖에 없다

아쉽게도 강의 상에서 위와 같이 보여주셨던 코드는 안 올리신 것 같다
그런데 현실 세계의 객체가 가상 세계의 객체로 넘어오면 수동적 존재였던 객체들은 모두 능동적으로 바뀌게 되고 그 객체에 생명이 깃든다. 그래서 현실 세계에서는 인간이라는 행동주체가 있어야 동작할 수 있던 객체들이 행동주체가 없이도 혼자서 동작할 수 있다.
어떤 클래스를 설계해야하나 따지지말고, 전체적인 숲을 살펴라라는 조언이다.
설계 시, 객체보다 클래스에 신경을 쓰다보면 어떤 속성과 어떤 메서드가 필요한지 신경쓰지 말아야한다.
되려, 협력관계의 객체들을 생각하고, 이 객체들의 상태와 행동들을 판단하자.
클래스를 설계할 때는, 어디까지 외부 객체의 간섭이 들어갈 수 있는지에 대해 주의해야한다.
지나친 허용은 객체를 수동적이게 만들고, 이러한 의존관계는 결합도가 높아 변경 용이하지 못 하기 때문이다.
따라서 메세지에 따라 객체가 상태를 변경할 수 있도록 캡슐화를 해주어야한다.
B객체가 A객체의 인스턴스 변수에 직접 변경해서 값을 변경 ❌
B객체에서 메세지를 날려주면 A객체가 스스로 상태를 변경 ✔
public class Theater {
private final TicketSeller ticketSeller;
public void enter(Audience audience){
// 잘못된 코드 Case#1
// - Theater가 TicketSeller의 인스턴스 변수 ticketOffice에 직접 접근
ticketSeller.getTicketOffice().getTicket();
// 수정된 코드 Case#1
// - TicketSeller 클래스에 getTicketOffice() 삭제;
ticketSeller.sellTo(audience);
...
}
public class TicketSeller {
private TicketOffice ticketOffice;
...
// 내부 구현을 노출하는 인터페이스(=METHOD)는 삭제
// 또는 접근 제한자를 public -> private or protected로 수정
private TicketOffice getTicketOffice(){
return this.ticketOffice;
}
public void sellTo(Audience audience){
// 기존 티켓 구매하는 코드를 여기로 옮긴다.
}
}
지난 강의의 Theater 시스템을 생각해보자.
저번 1강에서 강사님께서 짜신 코드가 잘 이해가 되지 않았다.
“왜 Bag 객체가 티켓이 있는지를 확인할까?? 현실과는 부합하지 않는 로직 아닌가”
public class Audience {
private final Bag bag;
public Audience(Bag bag){
this.bag = bag;
}
public long buy(Ticket t){
return bag.hold(t);
}
}
public class Bag {
private Long amount;
private final 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 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 boolean hasTicket() {
return 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;
}
}
결과를 먼저 말하자면, 이는 각 객체의 자율성을 부과했기 때문이다
(돌이켜보니, 솔직히 읽기 좋은 코드라고는 말 못 하겠지만, 객체의 자율성 측면에서는 좋은 코드라고 생각한다)
https://www.notion.so/vanillacake369/1-b0db09d3d301426fbe0aef2412fae4df
https://incheol-jung.gitbook.io/docs/study/object/2020-03-10-object-chap1
여기서 1) Theater의 의존성이 짙고, 2) 나머지 객체들이 수동적인 부분이라는 이유로 아래와 같이 변경했다.
질문으로 돌아가보자.
“왜 Bag 객체가 티켓이 있는지를 확인할까?? 현실과는 부합하지 않는 로직 아닌가”
이 질문을 바꾸자면 이렇게 바꿀 수 있다.
“왜 현실에서는 수동적인-움직일 수 없는 사물인-Bag 객체가 티켓을 확인하게끔 할까”
이는 협력,책임,역할을 몰라서 그런 것 같다.
위에서 설명했지만, 다시 정리해보자.
책임이란 객체가 무엇을 알고있는가, 무엇을 할 수 있는가를 부여하는 것이다.
협력이란 이렇게 정해진 객체들 간의 상호작용이다.
역할이란, 동일한 책임에 대한 추상 모델이다.
그렇다면 Theater를 책임과 협력에 따라 나눠보자면 아래와 같을 것이다.

따라서 현실 세계를 고려하지 않고, 객체에 자율성을 부과한다는 점을 고려해보았을 때, 가방이 캡슐화된 티켓을 확인한다는 로직은, 전혀 틀린 설계가 아니라는 소리이다.
개인적인 의견이지만, 고객이 티켓을 의존하는 ver2가 좋을지, 가방만이 티켓을 의존하게 하는 ver3가 좋을지는 케바케일 것 같다.
솔직히 ver3는 현실세계와 거리가 멀다.
ver3 코드는 나와 같은 슈퍼두퍼 주니어들이 난독하기 쉬운 코드일 것이다. 설계의 용이성 따위는 무시한 채 왜 이렇게 짰을까 싶을 것이다.
만약 팀A만이 사용하고, 내부에서 협의된 코드라면 해당 코드로 밀어붙이면 가장 좋을 것이다.
그렇지 않다면, 설계 방향을 모르는 사람들에게는 어려운 코드가 될 것이다.
무엇을 선택할지는 케이스 바이 케이스인 것 같다 😅
이걸 보고 나면, 백준 풀기가 이전보다 수월해진다고
찾아보니 토끼책은 1판, 오브젝트책은 2판 개념이다. 토끼책을 먼저 읽어봐야겠다.
아래 두 링크 모두 토끼책과 오브젝트 책에 대해서 서술해놓았는데 좋은 참고인 것 같다. 두고두고 보기를!!