🎁 모든 강의 30% + 무료 강의 선물🎁

워밍업 클럽 3기 BE 클린코드&테스트 - 1주차 발자국

워밍업 클럽 3기 BE 클린코드&테스트 - 1주차 발자국

회고

저는 그동안 제가 이상적인 코드를 작성하고 있다고 생각했습니다. 나름 많은 고민을 코드에 녹여냈고 가독성을 항상 신경썼습니다. 하지만 이번 강의를 통해서 제가 많은 부분들을 몰랐다는 점에서 놀랐습니다. 섹션 3을 통해 전반적인 안 좋은 습관들을 교정해나갈 기회는 저에게 값집니다. 남은 시간들도 기대가 됩니다.

 

강의 내용 요약

인트로

  • 우리는 왜 이 강의를 듣는가

    • 우리는 코드를 읽는 시간을 코드를 쓰는 시간보다 더 많이 할애한다

    • 우리가 읽어야 하는 코드 :

      • 여러 사람이 작성한 코드

      • 내가 한시간 전에 작성한 코드

    • 읽기 좋은 코드는 더 나은 코드 작성을 위해 필수적이다

     

  • 코드를 잘 짠다는 것은?

    • 읽기 좋은 코드를 작성하는 것

    • "코드는 작성한 순간부터 레거시다."

    • 코드의 독자 :

      • 미래의 동료

      • 미래의 나

        • 읽기 어려운 코드는 추후의 모두에게 악영향을 미친다

    • 이 강의에서는 읽기 좋은 코드를 위해 어떤 관점으로 어떻게 접근해야 좋을지 이야기한다

     

    추상

    우리가 클린 코드를 추구하는 이유

  • 클린 코드를 추구함으로써 가독성을 확보할 수 있다

  • 가독성이 높으면 글이 잘 읽힌다

    • = 이해가 잘된다

  • 가독성이 높으면 코드가 잘 읽힌다

    • = 이해가 잘된다

    • = 유지보수하기가 수월하다

    • = 우리의 시간과 자원이 절약된다.

  • 클린 코드를 작성하기 위해 우리는 추상화에 집중해야 한다

     

     

    추상과 구체

  • 추상이란?

    • 어떤 모습에서 형상을 뽑아내는 것

    • 구체적인 정보에서 어떤 이미지를 뽑아내는 것

    • 특정한 측면만을 가려내어 포착하는 것

    • 특정한 측면 외 나머지는 버린다는 것

      • 중요한 정보는 남기고, 덜 중요한 정보는 생략하여 버린다.

     

  • 추상화 레벨

    • 추상화 정도에 따라 레벨이 나뉜다

    • 추상화 레벨이 높을 수록 중요한 부분만 남기고 나머지는 제한다.

     

  • 추상화의 가장 대표적인 행위

    • 이름 짓기

       

    image

    이름 짓기

  • 이름 짓기

    • 프로그래머가 가장 힘들어하는 일

    • 이름을 짓는다는 행위는 추상적 사고를 기반으로 한다.

    • 추상적 사고

      • 표현하고자 하는 구체에서 정말 중요한 핵심 개념만을 추출하여 잘 드러내는 표현

      • 우리 도메인의 문맥 만에서 이해되는 용어

     

  • 이름 짓기 유의 사항

    • 단수와 복수 구분하기

      • 말미에 '-(e)s'를 붙여 어떤 데이터가 단수인지 복수인지를 명확히 하는 것만으로도 읽는 이에게 중요한 정보를 같이 전달할 수 있다.

    • 이름 줄이지 않기

      • 줄임말이라는 것은 가독성을 제물로 바쳐 효율성을 확보하는 것

      • 보통 이름을 줄임으로써 얻는 것보다 잃는 것이 많아 자제하는 것이 좋다

      • 다만 관용어처럼 많은 사람들이 자주 사용하는 줄임말이 있다.

        • 이런 줄임말이 이해될 수 있는 바탕은 문맥에 있다.

           

    • 은어/방언 사용하지 않기

      • 특정 집단에서만 이해될 수 있는 은어 사용 금지

        • 기준 : 새로운 사람이 팀에 합류했을 때 이 용어를 단번에 이해할 수 있는가?

      • 도메인 용어 사용하기

        • 이 경우 도메인 용어를 먼저 정의하는 과정 (ex.도메인 용어 사전)이 선행되어야 할 수 있다

    • 이상적인 표현을 좋은 코드들을 통해 습득하기

      • 비슷한 상황에서 자주 사용하는 단어, 개념 습득하기

      • ex. pool, candidate, threshold 등

       

    메서드와 추상화

  • 한 문단의 주제는 반드시 하나다

  • 잘 쓰여진 코드 또한 하나의 주제만을 가진다

  • 생략할 정보와 의미를 정하고 드러낼 정보를 구분해야 한다.image

    메서드 선언부

  • 메서드명

    • 추상화된 구체를 유추할 수 있는, 적절한 의미가 담긴 이름

    • 파라미터와 연결 지어 더 풍부한 의미를 전달할 수도 있다.

  • 파라미터

    • 파라미터의 타입, 개수, 순서를 통해 의미를 전달

    • 파라미터는 외부 세계와 소통하는 창

  • 반환 타입

    • 메서드 시그니처에 납득이 가는, 적절한 타입의 반환값 돌려주기

      • 메서드의 반환 타입만 보고도 바로 이해가 되어야 한다.

         

    • void 대신 충분히 반환할만한 값이 있는지 고민해보기

      • void로 충분할 경우도 있지만 가급적 반환값 사용하기

      • 반환값을 둘 경우 테스트도 용이해진다.

     

