본문 바로가기
Java/[Clean Code]

[Clean Code] 2. 함수

2021. 8. 24.
반응형

길이가 길고 중복이 많으며 괴상한 문자열과 낯설고 모호한 자료 유형 및 API로 이루어진 코드는 쉽게 이해하기 어렵다. 메서드 몇 개를 추출하고 이름 몇 개를 변경하고 구조를 조금 바꾸면 훨씬 이해하기 쉬운 코드가 된다. 이번 포스트에서는 함수를 잘 짜는 법에 대해 알아볼 것이다.

 

읽고 이해하기 쉬운 함수 만들기

다양한 규칙을 통해 길고 복잡한 함수를 짧고 이해하기 쉬운 함수로 만들어보자.

 

함수를 작게 만들어라

함수가 작을수록 더 좋다는 증거나 자료를 제시하기는 어렵지만 작은 함수가 좋다고 확신한다. 함수는 재사용 측면뿐 아니라 가독성 측면에서도 작은 단위로 나누는 것이 좋다.

그럼 함수는 얼마나 짧아야 할까? 일반적으로 함수가 한 화면을 넘어가면 안 된다. 요즘은 화면이 크기 때문에 글자가 많이 들어갈 수 있으므로 수치로 얘기해보자면 가로 150자 세로 100줄을 넘어서는 안된다. 사실 20줄도 길다.

블록과 들여 쓰기

중첩 구조가 생길 만큼 함수가 커져서는 안 된다. if 문, else 문, while 문 등에 들어가는 블록은 한 줄이어야 한다. 대체로 여기서 함수를 호출한다. 호출하는 함수의 이름을 잘 짓는다면 코드를 이해하기도 쉬울 것이다.

함수는 한 가지를 해야 한다

함수는 한 가지만 해야 한다는데, 이때 한 가지라는 말의 의미는 무엇일까? 이는 함수 내 모든 문장의 추상화 수준이 동일해야 한다는 의미이다. 또한 의미 있는 이름으로 다른 함수를 추출할 수 있다면 그 함수는 여러 작업을 하는 것이다.

 

함수의 추상화 수준

그러면 추상화 수준은 무엇일까? 우리가 함수를 만드는 이유는 큰 개념을 다음 추상화 수준에서 여러 단계로 나눠 수행하기 위해서라고 한다.

예를 들어 함수 ABC기능 A, B, C를 단계별로 수행하고, 이 기능 각각은 함수가 된다. 그리고 함수 A, B, C는 또 각각 기능 a1, a2/b1, b2/c1, c2 등을 단계별로 수행한다. 이때 함수 A, B, C가 같은 추상화 수준을 가지며 그들의 바로 아래 단계 기능들(a, b, c,...)도 서로 같은 추상화 수준을 갖는다.

 

또한 추상화 수준은 높고 낮음을 표현할 수 있다. 여러 단계로 수행되는 기능이 하나의 이름으로 묶인 함수는 추상화 수준이 높다고 할 수 있고, 더 이상 나눌 수 없는 코드는 추상화 수준이 낮다고 생각할 수 있다.

한 함수 내에 추상화 수준이 섞여 있으면 코드를 읽는 사람이 헷갈리게 된다. 따라서 내려가기 규칙에 따라 위에서 아래로 이야기처럼 읽히는 코드가 좋은 코드이다. 한 함수 다음에는 추상화 수준이 한 단계 낮은 함수가 온다.

위에서 예시를 든 ABC 함수를 생각해보면, 정의하는 함수의 순서는 ABC - A - B - C - a1 - a2 - b1 -... 순서가 될 수도 있고 ABC - A - a1 - a2 - B - b1 - b2 - C -... 순서가 될 수도 있다. 정의하는 순서는 취향 차이로 여길 수 있지만 중요한 점은 위에서 아래로 이야기처럼 읽혀야 한다는 것이다.

 

Switch 문

본질적으로 switch 문은 N가지를 처리한다. 이 경우 반복을 피하기 위해 다형성을 이용해 각 switch 문을 저차원 클래스에 숨기고 절대로 반복하지 않을 수 있다. 아래는 추상 팩토리를 이용해 switch 문을 작성한 코드이다.

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 SalariedEmployee(r);
			default:
				throws new InvalidEmployeeType(r.type);
		}
	}
}

 

서술적인 이름을 사용하라

코드를 읽으면서 짐작했던 기능을 각 루틴이 그대로 수행한다면 깨끗한 코드라 불러도 되겠다.

한 가지만 하는 작은 함수에 좋은 이름을 붙인다면 위 원칙을 이미 절반은 달성했다고 볼 수 있다. 여러 단어를 사용해 함수의 기능을 잘 표현하는 서술적인 이름을 사용해라. 길고 서술적인 이름이 짧고 어려운 이름보다 좋다.

 

