[클린코드] 3장 함수

wlsh44·2022년 9월 27일
0

클린코드

목록 보기
3/8

작게 만들어라,

작은 함수는 가독성을 높이는 가장 기본적인 방법이다.
괜히 뉴스나 긴 글을 볼 때 사람들이 농담(?)으로 세 줄 요약을 해달라는 말이 나오는 것이 아니다.

함수가 길어지면 자연스럽게 코드를 읽기 전부터 한숨부터 나오고 거북함이 생긴다.

구글에 long code를 키워드로 검색했을 때 나오는 사진이다.
내가 작성하지도 않은 긴 코드를 봐야 하는 상황이 온다면 생각만 해도 끔찍할 것 같다..🙁

한 가지만 해라

위 주제와 거의 같은 내용이라고 생각된다.
함수에 단 하나의 책임만을 부여하고, 그에 맞게 코딩을 하면 자연스레 함수는 작아진다.

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

책에서 강조한 내용이다.

따로 정리하지는 않을 거지만 나중에 나올 주제인 부수 효과를 일으키지 마라와도 같은 내용이다.
한 가지만 하는 코드가 알지도 못하게 다른 효과를 낸다면 그건 한 가지만 하는 코드가 아니다.

그런데 종종 '한 가지'의 기준이 뭔지 헷갈릴 때가 많다.
이는 추상화 수준 이라는 이름으로 다음 주제에서 설명한다.

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

모든 코드에는 추상화 수준이라는 것이 있다.

추상화 수준이란 시스템 수준부터 애플리케이션 수준까지를 단계로 나눈 것을 말한다.
시스템 수준으로 갈 수록 추상화 수준이 낮고, 애플리케이션으로 갈 수록 추상화 수준이 높아진다.

예를 들어

System.out.println("string");

은 시스템과 가까운 함수이므로 낮은 추상화 수준을 갖고, 이를 수행하는 함수

public void printString() {
	System.out.println("string");
}

printString();

은 그보다는 높은 추상화 수준을 갖는다고 할 수 있다.

spring에서 자주 사용되는 controller, service, repository 구조를 생각해도 된다.

유저 인터페이스와 가장 가까운 controller 부터 가장 낮은 추상화 수준인 데이터에 접근하는 repository 까지 서로 다른 추상화 수준을 가지고 있음을 알 수 있다.

하지만 개인적으로 이런 추상화 수준을 나누는게 쉽지 않았다.

책에서는 내려가기 규칙으로 위에서 아래로 이야기처럼 읽히면 추상화 수준이 잘 나눠진다고 한다.
내려가기 규칙으로 다음과 같은 예제를 보여준다.

TO 설정 페이지와 해제 페이지를 포함하려면, 설정 페이지를 포함하고, 테스트 페이지 내용을 포함하고, 해제 페이지를 포함한다.

	TO 설정 페이지를 포함하려면, 슈트이면 슈트 설정 페이지를 포함한 후 일반 설정 페이지를 포함한다.

	TO 슈트 설정 페이지를 포함하려면, 부모 계층에서 "SuiteSetUp"페이지를 찾아 include 문과 페이지 경로를 추가한다.
    ...

이런 식으로 프로그램이 단계별로 추상화 수준이 낮아지면서 읽혀야 한다.

이 외에도 스터디에서 추상화 수준을 나누는 방법에 대해 질문을 했는데 맥락을 파악해서 추상화 수준을 나누라는 답변을 얻었다.

책에서 말하는 내려가기 규칙과 비슷한 말인 것 같지만 개인적으로 책보다 좀 더 와닿았다.

예제의 첫 번째 문장을 보면 TO 설정 페이지와 해제 페이지를 나누기 위한 조건을 여러 맥락으로 나눌 수 있다.

  • 설정 페이지를 포함한다.
  • 테스트 페이지 내용을 포함한다.
  • 해제 페이지를 포함한다.

또 각각의 맥락들을 수행하기 위한 맥락이 존재할 것이고 그렇게 추상화 수준을 일관되게 나눠갈 수 있다.

Switch 문

switch문은 한 문장으로 설명할 수 있을 것 같다.

switch문은 다형성을 위해 딱 한 번만 사용해라

switch문은 분기를 나누는 명령어이다.
그렇게 여러 행동을 하다보면 단일 책임을 갖는 함수를 만들기 힘들어질 수 있다.

책에 있는 예제를 약간 바꾼 코드이다.