추상화 레벨

  • 하나의 세계 안에서는, 추상화 레벨이 동등해야 한다.

 

매직 넘버, 매직 스트링

  • 상수를 추출한다는 것의 의미

    • 이름을 추출한다는 것은 그 자체로 추상화

    • 상수도 이와 같다

  • 매직 넘버, 매직 스트링이란?

    • 의미를 갖고 있으나, 상수로 추출되지 않은 숫자, 문자열 등

    • 상수 추출로 이름을 짓고 의미를 부여함으로써 다음의 이점들을 확보할 수 있다.

      • 가독성

      • 유지보수성

     

뇌 메모리 적게 쓰기

멀티 태스킹은 곧 저글링을 하는 것과 다름이 없다 (책 '도둑맞은 집중력' 발췌)

  • 사람은 한번에 하나의 일에만 집중할 수 있다.

  • 또한 하던 일을 다른 일로 전환할 경우 그에 따른 전환 비용이 발생한다.

  • 우리가 읽기 가장 좋은 코드는 한번에 읽히는 코드이다.

    • 이를 위해 아래 세가지 요소는 지양해야 한다.

      • 이해하려면 기억해야 하는 정보

      • 낮은 추상화 레벨

      • 불필요한 정보

     

    Early return

  • 조건문을 사용할 때 else if 나 else를 사용할 경우

    • 사용자는 else의 선행 조건을 파악하기 위해 앞선 if와 else if들을 확인해야 한다.

      • 이는 사용자가 이해를 위해 기억력을 할당해야 하는 안 좋은 패턴이다.

  • else if 와 else 는 지양해야 한다.

    • 가급적 지양

    • 코드가 짧은 등의 이유로 가독성의 문제가 없을 경우는 예외이다.

  • 같은 이치로 switch문도 가급적 지양해야 한다.

     

     

사고의 depth 줄이기

  • 중첩 분기문, 중첩 반복문

    • 중첩되는 분기문과 반복문은 함수로 따로 빼는 것이 좋을 수 있다.

    • 함수로 분리함으로써 읽는 사람으로 하여금 사고의 깊이를 줄여 가독성을 높여준다.

    • 다만 간단한 중첩문, 분기문의 경우 오히려 분리하지 않는게 좋다.

  • 사용할 변수는 가까이 선언

    • 사용된 변수가 20줄이 넘어가는 이전의 코드에서 선언될 경우

      • 읽는 입장에선 해당 변수의 존재를 확인하기 위해 다시 20줄 위로 올라가야 한다.

    • 변수 사용부와 선언부를 가까이 하여 가독성을 높이자

 

