IT Book/클린 코드(Clean Code)

[Clean Code] 3. 함수 - 클린 코드 정독하기

잇트루 2023. 4. 24. 00:04
반응형

과거 포트란(Fortran)과 PL/1 시절에는 시스템을 프로그램, 하위 프로그램, 함수로 나눴다.

현재에는 함수만 살아남았으며, 어떤 프로그램이든 가장 기본적인 단위가 함수다.

 

함수를 잘 이용하면 읽기 쉽고 이해하기 쉬운 코드로 작성이 가능하다. 클린 코드에서는 처음 읽는 사람이 내부 프로그램을 직관적으로 파악할 수 있고, 의도를 분명히 표현하는 함수 작성하는 방법에 대해서 소개한다.

 

 

작게 만들어라!

함수를 만드는 가장 중요한 규칙은 작게 만드는 것이다.

public static String renderPageWithSetupsAndTeardowns(PageData pageData, boolean isSuite) throws Exception {
		if (isTestPage(pageData))
				includeSetupAndTeardownPages(pageData, isSuite);
		return pageData.getHtml();
}
  • if 문/else 문/while 문 등에 들어가는 블록은 한 줄이어야 한다. (이곳에서 함수를 호출)
    • 블록 안에서 호출하는 함수 이름을 적절히 지어야 한다.
  • 함수에서 들여 쓰기 수준은 1단이나 2단을 넘어서면 안된다.

 

 

한 가지만 해라!

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

  • 지정된 함수 이름 아래에서 추상화 수준이 하나인 단계만 수행한다면 그 함수는 한 가지 작업만 한다.
  • 함수를 만드는 이유는 큰 개념을 다음 추상화 수준에서 여러 단계로 나눠 수행하기 위함이다.
  • 의미 있는 이름으로 다른 함수를 추출할 수 있다면 그 함수는 여러 작업을 하는 셈이다.

 

 

함수 당 추상화 수준은 하나로!

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

 

내려가기 규칙

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

 

 

Switch 문

switch 문은 한 가지 작업을 하도록 만들기 어렵고 작게 만들기도 어렵다.

  • 본질적으로 switch 문은 N가지 작업을 처리한다.

하지만, 저 차원 클래스에 숨기고 반복하지 않는 방법은 있다.

 

다음과 같이 swith 문을 사용하는 함수가 있다.

public Money calculatePay(Employee e) throws InvalidEmployeeType {
    switch (e.type) { 
        case COMMISSIONED:
            return calculateCommissionedPay(e); 
        case HOURLY:
            return calculateHourlyPay(e); 
        case SALARIED:
            return calculateSalariedPay(e); 
        default:
            throw new InvalidEmployeeType(e.type); 
    }
}

calculatePay 함수에는 몇 가지 문제가 있다.

  1. 함수가 길다.(새 직원 유형을 추가하면 더 길어진다.)
  2. 한 가지 작업만을 수행하지 않는다.
  3. 단일 책임 원칙(SRP)를 위반한다.
  4. 개방 폐쇄 원칙(OCP)를 위반한다.

 

이 문제를 해결하기 위해 추상 팩토리(Abstract factory)에 숨겨 아무에게도 보여주지 않는 방법이 있다.

public abstract class Employee {
    public abstract boolean isPayday();
    public abstract Money calculatePay();
    public abstract void deliverPay(Money pay);
}
-----------------
public interface EmployeeFactory {
    public Employee makeEmployee(EmployeeRecord r) throws InvalidEmployeeType; 
}
-----------------
public class EmployeeFactoryImpl implements EmployeeFactory {
    public Employee makeEmployee(EmployeeRecord r) throws InvalidEmployeeType {
        switch (r.type) {
            case COMMISSIONED:
                return new CommissionedEmployee(r) ;
            case HOURLY:
                return new HourlyEmployee(r);
            case SALARIED:
                return new SalariedEmploye(r);
            default:
                throw new InvalidEmployeeType(r.type);
        } 
    }
}

 

 

서술적인 이름을 사용하라!

코드를 읽으면서 짐작했던 기능을 각 루틴이 그대로 수행한다면 깨끗한 코드라 할 수 있다. - 워드 커닝햄(Ward Cunningham)

  • 함수가 하는 일을 잘 표현하는 것이 좋은 이름이다.
  • 함수가 작고 단순할수록 서술적인 이름을 고르기도 쉬워진다.
  • 이름을 붙일 때는 일관성이 있어야 한다.
    • 모듈 내에서 함수 이름은 같은 문구, 명사, 동사를 사용한다.
    • includeSetupAndTeardownPages, includeSetupPages, includeSuiteSetupPage, includeSetupPage, …

 

 

함수 인수

