클린코드

[클린코드] 3. 함수

beekei 2023. 7. 12. 12:58
반응형

작게 만들어라

함수를 만드는 첫째 규칙은 "작게!"다.

함수를 만드는 둘째 규칙은 "더 작게!"다.

함수가 작을수록 더 좋다는 증거나 자료를 제시하기는 어렵지만 함수는 작고 명확하게 구현해야 한다.

그러면 바깥을 감싸는 함수가 작아질 뿐 아니라, 블록 안에서 호출하는 함수의 이름을 적절히 짓는다면 코드를 이해하기 쉬워질 것이다.

이 말은 중첩 구조가 생길만큼 함수가 켜져서는 안 된다는 뜻이다.

당연한 말이지만, 그래야 함수는 읽고 이해하기 쉬워진다.

 

한 가지만 해라!

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

지정된 함수 이름 아래에서 추상화 수준이 하나인 단계만 수행한다면 그 함수는 한 가지 작업만 한다.

단순히 다른 표현이 아니라 의미있는 이름으로 다름 함수를 추출할 수 있다면 그 함수는 여러 작업을 하는 셈이다.

가끔 함수 내에서 섹션으로 나눠지는 경우가 있는데 한가지 작업만 하는 함수는 자연스럽게 섹션으로 나누기 어렵다.

 

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

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

한 함수 내에 추상화 수준을 섞으면 코드를 읽는 사람이 햇갈린다. 특정 표현이 근본 개념인지 아니면 세부사항인지 구분하기 어려운 탓이다. 

근본 개념과 세부사항을 뒤섞기 시작하면, 깨어진 창문처럼 사람들이 함수에 세부사항을 점점 더 추가한다.

 

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

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

즉 위에서 아래로 프로그램을 읽으면 함수 추상화 수준이 한 번에 한 단계씩 낮아진다. 이것을 내려가기 규칙이라 부른다.

하지만 추상화 수준이 하나인 함수를 구현하기란 쉽지 않다. 핵심은 짧으면서도 한 가지만 하는 함수다.

 

Switch문

switch 문은 작게 만들기 어렵다. 그리고 한가지만 작업하도록 만들기도 힘들다. 본질적으로 switch 문은 N가지를 처리한다.

불행하게도 switch 문을 완전히 피할 방법은 없다.

하지만 각 switch 문을 저차원 클래스에 숨기고 절대로 반벅하지 않는 방법은 있다. 물론 다형성을 이용한다.

상속 관계로 숨긴 후 절대로 다른 코드에 노출하지 않는다.

public abstract class Car {
    public abstract String getBrand();
    public abstract Double getPrice();
    public abstract void engineStart();
    ...
}

public interface CarFactory {
    Car makeCar(String carName) throws InvalidCarName;
}

public class CarFactoryImpl implements CarFactory {
    public Car makeCar(String carName) throws InvalidCarName {
        switch(carName) {
            case "아반테"
                return new Avante();
            case "소나타"
                return new Sonata();
            default:
                throw new InvalidCarName(carName);
        }
    }
}

 

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

함수명은 함수가 하는 일을 좀 더 잘 표현하는 이름으로 사용해야 한다.

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

서술적인 이름을 사용하면 개발자 머릿속에서도 설계가 뚜렷해지므로 코드를 개선하기 쉬워진다.

이름을 붙일 때는 일관성이 있어야 한다. 모듈 내에서 함수 이름은 같은 문구, 명사, 동사를 사용한다.

 

함수 인수

함수에서 이상적인 인수 개수는 0개다. 함수에 인수는 가능한 피하는 편이 좋고 4개 이상일 경우는 특별한 이유가 있어도 사용하면 안 된다.

테스트 관점에서 보았을때 인수가 많아진다면 인수다마 유효한 값으로 모든 조합을 구성해 테스트하기가 상당히 부담스럽다.

  • 어쩔수 없는 경우가 아닌 이상 가능한 단항함수로 바꾸는 노력이 필요하다.
  • 플래그 인수는 절대로 사용해서는 안 된다. 인수에 따라 함수의 기능이 바뀌는 관례는 정말로 끔찍하다.
  • 인수가 2, 3개 필요하다면 일부를 독자적인 클래스 변수로 선언할 가능성을 짚어본다.
  • 가변 인수는 1개에 인수로 취급할 수 있다.
  • 함수의 의도나 인수의 순서를 제대호 표현하려면 좋은 함수명을 사용해야 한다.
// 4개의 인수
public void joinMember(String email, String password, String name, LocalDate birth);
// 독자적인 클래스로 1개의 인수로 바꾼다.
pubilc void joinMember(MemberInfo memberInfo);
// 가변 인수가 존재하지만 이항 삼수로 취급할 수 있다.
public static String format(String format, Object... args) { ... }
// 함수의 의도와 인수의 순서를 알 수 있는 함수명 사용
Product findByProductIdAndProductStatus(Long productId, ProductStatus productStatus);

 

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

함수의 부수 효과는 위에 정리한 한 가지만 해라!라는 약속을 파괴시키는 것이다.

만약 로그인이라는 함수 내부에서 세션을 컨트롤한다면 부수 효과라고 할 수 있다.

