Skip to content

클린코드 17장 Smells and Heuristics #48

@hyeonjoo

Description

@hyeonjoo

그동안 다뤄온 내용을 간단하게 정리해보는 장이다.

주석

  • 적절한 정보 담기. 코드와 설계에 관하여 technical하게 내용을 적는 공간이어야 한다. 안좋은 예: Source code control system, issue tracking system, 작성자
  • 시간이 지나도 변하지 않는 정보 담기. 코드를 수정하다가 주석 수정은 잊어버릴 수 있기 때문에, 주석은 오래된 정보가 되기 쉽다. 만약 오래된 내용을 발견했다면, 업데이트하거나 삭제한다.
  • 코드와 같은 내용은 담지 않기. 코드로 충분히 알수 있는 내용을 주석에 담는건 과하다.
  • 단어, 문법, 구두법을 신경써서 간결하게 잘 작성하기. 알잘딱깔쎈?
  • 주석처리한 코드를 보았다면 삭제하기. 어차피 source code control system이 기록하고 있으니 그 코드가 필요하다면 언제든 다시 불러올 수 있다.

환경

  • 간단한 빌드와 테스트 단계. 여러 단계를 거치지 않고 단 하나의 command로 전체 빌드, 전체 테스트가 실행되어야 한다.

함수

  • 최소한의 arguments
  • Output arguments 피하기
  • Flag arguments 피하기. Flag arguments를 가지는 함수라면, 그 함수는 하는일이 많다는 뜻이다.
  • 안쓰는 함수 없애기

