유연한 코드 :: Clean Code

tony·2023년 9월 12일

코드 패러다임

목록 보기
1/6
post-thumbnail

강사님


원티드 주관 9월 백엔드 프리온보딩 챌린지 :: 유연한 코드 설계

강사님 : data 엔지니어 유진호님

공부한 기술은 왜 쓰는가? 🤷‍♂️


컨벤션 ⇒ 클린 코드 ⇒ 읽기 좋은 코드 ⇒ 효율적인 리팩토링 ⇒ 확장에 따른 개발 속도 증가

그래서 어떻게 쓰는가? 🤔


https://brawny-yellowhorn-474.notion.site/Clean-Code-bc5075361bdd4ebd98fd655999e37f73

강사님께서 클린코드(마틴 옹 저)에 대해 직접 예시를 들며 서술한 노션 페이지이다.

이만큼 자세히 설명할 수 없었다…다만 수강을 하며 적었던 노트를 정리하고자 한다!

애매하다면 위 페이지를 참조하자.

We are reader and as writer

개발자란 시스템을-우리가 만들었던 티켓을 끊어주는 시네마 시스템처럼- 만들기 위한 코드를 읽는 사람이자 저술하는 사람이다.

해당 시스템에 필요한 로직을 읽는 독자이거나 혹은 이를 저술하는 작가이다.

What is Clean Code?? How to keep up the rules??

Clean Code should be easily readable

Clean Code 는 Code review할 때, “WTF” 모먼트가 덜 나온다.

그렇다고 이게 Clean Code의 정의인 것은 아니다

Meaningful names

Bad names

⇒ Ex) topMenuGubunPer, 시군구(이건 쓰는 게 유효하다는 의견이 다수)

⇒ 강사님께서 이러한 애매한 이름을 쓰기 전에 어떻게 해야하는지를 회의하는 시간을 많이 가졌다고 한다. (특히 DB 컬럼명,,)

⇒ 테이블 켄벤션 네이밍 컬쳐를 정하든지, 컬럼명 룰을 정하게 되면 이를 위키에 저장하여 메타데이터를 만드는 경우도 있다(회바회,,)

  • 의도를 분명히 밝혀라

  • 불용어를 지양하라

    new Customer();
    // ✅
    new CustomerObejct(); 
    // ❌ 이렇게 Type을 명시하는 것은 불용어(불필요한 용어)를 사용하는 일이다. 
    // 단, 만약 특정 Type만을 (강제)사용해야하는 경우에는 사용해도 무관!
  • 그릇된 정보를 피하라 == 오해를 불러일으킬만한 정보를 담지 말아라

    // List구조가 아닌데 List라는 이름을 사용 -> 개발자가 오해할 수 있음
    String accountList = "송혜교, 전지현, 김태희, ..."; // ❌
  • 발음하기 쉬운 이름으로 정하자

    ⇒ 우리는,,,혼자 개발하지 않는다!

  • 검색하기 쉬운 이름으로 사용하자

    ⇒ 로그를 볼 때 검색을 해야하는데,

  • 타입과 관련된 문자열은 넣지 말자.

    ⇒ 왜????

    ⇒ 추후에 해당 인스턴스(혹은 변수)의 타입이 변경되는 경우에 이름 또한 바꿔줘야 한다!

    ⇒ 예외사항은 존재한다

    ⇒ Inteface / IntefaceImpl

    근데 이것 또한 요즈음은 기피한다고 한다.

  • 동사에 대해서는 되도록 통일화하자

    ⇒ 보통은 팀 내부적으로 논의.

    ⇒ 신입 같은 경우, 레거시 코드를 읽어보게끔 하는데, 이 때, 내부 네이밍 컨벤션의 패턴을 파악할 수 있다.

    ⇒ 신입 같은 경우에,,, 코드에 건의를 하게 되면,,, 팀장이나 정치질 빨래질 당할 수도 있다,,

    ⇒ 어느 정도 자리를 잡기 전까지는,,,주의하자,,,

  • 의미없는 접두사 X

    ⇒ 짧은 변수명도 좋지만, 변수의 의미가 제대로 전달되지 않으면, 변수네이밍의 목적성을 잃는다.

    ⇒ 의미가 제대로 전달되는 긴 변수명을 지향하자.

    boolean isValid;
    boolean isValidTempDirectory;

Function

  • 작게 쪼개라

강사님 Tip


TDD

서비스, 컨트롤러 먼저 설계 X

대신, 요구사항을 테스트문에 녹이고, 그 다음 서비스나 해당 비즈니스 로직들을 설계해나가시기 시작한다고 한다.

테스트문들을 만들고, 테스트문들을 쪼개놓으면, 어떻게 설계할지 눈에 보이신다고 한다.

컨벤션 지키기 ← 팀에 얼만큼 융화되는가

  • 신입을 뽑을 때,
    • 얼만큼 개발을 잘 하는가(X)

    • 팀의 컨벤션에 얼만큼 융화되고 얼만큼 같이 일할 수 있는 사람인가(O)

      단, 기본적인 지식(:: 스프링 기초 & DB 기초)를 안다는 가정하에

Encapsulation ≠ private instance & public getter/setter

Encapsulation ??

https://www.coursera.org/articles/encapsulation

