클린 코드

[ Programming > Clean Code ]

[클린 코드] 1-3장 깨끗한 코드 ~ 함수

 Carrot Yoon
 2025-10-27
 14

클린 코드

1장. 깨끗한 코드

좋은 코드(Clean Code, 깨끗한 코드)는 왜 필요한 이유는 나쁜 코드로 치르는 대가가 점점 커지기 때문이에요.

이제 프로젝트 진행을 하는데 있어 시간이 지남에 따라 클린 코드가 왜 필요한지 알아봐요.

클린 코드의변경 비용 그래프

image.webp

프로젝트 규모가 커짐에 따라 결국 더러운 코드의 생산 비용이 더 커지게 되요. 그러면 항상 클린 코드로 코드 관리를 하는 것이 좋을까요? 간단한 토이 프로젝트 같은 경우에는 분기점의 왼쪽의 규모에 해당한다면 그냥 대충 코드를 짜는게 오히려 생산성이 더 좋다고 볼 수 있어요. 프로젝트 관리자는 이러한 경험들을 바탕으로 어느 정도로 클린하게 코드를 짤 지, 그 분기점이 어느 정도 규모일지를 생각해서 코드 관리를 하는 것이 좋다고 생각해요.

클린 코드의 생산성 변화

image.webp

위 그래프는 코드 관리에 따른 생산성 변화 그래프를 나타내요. 코드가 엄청 더러운데 계속 추가하면, 기존 담당자 뿐 아니라 새로 들어온 사람도 코드 파악도 힘들고 고치기도 힘들꺼에요. 그러면 코드베이스 파악이 힘들고, 새로운 사람의 투입 시간도 오래 걸릴거에요.

나쁜 코드로 치르는 대가

image.webp

나쁜 코드가 증가하면 생산성 저하로 이어져요. 그러면 프로젝트 관리자는 생산성을 위해 인력을 추가할 거에요. 그러면 추가된 인력은 기존 코드베이스를 파악하기 어렵고 설계 의도를 적절히 반영하기도 힘들어져요. 결국 이는 나쁜 코드가 또 증가하게 되고 악순환이 반복되요. 그래서 우리는 생산성 저하를 해결하기 위해 인력 추가만을 생각할 것이 아니라 코드 관리도 생각해야해요.

코드 감각

이제까지 클린 코드가 왜 중요한지 알아봤어요. 그러면 어떻게 깨끗한 코드를 작성할까요? 바로 '청결'이라는 힘겹게 습득한 감각을 활용해 자잘한 기법들을 적용하는 절제와 규율이 필요해요. 이를 '코드 감각'이라고 해요. 이 '코드 감각'은 누군가는 타고나기도 하고, 누군가는 투쟁해서 얻어내요. 우리는 '코드 감각'을 통해 나쁜 모듈을 좋은 모듈로 개선할 방안을 떠올리고 이를 지속적으로 개선해야 해요.

1장 마무리(사견 추가)

이 책을 읽는다고 좋은 프로그래머가 되는 것은 아니에요. 이 책이 정답도 아니에요. 단지 "오브젝트 멘토 진영이 생각하는 깨끗한 코드"를 설명할 뿐이에요. 여기서 얻어야할 것은 뛰어난 프로그래머가 생각하는 방식과 그들이 사용하는 기술과 기교와 도구를 통해 여러분들의 더 적절하다고 판단되는 코드 감각을 얻는 것이에요. 이 책에서도 항상 말해요. 절대적으로 옳은 문파는 없다고요. 여러분들은 뛰어난 프로그래머의 생각하는 방식들을 여러분들이 그런 생각을 스스로 할 수 있는게 중요한 거 같아요. 이 책의 내용들은 단지 하나의 본인의 판단 근거가 될 수도 있고 아닐 수도 있어요. 여러분들은 이 책의 내용들에서 "이 내용이 판단 근거로서 본인 코드에 쓰여도 될지??", "판단 근거가 옳은 거 같은지", "이 상황에서는 괜찮은 것 같고 이런 상황에서는 별루 같은지"같은 판단을 스스로 내리고 스스로 적용할 수 있으면 좋겠어요.


2장. 의미 있는 이름

소프트웨어에서 이름은 어디나 쓰여요. ".tsx", ".war"같은 파일명부터 폴더명까지 이름을 통해서 내용을 추측해요. 이 장에서는 이름을 잘 짓는 간단한 규칙을 소개해요.

의도를 분명히 밝혀라

"의도를 분명하게 이름을 지으라"는 원칙을 저자는 매우매우 중요하게 생각해요. 왜냐하면 의도가 드러나는 이름은 "코드 이해"와 "변경"이 쉬워지고 그만큼 절약하는 시간이 많아지기 때문이에요. 그리고 의도를 들어내느냐의 판단 기준은 변수, 함수, 클래스 이름이 "존재 이유", "수행 기능", "사용 방법"과 같은 것들이 주석으로 필요하다면 의도를 분명히 드러내지 못했음을 뜻해요.

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

위 코드는 단순하지만 함축성이 없어요. 코드 맥락이 전혀 드러나있지 않아요. 위 코드에서는 어떤 정보를 알고있어야 한다고 생각하나요?

  • theList에 무엇이 들어있는가?

  • theList의 0번째 값은 왜 사용되었는가?

  • 값 4는 무엇을 의미하는가?

  • 반환하는 list1은 어떻게 사용되는가?

그러면 이제 위 코드가 지뢰찾기 게임 코드라는 것을 알았다고 가정해봐요. list가 게임판이고, 배열에서 0번째 값은 칸의 상태를 의미해요. 그리고 값 4는 깃발인 상태를 의미하고요. 이제 이러한 개념을 이름으로 표현해봐요.

