클린 코드를 읽고

이완희·2022년 10월 3일
0
post-thumbnail

2. 의미 있는 이름

메소드, 함수, 변수, 클래스등의 이름을 작성하는건 늘 어렵다. 나 혼자 본다면 AClass, sumAtoB()등으로 지을 수 있겠으나 협업을 해야하는 프로젝트에는 매우 위험하다. 의미가 명확해야 하고 적당히 길어야 하며, 의도를 충분히 파악 할 수 있어야 한다.
아래 예시는 안좋은 네이밍이다.

public int getRatio(RouteCharge rc) {
        String routePlanTypeCd = rc.getRoutePlanType();
        if ("E".equals(routePlanTypeCd) || "T".equals(routePlanTypeCd)) {
            return calc(rc);
        } else if ("A".equals(routePlanTypeCd) || "D".equals(routePlanTypeCd) || "H".equals(routePlanTypeCd)) {
            return 100;
        } else if ("B".equals(routePlanTypeCd)) {
            return decide(routePlanTypeCd, rc.getDistance());
        } else if ("C".equals(routePlanTypeCd) || "Y".equals(routePlanTypeCd)) {
            return decide(routePlanTypeCd, rc.getDistance());
        }
        return rc.getAdjustmentRatio();
    }

메소드명이 get+변수인데 Getter가 아니라 input값에 따라 결과를 다르게 반환하고 있다.
이는 코드 리뷰에서 지적받았던 사항이여서 수정했다.

public int retrieveAdjustmentRatio(RouteCharge rc) {
        String routePlanTypeCd = rc.getRoutePlanType();
        if ("E".equals(routePlanTypeCd) || "T".equals(routePlanTypeCd)) {
            return calcAdjustmentRatio(rc);
        } else if ("A".equals(routePlanTypeCd) || "D".equals(routePlanTypeCd) || "H".equals(routePlanTypeCd)) {
            return 100;
        } else if ("B".equals(routePlanTypeCd)) {
            return decideWithDistance(routePlanTypeCd, rc.getDistance());
        } else if ("C".equals(routePlanTypeCd) || "Y".equals(routePlanTypeCd)) {
            return decideWithDistance(routePlanTypeCd, rc.getDistance());
        }
        return rc.getAdjustmentRatio();
    }

로직이 변경된건 아니지만 무엇을 처리하는지 보다 명확해 졌다.
내가 추후에 볼때도 "이 메소드는 이런 의도를 가지고 작성했군!"이라고 단번에 알아챌 수 있다.
또한 다른 개발자가 보더라도 의도를 파악하기 더 쉽다.

메소드로만 예시를 들었지만 변수, 함수, 메소드, 클래스등의 이름을 지을 때 보다 고민하고 신중해져야 겠다는 생각이 들었다.

3. 함수

함수는 너무 길면 안된다. 추상화 수준이 너무 다양해도 안된다. 한 번에 구조를 이해할 수 있어야 하고 중복된 코드는 없어야 한다.

위 문장은 3장의 핵심을 잘 나타낸다.

- 작게 만들어라

함수의 라인수가 너무 길면 안된다. max 100줄로 하자. if문이나 while문도 한줄에 들어가야 한다.

- 한 가지만 해라

함수는 한 가지를 해야 한다. 그 한가지를 잘 해야 한다. 그 한 가지만을 해야 한다.
돈 계산을 하는 함수에서 더하기만 하고 뺄셈만 하라는 의미는 아니라고 생각 한다. "돈 계산"만 해야 한다. 추상화 수준을 위해 여러 단계로 나눠 수행해야 한다.
함수 당 추상화 수준은 하나가 되어야 한다.

- 서술적인 이름을 사용해라

2장 내용의 복습이다. 함수명 만으로 무엇을 하고자 하는지 파악해야 한다.
ex)calculateTotalCharge와 같이 동사+명사의 조합이 좋다고 생각한다.
함수의 인수도 2개, 많아야 3개만 쓰지 4개 이상이 되지 않도록 하자. 4개가 넘는다면 Object 객체를 쓰도록 하자.\

- 반복하지 마라

특정 코드나 알고리즘이 반복된다면 함수로 추출해야 한다. 함수의 수가 많아지는 걸 두려워 하지 말고 반복되는 코드가 쓰이는걸 경계하자.

4. 주석