함수에서 이상적인 인수 개수는 0개(무항)다. 다음은 1개(단항)고, 다음은 2개(이항)다. 3개(삼항)는 가능한 피하는 편이 좋다. 최선은 입력 인수가 없는 경우이며, 차선은 입력 인수가 1개뿐인 경우다.

 

많이 쓰는 단항 형식

  • 인수에 질문을 던지는 경우
    • ex) boolean fileExists(”MyFile”)
  • 인수를 변환하여 결과를 반환하는 경우
    • InputStream fileOpen(”MyFile”)
    • String 형의 파일 이름을 InputStream으로 변환
  • 단항 함수 형식의 이벤트는 다소 드물게 사용
  • 이 외의 경우에는 단항 함수는 가급적 피한다.

 

플래그 인수

  • 플래그 인수는 함수가 여러 가지를 처리한다고 대놓고 공표하는 셈이다.

 

이항 함수

  • 인수가 2개인 함수는 인수가 1개인 함수보다 이해하기 어렵다.
  • 2항 함수가 적절한 경우
    • Point p = new Point(0, 0)과 같은 좌표계
  • 이항 함수가 무조건 나쁘다는 것은 아니며, 개발을 하다 보면 불가피한 경우도 생긴다.
  • 그럼에도 가능하면 단항 함수로 바꾸도록 애써야 한다.

 

삼항 함수

  • 인수가 3개인 함수는 인수가 2개인 함수보다 훨씬 더 이해하기 어렵다.
  • 삼항 함수를 만들 때는 신중히 고려해야 한다.

 

인수 객체

  • 인수가 2-3개 필요하다면 일부를 독자적인 클래스 변수로 선언할 가능성을 짚어본다.
    • Circle makeCircle(double x, double y, double radius);
    • Circle makeCircle(Point center, double radius);

 

인수 목록

  • 때로는 인수 개수가 가변적인 함수도 필요하다.
    • String.format(”%s worked %.2f hours.”, name, hours);
  • String.format은 사실상 이항 함수이다.
    • public String format(String format, Object… args)
  • 가변 인수를 취하는 함수는 단항, 이항, 삼항 함수로 취급할 수 있지만, 이를 넘어서는 인수를 사용할 경우에는 문제가 있다.

 

동사와 키워드

함수의 의도나 인수의 순서와 의도를 제대로 표현하려면 좋은 함수 이름이 필수다.

  • 단항 함수는 함수와 인수가 동사/명사 쌍을 이뤄야 한다.
    • write(name)
  • 함수 이름에 키워드(인수)를 추가한다.
    • assertExpectedEqualsActual(expected, actual)

 

 

부수 효과를 일으키지 마라!

부수 효과는 거짓말

  • 함수에서 한 가지를 하겠다고 약속하고는 남몰래 다른 것도 한다.
    • 예상치 못하게 클래스 변수를 수정
    • 인수나 시스템 전역 변수를 수정
    • 시간적인 결합이나 순서 종속성을 초래

 

다음은 userName과 password를 인수로 참이면 true, 거짓이면 false를 반환한느 함수이다.

public class UserValidator {
    private Cryptographer cryptographer;
    public boolean checkPassword(String userName, String password) { 
        User user = UserGateway.findByName(userName);
        if (user != User.NULL) {
            String codedPhrase = user.getPhraseEncodedByPassword(); 
            String phrase = cryptographer.decrypt(codedPhrase, password); 
            if ("Valid Password".equals(phrase)) {
                Session.initialize();
                return true; 
            }
        }
        return false; 
    }
}
  • Session.initialize() : 세션을 초기화하는 함수
  • 함수 이름에는 세션을 초기화한다는 사실이 드러나지 않는다.
  • 위 함수를 호출하는 사용자는 사용자를 인증하면서 기존 세션 정보를 지워버릴 수 있다.

 

출력 인수

일반적으로 우리는 인수를 함수 입력으로 해석한다.

  • 출력 인수는 사용하지 않아야 하며, 객체지향 언어에서는 사용할 필요가 거의 없다.
  • appendFotter(s) 보다는 report.appendFooter()

 

 

명령과 조회를 분리하라

함수는 객체 상태를 변경하거나, 객체 정보를 반환하거나 둘 중 하나다.

public boolean set(String attribute, String value);
  • attribute인 속성을 찾아 값을 value로 설정한 후 성공하면 true, 실패하면 false 반환하는 함수

 

if (set(”username”, “unclebob”))…
  • username을 unclebob으로 설정하는 것인지, 설정되어 있음을 확인하는 것인지 모호하다.
  • set이라는 단어가 동사인지 형용사인지 분간하기 어렵기 때문

 

