Clean Code: 깨끗한 코드에 대해

Yeseong31·2023년 9월 6일
0
post-thumbnail

개발자는 무엇을 하는 사람일까?
개발자는 코드를 작성하는 시간보다 코드를 읽고 해석하는 데 많은 시간을 소비한다.
즉 개발자는 “직면한 문제를 해결하기 위해 해결 방법을 작성하는 저자이자 독자이다.”
따라서 개발자는 미래의 나, 동료들이 쉽게 이해할 수 있는 깨끗한 코드를 작성해야 한다.



클린 코드


바보도 컴퓨터가 이해하는 코드는 작성할 수 있다. 훌륭한 프로그래머는 인간이 이해하는 코드를 작성한다. - 마틴 파울러

개발자라면 누구나 코드를 깔끔하게 짜고 싶어 한다. 괜히 “클린 코드”라는 용어가 생긴 것이 아니고, 또 괜히 인터넷에 관련 내용이 많은 것이 아니다. 여기서는 클린 코드에 대해 내가 알면 좋을 만큼만 알아볼 것이다.


“나쁜” 코드가 만들어지는 배경

클린하지 않은 코드가 만들어지는 건 그리 어렵지 않다. 오히려 너무 쉽게 만들어진다.

  • 무작정 프로젝트나 아이디어를 코딩으로 설계/구현할 때
  • 나만 아는 단어, 임의의 단어, 축약어로 변수 이름을 지을 때
  • 클래스를 넘나들면서 서로의 코드에 많의 의존할 때

이 외에도 수많은 이유로 인해 흔히 말하는 “스파게티 코드”가 탄생한다.
따라서 우리는 더 이상 머리 아플 일이 없도록 클린 코드를 작성하는 법을 알아야 한다.


클린 코드의 공통점

클린 코드라는 용어는 다양한 프로그래밍 언어에서 사용될 만큼 중요한 개념이다. 언어는 다를지라도 “어떤 것이 클린 코드인가”라는 질문에는 대부분 비슷한 시각을 가지고 있다.

  • 중복이 적은 코드
  • 내가 작성한 코드를 처음 보는 동료, 미래의 나까지도 쉽게 읽을 수 있는 코드
  • 함수가 한 가지의 기능만 수행하는 경우
  • 코드를 작게 쓰고 추상화한 경우

위의 내용은 분명 대부분이 알고 있는 사실이겠지만, 이를 실제에 적용하는 것은 절대 쉽지 않다.


클린 코드의 예시

splitAndSum()을 처음 읽는 사람에게 어느 코드가 더 읽기 좋을까?
[우아한테크세미나] 190425 TDD 리팩토링 by 자바지기 박재성님

// CASE 1
public class StringCalculator {
	
	public static int splitAndSum(String text) {
		int result = 0;
		if (text == null || text.isEmpty()) {
			return result;
		}
		for (String value : text.split(",|;")) {
			result += Integer.parseInt(value);
		}	
		return result;
	}
}
// CASE 2
public class StringCalculator {

	public static int splitAndSum(String text) {
		if (isBlank(text)) {
			return 0;
		}
		return sum(toInts(split(text)));
	}

	private static boolean isBlank(String text) {
		return text == null || text.isEmpty();
	}

	private static String[] split(String text) {
		return text.split(",|:");
	}

	private static int[] toInts(String[] values) {
		// ...
	}

	private static int sum(int[] numbers) {
		// ...
	}
}



깨끗한 변수명: 9가지 규칙


개발자에게 있어 가장 어려운 것은 이름을 짓는 것이다.

코드를 작성하다 보면 이름을 붙여야 할 대상이 너무나 많다. 이름을 붙일 일도 많은데, 이름을 “어떻게” 정해야 할지도 고민이 많아진다. 여기서는 이름을 잘 짓기 위한 9가지 규칙을 하나씩 알아본다.


1. 의미 있는 이름 사용하기

좋은 이름을 지으려면 그만큼 시간이 걸린다. 하지만 좋은 이름으로 절약하게 되는 시간이 더 많다.

다음의 예시를 살펴보자.

// Bad
int d;

// Good
int elapsedTimeInDays;
int daysSinceCreation;