함수명으로만 봐서는 내부에서 세션을 등록하는지, 초기화하는지 알 수 없다. 결국 세션을 어디서 컨트롤 하는지 여기저기 함수를 찾아다녀야 하므로 점점 더 혼란스러워 질 것이다.

 

명령과 조회를 분리하라!

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

예를 들어보자.

public boolean joinMember(MemberInfo memberInfo);

 

위 함수는 회원가입을 진행하고 성공하면 true, 실패하면 false를 반환한다.

사용할때는 아래처럼 괴상한 코드로 사용할 수 있다.

if (joinMember(memberInfo)) {
    ...
}

처음 이 코드를 접한 사람은 회원가입을 진행하는 것인지, 회원가입 여부를 확인하는 것인지 분간하기 어렵다.

명령과 조회를 분리해 혼란을 애초에 뿌리뽑아야 한다.

 

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

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

성공과 실패의 여부를 반환하지 않고, 실패할 경우 예외를 사용하면 오류 처리 코드가 원래 코드에서 분리되므로 코드가 깔끔해진다.

그렇다고 try catch 블록을 남발해서는 안 된다.

try catch 블록은 코드 구조에 혼란을 일으키며, 정상 동작과 오류 처리 동작을 뒤 섞는다. 그러므로 별도 함수로 뽑아내는 편이 좋다.

오류 처리도 한 가지 작업에 속한다. 그러므로 오류를 처리하는 함수는 오류만 처리해야 한다.

public void join(MemberInfo memberInfo) {
    try {
        joinMember(memberInfo);
    } catch (Exception e) {
        logger.error(e.getMessage());
    }
}

 

반복하지 마라!

코드 중복은 코드 길이가 늘어날 뿐 아니라 알고리즘이 변하면 여러곳이나 손봐야 하는 문제가 있다.

같은 클래스 내 중복 코드가 있다면 include 방법으로 중복을 최대한 없애야 한다.

간단한 예를 들면 아래와 같다.

public class Member {
    	
    public void updateName(String name) {
        this.name = name; // 중복 코드
    }
    
    public void updateInfo(String name, LocalDate birthday) {
        this.name = name; // 중복 코드
        this.birthday = birthday;
    }
      
}

public class Member {
    	
    public void updateName(String name) {
        this.name = name; 
    }
    
    public void updateInfo(String name, LocalDate birthday) {
        this.updateName(name); // include 방법으로 중복코드 제거
        this.birthday = birthday;
    }
      
}

객체지향 프로그래밍은 중복 코드를 부모 클래스로 몰아 중복을 없앨 수 있다.

구조적 프로그래밍, AOP, COP 모두 어떤면에서 중복 제거 전략이다.

 

구조적 프로그래밍

어떤 프로그래머는 에츠허르 데이크스트라의 구조적 프로그래밍 원칙을 따른다.

데이크스트라는 모든 함수와 함수 내 모든 블록에 입구와 출구가 하나만 존재해야 한다고 말했다.

즉, 함수는 return 문이 하나여야 하고, 루프 안에서 break나 continue를 사용해선 안 되며 goto는 절대로 안된다.

goto란?
프로그래밍 에서 어느 특정 줄 번호나 레이블로 건너뛰거나 돌아갈 때 쓰는 명령이다.
프로그램의 흐름을 바꾸는 가장 기본적인 명령으로, 일부 고급 언어에서 공통적으로 사용되는 명령이다.

구조적 프로그래밍의 목표와 규율은 공감하지만 함수가 아주 클 때만 상당한 이익을 제공한다.

함수를 작게 만든닥면 return, break, continue를 여러 차례 사용해도 괜찮다.

반면 goto 문은 큰 함수에서만 의미가 있으므로 작은 함수에서는 피해야 한다.

 

함수는 어떻게 짜죠?

소프트웨어를 짜는 행위는 여느 글짓기와 비슷하다. 논문이나 기사를 작성할때는 먼저 생각을 기록한 후 읽기 좋게 다듬는다.

코드를 작성할때도 마찬가지다. 처음에는 길고 복잡하며 인수도 많을 것이지만 코드를 다듬고, 함수를 만들고 이름을 바꾸고, 중복을 최대한 제거하고 순서를 바꾼다.

물론 중요한 비지니스 로직은 변경되면 안되므로 테스트 코드를 미리 짜놓고 점차 코드를 정리한다. 코드가 정리하는 와중에 테스트 코드는 모두 통과해야 한다.

계속해서 리팩토링을 진행하다 보면 최종적으로는 원하는 코드가 나올것이다.

처음부터 탁 짜내기는 불가능에 가깝다. 그게 가능한 사람은 없으리라.

 

결론

함수 계층은 시스템에서 발생하는 모든 동작을 설명한다.

함수를 어떻게 만드느냐에 따라 그 시스템을 좀 더 쉽게 파악하고 관리할 수 있게 만든다.

위에서 좋은 함수를 만드는 여러가지 방법을 소개했지만 진짜 목표는 시스템이라는 이야기를 풀어가는데 있다는 사실을 명심해야 한다.

여러분이 작성하는 함수가 분명하고 정확한 언어로 깜끔하게 같이 맞아 떨어져야 시스템이라는 이야기를 풀어가기가 쉬워질 것이다.

 

반응형