착한 주석과 나쁜 주석이 있다.
착한 주석은 유지보수를 쉽게 만들지만 나쁜 주석은 잘못된 정보를 퍼트릴 수 있다.

- 좋은 주석

  • 법적인 주석
  • 정보를 제공하는 주석
  • 의도를 명료하게 설명하는 주석
  • 결과를 경고하는 주석
  • TODO 주석
  • 중요성을 강조하는 주석

- 나쁜 주석

  • 주절거리는 주석
  • 같은 이야기를 반복하거나 코드를 더럽히기만 하는 의미없는 주석
  • 주석으로 처리하는 코드

5. 형식 맞추기

어찌보면 너무나도 당연하다고 생각하면서 읽은 챕터였다.
들여쓰기 잘하고, 가로 너무 길게 쓰지 말고, 선언 변수는 선언 변수끼리 메소드는 메소드끼리 모아두자 등등의 얘기였다.

가장 중요하다고 생각 했던것은 개념적 유사성이다. 친화도가 높은 코드를 가까이 둬야 한다. 코드는 위에서 아래로 읽힌다.
그러므로 선언한 함수가 위에 있고 선언된 함수가 아래에 있어야 한다.

7.오류처리

으으음 크게 와닿지는 않았던 챕터다.
가장 중요한 부분은 오류 코드보다 예외를 사용하라 부분이다.
if else 중첩에 빠져 에러를 뱉어내는게 아니라 깔끔하게 try catch문을 사용하라는 내용이다.
아래는 실제로 작성한 코드이다.

    @Transactional
    public CommonCommandResponseEx save(List<RouteChargeCalcRequest> params) {
        CommonCommandResponseEx<RouteChargeCalcRequest, Integer> response = new CommonCommandResponseEx();

        Map<Long, RouteChargeCalcRequest> paramContextMap = this.toMap(params);
        routeChargeRepository.findByRouteChargeIdIn(Lists.newArrayList(paramContextMap.keySet())).forEach(data -> {
            RouteChargeCalcRequest request = paramContextMap.get(data.getRouteChargeId());
            try {
                RouteChargeCalcRequest reqResult = routeChargeSaveService.save(data, request);
                response.addSuccess(reqResult);
            } catch (InvalidSpecException e) {
                response.addFail(request.getRowId(), e.getPreparedMessage());
            }
            paramContextMap.remove(data.getRouteChargeId());
        });
        response.addAllFail(this.toRowIdCollection(paramContextMap), PreparedMessages.NOT_EXIST_ID_WARN);
        return response;
    }

예전 버전을 보여주기는 민망하지만 수 많은 if와 else문의 향연이었다. routeChargeSaveService.save()를 가보면

    private void preflightSave(RouteCharge rc) throws InvalidSpecException {
        spec.isInProgressElseThrow(rc);
        spec.isDoneElseThrow(rc);
        spec.isNotApproveElseThrow(rc);
    }

위 코드가 있고 요건사항에 맞게 exception을 던지고 있다. 코드의 라인수도 줄었을 뿐더러 에러의 의미도 명확해졌다.
try-catch문 짱짱

null을 반환하지 마라.

자바나 스프링을 할때 무슨 exception을 많이 마주치세요? 라고 물어보면 대부분 "NPE"요 라고 대답할 것이다.
null처리에 주의하자. null이 아닌 빈 객체를 반환하거나 Optional을 적극 사용하도록 하자.

8.경계

이상한 내용인거 같다. 정리 안하고 넘길래

9.단위 테스트

테스트코드 작성의 중요성은 한 챕터로 끝날 내용이 아니다. 많은 프로젝트가 TDD로 작성되고 있으며 그 중요성은 아무리 강조해도 지나치지 않다.
그러므로 이 챕터에서 내용도 가볍게 읽고 넘긴 뒤에 추후에 테스트 주도 개발을 읽고 정리할 예정이다.

F.I.R.S.T

깨끗한 테스트코드를 작성하기 위한 5가지 규칙이다. 지키도록 하자.

  • Fast: 테스트는 빨리 돌아야 한다.
  • Independent: 테스트는 서로 의존하면 안된다. 각 테스트는 독립적으로 그리고 어떤 순서로 실행해도 괜찮아야 한다.
  • Repetable: 테스트는 어떤 환경에서도 반복 가능해야 한다. 네트워크가 연결이 되지 않은 상태라도 테스트는 돌아야 한다.
  • Self-Validation: 테스트는 bool값을 내야 한다. 성공 아니면 실패만 반환해야 한다.
  • Timely: 테스트는 적시에 작성해야 한다. 단위 테스트는 테스트 하려는 실제 코드를 구현하기 직전에 구현한다.