무엇이 문제일까? 바로 한 글자로 된 이름에는 이유와 의미가 있을 수 없다는 것이다.

  • int형 변수 d에는 아무런 의미가 드러나지 않는다.

하지만 다른 이름의 변수들은 조금 길더라도 변수의 의미를 명확히 드러내고 있다.

변수의 길이가 길어서 코드가 비효율적으로 보일 수 있다. 하지만 의미 파악에 시간을 낭비하는 것보다 이렇게 명확히 의미를 드러내는 것이 훨씬 좋다. 또한 컴파일러는 컴파일 타임에 변수명들을 바이트 코드로 대체하기 때문에 코드의 길이를 신경쓰지 않아도 된다.

또 다른 예시를 살펴보자.

// Bad
public void getThem() {
	List<int[]> list1 = new ArrayList<int[]>();
	int [][] lst = new int[0][];
	for (int[] x : lst) {
		if (x[0] == 4)
			list1.add(x);
	}
}

// Good
public void getFlaggedCells() {

	List<int[]> flaggedCells = new ArrayList<>();
	int[][] gameBoard = new int[0][];

	for (int[] cell : gameBoard) {
		if (cell[STATUS_VALUE] == FLAGGED) {
			flaggedCells.add(cell);
		}
	}
}

위의 두 메서드 getThem()getFlaggedCells()는 동일한 기능을 하는 메서드이다. 하지만 getThem()보다는 getFlaggedCells()가 더 이해하기 쉽고 명확하다.

변수명 외에도 코드를 새 줄로 그루핑해서 코드를 더 이해하기 쉽게 했다.


2. 그릇된 정보 피하기

다음 예시를 살펴보자.

// Bad
int accountList;

위의 accountList는 분명 int 형으로 선언되어 있지만 이름에 List가 붙는다.

  • List“여러 개”라는 특수한 의미를 가지고 있기 때문에 변수명에 직접 사용하면 안 된다.

변수의 선언형, 이름은 언제든 달라질 수 있음에 유의하자.

또한 아래와 같이 서로 흡사한 이름을 사용하지 않도록 주의해야 한다.

// Bad
String XYZControllerForEffecientHandlingOfStrings;
String XYZControllerForEffecientStorageOfStrings;

IDE의 자동 완성을 활용해도 좋다. 다만 변수의 이름에 의미가 꼭 드러나야 한다.

유사한 문자는 섞어서 사용하지 말자.
특히 소문자 l과 숫자 1, 대문자 O와 숫자 0은 서로 상성이 최악이다.


3. 의미 있게 구분하기

다음의 예시를 살펴보자.

// Good
new Customer("gildong");

// Bad
new CustomerObject("gildong");
new CustomerInfo("gildong");

위의 Customer, CustomerObject, CustomerInfo 클래스는 너무도 비슷하다.

  • Object, Info라는 이름이 붙었지만, Customer의 의미를 구분하기에는 좀 애매하다.
  • Customer는 “소비자”이면서 분명 “정보”이기도 한데, 왜 CustomInfo와 구분지었을까?

불용어를 추가한 이름은 아무 정보도 제공하지 못한다.

  • Object, Info같은 이름은 있어도 그만, 없어도 그만이다.
  • 다른 사람이 이 메서드들의 차이점을 한 눈에 파악하지 못한다면 필요가 없는 이름일 뿐이다.

4. 발음하기 쉬운 변수명 사용하기

다음의 예시를 살펴보자.

// Bad
class DtaRcrd102 {
    private Date genydhms;
    private Date modymdhms;
    private final String pszqint = "102";
}

// Good
class Customer {
    private Date generationTimeStamp;
    private Date modificationTimeStamp;
    private final String recordId ="102";
}

변수 이름을 읽기 힘들다면 코드를 이해하기 힘들고, 다른 사람에게 코드를 설명하는 것도 어렵다. 위의 두 코드는 내용이 동일하지만, Customer의 경우가 변수 이름을 발음하는 게 훨씬 편하다.