공백 라인을 대하는 자세

  • 공백 라인도 의미를 가진다.

  • 복잡한 로직의 의미 단위를 나누어 읽는 사람에게 추가적인 정보를 제공할 수 있다.

 

부정어를 대하는 자세

  • 부정 연산자의 경우 가독성이 떨어진다

    • 독자로 하여금 사고의 반전을 강제한다.

  • 부정어 대처법

    • 부정어구를 쓰지 않아도 되는 상황인지 체크

    • 부정의 의미를 담은 다른 단어가 존재하는지 고민

    • 부정어구로 메서드명 구성

 

해피 케이스와 예외 처리

  • 예외를 대하는 자세

    • 예외가 발생할 가능성 낮추기

    • 어떤 값의 검증이 필요한 부분은 주로 외부 세계와의 접점인 점에 유의하기

      • ex. 사용자 입력, 객체 생성자, 외부 서버의 요청 등

    • 의도한 예외와 예상하지 못한 예외를 구분하기

      • 사용자에게 보여줄 예외와 개발자가 직접 보고 처리해야 할 예외 구분

  • Null을 대하는 자세

    • 항상 NullPointException을 방지하는 방향으로 경각심 가지기

    • 메서드 설계 시 return null 자제하기

      • 만약 어렵다면 Optional 사용을 고려

  • Optional을 대하는 자세

    • Optional은 비싼 객체

      • 꼭 필요한 상황에서만 활용

    • Optional을 파라미터로 받지 않도록 한다

      • 이 경우 분기 케이스가 세가지나 된다

        • Null인 경우

        • Null이 아닌 경우

        • Optional 자체가 Null인 경우

    • Optional을 반환 받았다면 최대한 빠르게 해소한다

    • Optional을 해소하는 방법

      • 분기문을 만드는 isPresent()-get() 대신 풍부한 Optional의 API 사용

        • ex. orElseGet(), orElseThrow(), ifPresent(), ifPresentOrElse()

      • orElse(), orElseGet(), orElseThrow()의 차이를 숙지해야 한다

        • orElse() : 항상 실행, 확정된 값일 때 사용

        • orElseGet() : null인 경우 실행, 값을 제공하는 동작 정의

 

추상의 관점으로 바라보는 객체 지향

  • 객체란?

    • 추상화된 [데이터 + 코드]

  • 관심사의 분리

    • 특정한 관심사에 따라서 객체를 만들어낼 수 있다.

    • 관심사에 따라 기능과 책임을 나눈다

    • 나눈 관심사를 바탕으로 어플을 만든다.

      • 이를 통해 유지보수성을 높일 수 있다.

    • 높은 응집도, 낮은 결합도

      • 특정한 관심사끼리 응집도가 높아야 한다

      • 관심사 내의 기능들 간의 결합도가 낮아야 한다

        • 뜻A를 수정했을 때 B가 큰 영향을 받아선 안된다.

 

객체 설계하기

  • 객체로 추상화하기

    • 사용자는 객체의 내부 로직을 알 필요가 없다

       

    • 공개 메서드 선언부를 통해 외부 세계와 소통하고 나머지 필드나 로직들은 비공개를 함으로써 캡슐화한다.

    • 객체의 책임을 나눔으로써 객체 간 협력을 유도한다.

  • 객체가 제공하는 것

    • 절차 지향에서 잘 보이지 않았던 개념의 가시화

    • 관심사를 한 군데에 모음으로써 높은 응집도 확보

      • = 유지보수성 증가

    • 객체를 사용하는 입장에선 구체적인 내부 구현을 신경쓰지 않고 높은 추상화 레벨의 도메인 로직을 다룰 수 있다.

  • 새로운 객체를 만들 때 주의할 점

    • 1개의 관심사로 명확하게 책임이 정의되어 있는 지 확인하기

       

    • setter 사용 자제

      • 객체 내부에서 외부 세계의 개입 없는 방식을 추구함으로써 의도치 않은 버그를 사전에 방지

      • 사용이 필연적일 경우 'set~'이라는 이름 대신 'update~'와 같은 더 명확한 이름을 고려

    • getter 사용 자제

      • setter 와 같은 이치로 직접 꺼내서 사용하기보다 가급적 객체 내에서 해결하게끔 설계

      • getter의 경우 setter와는 달리 필요할 경우 사용해도 된다.

    • 필드의 수 최소화

      • 불필요한 데이터가 늘수록 복잡도가 올라 유지보수성이 낮아진다.

      • 기존의 필드들을 통해 계산할 수 있는 기능들은 메서드를 통해 제공하자.

        • 단, 미리 계산하는 것이 성능상의 이점을 가질 경우 필드로 사용 가능

 

