4 minute read

클린코드의 핵심, 함수를 깔끔하게 작성하는 방법에 대해 알아보자.

1. 작게 만들어라

함수를 만드는 첫번째 규칙은 작게 만드는 것이다. 함수가 작을수록 이해하기 쉽고, 테스트하기 쉽고, 유지보수하기 쉽다.

// Bad - 너무 긴 함수
public void processOrder(Order order) {
    // 주문 검증 (20줄)
    // 재고 확인 (15줄)
    // 결제 처리 (25줄)
    // 배송 준비 (20줄)
    // 알림 전송 (10줄)
}

// Good - 작은 함수들로 분리
public void processOrder(Order order) {
    validateOrder(order);
    checkInventory(order);
    processPayment(order);
    prepareShipment(order);
    sendNotification(order);
}

1.1 블록과 들여쓰기

if/else/while 문 등에 들어가는 블록은 한 줄이어야 한다. 그 한 줄에서 함수를 호출하면 바깥을 감싸는 함수가 작아질 뿐 아니라, 블록 안에서 호출하는 함수 이름을 적절히 짓는다면 코드를 이해하기도 쉬워진다.

// Bad
public void renderPage() {
    if (isTestPage()) {
        // 10줄의 테스트 페이지 렌더링 코드
    } else {
        // 15줄의 일반 페이지 렌더링 코드
    }
}

// Good
public void renderPage() {
    if (isTestPage()) {
        renderTestPage();
    } else {
        renderNormalPage();
    }
}

2. 한 가지만 해라

함수는 한 가지를 해야 한다. 그 한 가지를 잘 해야 한다. 그 한 가지만을 해야 한다.

함수가 한 가지만 하는지 판단하는 방법은 의미 있는 이름으로 다른 함수를 추출할 수 있는지 확인하는 것이다. 추출한 함수가 단순히 다른 표현이 아니라 의미 있는 이름으로 존재한다면, 그 함수는 여러 가지를 하는 셈이다.

// Bad - 여러 가지를 하는 함수
public void emailClients(List<Client> clients) {
    for (Client client : clients) {
        ClientRecord record = database.lookup(client);
        if (record.isActive()) {
            email(client);
        }
    }
}

// Good - 한 가지만 하는 함수들
public void emailActiveClients(List<Client> clients) {
    clients.stream()
           .filter(this::isActiveClient)
           .forEach(this::email);
}

private boolean isActiveClient(Client client) {
    ClientRecord record = database.lookup(client);
    return record.isActive();
}

3. 함수 당 추상화 수준은 하나로

함수가 확실히 한 가지 작업만 하려면 함수 내 모든 문장의 추상화 수준이 동일해야 한다.

// Bad - 추상화 수준이 섞여 있음
public String getHtml() {
    String html = "";
    html += "<html>";  // 낮은 추상화 수준
    html += generatePageContent();  // 높은 추상화 수준
    html += "</html>";  // 낮은 추상화 수준
    return html;
}

// Good - 추상화 수준 통일
public String getHtml() {
    return wrapInHtmlTag(generatePageContent());
}

private String wrapInHtmlTag(String content) {
    return "<html>" + content + "</html>";
}

3.1 위에서 아래로 코드 읽기: 내려가기 규칙

코드는 위에서 아래로 이야기처럼 읽혀야 좋다. 한 함수 다음에는 추상화 수준이 한 단계 낮은 함수가 온다.

4. 서술적인 이름을 사용하라

함수가 하는 일을 좀 더 잘 표현할 수 있도록 서술적인 이름을 선택한다.

// Bad
public void doIt(User u) { ... }
public void handle(Request r) { ... }
public void process(Data d) { ... }

// Good
public void sendWelcomeEmail(User user) { ... }
public void validatePaymentRequest(Request request) { ... }
public void parseJsonToUserProfile(Data data) { ... }

길고 서술적인 이름이 짧고 어려운 이름보다 좋다. 길고 서술적인 이름이 길고 서술적인 주석보다 좋다.

5. 함수 인수

함수에서 이상적인 인수 개수는 0개(무항)다. 다음은 1개(단항)이고, 다음은 2개(이항)다. 3개(삼항)는 가능한 피하는 편이 좋다. 4개 이상(다항)은 특별한 이유가 필요하다.

5.1 많이 쓰는 단항 형식

  • 인수에 질문을 던지는 경우: boolean fileExists("MyFile")
  • 인수를 변환하여 결과를 반환하는 경우: InputStream fileOpen("MyFile")
  • 이벤트 함수: void passwordAttemptFailedNtimes(int attempts)

5.2 플래그 인수