public double getGeometryArea(Geometry g) throws InvalidGeometryType {
	switch (g.type) {
		case SQUARE:
			return getSquareArea(g);
		case RECTANGLE:
			return getRectangleArea(g);
		case CIRCLE:
			return getCircleArea(g);
		default:
			throw new InvalidGeometryType(g.type);
}

위 코드는 몇 가지 문제점을 가지고 있다.

  1. 함수가 길거나, 길어질 가능성이 내포되어 있다.
    만약에 넓이를 계산하고 싶은 도형이 늘어난다면 이 함수는 끊임없이 길어질 것이다.
  2. SRP를 위반한다.
    이 함수는 정사각형의 넓이도 구하고, 삼각형의 넓이도 구하고, 원의 넓이도 구한다.
    책임이 너무 많다.
  3. OCP를 위반한다.
    첫 번째 이유의 근거가 되기도 하는데, 만약 넓이를 구하려는 도형이 늘어날수록 case를 추가해줘야 한다. 결국 외부 객체인 Geometry의 변화에 의존하게 되는 코드가 되는 셈이다.
  4. 비슷한 switch문이 반복될 가능성이 많다.
    만약 여기서 둘레를 구하는 함수를 만들려고 하면 또 getXXXRound(g)가 잔뜩 존재하는 함수를 만들어야 한다.

그렇기 때문에 해당 책임들을 Geometry에 주고 switch문은 객체를 생성하는 팩토리 패턴에서만 사용해야 한다.

public interface Geometry {
	double getRound();
	double getArea();
}

public class GeometryFactory {

	public static Geometry makeGeometry(GeometryType type) throws InvalidGeometryType {
		switch (type) {
			case SQUARE:
				return new Square();
			case RECTANGLE:
				return new Rectangle();
			case CIRCLE:
				return new Circle();
			default:
				throw new InvalidGeometryType(type);
	}


g.getRound();
g.getArea();

이렇게 코드가 작성이 되면, 이후에 넓이를 구하거나 둘레를 구하는 책임을 해당 클래스에 넘어간다.
또한 아무리 도형이 추가가 된다고 하더라도 맨 처음 코드처럼 쓸데없이 길어지지 않는다.
그리고 getRound(), getArea()의 내부가 변경이 되어도 이를 사용하는 함수는 전혀 수정할 필요가 없어 OCP도 지킬 수 있게 된다.

서술적인 이름을 사용하라

함수가 하는 일을 잘 표현하기 위해 이름을 서술적으로 표현하는 것이 좋다.

짧고 유추하기 어려운 이름보다는 길어도 함수가 어떤 행동을 하는지 파악할 수 있어야 한다.

개발을 하다보면 대부분 함수나 변수의 이름을 짓는데 시간을 많이 쓴다.
하지만 이렇게 시간을 들여 좋은 이름을 정한다면 오히려 추후에 리팩토링 할 때나 다른 사람이 나의 코드를 볼 때 시간을 더 절약할 수 있다.
또한 IDE를 이용해 쉽게 이름을 바꿀 수 있기 때문에 여러 시도를 하기에도 좋다.

함수 인수

함수 인수는 적을 수록 좋다.
함수 인수가 늘어날수록 함수에 대한 해석이 힘들어지고 '이 인수가 왜 들어가지'라는 생각을 한 번씩은 더 하게 만든다.

단항 함수

단항 함수는 흔히 두 가지로 사용된다.

  1. 인수를 이용해 질문을 던지는 경우
  2. 인수를 이용해 변환된 결과를 반환하는 경우

그 중에서도 두 번째에 집중해보면, 책에서는 다음과 같이 말한다.

입력 인수를 그대로 돌려주는 함수라 할지라도 변환 함수 형식을 따르는 편이 좋다.

다시 설명하면 리턴 타입으로 void를 최대한 사용하지 말라는 것이다.

void를 사용하면 안 되는지에 대해서는 이 글에서 잘 나와있다.
void 함수를 줄이는 것만으로도 발생할 수 있는 몇 가지 버그를 줄일 수 있다.

다항 함수

왜 함수 인수가 많아지면 안 좋은지 잘 보여주는 대표적인 예를 여기서부터 들 수 있다.

assertEquals(expected, actual);
assertEquals(actual, expected);

혹시 두 assert 중 어느 것이 정답인지 바로 아는 사람은 이를 외우려고 노력했거나, 자연스럽게 외워질 정도로 많이 써본 사람 정도일 것 같다.

그렇기 때문에 왜 AssertJ 가 spring-boot-test 의존성에 포함되는지도 쉽게 설명이 된다.

assertThat(actual).isEqualTo(expected);

내가 기대한 것과 같냐고 물어보는 것 처럼 쉽게 읽힌다.

사실 이렇게 말해도 코드를 작성하다보면 매번 인수가 여러 개인 함수가 많이 만들어진다.
특히나 IDE에서 제공하는 메서드 추출을 사용하면 더욱 더...😭

느낀 점

전 장인 의미있는 이름과 더불어 클린코드의 가장 기초적인 장이라고 생각한다.
사실 클린코드를 읽기 전에는 이 내용이 전부일 줄 알았다.
현재 14장을 읽고 있는데 책이 생각보다 많이 어렵다...😂
나중에 좀 더 개발을 많이 하고 보면 또 다를 것 같다.

profile
정리정리

0개의 댓글