간결성과 명명: 끝없는 투쟁
간결한 명명이 항상 미덕인 것은 아니다. 알아 들을 수 없는 축약어 대신 길지만 의미 있는 서술적인 이름이 낫다. 이름은 쓸 일보다 읽힐 일이 훨씬 많기 때문에 명확함을 추구하는 것이 좋다.


5. 검색하기 쉬운 변수명 사용하기

다음의 예시를 살펴보자.

// Bad
for(int j = 0; j < 34; j++) {
    sj += (t[j] * 4) / 5;
}

// Good
int realDaysPerIdealDay = 4;
int WORK_DAYS_PER_WEEK = 5;
        
for(int j = 0; j < NUMBER_OF_TASKS; j++) {
    int realTaskDays = taskEstimate[j] * realDaysPerIdealDay;
    int realTaskWeeks = (realTaskDays / WORK_DAYS_PER_WEEK);
    sum += realTaskWeeks;
}

위쪽의 코드는 이 코드가 무슨 역할을 하는지 단번에 알아차리기 힘들다. 하지만 아래쪽의 코드는 명확한 변수 이름과 상수를 사용하여 코드 의미 파악이 쉽다.

이름의 길이는 범위(scope)의 크기에 비례해야 한다. 변수나 상수를 여러 곳에서 사용해야 한다면 검색하기 쉬운 이름을 사용해야 한다.

재차 강조하지만 중요한 것은 쉽게 알아들을 수 있는 이름을 지어야 한다는 것이다.


6. 인코딩 피하기

다음의 예시를 살펴보자.

// Bad
String phoneNumberString = "010-1234-5678";
PhoneNumber phoneNumberString;

이름 안에 데이터 타입이 인코딩되어 있으면 이름의 의미가 흐려질 수 있다. 또한 변수의 타입이 변경된다면, 이름으로 인해 사용에 혼란이 올 수 있다. 따라서 명명 규칙에 데이터 타입을 인코딩하지 말아야 한다.

때로는 인코딩이 필요할 때가 있다.
인터페이스는 접두어를 붙이지 않고, 구현 클래스에는 접두어를 붙이는 것이 좋다.
ShapeFactory 인터페이스와 ShapeFactoryImpl 구현 클래스를 예로 들 수 있다.

자바의 경우 자바 코드 규칙에 따라 이름을 작성하는 것이 좋다.

  • 클래스, 인터페이스, enumCamelCase로 작성한다.
  • 상수는 CAPITAL_SNAKE_CASE로 작성한다.
  • 메서드, 필드 매개변수, 변수는 첫 글자가 소문자로 시작하는 변형된 camelCase로 작성한다.
  • 메서드는 동사로 명명하거나 is, has, save, get 등과 같이 동사로 시작해야 한다.

자바 코드 규칙은 1997년부터 있었다.
이 규칙은 이름을 비롯해 자바 코드를 서식화하는 실질적 표준이다.


7. 한 개념에 하나의 단어만 사용하기

다음의 예시를 살펴보자.

// Bad
userService.getUserNameAndPassword();
boardService.fetchBoardList();
boardService.retrieveBookmark();

위의 메서드들은 모두 “조회” 목적으로 사용된다. 하지만 get, fetch, retrieve로 제각기 이름이 붙어서 괜히 혼란스럽다.

// Good
userService.getUserNameAndPassword();
boardService.getBoardList();
boardService.getBookmark();

용도에 차이가 없다면 일관성 있는 어휘를 사용하여 이름을 붙이는 것이 좋다.


8. 의미 있는 맥락 추가하기

다음의 예시를 살펴보자.

String firstName;
String lastName;
String zipCode;
String city;
String state;

스스로 의미가 뚜렷한 이름은 분명 존재한다. 하지만 그렇지 않은 경우도 많다.

위 데이터를 처음 마주하는 상황을 가정해보자. firstName, lastName만 보면 이 데이터들이 “회원 정보”라고 생각할 수 있다. 하지만 zipCode, state를 보면 이 데이터들이 “주소 정보”임을 뒤늦게 알 수 있다. 즉 변수를 모두 훑어봐야지만 데이터가 가지는 의미를 파악할 수 있다.

// Good
String addrFirstName;
String addrLastName;
String zipCode;
String addrCity;
String addrState;