SOLID

SRP

  • 단일 책임 원칙 (Single Responsibility Principle)

    • 하나의 클래스는 단 한가지의 변경 사유만을 가져야 한다

      • 변경 사유 = 책임 = 관심사

    • SRP 원칙을 잘 지킬 경우의 이점 :

      • 관심사의 분리

      • 높은 응집도

      • 낮은 결합도

    OCP

  • 개방-폐쇄 원칙 (Open-Closed Principle)

    • 확장에는 열려있고 수정에는 닫혀 있어야 한다.

    • 기존 코드의 변경 없이, 시스템의 기능을 확장할 수 있어야 한다.

    • 필수 요소 :

      • 추상화

      • 다형성

LSP

  • 리스코프 치환 원칙 (Liskov Substitution Principle

    • 상속 구조에서, 부모 클래스의 인스턴스를 자식 클래스의 인스턴스로 치환할 수 있어야 한다.

      • 자식 클래스는 부모 클래스의 책임을 준수하고

        부모 클래스의 행동을 변경하지 않아야 한다

    • LSP를 위반할 경우의 문제점 :

      • 어플리케이션 오동작

      • 예상 밖의 예외

      • 위 두 문제를 방지하기 위한 불필요한 타입 체크 동반

ISP

  • 인터페이스 분리 원칙 (Interface Segregation Principle)

    • 클라이언트는 자신이 사용하지 않는 인터페이스에 의존하면 안된다.

    • 인터페이스를 잘게 쪼개자.

    • 기능 단위로 인터페이스를 나눠서 사용하자.

    • ISP를 위반할 경우의 문제점 :

      • 불필요한 의존성으로 인한 결합도 상승

    DIP

  • 의존성 역전 원칙 (Dependency Inversion Principle)

    • 상위 수준의 모듈은 하위 수준의 모듈에 의존해서는 안된다.

      • 추상화에 의존해야 한다.

    • 의존성의 순방향 : 고수준 모듈이 저수준 모듈을 참조

    • 의존성의 역방향 : 고수준, 저수준 모듈이 모두 추상화(인터페이스, 추상 클래스)에 의존

    • DIP를 잘 지킬 경우 저수준 모듈이 변경되어도 고수준 모듈에는 영향이 가지 않는다.

 

상속과 조합

  • 상속보다는 조합을 사용해야 한다.

    • 상속은 시멘트처럼 굳어지는 구조다.

    • 상속의 단점

      • 수정이 어려움

      • 부모와 자식 간의 결합도가 높음

        • 부모가 수정될 경우 모든 자식들에게 영향

    • 조합과 인터페이스를 활용하여 유연한 구조를 얻을 수 있다.

    • 상속을 통한 코드 중복 제거가 주는 이점보다 중복이 생기더라도 유연한 구조 설계가 가능한 조합이 주는 이점이 더 크다.

 

Value Object

  • 도메인의 어떤 개념을 추상화하여 표현한 값 객체

  • 값으로 취급하기 위해서 아래 세 가지 요소를 보장해야 한다.

    • 불변성

      • final 필드 사용

      • setter 금지

    • 동등성

      • 서로 다른 인스턴스여도(=동일성이 달라도), 내부의 값이 같으면 같은 값 객체로 취급

      • equals() & hashCode() 재정의 필요

    • 유효성 검증

      • 객체가 생성되는 시점에 값에 대한 유효성 보장

  • VO vs Entity

    • VO와 Entity의 가장 큰 차이점은 식별자 유무이다

    • Entity

      • 식별자가 있다

      • 식별자만 같으면 다른 필드가 달라도 동등한 객체로 취급

      • 식별자가 다르지만 필드가 다를 경우 시간이 지남에 따라 변화한 것으로 취급

    • VO

      • 식별자가 없다

      • 내부의 모든 값이 다 같아야 동등한 객체로 취급

        • 이는 곧, 전체 필드가 식별자 역할을 한다고 볼 수 있다.

 

일급 컬렉션

  • 컬렉션을 포장하면서 컬렉션만을 유일하게 필드로 가지는 객체

    • 컬렉션을 다른 객체와 동등한 레벨로 다루기 위해 사용한다

    • 단 하나의 컬렉션 필드만을 가진다

  • 컬렉션을 추상화하여 의미를 담을 수 있고, 가공 로직의 보금자리가 생긴다.

    • 가공 로직에 대한 테스트도 작성할 수 있다.

  • 만약 컬렉션을 반환해야 할 경우 새로운 컬렉션을 반환해야 한다.

    • 기존의 컬렉션을 변경할 여지를 없앤다.

 

Enum의 특성과 활용

  • Enum은 상수의 집합

    • 상수와 관련된 로직을 담을 수 있는 공간

    • 상태와 행위를 한 곳에서 관리할 수 있는 추상화된 객체

    • 특정 도메인 개념에 대해 그 종류와 기능을 명시적 표현 가능

  • 만약 변경이 잦은 개념은 Enum보다 DB로 관리하는 것이 나을 수 있다.

 

숨겨져 있는 도메인 개념 도출하기

  • 도메인 지식은 만드는 것이 아니라 발견하는 것

  • 객체 지향은 현실을 100% 반영하는 것이 아닌 흉내내는 것이다.

    • 이를 통해 현실 세계에서 쉽게 인지하지 못하는 개념도 도출해서 사용할 수 있다.

  • 완벽한 설계라는 것은 불가능하다

    • 근시적, 거시적 관점에서 최대한 미래를 예측해야 한다

    • 시간이 지나 만약 틀렸다는 것을 인지할 경우를 상정하고 코드를 작성해야 한다.

    미션

    Day 2

    미션 설명

    "추상과 구체"의 강의를 듣고 생각나는 추상과 구체의 예시가 있다면 한번 3~5문장 정도로 적어봅시다. 일상 생활, 자연 현상, 혹은 알고 있는 개발 지식 등 어느 것이든 상관 없습니다.

     

    추상에서 구체로, 또는 구체에서 추상으로 방향은 상관 없으나, 어떤 것이 추상이고 어떤 것이 구체 레벨인지 잘 드러나게 작성해 보아요

    :)

     

    나의 답

  • 1.

    • 추상

      • 코드를 꼽는다

    • 구체

      • 기기에 연결되어 있는 코드를 콘센트의 두 구멍에 맞춰 연결함으로써 전력을 공급한다.

  • 2.

    • 추상

      • 커피를 마신다

    • 구체

      • 커피가 담긴 컵을 손으로 잡아 고정시킨 상태에서 컵의 각도를 조절하여 커피를 입에 주입한다.

  • 3.

    • 추상

      • 지하철을 탄다

    • 구체

      • 지하철 역사의 입구를 찾아 들어간 후 전철에 탑승하여 원하는 정거장에서 하차한다.

 