10. 클래스

클래스를 어떤 원칙을 고수해야 하는지 말하고 있는 챕터이다.

클래스는 작아야 한다!

클래스 크기에 신경써야 한다. 최소한의 기능만 작동해야 하고 중복된 코드가 없어야 한다.
여기서 중요한 SRP(단일 책임 원칙)이 나온다. 클래스나 모듈을 변경할 이유가 단 하나뿐이어야 한다는 원칙이다. 수 많은 책임을 떠안은 클래스를 작성하지 말도록 하자.
또 중요한 응집도도 소개 한다. 클래스는 인스턴스 변수 수가 작아야 한다. 응집도가 높다는 말은 클래스에 속한 메소드와 변수가 서로 의존하며 논리적인 단위로 묶인다는 의미이다.
마지막으로 변경으로부터 격리되어야 한다. 외부 API를 사용하다 보면 이를 사용하는 내 메소드도 변화하기 마련이다. 이 변화가 내 의도대로 변화한다면 문제 없지만 의도하지 않는 결과를 나타낼 수도 있다면 외부 API의 직접적인 사용을 조심해야 한다.

TODO: 예시를 들어서 설명하도록 하자. 추상적이므로 코드없이는 이해하기가 어렵다.

11. 시스템

알기 어려운 설명으로 가득한 챕터이다.
딱히 정리는 하지 않으려고 한다. DDD, Spring의 대한 설명을 얕게 하고 있다.
DDD야 저번에 작성하였으므로 또 하지 않을 것이고 Spring의 설명을 하자면 내용이 너무 방대해지기 때문에 생략한다.

12. 창발성

단순한 설계 규칙 네 가지는 아래와 같다.

(1) 모든 테스트를 실행한다.

가능한 많은 테스트를 작성하자. 결합도가 높으면 테스트를 작성하기 어려우니 DIP 원칙을 적용하고 DI, 인터페이스, 추상화 등과 같은 도구를 사용해 결합도를 낮추도록 하자. 테스트를 작성했다면 코드와 클래스를 정리하자. 리팩토링을 진행할 차례이다.
테스트 코드를 잘 짰다면 리팩토링을 진행해도 문제 없다. 응집도롤 높이고, 결합도를 낮추고, 관심사를 분리하고, 시스템 관심사를 모듈로 나누고,
함수와 클래스 크기를 줄이고, 더 나은 이름을 선택하도록 하자.

(2) 중복을 없애라.

똑같은 코드가 여러 함수에서 사용되고 있다면 중복된 코드를 함수로 추출하여 사용하도록 하자. 깔끔한 시스템을 만들기 위해서라면
몇 줄이라도 중복을 제거하겠다는 의지가 필요하자. 다만, 추출한 함수가 SRP를 위반하고 있지 않은지 확인하자.

(3) 표현하라.

주석을 달고 개발자의 의도를 명확하게 하자. 내가 짠 코드는 반드시 누군가 보고 수정해야 할텐데 제대로 표현하지 않는다면 프로그램은 내가 원하는
방향과 다르게 움직일 것이다. 좋은 이름을 선택하고 함수와 클래스 크기를 가능한 줄이고 표준 명칭을 사용하도록 하자.
오늘 봤던 코드가 내일 보기엔 아닐 수도 있다. 여러번 반복해서 본다면 좋은 코드를 짤 수 있으리라

(4) 클래스와 메소드 수를 최소로 줄여라.

중복을 제거하고 의도를 표현할 때 클래스와 메소드를 최대한 나누라고 했지만 무한정으로 나눌 수는 없다. 그래서 가능한 최소의 수로 줄이라는 것이다.
근데 이게 말이 쉽지 뭐가 최대한 나누면서 최소의 수로 만들라는 것인가? 충분한 경험을 쌓는 수 밖에 없다고 생각한다.

13. 동시성

이건 너무 어렵다...
학부생 OS때 배웠던 스레드에 관한 내용인데 아직 실무에서 접하지도 않았기 때문에 읽히지도 않았고 이해하지도 못했다.
몇번 더 보고 이해할 수 있을때 쯤 다시 정리해보겠다.

profile
인생을 재밌게, 자유롭게

0개의 댓글