int STATUS_VALUE = 0;int FLAGGED = 4public List<int[]> getFlaggedCells() {    List<int[]> flaggedCells = new ArrayList<int[]>();    for(int[] cell : gameBoard)        if(cell[STATUS_VALUE] == FLAGGED)            flaggedCells.add(cell);    return flaggedCells;}

그리고 이 상태에서 좀더 명시적으로 상수들도 감춰봐요.

public List<int[]> getFlaggedCells() {    List<int[]> flaggedCells = new ArrayList<int[]>();    for(int[] cell : gameBoard)        if(cell.isFlagged())            flaggedCells.add(cell);    return flaggedCells;}

이제 코드를 보면 쉽게 읽히나요?? 이게 바로 좋은 이름의 위력이에요. 단순히 이름만 바꿨는데도(사실 리팩터링도 한거임 ㅇㅅㅇ) 함수가 하는 일을 이해하기 쉬워졌어요.

그릇된 정보는 피해라

코드에 그릇된 단서를 남기면 안되요. 다음과 같이 그릇된 단서를 남기지 않기 위한 다음과 같은 원칙들이 있어요.

  • 널리 쓰이는 의미가 있는 단어를 다른 의미로 사용하면 안되요. 직각삼각형의 빗변이 hypotenuse인데 이를 악어로 hp라고 변수명을 지으면 독자에게 그릇된 단서를 줄 수 있기 때문이에요.

  • accountList같이 실제 List 자료형이 아닌데 List라는 특수한 단어를 붙이면 그릇된 정보를 줄 수 있어요. accountGroup이나 bunchOfAccounts나 Accounts같이 명명하는게 좋아요.

  • 흡사한 이름을 사용하면 안되요. 예를 들면 XYZControllerForEfficientOfStrings와 XYZControllerForEfficientStorageOfStrings라는 이름을 서로 조금 떨어진 모듈에서 사용하면 차이를 알아채기 힘들 수 있어요.

  • 유사한 개념은 유사한 표기법을 사용해야해요. 일관성이 떨어지는 표기법은 그릇된 정보이에요. 이는 IDE 자동완성 기능에서 알파벳 순으로 보여주는 기능을 사용할 때 대부분 이름만 보고 선택하기 때문에 유사한 개념은 유사한 표기법을 사용하는게 좋아요.

의미 있게 구분하라

변수 구분을 위해 단순히 숫자(1,2,3)을 붙이는 경우가 있어요. 그런 이름은 의도를 전혀 드러내지 않아요.

  • 만약 같은 범위 내에서 Product, ProductInfo, ProductData라는 클래스 명을 같이 쓴다고 해요. 사용자가 상품의 가격을 가져오려면 어디서 가져와야할까요?

  • 만약 NameString이라는 변수명을 사용했는데 이름이 숫자가되는 경우가 생길 수 있을까요?

  • zork라는 변수가 이미 있어서 그냥 the를 붙여서 theZork라는 변수를 만들면 잘 구분할 수 있을까요?

중요한 것은 읽는 사람이 차이를 알도록 이름을 지어야한다는 것이에요. 의미없이 어떤 접두어나 접미어를 붙이면 안되요.

발음하기 쉬운 이름을 사용하라(사견, 한국인 버전)

괄호가 영어로 뭘까요? parentheses 에요. 단어도 길고 발음하기도 어려워요. 이럴때는 차라리 gwalho라고 쓰거나 한글 변수명 "괄호"를 사용하는게 훨씬 가독성이 좋아요.

검색하기 쉬운 이름을 사용하라

문자 하나를 사용하는 이름과 상수는 쉽게 눈에 띄지 않아요.

MAX_CLASSES_PER_STUDENT는 눈에 띄지만, 단순히 숫자 7을 쓰면 IDE로 찾는 것 조차 까다로워요. 마찬가지로 "e"라는 문자도 변수 이름으로 적합하지 않아요. e라는 문자는 너무 많이 사용되서 찾기 힘들어져요. 이런 관점에서 긴 이름은 짧은 이름보다 좋아요.

물론 간단한 메서드의 로컬 변수같은 경우는 한 문자를 사용해도 좋아요. 정리하면, 이름 길이는 범위 크기에 비례해야 해요.

인코딩을 피하라

이는 옛날에 IDE와 Compiler가 발전하지 못해서 있는 내용이에요. 짧게 말하자면, 어떤 값을 저장하는데 이 타입을 컴파일러에서 검사를 하지 않으며, 개발자도 이 타입을 알기 힘든 경우가 있었어요. 그래서 phoneString과 같이 변수의 타입까지 같이 적어주거나, 멤버 변수에 m_description같이 m이라는 단어를 적어주는 것과 같은 컨벤션을 사용했었어요.

그러면 인코딩이 아에 필요 없을까요? 아니에요. 추상 팩토리를 구현한다고 해봐요 이때 ShapeFactory라는 인터페이스가 있고, ShapeFactoryImpl이라는 구현 클래스가 생기겠네요. 이러한 경우에는 인코딩이 필요해요.

자신의 기억력을 자랑하지 마라

독자가 코드를 읽으면서 변수 이름을 자신이 아는 이름으로 변환해야 한다면 그 변수 이름은 바람직하지 않아요. 언제나 명료함이 최고에요.

  • 루프에서 반복 횟수를 세는 i, j, k는 괜찮아요. 단, 루프 범위가 아주 작고 다른 이름과 충돌하지 않는 경우에만 이에요.

  • 기억력 자랑하려고 r이라는 변수를 URL을 나타내는데 쓰면 뚜둘겨 맞아야해요.

클래스 이름

클래스 이름과 객체 이름은 명사명사구가 좋아요.

  • Customer, WikiPage, AddressParser같은 이름이 좋은 예에요.

  • Manager, Processor, Data, Info 같은 단어는 피하고, 동사는 사용하지 않아요.

메서드 이름

