원티드 주관 9월 백엔드 프리온보딩 챌린지 :: 유연한 코드 설계
강사님 : data 엔지니어 유진호님
컨벤션 ⇒ 클린 코드 ⇒ 읽기 좋은 코드 ⇒ 효율적인 리팩토링 ⇒ 확장에 따른 개발 속도 증가
https://brawny-yellowhorn-474.notion.site/Clean-Code-bc5075361bdd4ebd98fd655999e37f73
강사님께서 클린코드(마틴 옹 저)에 대해 직접 예시를 들며 서술한 노션 페이지이다.
이만큼 자세히 설명할 수 없었다…다만 수강을 하며 적었던 노트를 정리하고자 한다!
애매하다면 위 페이지를 참조하자.
개발자란 시스템을-우리가 만들었던 티켓을 끊어주는 시네마 시스템처럼- 만들기 위한 코드를 읽는 사람이자 저술하는 사람이다.
해당 시스템에 필요한 로직을 읽는 독자이거나 혹은 이를 저술하는 작가이다.
Clean Code should be easily readable
Clean Code 는 Code review할 때, “WTF” 모먼트가 덜 나온다.
그렇다고 이게 Clean Code의 정의인 것은 아니다
Bad names
⇒ Ex) topMenuGubunPer, 시군구(이건 쓰는 게 유효하다는 의견이 다수)
⇒ 강사님께서 이러한 애매한 이름을 쓰기 전에 어떻게 해야하는지를 회의하는 시간을 많이 가졌다고 한다. (특히 DB 컬럼명,,)
⇒ 테이블 켄벤션 네이밍 컬쳐를 정하든지, 컬럼명 룰을 정하게 되면 이를 위키에 저장하여 메타데이터를 만드는 경우도 있다(회바회,,)
의도를 분명히 밝혀라

불용어를 지양하라
new Customer();
// ✅
new CustomerObejct();
// ❌ 이렇게 Type을 명시하는 것은 불용어(불필요한 용어)를 사용하는 일이다.
// 단, 만약 특정 Type만을 (강제)사용해야하는 경우에는 사용해도 무관!
그릇된 정보를 피하라 == 오해를 불러일으킬만한 정보를 담지 말아라
// List구조가 아닌데 List라는 이름을 사용 -> 개발자가 오해할 수 있음
String accountList = "송혜교, 전지현, 김태희, ..."; // ❌
발음하기 쉬운 이름으로 정하자
⇒ 우리는,,,혼자 개발하지 않는다!
검색하기 쉬운 이름으로 사용하자
⇒ 로그를 볼 때 검색을 해야하는데,
타입과 관련된 문자열은 넣지 말자.
⇒ 왜????
⇒ 추후에 해당 인스턴스(혹은 변수)의 타입이 변경되는 경우에 이름 또한 바꿔줘야 한다!
⇒ 예외사항은 존재한다
⇒ Inteface / IntefaceImpl
근데 이것 또한 요즈음은 기피한다고 한다.
동사에 대해서는 되도록 통일화하자
⇒ 보통은 팀 내부적으로 논의.
⇒ 신입 같은 경우, 레거시 코드를 읽어보게끔 하는데, 이 때, 내부 네이밍 컨벤션의 패턴을 파악할 수 있다.
⇒ 신입 같은 경우에,,, 코드에 건의를 하게 되면,,, 팀장이나 정치질 빨래질 당할 수도 있다,,
⇒ 어느 정도 자리를 잡기 전까지는,,,주의하자,,,

의미없는 접두사 X
⇒ 짧은 변수명도 좋지만, 변수의 의미가 제대로 전달되지 않으면, 변수네이밍의 목적성을 잃는다.
⇒ 의미가 제대로 전달되는 긴 변수명을 지향하자.
boolean isValid;
boolean isValidTempDirectory;