이처럼 코드에 의미 전달이 필요하다면, 접두어를 붙여서 의미를 명확히 전달하는 것이 좋다.


9. 불필요한 맥락 없애기

그렇다고 접두어를 매번 변수에 붙여놓는 것은 바람직하지 않다.

WebSocket으로 채팅을 하는 애플리케이션을 만든다고 가정해보자. 모든 클래스 명칭에 WebSocket이라는 접두어를 붙여야 할까? 그렇지 않다. 접두어에 특별한 의미가 있는 것이 아니라면 불필요할 뿐이다.

// Bad
boolean isExisted;

// Good
boolean isExistTempDirectory;

변수의 이름은 짧으면 짧을수록 좋다. 단, 의미가 잘 전달된다는 조건에서의 이야기이다. 변수명이 길어지더라도 의미를 명확히 전달하는 변수명으로 바꾸자.



클린 코드도 적당히


가독성을 잃어버린 함수

함수에 대한 클린 코드 조건은 다음과 같이 요약해볼 수 있다.

  • 함수는 무조건 하나의 일만 해야 하고 사이드 이펙트가 없어야 한다.
  • 함수 내 코드 라인의 수는 짧을수록 좋다.
  • 함수의 인자/매개변수의 수가 3개 이상이면 좋지 않다.

그런데, 클린 코드가 마냥 좋은 것만은 아니다. 클린한 코드를 짜게 되면 가독성이 망가질 수 있다.

다음의 예를 살펴보자.

// CASE 1
public void includeSetupAndTeardownPages() {
    if (isSuite()) {
        include("Suite", "-setup");
    }
    include("SetUp", "-setup");

    newPageContent.append(pageData.getContent());

    if (isSuite()) {
        include("Suite", "-teardown");
    }
    include("TearDown", "-teardown");

    pageData.setContent(newPageContent.toString());
}
// CASE 2
public void includeSetupAndTeardownPages() {
    includeSetupPages();
    includePageContent();
    includeTeardownPages();
    updatePageContent();
}

private void includeSetupPages() {
    if (isSuite()) {
        includeSuiteSetupPage();
    }
    includeSetupPage();
}

private void includeSuiteSetupPage() {
    include("Suite", "-setup");
}

private void includeSetupPage() {
    include("SetUp", "-setup");
}

private void includePageContent() {
    newPageContent.append(pageData.getContent());
}

private void includeTeardownPages() {
    if (isSuite()) {
        includeSuiteTeardownPage();
    }
    includeTeardownPage();
}

private void includeSuiteTeardownPage() {
    include("Suite", "-teardown");
}

private void includeTeardownPage() {
    include("TearDown", "-teardown");
}

private void updatePageContent() {
    pageData.setContent(pageContent.toString());
}

CASE 1은 클린 코드를 적용하기 전의 모습이다. 함수 하나가 한 가지의 일을 하지는 않지만, 로직이 복잡하지 않아서 코드의 동작을 이해하는 것은 어렵지 않다.

CASE 2는 클린 코드를 적용한 후의 모습이다. 클린 코드 적용으로 인해 각각의 함수의 길이가 짧아진 점은 좋지만, 코드의 흐름을 파악하기 위해서는 해당 함수를 찾아서 내부의 동작을 살피는 과정이 추가되어 복잡해졌다. 함수의 개수도 늘어났는데, 이것이 재사용이나 유지보수에는 용이할 수 있지만 어쩌면 함수를 그저 “포장”하는 용도로 사용하는, 함수 사용의 의의가 조금 퇴색되어버릴 수 있다는 점을 유의해야 한다.

“함수는 한 가지 작업만 해야 한다”
하지만 로직이 복잡하지 않을 때는 클린 코드를 적용하지 않는 편이 가독성이 더 나을 수 있다.


무작정 지워버린 주석

Why am I so down on comments? Because they lie. Not always, and not intentionally, but too often. The older a comment is, and the farther away it is from the code it describes, ...
제가 왜 주석에 크게 연연하지 않는 줄 아세요? 주석은 거짓말을 하기 때문입니다. 항상 그렇지는 않고, 의도적이지도 않지만, 너무 자주 하죠. 특히 주석이 오래된 것일수록, 그리고 주석이 코드에서 멀어질수록, ...