메서드 이름은 동사동사구가 좋아요.

  • postPayment, deletePage, save 등이 좋은 예에요.

  • 접근자, 변경자, 조건자는 자바빈 표준에 따라 get, set, is를 붙여요.

  • 생성자를 중복정의할 때는 정적 팩토리 메서드를 사용해요.(new Complex(23); => Complex.FromRealNumber(23.0);)

  • 생성자 사용을 제한하려면 private으로 정의해요.

기발한 이름은 피하라

모든 아이템을 없애는 함수를 HolyHandGrenade라고 하면 무엇을 하는지 알겠나요? 재밌지만, 그냥 DeleteItems가 더 좋아요.

한 개념에 한 단어를 사용하라

추상적인 개념 하나에 단어 하나를 선택해 이를 계속 사용하세요.

  • 데이터를 받아오는 메서드를 fetch, retrieve, get으로 다 다르게 쓰면 혼란스러워요.

  • 마찬가지로 controller, manager, driver를 섞어쓰면 혼란스러워요. DeviceManager와 ProtocolController가 어떻게 다른거죠?

말장난을 하지 마라

한 단어를 두가지 목적으로 사용하지 마세요. 말장난에 불과해요.

  • 지금까지 구현한 add 메서드는 모두 기존 값 두개를 더하거나 이어서 새로운 값을 만든다고 가정해봐요. 그런데 새로 만든 add는 집합에 값 하나를 추가한다고 해봐요. add라는 메서드가 많아서 일관성을 지키려면, insert나 append라는 이름이 적당해요.

프로그래머는 집중적인 탐구가 필요한 코드가 아니라 대충 훑어봐도 이해할 코드 작성이 목표에요.

해법 영역에서 가져온 이름을 사용하라

코드를 읽을 사람도 프로그래머에요. 그래서 알고리즘 이름, 패턴 이름 등을 사용해도 좋아요. 하지만 모든 이름을 문제 영역(도메인, domain)에서 가져오는 것은 현명하지 못해요. 왜냐하면 같은 개념을 다른 이름으로 이해하던 동료들이 매번 그 의미를 물어봐야하기 때문이에요.

  • VISITOR 패턴에 익숙한 프로그래머는 AccountVisitor라는 것을 금방 이해할 거에요.

  • Order를 OrderController, OrderRepository 등으로 나눠서 개발하는 것이 해법 영역에서 가져오는 것이에여.

  • Order, CartItem, Customer같이 기획자같은 분들도 함께 이해할 수 있는 실제 문제의 세계가 문제 영역(domain)이에요.

문제 영역에서 가져온 이름을 사용하라

적절한 프로그래머 용어가 없으면 문제 영역에서 이름을 가져오세요. 그러면 코드 보수하는 프로그래머가 분야 전문가에게 의미를 물어 파악할 수 있어요. 우수한 프로그래머는 해법 영역과 문제 영역을 구분할 수 있어야해요. 문제 영역에 연관이 깊은 코드면 문제 영역에서 이름을 가져오세요.

의미 있는 맥락을 추가하라

스스로 의미가 분명한 이름은 분명 있을거에요! 하지만 대다수 이름이 그렇지 못해요. 그래서 클래스, 함수, 이름 공간에 넣어 맥락을 부여해요. 그리고 모든 방법이 실패하면 마지막 수단으로 접두어를 붙여요.

  • firstName, lastName, street, houseNumber, state, city, zipcode같은 변수가 있으면 주소 관련된 것이란 것 금방 알아차려요. 하지만 메서드에 state라는 변수 하나만 사용하면 state가 뭘 말하는지 한번에 캐치할 수 있을까요?

  • addr이라는 접두어를 추가해 addrFirstName, addrLastName, addrState라 쓰면 의미가 좀 더 분명해져요.

  • Address라는 클래스를 생성해서 사용하면 변수가 더 큰 개념에 속한다는 사실을 분명히할 수 있어요.

private void printGuessStatistics(char candidate, int count) {    String number;    String verb;    String pluralModifier;    if(count == 0) {        number = "no";        verb = "are";        pluralModifier = "s";    } else if (count == 1) {        number = "1";        verb= "is";        pluralModifier = "";    } else {        number = Integer.toString(count)        verb = "are";        pluralModifer = "s";    }    String guessMessage = String.format(        "There %s %s %s%s", verb, number, candidate, pluralModifier);    print(guessMessage);}

함수 이름은 맥락 일부를 제공하고, 알고리즘이 나머지 맥락을 제공해요. 위 함수를 보면 어떤가요? 함수가 길고 3개의 변수가 함수 전반에 사용되요. 그리고 맥락을 읽어봐야 마지막에 guessMessage에 number, verb, pluralModifier가 사용된다는 것을 알 수 있어요. 이제 이 함수를 좀더 맥락이 분명해지게 만들어봐요.

public class GuessStatisticMsessage {    private String number;    private String verb;    private String pluralModifier;        public String make(char candidate, int count) {        createPluralDependentMessageParts(count);        return String.format(            "There %s %s %s%s",                verb, number, candidate, pluralModifier);    }    private void createPluralDependentMessagerParts(int count) {        if(count == 0) {            thereAreNoLetters();        } else if (count == 1) {            thereIsOneLetter();        } else {            thereAreManyLetters(count);        }    }    private void thereAreManyLetters(int count) {        number = Interger.toString(count);        verb = "are";        pluralModifier = "s";    }        private void thereIsOneLetter() {        number = "1";        verb = "is";        pluralModifier = "";    }    private void thereAreNoLetters() {        number= "no";        verb = "are";        pluralModifier = "s";    }}

어때요? 세 변수를 GuessStatisticsMessage라는 클래스에 속하게 만들고, 역할별로 이름을 붙이니 확실히 맥락이 명확해졌죠?

불필요한 맥락을 없애라