Day 4

미션 1 설명

1. 아래 코드와 설명을 보고, [섹션 3. 논리, 사고의 흐름]에서 이야기하는 내용을 중심으로 읽기 좋은 코드로 리팩토링해 봅시다.

public boolean validateOrder(Order order) {
    if (order.getItems().size() == 0) {
        log.info("주문 항목이 없습니다.");
        return false;
    } else {
        if (order.getTotalPrice() > 0) {
            if (!order.hasCustomerInfo()) {
                log.info("사용자 정보가 없습니다.");
                return false;
            } else {
                return true;
            }
        } else if (!(order.getTotalPrice() > 0)) {
            log.info("올바르지 않은 총 가격입니다.");
            return false;
        }
    }
    return true;
}

나의 답

public static final String NO_ORDER_ITEM = "주문 항목이 없습니다.";
public static final String INVALID_TOTAL_PRICE = "올바르지 않은 총 가격입니다.";
public static final String NO_USER_INFO = "사용자 정보가 없습니다.";

public boolean validateOrder(Order order) throws OrderException {

    if (order.doesHaveItem()) {
        throw new OrderException(NO_ORDER_ITEM);
    }

    if (order.doesNotHaveValidTotalPrice()) {
        throw new OrderException(INVALID_TOTAL_PRICE);
    }

    if (order.doesNotHaveCustomerInfo()) {
        throw new OrderException(NO_USER_INFO);
    }

    return true;
}