일반

  • 하나의 소스코드에는 하나의 언어만 담기
  • 예상되는 동작이 담기도록 함수/클래스 구현하기. The Principle of Least Surprise.
  • Boundary condition에 주의하기. 직관에 의존하지 말고 모든 boundary condition을 살피고 테스트도 작성하자.
  • Overridden Safeties. Safety warning 절대 무시하지 않기. 컴파일러, 테스트 실패 warning 등
  • 중복 제거. 이 책의 가장 중요한 법칙 중 하나. DRY(Don't Repeat Yourself). 중복 코드가 있으면, 추상화할 기회라는 것이다. 내가 추상화 단계를 높여두면, 다른 개발자들이 빨리 코딩할 수 있고 에러도 줄어든다. Switch/case, if/else 체인은 다형성(polymorphism)으로 바꿀 수 있다.(?)
  • 추상화 단계에 맞는 코드 짜기. 잘못된 예: 베이스 클래스에 상수, 변수, 유틸함수 같은 구체적인 구현체에 들어가야할 것들을 넣는 것
  • 너무 많은 정보 담지 않기. 모듈이 잘 정의되어있다면 작은 인터페이스만 가질 것이다. 과하게 많은 함수를 갖고 있지 않을 것이므로 커플링정도도 낮을 것이다. 인터페이스에 무엇을 드러낼 것인지에 대한 limit 설정을 잘 하면 좋은 개발자.
  • 변수는 사용되는 함수 근처 가까이에 정의하기.
  • 일관성 갖추기. 비슷한 동작을 한다면 비슷한 방식으로 구현한다. The Principle of Least Surprise.
  • 고의적 커플링 피하기. 종속성이 없는데도 편의를 위해서 커플링하는건 피해야한다.
  • Feature Envy.
public class HourlyPayCalculator {
    public Money calculateWeeklyPay(HourlyEmployee e) {
        int tenthRate = e.getTenthRate().getPennies();
        int tenthsWorked = e.getTenthsWorked();
        int straightTime = Math.min(400, tenthsWorked);
        int overTime = Math.max(0, tenthsWorked - straightTime); int straightPay = straightTime * tenthRate;
        int overtimePay = (int)Math.round(overTime*tenthRate*1.5);
        return new Money(straightPay + overtimePay);
    }
}

calculateWeeklyPayHourlyEmployee의 데이터를 여러번 가져온다. 차라리 이 함수가 HourlyEmployee 안에 있으면 클래스의 데이터를 외부로 노출시키지 않을 수 있게 되고 feature envy를 제거할 수 있게 된다. Feature envy를 제거하는 것이 마냥 좋을 것 같지만 사실 주의를 요하는 작업이다. 다음 예시를 보자.

public class HourlyEmployeeReport {
    private HourlyEmployee employee ;
    public HourlyEmployeeReport(HourlyEmployee e) {
        this.employee = e;
    }
    String reportHours() {
        return String.format(
            "Name: %s\tHours:%d.%1d\n", employee.getName(), employee.getTenthsWorked()/10, employee.getTenthsWorked()%10);
    }
}

reportHoursHourlyEmployee에 있는게 이상적으로 보인다. 하지만 HourlyEmployee는 report format을 알 필요가 없다. feature envy를 제거하면, HourlyEmployee가 report format에 커플링이 생겨버린다.

Java

  • 긴 Import lines 피하기. Bulk import 하기. IDE가 잘 접어줘서 이제는 크게 상관없는데.. 명시적 import가 낫지 않나? 한번 이야기 나누고 싶은 부분.
  • Constant는 상속되지 않게 따로 분리하기. 부모 클래스에 종속된 constant는 계속 상속이 될 수 있고 그럼 자식의 자식의 자식 클래스는 그 constant가 정의된 곳을 찾기 위해 상속 체인을 타고타고 올라가야 한다. 상수는 분리해서 static import하자. 예시: import static PayrollConstants.*;
  • Constant말고 enum쓰기. Java의 enum은 method와 field가 들어갈 수 있으니 문법에 주의할 것. Enum의 표현력과 융통성은 단순 상수(public static final int)보다 강력하다.

네이밍

  • 설명이 충분히 되는 이름을 고르기. 시간을 충분히 들여서 이름을 신중하게 잘 고르면, 코드 구조가 이름에 드러나게 된다. 그렇게 되면 코드가 직관성을 갖게 된다. (It will be pretty much what you expected.)
  • 추상화 정도에 맞는 이름 고르기.
public interface Modem {
    boolean dial(String phoneNumber); boolean disconnect();
    boolean send(char c);
    char recv();
    String getConnectedPhoneNumber();
}

만약 위와 같은 모뎀 인터페이스가 있는데, 어떤 프로그램에서는 dial이 아닌 인터넷 선으로 연결하는 모뎀일 수 가 있다. 그러니 범용성을 갖추기 위해서 위의 모뎀은 다음과 같이 renaming이 필요하다.

public interface Modem {
    boolean connect(String connectionLocator); boolean disconnect();
    boolean send(char c);
    char recv();
    String getConnectedLocator();
}

관련 회사 경험이 하나 있는데... integrate하는 브랜드(Postgre, Microsoft, Amazon 등) 아이콘이 있고, 그 아이콘들은 connection에 쓰이는 것들이라서 prefix가 connection 이었다. 기능 구현팀에서는 이름을 기능 구현 의도를 살려서 그대로 두고 싶은데 디자인시스템 팀에서는 prefix를 logo 로 바꿨다.

  • 회사 모든 개발자가 알만한 단어 사용하기. 개발자들이 아는 term도 좋은데 사내 term이 있다면 그걸 사용하면 더 쉽게 코드를 설명할 수 있다.
  • 애매모호한 이름 쓰지 않기. 예) doRename() 대신 renamePageAndOptionallyAllReferences(). 길어도 이게 낫다.
  • Long names for Long Scopes. 간단한 한 줄짜리 for문 이라면 i를 써도 큰 문제가 없지만 긴 line을 다뤄야한다면 변수명이 길어져야 한다.
  • 인코딩 피하기. m_, f, vis_ for visual imaging system 같은 prefix 피하기.
  • Side effect가 표현되게 이름짓기.
public ObjectOutputStream getOos() throws IOException { if (m_oos == null) {
    m_oos = new ObjectOutputStream(m_socket.getOutputStream()); }
    return m_oos;
}

위의 경우 단순히 Oos(ObjectOutputStream)를 리턴하는 함수가 아니다. ObjectOutputStream이 없으면 새로 만들기도 하기 때문에, getOos보다는 createOrReturnOos라고 이름짓는 게 낫다.

테스트

  • 충분한 테스트. 단순히 "음, 충분한 것 같은데?" 라고 판단하고 넘길 것이 아니라 오류가 발생할 수 있는 모든 곳을 테스트 해야한다.
  • 커버리지 테스트 툴 사용하기
  • 자잘한 테스트 그냥 넘기지 않기
  • Test boundary condition 설정에 주의하기
  • 버그가 발생했다면 그 함수를 철저하게 테스트하기. 버그 하나 발생했다고 하나만 있는게 아닐 수 있다. 아마 몇개 더 찾을걸
  • 실패한 테스트에서 패턴 찾기. 테스트 케이스가 실패한 흐름에서 패턴을 발견할 수 있고, 거기에서 문제를 진단할 수 있다. 예: 모든 테스트에서 input string이 5자를 넘어갔을 때 fail된다.
  • 빠른 테스트

Metadata

Metadata

Assignees

Type

No type

Projects

No projects

Milestone

No milestone

Relationships

None yet

Development

No branches or pull requests

Issue actions