고급 휘발유 충전소(Gas Statetion Deluxe)라는 애플리케이션을 만든다고 가정 해요. 만약 모든 클래스 이름을 GSD로 시작하겠다는 생각을 하면 IDE에서 G만 입력해도 모든 클래스 이름이 자동완성으로 열거될 거에요. IDE는 모든 개발자가 사용하는 에디터인데, 많은 방해를 받을거에요.

그리고 일반적으로 짧은 이름이 긴 이름보다 좋아요. 단, 불필요한 맥락없이 의미가 분명하다면 말이죠.

  • 만약 GSD 회계 모듈에서 MailingAddress라는 클래스를 GSDAccountAddress라는 이름으로 바꿔서 사용했다고 해요. 그러면 다른 고객 관리 프로그램에서는 GSDAccountAddress라는 클래스를 찾아서 사용할까요? 이름이 적절해 보이나요?

  • accountAddress와 customerAddress는 Address클래스의 인스턴스로 좋은 이름이지만, 클래스 이름으로는 적합하지 않아요. Address는 클래스 이름으로 적합하고요.

2장 마무리

좋은 이름을 선택하려면 설명 능력이 뛰어나고 문화적 배경도 같아야해요. 그리고 좋은 이름을 선택하는 능력은 기술, 비즈니스, 관리 문제가 아니고 교육의 문제라고 이 책에서는 봐요.

사람들이 이름을 바꾸지 않는 이유는 다른 개발자가 반대할까봐 두려워서 라고 이책에서는 언급해요. 하지만 오히려 좋은 이름으로 바꿔주면 반갑고 고마워요. 우리는 자신이 짠 모든 클래스, 메서드, 변수명을 외울 수 없어요. 그래서 문장이나 문단처럼 읽히는 코드 아니면 적어도 표나 자료 구조처럼 읽히는 코드를 짜는데 집중해야 해요.

리팩터링이랑 마찬가지로 이름 역시 나름대로 바꿨다가 누군가 질책할 수 있어요. 하지만 그렇다고 코드를 개선하려는 노력을 중단해서는 안되요.


3장. 함수

프로그래밍 초기에는 시스템을 "루틴과 하위 루틴"으로 나뉘었고요. 포트란과 PL/1 시절에는 시스템을 "프로그램, 하위 프로그램, 함수"로 나눴어요. 그리고 현재는 "함수"만 살아남았어요. 어떤 프로그램이든 가장 기본적인 단위에요. 이 함수를 잘 만드는 법을 소개할게요.

// HtmlUtil.java (FitNesse 사용코드)public static String testableHtml {	PageData pageData,	boolean includeSuiteSetup} throws Exception {	Wikipage wikiPage = pageData.getWikiPage();	StringBuffer buffer = new StringBuffer();	if (pageData.hasAttribute("Test")) {		if (includeSuiteSetup) {			WikiPage suiteSetup = 				PageCrawlerImpl.getInheritedPage(						SuiteResponder.SUITE_SETUP_NAME, wikiPage				);			if (suiteSetup != null) {				WikiPagePath pagePath =					suiteSetup.getPageCrawler().getFullPath(suiteSetup);				String pagePathName = PathParser.render(pagePath);				buffer.append("!include -setup .")							.append(pagePathName)							.append("\\n");			}		}		WikiPage setup =			PageCrawlerImpl.getInheritedPage("SetUp", wikiPage);		if (setup != null) {			WikiPagePath setupPath =				wikiPage.getPageCrawler().getFullPath(setup);			String setupPathName = PathParser.render(setupPath);			buffer.append("!include -setup .")						.append(setupPathName)						.append("\\n");		}	}	buffer.append(pageData.getContent());	if (pageData.hasAttribute("Test")) {			WikiPage teardown = 				PageCrawlerImpl.getInheritedPage("TearDown", wikiPage);			if (teardown != null) {				WikiPagePath teardownPath =					suiteSetup.getPageCrawler().getFullPath(teardown);				String teardownPathName = PathParser.render(teardownPath);				buffer.append("!include -teardown .")							.append(pagePathName)							.append("\\n");			}			if (includeSuiteSetup) {				WikiPage suiteTeardown = 				PageCrawlerImpl.getInheritedPage(						SuiteResponder.SUITE_TEARDOWN_NAME,						wikiPage				);			if (suiteTeardown != null) {				WikiPagePath pagePath =					suiteSetup.getPageCrawler().getFullPath(suiteTeardown);				String pagePathName = PathParser.render(pagePath);				buffer.append("!include -teardown .")							.append(pagePathName)							.append("\\n");			}		}	}	pageData.setContent(buffer.toString());	return pageData.getHtml();}

위 함수를 보면 3분 안에 코드를 이해할 수 있으신가요? 아마 아닐거에요. 왜냐하면 추상화 수준이 다양하고, 코드가 너무 길어요. 그리고 중첩된 if문은 이상한 플래그를 확인해요. 하지만 위 코드에서 메서드를 추출하고, 이름을 변경하고 구조도 변경해서 개선해봐요.

// HtmlUtil.java 리팩터링 후public static String renderPageWithSetupsAndTeardowns(	PageData pageData, boolean isSuite) throws Exception {	boolean isTestPage = pageData.hasAttribute("Test");	if (isTestPage) {		WikiPage testPage = pageData.getWikiPage();		StringBuffer newPageContent = new StringBuffer();		includeSetupPages(testPage, newPageContent, isSuite);		newPageContent.append(pageData.getContent());		includeTeardownPages(testPage, newPageContent, isSuite);		pageData.setContent(newPageContent.toString());	}	return pageData.getHtml();}