서비스, 컨트롤러 먼저 설계 X
대신, 요구사항을 테스트문에 녹이고, 그 다음 서비스나 해당 비즈니스 로직들을 설계해나가시기 시작한다고 한다.
테스트문들을 만들고, 테스트문들을 쪼개놓으면, 어떻게 설계할지 눈에 보이신다고 한다.
얼만큼 개발을 잘 하는가(X)
팀의 컨벤션에 얼만큼 융화되고 얼만큼 같이 일할 수 있는 사람인가(O)
단, 기본적인 지식(:: 스프링 기초 & DB 기초)를 안다는 가정하에
https://www.coursera.org/articles/encapsulation
Encapsulation 를 사용하는 이유부터 알아보자.
어떤 행동을 하기 위해서라기보다, 장점이 있기 때문에 사용한다.
Encapsulation 이란
- 내부의 인스턴스에 대한 외부 접근을 제어(제한)하고
- 특정 루트로만 강제화
이렇게 하면 여러 장점이 있는데
- 외부 조작으로부터 보호 ⇒ 외부에서 함부로 조작하여 값이 틀어지는 일을 방지
- 코드 추상화에 용이 ⇒ 코드 변경에 유연하고, 리팩토링에 용이
- 비즈니스 로직과 인스턴스를 분리 ⇒ 코드 변경에 유연하고, 리팩토링에 용이
정리하기 전까지는 Encapsulation과 Abstraction의 명확한 차이를 잘 몰랐다.
하지만 간단히 요약을 하자면, 아래와 같이 정리할 수 있을 것이다.
Encapsulation
: 외부로부터 비즈니스로직만 허용하고-이는 인터페이스로 접근하되-, 직접 접근을 막아 의존성을 낮춤으로서 변경에 유연하게 할 것이냐
Abstraction
: 하나의 동작을 N개의 인스턴스가 N가지의 다른 경우로 사용할 수 있게 함으로서 변경에 유연하게 할 것이냐
| Encapsulation | Abstraction | |
|---|---|---|
| 어떨 때 ?? | 외부에서 클래스 A의 내부멤버들까지 딥하게 사용한다면 ? | |
| (= 안다면, has-a 관계라면 ) | 클래스 A의 동작 a를 | |
| N개의 인스턴스가 N가지 방법으로 달리 수행해야 한다면? | ||
| 어느 부분에서 악취가 나는가 ?? | has-a 관계의 어느 인스턴스라도 변경되면 코드 전부가 바뀌어야함 | 동작 a가 고착화되어있어 if-else, switch문을 통해 코드가 계속 추가되어야 함 |
Abstraction에 있어서는 팩토리 메서드 패턴을 참조하거나 강사님의 노션 페이지의 switch문을 추상 팩토리에 숨기기를 보면 될 거 같다.
아직까지는 직접 정리해서 스스로 헷갈려하고 애매해지는 것보다, 좋은 레퍼런스가 있다면 그것을 참조하는 것이 낫다고 본다.
package com.wanted.preonboarding.theater.service.handler;
import lombok.RequiredArgsConstructor;
import org.springframework.stereotype.Component;
@Component
@RequiredArgsConstructor
public class Theater {
public void enter(Audience audience, TicketSeller ticketSeller){
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가 너무 많은 것을 안다는 것이다.
Theater의 enter()에서 너무 많은 인스턴스를 사용한다고 지적하셨다. (안다고,has-a관계라고 표현할 수도 있을 것 같다)
Theater가 사용하는 관계도, 즉 의존관계는 아래와 같을 것이다.
audience.getBag())ticketSeller.getTicketOffice())이렇게 되면 어느 하나라도 변경 혹은 수정이 되면, 이 모든 고착화된 의존관계를 다시 코딩해줘야하는 일이 발생한다.
그렇다면 어떻게 해야할까.
getter를 통해 완전 세부 멤버들을 알게 하는 것보다,
이 세부멤버들은 세부 멤버들을 캡슐화하고-내부 로직에서만 사용할 수 있게-
비즈니스 로직만을 public으로 오픈하자!
즉 아래와 같이 바꾸셨다.
Ticket ticket = ticketSeller.getTicketOffice().getTicket();을 분리할 수 있을 것이다.TicketOffice는 티켓을 발권해서 고객에게(가방에) 전달해주는 역할 뿐이다.
따라서 TicketOffice의 주인인 TicketSeller와 고객으로 의존성을 분리할 수 있을 것이다.
@Component
@RequiredArgsConstructor
public class Theater {
public void enter(Audience audience, TicketSeller ticketSeller){
long ticketFee = ticketSeller.sellTo(audience);
ticketSeller.receivePay(ticketFee);
}
}
public class TicketSeller {
private final TicketOffice ticketOffice;
public TicketSeller(TicketOffice ticketOffice) {
this.ticketOffice = ticketOffice;
}
public long sellTo(Audience a) {
return a.buy(ticketOffice.publishTicket());
}
public void receivePay(long ticketFee){
ticketOffice.increaseSalesAmount(ticketFee);
}
}
public class TicketOffice {
private long amount;
private final List<Ticket> tickets;
public TicketOffice(Long amount, Ticket ... tickets) {
this.amount = amount;
this.tickets = Arrays.asList(tickets);
}
public Ticket publishTicket(){
return getTicket();
}
public void increaseSalesAmount(long amount){
plusAmount(amount);
}
public Ticket getTicket(){
return tickets.get(0);
}
public void minusAmount(long amount) {
this.amount -= amount;
}
private void plusAmount(long amount) {
this.amount += amount;
}
}
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;
}
}
TicketSeller와 Bag을 보면 알 수 있듯이, 각 로직에 필요한 인스턴스들은 캡슐화하였고, 비즈니스 로직에만 사용된다.
이로써 비즈니스 로직과 인스턴스를 분리할 수 있다.
왜 분리하냐고??
분리함에 따라 로직에 대한 변경과 각 인스턴스에 대한 변경의 의존관계를 낮출 수 있고,
변화에 용이한 코드 작성이 가능하다.
현실에서의 로직대로 판단하면 안 된다. 현실에서의 로직처럼 만드는 것이 우리의 일인 것 같다.
현실에서는
하지만 코딩 세계에서는