플래그 인수는 추하다. 함수로 boolean 값을 넘기는 것은 함수가 여러 가지를 처리한다고 대놓고 공표하는 셈이다.

// Bad - 플래그 인수 사용
public void render(boolean isSuite) {
    if (isSuite) {
        renderForSuite();
    } else {
        renderForSingleTest();
    }
}

// Good - 함수 분리
public void renderForSuite() { ... }
public void renderForSingleTest() { ... }

5.3 인수 객체

인수가 2-3개 필요하다면 일부를 독자적인 클래스 변수로 선언할 수 있는지 살펴본다.

// Bad
Circle makeCircle(double x, double y, double radius);

// Good
Circle makeCircle(Point center, double radius);

6. 부수 효과를 일으키지 마라

부수 효과는 거짓말이다. 함수에서 한 가지를 하겠다고 약속하고 남몰래 다른 짓을 하는 것이다.

// Bad - checkPassword 인데 세션 초기화라는 부수 효과가 있음
public boolean checkPassword(String userName, String password) {
    User user = UserGateway.findByName(userName);
    if (user != null) {
        if (user.checkPassword(password)) {
            Session.initialize();  // 부수 효과!
            return true;
        }
    }
    return false;
}

// Good - 이름을 명확하게 변경
public boolean checkPasswordAndInitializeSession(String userName, String password) {
    // ...
}

7. 명령과 조회를 분리하라

함수는 뭔가를 수행하거나 뭔가에 답하거나 둘 중 하나만 해야 한다.

// Bad - 명령과 조회가 섞임
public boolean set(String attribute, String value);

// 사용시 혼란
if (set("username", "luke")) { ... }

// Good - 분리
public boolean attributeExists(String attribute);
public void setAttribute(String attribute, String value);

// 사용시 명확
if (attributeExists("username")) {
    setAttribute("username", "luke");
}

8. 오류 코드보다 예외를 사용하라

오류 코드를 반환하면 호출자는 오류 코드를 곧바로 처리해야 하는 문제에 부딪힌다.

// Bad - 오류 코드 사용
if (deletePage(page) == E_OK) {
    if (registry.deleteReference(page.name) == E_OK) {
        if (configKeys.deleteKey(page.name.makeKey()) == E_OK) {
            logger.log("page deleted");
        } else {
            logger.log("configKey not deleted");
        }
    } else {
        logger.log("deleteReference failed");
    }
} else {
    logger.log("delete failed");
}

// Good - 예외 사용
try {
    deletePage(page);
    registry.deleteReference(page.name);
    configKeys.deleteKey(page.name.makeKey());
} catch (Exception e) {
    logger.log(e.getMessage());
}

8.1 Try/Catch 블록 뽑아내기

try/catch 블록은 코드 구조에 혼란을 일으키므로 별도 함수로 뽑아내는 편이 좋다.

public void delete(Page page) {
    try {
        deletePageAndAllReferences(page);
    } catch (Exception e) {
        logError(e);
    }
}

private void deletePageAndAllReferences(Page page) throws Exception {
    deletePage(page);
    registry.deleteReference(page.name);
    configKeys.deleteKey(page.name.makeKey());
}

9. 반복하지 마라 (DRY 원칙)

중복은 소프트웨어에서 모든 악의 근원이다. 많은 원칙과 기법이 중복을 없애거나 제어할 목적으로 나왔다.

// Bad - 중복 코드
public void sendEmail(String to) {
    String from = config.getEmailFrom();
    String host = config.getSmtpHost();
    // 이메일 전송 로직 20줄
}

public void sendSMS(String to) {
    String from = config.getSmsFrom();
    String host = config.getSmsHost();
    // SMS 전송 로직 20줄 (이메일과 유사)
}

// Good - 공통 로직 추출
public void sendEmail(String to) {
    sendMessage(createEmailMessage(to));
}

public void sendSMS(String to) {
    sendMessage(createSmsMessage(to));
}

private void sendMessage(Message message) {
    // 공통 전송 로직
}

10. 결론

함수는 그 언어에서 동사며, 클래스는 명사다. 프로그래밍의 기술은 언제나 언어 설계의 기술이다.

대가 프로그래머는 시스템을 구현할 프로그램이 아니라 풀어갈 이야기로 여긴다. 프로그래밍 언어라는 수단을 사용해 좀 더 풍부하고 좀 더 표현력이 강한 언어를 만들어 이야기를 풀어간다.

처음부터 완벽한 함수를 작성하기는 어렵다. 초안을 작성하고, 테스트 코드를 만들고, 리팩토링하면서 점점 더 좋은 코드로 다듬어 나가자.

Categories:

Updated:

Comments