리팩터링 후의 코드에요. 이 코드는 사실 FitNesse에 익숙하지 않으면 100% 이해하기는 힘들어요. 그래도 Test인 경우엔 설정(setup) 페이지와 해제(teardown) 페이지를 함께 넣어서 렌더링 한다는 것을 알 수 있어요. 훨씬 더 읽기가 편해졌죠? 이제 어떻게 이해하기 쉽게 만들까? 어떻게 의도를 분명히 표현하도록 구현할 까? 어떤 속성을 부여해야 처음 읽는 사람도 프로그램 내부를 직관적으로 파악할 수 있을지 알아봐요.

작게 만들어라!

함수를 만드는 첫번째 규칙은 '작게'에요. 두번째 규칙은 '더 작게'고요. 사실 이 규칙에 대해 근거를 대기는 힘들어요. 하지만 저자는 경험을 바탕으로 작은 함수가 더 좋다고 확신을 해요.

그러면 함수가 얼마나 짧아야 할까요? 보통 위의 리팩터링 한 버전보다는 짧아야 한다고 저자가 주장해요. 심지어는 아래의 코드만큼 줄여야 마땅하다고 주장해요.

// HtmlUtil.java 2번 리팩터링 후public static String renderPageWithSetupsAndTeardowns(	PageData pageData, boolean isSuite) throws Exception {	if (isTestPage(pageData)) 		includeSetupAndTeardownPages(pageData, isSuite);	return pageData.getHtml();}
  • if 문, else 문, while 문 등에 들어가는 블록은 한줄이어야 해요. 그러면 바깥을 감싸는 함수가 작아지고, 블록 안에서 호출하는 함수 이름도 적절히 짓는다면 코드를 이해하기 쉬워져요.

  • 위 말은 중첩 구조가 생길만큼 함수가 커져서는 안 된다는 뜻이에요. 그래서 함수에서 들여쓰기 수준은 1단이나 2단을 넘으면 안되요.

  • 사견 - 저는 이렇게 까지 하는것은 옳을까? 라는 의문이 많이 생겨요. 물론 작으면 좋다는 것은 동의해요. 하지만 너무 나누면 오히려 파악이 힘들어 지는 경우도 많이 생기더라고요.

한가지만 해라!

우리가 처음 봤던 HtmlUtil.java 코드는 여러 가지를 처리해요. 버퍼 생성하고, 페이지 가져오고 등등 많은 일을 해요. 반면에 마지막으로 리팩터링한 코드는 테스트일 경우에 설정 페이지와 해제 페이지를 넣는 일 1가지를 처리해요. 함수는 한가지를 해야하며, 그 한가지를 잘 해야해요.

그런데 페이지가 테스트인지 확인하고 -> 설정 페이지 넣고 -> 해제 페이지 넣고 이렇게 3가지 일을 한다고 주장할 수도 있어요. 어느쪽일까요? 저자는 지정된 함수 이름 아래에서 추상화 수준이 하나이면 한가지 일을 한다고 봐요. 아무튼 함수를 만드는 이유는 큰 개념을 다음 추상화 수준에서 여러 단계로 나눠 수행하기 위해서에요. (Setup넣고 TearDown 페이지도 넣고 -> Setup을 넣는데 Buffer를 만들고..)

위의 처음 리팩터링한 코드는 추상화 수준이 둘이고, 마지막 리팩터링한 코드는 하나에요. 함수가 '한 가지'만 하는지 판단하려면 의미 있는 이름으로 다른 함수를 추출할 수 있으면 그 함수는 여러 작업을 하는 것이에요. 물론 마지막 리팩터링 코드에서 if문을 제거하고 includeSetupsAndTeardownsIfTestrPage라는 함수로 만든다면 똑같은 내용을 다르게 표현할 뿐 추상화 수준은 바뀌지 않아요.

사견 - 사실 저는 저자의 말이 애매해 보여요. renderPageWithSetupsAndTeardowns가 test인 경우에만 첨부한다는 것을 나타내고 있을까요? 10가지 일을 하나의 함수에 담으면 추상화 수준은 맞으니 1가지 일을 하나요? if문을 함께 하나의 함수로 묶으면 추상화 수준이 같다고 확실히 말할 수 있을까요? 물론 적절히 사용하면 좋은 추상화를 할 수 있다고 생각해요!! 또한 저자 말도 틀린건 없어요. 하지만 언제나 TradeOff 겠죠? 스스로 생각하고 적절히 사용할 수 있으면 좋을 거 같아요.

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

함수가 확실히 '한 가지' 작업만 하려면 함수 내 모든 문장의 추상화 수준이 동일해야해요. getHtml()은 추상화 수준이 아주 높고, String pagePathName = pathParser.render(pagepath);는 보통이고, .append는 아주 낮아요.

한 함수 내에 추상화 수준을 섞으면 코드를 읽는 사람이 헷갈려요. 특정 표현이 근본 개념인지, 세부사항인지 구분하기 어렵기 때문이에요. 그리고 문제는 그정도에 그치지 않고, 근본 개념과 세부사항이 뒤섞이며, 사람들이 함수에 세부사항을 점점 더 추가하고 힘들어 질거에요.

코드는 위에서 아래로 읽혀야 좋아요. 그리고 한 함수 다음에는 추상화 수준이 한 단계 낮은 함수가 오는게 좋아요. 맨 위에는 renderHtml이 오고, 그 아래에는 htmlparse, htmlToDom 이란 함수들을 선언하면 위에서 아래로 읽으면서 자연히 추상화 수준에 따라서도 읽히겠죠?

물론 추상화 수준이 하나인 함수를 구현하기는 매우 힘들어요. 저도 이것 때문에 고민하는 경우가 많아요. 하지만 핵심은 위에서 아래로 이야기 읽듯이 읽을 수 있도록 구현하면 추상화 수준을 일관되게 유지하기 쉬워져요.

Switch 문