if (attributeExists("username")) {
    setAttribute("username", "unclebob");
    ...
}
  • 명령과 조회를 분리하여 혼란으로부터 방지해야 한다.

 

 

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

명령 함수에서 오류 코드를 반환하는 방식은 명령/조회 분리 규칙을 미묘하게 위반한다.

  • if 문에서 명령을 표현식으로 사용하기 쉽기 때문
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 from registry failed"); 
    } 
} else {
    logger.log("delete failed"); return E_ERROR;
}
  • 위 코드는 동사/형용사 혼란을 일으키지 않는 대신 여러 단계로 중첩되는 코드를 야기한다.
  • 오류 코드를 반환하면 호출자는 오류 코드를 곧바로 처리해야 한다는 문제에 부딪힌다.

 

Try/Catch 블록 뽑아내기

try {
    deletePage(page);
    registry.deleteReference(page.name);
    configKeys.deleteKey(page.name.makeKey());
}
catch (Exception e) {
    logger.log(e.getMessage());
}
  • 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());
}

private void logError(Exception e) { 
    logger.log(e.getMessage());
}
  • 오류를 처리하는 함수는 오류만 처리해야 마땅하다.
  • 함수에 try가 있다면, 해당 함수는 try 문으로 시작해 catch/finally 문으로 끝나야 한다.

 

Error.java 의존성 자석

오류 코드를 반환한다는 것은 클래스, 열거형 변수 등 어디선가 오류 코드를 정의한다는 것이다.

public enum Error {
    OK,
    INVALID,
    NO_SUCH,
    LOCKED,
    OUT_OF_RESOURCES, 	
    WAITING_FOR_EVENT;
}
  • 위와 같은 클래스는 의존성 자석(magnet)이다.
  • 다른 클래스에서 Error enum을 import 해서 사용해야 하므로 Error enum이 변하면, Error enum을 사용하는 클래스 전부를 다시 컴파일하고 다시 배치해야 한다.
    • Error 클래스 변경이 어려워진다.
  • 재컴파일/재배치가 번거롭기에 새 오류코드를 정의하지 않고, 기존 오류 코드를 재사용한다.
  • 오류 코드 대신 예외를 사용하면 새 예외는 Exception 클래스에서 파생된다.
    • 재컴파일/재배치 없이도 새 예외 클래스를 추가할 수 있다.

 

 

반복하지 마라!

  • 중복은 소프트웨어에서 모든 악의 근원이다.
  • 많은 원칙과 기법이 중복을 없애거나 제어할 목적으로 나왔다.
  • 중복을 제거하려는 지속적인 노력이 필요하다.

 

 

구조적 프로그래밍

  • 다익스트라의 구조적 프로그래밍 원칙
    • 모든 함수와 함수 내 모든 블록에 입구(entry)와 출구(exit)는 하나만 존재해야 한다.
    • 즉, 함수는 return 문이 하나여야 한다.
    • 루프 안에서 break나 continue를 사용해선 안되며, goto는 절대로 안된다.
  • 구조적 프로그래밍의 목표와 규율은 공감하지만, 함수가 작다면 위 규칙은 별 이익을 제공하지 못한다.
    • 함수를 작게 만든다면 return, break, continue를 여러 차례 사용해도 괜찮다.
    • 단일 입/출구 규칙보다 의도를 표현하기 쉬워진다.
    • goto 문은 피해야 한다.

 

 

함수를 어떻게 짜죠?

소프트웨어를 짜는 행위는 어느 글짓기와 비슷하다. 처음부터 한 번에 모든 규칙을 지키면서 함수를 작성하는 사람은 없다.

  • 처음에는 길고 복잡하다.
    • 들여 쓰기도 많고 중복된 루프도 많다.
    • 인수 목록도 아주 길다.
    • 이름은 즉흥적이고 코드는 중복된다.
  • 서투른 코드를 빠짐없이 테스트하는 단위 테스트 케이스를 만든다.
  • 이후 코드를 다듬고, 함수를 만들고, 이름을 바꾸고, 중복을 제거한다.
    • 메서드를 줄이고 순서를 바꾸며, 때로는 전체 클래스를 쪼개기도 한다.
    • 이 와중에도 코드는 항상 단위 테스트를 통과한다.

 

 

결론

  • 이 장은 함수를 잘 만드는 기교를 소개한다.
  • 설명한 규칙을 따른다면 길이가 짧고, 이름이 좋고, 체계가 잡힌 함수가 나올 것이다.

 

https://link.coupang.com/a/V3Ggo

 

Clean Code(클린 코드):애자일 소프트웨어 장인 정신

COUPANG

www.coupang.com

이 포스팅은 쿠팡 파트너스 활동의 일환으로, 이에 따른 일정액의 수수료를 제공받습니다.

반응형