Encapsulation 를 사용하는 이유부터 알아보자.

어떤 행동을 하기 위해서라기보다, 장점이 있기 때문에 사용한다.

Encapsulation 이란

  • 내부의 인스턴스에 대한 외부 접근을 제어(제한)하고
  • 특정 루트로만 강제화

이렇게 하면 여러 장점이 있는데

  • 외부 조작으로부터 보호 ⇒ 외부에서 함부로 조작하여 값이 틀어지는 일을 방지
  • 코드 추상화에 용이 ⇒ 코드 변경에 유연하고, 리팩토링에 용이
  • 비즈니스 로직과 인스턴스를 분리 ⇒ 코드 변경에 유연하고, 리팩토링에 용이

Encapsulation ≠ Abstraction

정리하기 전까지는 Encapsulation과 Abstraction의 명확한 차이를 잘 몰랐다.

하지만 간단히 요약을 하자면, 아래와 같이 정리할 수 있을 것이다.

Encapsulation

: 외부로부터 비즈니스로직만 허용하고-이는 인터페이스로 접근하되-, 직접 접근을 막아 의존성을 낮춤으로서 변경에 유연하게 할 것이냐

Abstraction

: 하나의 동작을 N개의 인스턴스가 N가지의 다른 경우로 사용할 수 있게 함으로서 변경에 유연하게 할 것이냐

  • 표로 정리해보았는데, 확실히 OOP는 말로 정리하기 보다 코드로 설명하는 게 간편하다.
    EncapsulationAbstraction
    어떨 때 ??외부에서 클래스 A의 내부멤버들까지 딥하게 사용한다면 ?
    (= 안다면, has-a 관계라면 )클래스 A의 동작 a를
    N개의 인스턴스가 N가지 방법으로 달리 수행해야 한다면?
    어느 부분에서 악취가 나는가 ??has-a 관계의 어느 인스턴스라도 변경되면 코드 전부가 바뀌어야함동작 a가 고착화되어있어 if-else, switch문을 통해 코드가 계속 추가되어야 함
    ⇒ SRP 위반
    : case문을 통해 인스턴스 선택 & 비즈니스 수행 ⇒ OCP 위반(
    : 코드가 계속 추가되어야 함 |
    | 어떻게 ?? | 클래스 A의 비즈니스 로직 메서드를 외부에 오픈,
    이외 인스턴스 혹은 내부 메서드는 private으로 접근제한 *이렇게 하면 변경에 있어서 has-a관계의 타 인스턴스들의 변경을 최소화할 수 있음 | 클래스 A를 인터페이스화, 추상 팩토리 |

Abstraction에 있어서는 팩토리 메서드 패턴을 참조하거나 강사님의 노션 페이지의 switch문을 추상 팩토리에 숨기기를 보면 될 거 같다.

아직까지는 직접 정리해서 스스로 헷갈려하고 애매해지는 것보다, 좋은 레퍼런스가 있다면 그것을 참조하는 것이 낫다고 본다.

Encapsulation in “Theater Project”

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
    • Bag (audience.getBag())
      • Invitation
  • TicketSeller
    • TicketOffice (ticketSeller.getTicketOffice())
      • Ticket

이렇게 되면 어느 하나라도 변경 혹은 수정이 되면, 이 모든 고착화된 의존관계를 다시 코딩해줘야하는 일이 발생한다.

그렇다면 어떻게 해야할까.

getter를 통해 완전 세부 멤버들을 알게 하는 것보다,

이 세부멤버들은 세부 멤버들을 캡슐화하고-내부 로직에서만 사용할 수 있게-

비즈니스 로직만을 public으로 오픈하자!

즉 아래와 같이 바꾸셨다.

  • Invitation, Bag를 각각 캡슐화
  • Invitation에 따라 값을 지불해야하는 로직을 Bag에 위임, 이를 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을 보면 알 수 있듯이, 각 로직에 필요한 인스턴스들은 캡슐화하였고, 비즈니스 로직에만 사용된다.

이로써 비즈니스 로직과 인스턴스를 분리할 수 있다.

왜 분리하냐고??

분리함에 따라 로직에 대한 변경과 각 인스턴스에 대한 변경의 의존관계를 낮출 수 있고,

변화에 용이한 코드 작성이 가능하다.

공부하며 든 의문점 & 해결점 💡


현실 로직 ≠ 코딩 세계 로직

현실에서의 로직대로 판단하면 안 된다. 현실에서의 로직처럼 만드는 것이 우리의 일인 것 같다.

현실에서는

  • 먼저 고객이 가방에서 티켓이 있는지 확인하고
  • 서버한테 티켓을 발권해달라고 요청한다.
  • 서버가 티켓값을 세일즈에 추가하고
  • 고객은 가방에 티켓을 담는다.

하지만 코딩 세계에서는

  • 고객이 서버로부터 티켓 가격을 물어보고
  • 가방에 티켓이 있는지 확인하고,
  • 티켓이 있다면 0을 서버에게 리턴하고 , 티켓이 없다면 티켓 가격을 서버에게 리턴한다.
  • 서버는 반환된 티켓값을 세일즈에 추가한다.

레퍼런스 🔍


profile
내 코드로 세상이 더 나은 방향으로 나아갈 수 있기를

0개의 댓글