switch 문은 작게 만들기 어려워요. 그리고 "한 가지" 작업만 하는 switch문 만들기도 어렵죠. 본질적으로 switch문은 N가지 일을 처리해요. 하지만 switch문을 아에 안쓰기는 힘들어요. 하지만 switch 문을 저차원 클래스에 숨기고 반복하지 않는 방법이 있어요. 바로 다형성을 이용하는 거에요.(리팩터링 읽었으면 아실거에요)

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);   }}

위 함수에는 3가지 문제점이 있어요.

  1. 함수가 길다.

  2. 새 직원 유형을 추가하면 더 길어진다. (OCP)

  3. 한 가지 작업만을 하지 않는다.(SRP)

이를 다형성을 통해 해결해 볼까요?

public abstract class Employee {  public abstract boolean isPayday();  public abstract Money calculatePay();  public abstract void deliverPay(Money pay);}// 직원 팩토리public interface EmployeeFactory { 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) ; // Employee 구현체에 각각 알맞는 calculatePay 로직 생성      case HOURLY:        return new HourlyEmployee(r);      case SALARIED:        return new SalariedEmploye(r);      default:        throw new InvalidEmployeeType(r.type);    }  }}

위와 같이 Employee을 활용하여 Simple 팩토리 패턴을 사용하여 switch문을 하위 클래스에 숨겼어요.

그런데 저는 사견을 달고 싶어요. 위와같이 만드는 건 좋아요. 그런데 그만큼 파일도 많아져요. 그래서 간단한 기능인데 위와 같이 구현하면 오버 엔지니어링이라고 말하는 사람도 많아요. 오히려 쉽게 파악 가능했던 코드를 더 어렵게 파악 가능하게 만들었으니까요. 뭘 사용할 지는 여러분의 생각과 경험을 통해 결정해야해요. 글쓴이도 불가피한 상황이 생겨서 이 규칙을 위반한 경험이 많다고 했어요. 그리고 새 유형을 추가보다 새 함수를 추가해야하는 경우에는 switch문 쓰는게 더 적합하다고 말하기도 해요.

서술적인 이름을 사용해라

지금까지 함수를 리팩터링 하면서 좀 더 서술적인 함수명을 사용했어요. 예를 들면 getThem()보다는 getFlaggedCells()와 같이 함수가 하는 일을 좀 더 잘 표현했어요. 코드를 읽으면서 짐작했던 기능을 각 루틴이 그대로 수행하면 깨끗한 코드라 부를 수 있어요.

그리고 이름이 길어도 괜찮아요. 짧고 어려운 이름보다는 길고 서술적인 이름이 더 이해하기 좋아요. 그리고 길고 서술적인 주석보다도 길고 서술적인 이름이 더 나아요. 함수의 기능을 잘 표현하는 이름을 선택하세요.

서술적인 이름을 사용하면 개발자 머릿속에서도 설계가 또렷해져서 코드 개선도 쉬워져요.

함수 인수

함수에서 이상적인 인수 개수는 0개에요. 그다음 1개, 2개 이고요. 하지만 3개는 가능한 피하면 좋고, 4개 이상부터는 특별한 이유가 필요해요. 사실 특별한 이유가 있어도 사용하지 않으면 좋겠어요.

그리고 인수는 개념을 이해하기 어렵게 만들어요. 예를 들면 StringBuffer가 있다고 쳐봐요. 인스턴스 변수로 선언하는 대신 함수 인수로 넘길 수 있어요. 하지만 그랬다면 코드를 읽는 사람은 StringBuffer를 발견할 때마다 의미를 해석해야 해요. 예를 들면 includeSetupPageInto(buffer)보다 includeSetupPage()가 더 이해하기 쉬워요. 함수 이름과 인수 사이에 추상화 수준도 다르고요. 코드 읽는 사람은 현 시점에서 별로 중요하지 않은 세부사항, 즉 StringBuffer를 알아야 하기도 해요.

테스트 관점에서도 인수는 테스트를 어렵게 해요. 인수가 없으면 간단한데, 인수가 3개 4개면 인수마다 유효한 값으로 모든 조합을 구성해 테스트해야해요. 더 부담스러워지겠죠.

그리고 출력 인수는 입력 인수보다 이해하기가 어려워요. 대게 인수로 입력을 넘기고 반환값으로 출력을 받는데 익숙하지, 반환값으로 인수를 받으리라 기대하지 않아요. 이는 코드를 다시 확인하게 만들겠죠?

최선은 입력 인수가 없거나 1개인 경우에요.

많이 쓰는 단항 형식

함수에 인수 1개를 넘기는 이유로 가장 흔한 경우는 2가지 에요.

  1. boolean fileExists("MyFile")과 같이 질문을 던지는 경우.

  2. InputStream fileOpen("MyFile")과 같이 인수를 뭔가로 변환해 결과를 반환하는 경우.

이들 두 경우 독자가 당연하게 받아들여요. 함수 이름을 지을 때는 두 경우를 분명히 구분해야하고, 일관적인 방식으로 사용해야 해요. (명령과 조회 구분과 같이)

그리고 드물게 이벤트에 단항 함수 형식이 많이 사용되요. 보통 이벤트 함수는 입력 인수만 있고 출력 인수가 없어요. 프로그램 함수 호출을 이벤트로 해석해 입력 인수로 시스템 상태를 바꿔요. passwordAttemptFailedNtimes(int attempts)가 좋은 예에요. 그런데 이벤트 함수는 조심해서 사용해야 해요. 이벤트라는 사실이 코드에 명확히 드러나야 해요.

지금까지 설명한 경우가 아니면 가급적 단항 함수는 피해요. 변환 함수에서 출력 인수를 사용하면 혼란을 일으켜요. 입력 인수를 변환하는 함수라면 반환 결과는 반환값으로 돌려주세요. StringBuffer transform(StringBuffer in)보다는 void transform(StringBuffer in)이 더 좋다는 의미에요. 만약 StringBuffer를 그대로 돌려주더라도, 인수 in을 리턴하지 마세요.