함수의 인수

함수에서 이상적인 인수 개수는 0개(무항)이다. 다음은 1개(단항), 다음은 2개(이항)이며, 3개(삼항)는 가능한 피하는 것이 좋고 4개 이상(다항)은 특별한 이유가 필요하며 이유가 있어도 사용하면 안 된다. 다만 이 부분은 객체지향 프로그래밍에서는 중요하지만 다른 방식에서는 크게 중요하지 않을 수 있다.

 

인수는 함수를 이해하기 어렵게 만든다. 코드를 읽는 사람이 현시점에서 별로 중요하지 않은 세부사항을 알아야 한다. 또한 테스트 관점에서는 인수가 늘어날수록 인수마다 유효한 값으로 모든 조합을 구성해 테스트하기가 상당히 부담스러워진다.

단항 형식

  • 위 두 가지 경우를 분명히 구분해서 함수 이름을 지어야 하며 언제나 일관적인 방식으로 사용해야 한다. 두 가지 경우 외에 입력 인수만 있는 이벤트 함수도 단항 함수 형식의 유용한 예시이다.

플래그 인수

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

이항 함수

  • 이항 함수는 단항 함수보다 이해하기 어렵다. 물론 인수 2개가 하나의 값을 표현하는 Point(x, y) 등은 이항 함수가 적절하다. 그렇지만 이를 단항 함수로 바꾸도록 애쓰는 것이 좋다.

인수 객체

  • 객체를 생성해서 인수를 줄이는 방법은 결국 해당 인수에 대한 개념을 표현한 것이므로 단순한 눈속임이 아니다.
  • 예를 들어 makeCircle(double x, double y, double radius)makeCircle(Point point, double radius)로 바꿀 수 있다.

함수뿐만 아니라 다른 곳에서도 인수의 개수가 많아지면 이해하기 어려울 수 있다. 예를 들어 객체의 생성자는 결국 해당 객체가 갖는 모든 필드를 인수로 받아야 할 수 있다. 이때 단순히 필드 값을 나열하는 생성자를 사용하기보다는 Builder를 이용해서 필드를 명확히 알고 값을 할당하는 것이 좋을 것이다.

 

부수 효과를 일으키지 마라

함수가 수행할 것이라고 예상할 수 없는 부수 효과를 일으켜서는 안 된다. 예를 들어 checkPassword라는 이름의 함수에서 비밀번호를 확인한 뒤에 세션을 초기화한다면? 함수를 사용하는 개발자는 세션이 초기화될 거라고 예상하지 못하고 세션을 또다시 초기화할 수도 있다.

또한 이런 함수의 경우에는 세션을 초기화해도 되는 경우에만 함수를 호출할 수 있기 때문에 시간적인 결합이 생거 특정 상황에서 밖에는 사용할 수 없다. 만약 이런 함수가 필요한 경우에는 함수 이름에 반드시 명시해야 한다.

 

인수를 출력으로 사용하면 안 된다. 우리는 대부분 인수를 함수의 입력으로 해석한다. appendFooter(report)에서 report를 변경한다고 예상하기는 어렵다. 만약 함수에서 상태를 변경해야 한다면 함수가 속한 객체 상태를 변경하는 방식으로 해야 한다. 그러면 appendFooter(report)는 report.appendFooter()로 바꿀 수 있다.

 

명령과 조회를 분리하라

함수는 객체 상태를 변경하거나(명령) 객체 정보를 반환하거나(조회) 둘 중 하나만 해야 한다. 만약 분리하기 힘든 경우에는 함수 이름에 반드시 명시해야 한다.

 

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

오류 코드를 반환하는 함수를 호출하면 오류 코드를 곧바로 처리해야 하며, 이는 명령과 조회를 분리하는 규칙을 미묘하게 위반한다. 대신 try/catch문을 사용하면 오류 처리 코드가 원래 코드에서 분리되면서 깔끔해진다. 또한 try/catch도 블록당 한 줄의 코드만 들어가도록 해야 한다.

오류 코드를 반환한다는 것은 어디선가 오류코드를 정의한다는 의미이다. enum으로 오류 코드를 정의하게 되면 enum 클래스와 그에 대한 Error 클래스를 변경하기 어려워지고 프로그래머는 결국 기존에 정의된 오류 코드를 재사용하게 된다.

 

 

참고 서적: <Clean Code(클린 코드):애자일 소프트웨어 장인 정신>

 

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

COUPANG

www.coupang.com

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

728x90
반응형

'Java > [Clean Code]' 카테고리의 다른 글

[Clean Code] 3. 주석  (0) 2021.09.02
[Clean Code] 1. 의미 있는 이름  (0) 2021.08.05

댓글