주석은 대부분 코드 구현에 대한 내용을 설명하기 위해 사용되는 경우가 많다. 하지만 나쁜 코드에 딸려 있는 주석은 코드를 보완해주지 않는다. 그래서 클린 코드에서는 주석 사용에 대한 Best Practice를 다음과 같이 정의한다.

”주석이 없도록 코드로 충분히 설명할 수 있을 정도로 설계해야 한다.“

그렇다면 주석을 완전히 배제해야 할까? 그렇지 않다. 주석은 코드의 이해를 뒷받침해주는 좋은 도구이다. 그릇된 정보를 전달하지 않도록 신경을 써서 작성한다면 주석의 장점을 더욱 부각시킬 수 있다.

예를 들어보자. 주석은 기본적인 정보를 나타낼 때 유용하다. 가능하다면 코드로만 설명하는 것이 좋지만, 정규 표현식이나 포맷터와 같이 코드만 보고 결과물이 어떨지 바로 예측하지 못하는 경우에는 주석을 통해 추가 설명을 붙이는 것이 좋다.

// kk:mm:ss EEE, MMM dd, yyyy 형식
Pattern timeMatcher = Pattern.compile("\\dt:\\d*:\\d* \\w*, \\w*:\d*, \\d*");

또 하나의 예를 들어보자. 주석은 때로 의미가 모호한 인수나 반환 값에 대한 설명에 적합하다. 표준 라이브러리와 같이 변경하지 못하는 코드에 붙이는 경우가 대부분인데, 이 방식에서는 그릇된 주석을 달지 않도록 유의해야 한다.

assertTrue(a.compareTo(a) == 0)  // a == a

이 외에도 좋은 주석의 예는 다음과 같다.

  • 코드의 의도를 설명하는 주석
  • 결과를 경고하는 주석
  • TODO
  • 중요성을 강조하는 주석
  • 공개 API에서 Javadocs

물론 나쁜 주석도 존재한다. 같은 내용을 반복적으로 나타내는 주석이나 오해를 살 여지가 있는 주석, 있으나 마나 한 주석, 주석으로 처리한 코드, 장황하게 적힌 주석 등이 이에 해당한다.

개발자는 코드를 깔끔히 정리하고 표현력을 강화하는 방향으로, 애초에 주석이 필요 없는 방향으로 힘을 쏟되, 설명이 꼭 필요한 부분에 주석을 일부 작성하는 것이 좋다.


일단 쪼개보는 클래스

단일 책임 원칙(SRP, Single Responsibility Principle)에 따르면 하나의 클래스는 하나의 책임(기능)만 가져야 한다. 여기서 책임을 결정하는 기준은 변경이고, 이 변경으로 인한 영향과 파급 효과가 적으면 단일 책임 원칙을 잘 따른 것으로 볼 수 있다.

클래스가 하나의 기능만 담당하도록 하면 테스트 케이스를 작성하는 것이 용이하다. 각 책임을 분리하기 때문에 테스트 케이스의 범위를 각 기능에 맞추어서 할 수 있고, 하나의 기능을 비즈니스 레벨까지 만들어내는 것이 더 좋다.

다만 여기서 생각해보아야할 점은 가독성이다. 적절한 단일 책임 원칙은 해당 클래스가 만들어진 목적을 이해하는 데 도움을 줄 수 있다. 하지만 단일 책임 원칙에 과도하게 의지해서 너무나 많은 클래스를 만들어버렸다면 클래스 간 관계를 명확히 이해하는 데 어려움이 있을 수 있다. 로직에 대한 이해가 부족하면 중복된 코드를 여러 곳에서 만들어버릴 수 있으므로 또 다른 문제를 불러일으킬 수 있다.

단일 책임 원칙의 적용은 팀이 커뮤니케이션을 잘 이루어내고 신경쓴다면 충분히 문제를 완화할 수 있다.



참고


자바 코딩의 기술
Clean Code
클린 코드 Chatper 4. 주석

profile
역시 개발자는 알아야 할 게 많다.

0개의 댓글

관련 채용 정보