플래그 인수

플래그 인수는 추해요. 왜냐하면 함수가 한꺼번에 여러개를 처리하겠다고 공표하는 것과 같아요. 예를 들면 render(boolean isSuite)이 있다고 해봐요. 뭘하는지 알기 힘들어요. renderForSuite()과 renderForSingleTest()로 나누는게 더 좋아 보일거 같네요.

이항 함수

인수가 2개인 함수는 단항 함수보다 이해하기 힘들어요. writeField(name)이 writeField(outputStream, name)보다 이해하기 쉬운것 처럼요. 물론 이항 함수가 적절한 경우도 있어요. Point p = new Point(0, 0);가 좋은 예에요. 직교 좌표계 점은 인수 2개를 취하니까요. 오히려 new Point(0)을 쓰면 놀라겠죠.

그리고 당연하게 여겨지는 assertEquals(expected, actual)이라는 이항 함수도 문제가 있어요. 순서가 헷갈리는 경우가 많은게 문제에요. 인위적으로 순서를 기억해줘야해요. 그래서 특정 메서드를 클래스 구성원으로 만들어 인수를 줄이는 등의 방법으로 인수를 줄여주면 좋아요.

삼항 함수

인수가 3개인 함수는 이해하기 엄청 어려워요. 그래도 괜찮은 예를 소개해 보자면 assertEquals(1.0, amount, .001)와 같이 사용하는 거에요. amount의 값이 1.0이라는 값에서 오차범위 .001안에 있으면 같다고 보는 거에요. 3가지 인수가 필요할 수밖에 없어요.

인수 객체

인수가 2-3개 필요하다면, 일부를 독자적인 객체 혹은 클래스 변수로 선언해봐요.

Circle makeCircle(double x, double y, double radius);를

Circle makeCircle(Point center, double radius); 이런식으로 Point라는 클래스로 추상화하여 개념을 표현하게 만든 거에요.

인수 목록

때로는 인수 개수가 가변적인 함수도 필요해요. 예를 들면 String.format("%s worked %.2f hours.", name, hours); 같은 메서드가 좋은 예에요.

동사와 키워드

함수의 의도나 인수의 순서와 의도를 제대로 표현하려면 좋은 함수 이름이 필요해요. 단항 함수는 함수와 인수가 동사/명사 쌍을 이뤄야해요. 예를들어 write(name)은 누구나 곧바로 이해해요. 좀 더 나은 표현은 writeField(name)으로, 이름이 필드라는 사실이 더 분명하게 드러나요. 마지막은 함수 이름에 키워드를 추가하는 형식이에요. 즉 함수 이름에 인수 이름을 넣는거에요. assertEquals => assertExpectedEqualsActual(expected, actuals)로 바꾸면 인수 순서가 헷갈리지 않겠죠?

부수 효과를 일으키지 마라

부수 효과는 거짓말을 말해요. 함수에서 한 가지를 하겠다고 약속해놓고 남몰래 다른 짓도 하는 거에요. 예상치 못하게 클래스 변수를 수정하거나, 함수로 넘어온 인수나 시스템 전역 변수를 수정하는 것 과 같은 행동이에요. 이는 시간적인 결합(temporal coupling)이나 순서 종속성(order dependency)을 초래해요.

예시 함수를 통해 알아봐요.

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;     }}

함수는 무해하게 보여요. 표준 알고리즘을 통해 userName과 password를 확인해요. 그리고 올바른지에 따라 true, false를 반환해요. 그런데 문제점이 있어요. Session.initialize() 호출이 문제점이에요.checkPassword는 말 그대로 비밀번호를 확인하는 함수인데 세션을 초기화하고 있어요. 그래서 사용자는 사용자를 인증하면서 기존 세션 정보를 지워버릴 위험에 쳐해요.

이런 부수 효과가 시간적인 결합 초래해요. 즉, checkPassword 함수는 특정 상황에서만 호출이 가능해요. 다시 말하면 세션을 초기화해도 괜찮은 경우에만 호출이 가능해지죠. 그래서 혼란을 일으키게 됩니다. 만약 이런 결합이 필요하다면 함수이름을 checkPasswordAndInitializeSession으로 바꿔 확실히 알려줘야해요.

출력 인수

부수 효과의 대표적인 예가 출력인수에요.

appendFooter(s)같은 함수가 있을때, s를 바닥글로 첨부할지, s에 바닥글을 첨부할지 파악하기 힘들어요. 그래서 함수를 살펴보면 public void appendFooter(StringBuffer report)로 되어있어 report에 바닥글을 첨부함을 알 수 있어요. 읽는 사람을 주춤하게 만들어요.

그리고 객체지향 언어가 나오면서 이런 모호함을 없앴어요. 바로 this라는 키워드가 생겼어요. 다시말해 report.appendFooter()와 같이 바꿀 수 있다는 것을 말해요.

명령과 조회를 분리하라

함수는 뭔가를 수행하거나 뭔가에 답하거나 둘 중 하나만 해야 해요. 둘다 하면 혼란을 초래해요.

예를 들면 public boolean set(String attribute, String value); 함수가 있다고 해요. attribute에 value로 설정하고 성공하면 true, 실패하면 false를 반환해요. 그래서 if(set("username", "YoonCarrot"))으로 쓰면 독자 입장에서는 set이 설정하는 함수인지 확인하는 함수인지 헷갈려요. 동사인지 형용사인지 분간하기 어려워요.

개발자는 set을 동사로 의도했지만, if 문에 넣고 보면 형용사로 느껴지는 거에요. 그래서 setAndCheckIfExists로 바꿀 수도 있지만, if문에 넣으면 여전히 쫌 어색해요. 그래서 진짜 해결책은 명령과 조회를 분리하는 거에요