Order

public abstract class Order {

    public abstract boolean doesHaveItem();

    public abstract boolean doesNotHaveValidTotalPrice();

    public abstract boolean doesNotHaveCustomerInfo();
}

변경 사항

  1. if문의 조건들을 하나의 함수로 정의함으로써 조건의 의미를 명확히 했습니다.

  2. if문의 조건에 부합하지 않을 경우 바로 결과를 반환하게 했습니다.

  3. 불필요한 부정 조건을 제거했습니다.

  4. 별개의 예외 처리 클래스를 생성하여 예외를 명확히 했습니다.

  5. 관심사를 기준으로 공백을 두었습니다.

미션 2 설명

SOLID에 대하여 자기만의 언어로 정리해 봅시다.

 

S

  • SRP (단일 책임 원칙)

    • 하나의 클래스는 하나의 책임(=관심사)을 가져야 한다.

    • SRP를 지킴으로써 객체들을 관심사 기준으로 분리할 수 있다.

    • 높은 응집도와 낮은 결합도를 제공한다.

      • 응집도

        • 클래스나 모듈 내 요소들이 긴밀하게 연관되어있는 정도

      • 결합도

        • 한 요소가 변경되었을 때 다른 요소들이 영향을 받는 정도

O

  • OCP (개방-폐쇄 원칙)

    • 확장에는 열려 있고, 수정에는 닫혀 있어야 한다.

    • 기존 코드의 변경 없이도 시스템의 기능을 확장할 수 있어야 한다.

    • 추상화와 다형성을 활용함으로써 구현할 수 있다.

L

  • LSP (리스코프 치환 원칙)

    • 두 클래스가 상속 구조를 가질 때 부모 클래스의 인스턴스를 자식 클래스로 치환하여도 기능 상에 문제가 없어야 한다.

    • 자식 클래스는

      • 부모 클래스의 책임을 준수해야 한다.

      • 부모 클래스의 행동을 변경하지 않아야 한다.

    • LSP를 위반할 경우 아래 문제가 발생할 수 있다.

      • 오동작

      • 예상 밖의 예외

      • 위 두 문제를 방지하기 위한 불필요한 타입 체크 동반

I

  • ISP (인터페이스 분리 원칙)

    • 클라이언트는 자신이 사용하지 않은 인터페이스에 의존하면 안된다.

    • ISP를 위반할 경우

      • 불필요한 의존성으로 인해 결합도가 높아진다.

      • 특정 기능의 변경이 여러 클래스에 영향을 미칠 수 있다.

    • 필요한 기능 단위로 인터페이스를 나눠서 사용해라.

D

  • DIP (의존성 역전 원칙)

    • 상위 수준의 모듈은 하위 수준의 모듈에 직접 의존해서는 안된다.

    • 추상화(인터페이스, 추상 클래스)에 의존해야 한다.

    • 의존성의 순방향

      • 고수준 모듈이 저수준 모듈을 직접 참조

    • 의존성의 역방향

      • 고수준, 저수준 모듈 모두 추상화를 참조

    • DIP를 지킬 경우 저수준 모듈의 변경이 고수준 모듈에 영향을 미치지 않게 된다.

       

댓글을 작성해보세요.


채널톡 아이콘