if(attributeExists("username"))과 setAttribute("username", "YoonCarrot")으로 나누는 거죠.

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

명령 함수에서 오류 코드를 반환하는 방식은 명령/조회 분리 규칙을 위반하고, if문에서 명령을 표현식으로도 쓰게 만들어서 혼란스럽게 해요. 예를 들면 if(deletePage(page) == E_OK) 와 같은 코드에요.

이런식으로 코드를 통해 확인하지 않고 try{} catch{}와 같이 예외를 사용하는 것이 분리해야 오류 처리 코드가 원래 코드에서 분리되어 좋아요.

Try/Catch 뽑아내기

저자에 따르면 try/catch 블록은 추해요. 코드 구조에 혼란을 일으키며, 정상 동작과 오류 처리 동작을 섞어요. 그래서 별도로 뽑아내는게 좋다고 주장해요. 그리고 오류 처리도 한 가지 작업이니 오류만을 처리하는게 좋아요.

public void delete(Page page) {    try {         deletePageAndAllReferences(page); // 실제 제거하는 함수    } catch (Exception e) {        logError(e); // delete에서 오류 처리    }}

반복하지 마라

맨 처음 SetUp, SuiteSetUp, TearDown, SuiteTRearDown 하는 코드를 보면, 각각 반복되는 알고리즘이 보여요. 다른 코드와 섞이면서 모양새가 조금씩 달라보여서 잘 드러나지는 않아요. 하지만 그래도 코드길이가 늘어나고, 알고리즘이 변하면 네곳 모두 바꿔야하기 때문에 그만큼 오류 확률도 높아져요. 그리고 이런 반복을 제거하기 위해 나온 개념들이 많아요. 데이터베이스 정규화 방식, AOP, COP등이 모두 중복 제거를 위한 전략들이에요.

구조적 프로그래밍

어떤 프로그래머는 에츠허르 데이크스트라의 구조적 프로그래밍 원칙을 따라요. 모든 함수는 입구와 출구가 1개 존재해야 한다고 말해요. 즉, 함수는 return 문이 1개여야 하고, 루프 안에서 break이나 continue는 사용해서는 안되며, goto는 절대로 안되요. 그런데 저자는 위 규칙은 함수가 클 때만 상당한 이익을 제공하고, 함수가 작으면 return, break, continue를 여러 차례 사용해도 괜찮다고 봐요. 오히려 단일 출입구 규칙보다 의도를 표현하기 쉽다고도 해요.

함수를 어떻게 짜죠?

소프트웨어를 짜는 행위는 글짓기와 비슷해요. 글을 쓸 때 먼저 생각을 기록하고 읽기 좋게 다듬죠? 초안은 어수선해서 결국 다듬는 과정이 필요해요. 코드도 마찬가지로 원하는 대로 읽힐때 까지 다듬고 정리해야해요.

처음에는 길고 복잡하고, 중복 루프도 많고, 인수도 많을 수 있어요. 이름도 즉흥적이겠죠. 하지만 이를 지속적으로 리팩터링해서 깔끔하게 다듬으면 되요.

3장 마무리

모든 시스템은 즉정 응용 분야 시스템을 기술할 목적으로 프로그래머가 설계한 도메인 특화 언어로 만들어져요. 함수는 그 언어에서 동사, 클래스는 명사에요. 프로그래밍의 기술은 언어 설계의 기술이에요.

대가 프로그래머는 시스템을 구현 프로그램이 아니라 풀어갈 이야기로 여겨요. 시스템에서 발생하는 모든 동작을 설명하는 함수 계층이 바로 그 언어에 속해요.

이 장에서는 함수를 잘 만드는 기교를 소개했어요. 여기서 설명한 규칙을 따르면 좋은 함수가 나올 수 있겠죠. 하지만 우리의 진짜 목표는 시스템이라는 이야기를 풀어가는 데 있다는 것을 명심하길 바래요. 여러분이 작성하는 함수가 분명하고 정확한 언어로 깔끔하게 맞아 떨어져야 이야기를 풀어가기 쉬워져요.

1-3장 쓴 소감

이번에 디자인 패턴, 리팩터링을 제대로 다시 공부하고 또 클린코드를 보니 확실히 처음보다 많은게 보이는 것 같아요. 클린코드 책에서 코드를 보더라도, 어떤 리팩터링을 했는지, 무슨 패턴을 사용했는지가 다 보이게 되었어요. 근데 확실히 책이 불친절한 부분들이 있는거 같아요.(사실 자세히 설명하면 책이 한도 끝도 없이 커질거 같긴 해요. 설명하고자하는 요점도 벗어나고)

혹시 이책을 읽으신다면, 꼭 리팩터링, 디자인 패턴부터 숙지하고 읽으면 좋을 것 같아요. 안그러면 많은 부분들을 그냥 생각없이 지나치고 그렇구나~~ 하고 지나갈 것 같아요. 왜냐면 저가 그랬으니까요.. ㅜㅜ. 그냥 이렇게 하는구나~~ 이상으로 무언가를 얻지 못했던거 같아요.

그리고 책에서 말했듯이 이 책은 어떠한 기교 방법만을 배우기 위한 책이 아니에요. 오히려 그 사람들이 어떠한 생각과 근거로 그런 기교들을 만들었을 지를 보는게 좋은거 같아요. 여러분들도 그사람들과 같이 생각과 근거를 만들고 어떠한 기교를 스스로 창작할 수 있도록 하는 힘을 갖게 되는 거겠죠. 책에서 말하는 것 처럼 완벽한 정답은 없어요!! 단지 거장들이 정답에 가까운 좀더 적절한 tradeOff를 가진 원칙들을 만든 이유와 왜 그렇게 개선을 하고 싶었을까?? 심지어 왜 개선할 생각을 했을까를 아는게 더 중요하지 않을까요?