아래 코드를 보자

Bad code는 모든 수행 작업을 다 풀어 적은 형태기 때문에 독자가 하나하나 코드를 읽어가며 의도를 추측해야 한다.
이전 챕터에서도 줄곧 이야기 했듯이, 의도를 직관적으로 알 수 없으면 좋은 코드라고 할 수 없다. 따라서 의도가 들어나도록, 함수를 만들고 함수명에 의도가 드러나도록 작성을 해야한다.
Good Code를 보면 printBanner()에서 배너를 프린트한다는 것을 바로 알 수 있고, getOutstanding() 역시 Outstanding값을 가져온다는 것을 알 수 있고, 이 값을 print한다는 의도를 printOwing()에서 몇 초만에 확인이 가능하다.
다음 예시를 보자~

여기서도 isFull일 경우에 배열 크기를 키우고, element를 추가한다는 것을 오른쪽 코드에서 단번에 파악 가능하지만, 왼쪽 코드는 독자가 읽어가며 의도를 생각해야한다.
또 다른 예시를 보면...

if 문 안에서 두 줄 이상의 코드가 적히면 일단 좋지 않다. 특히 세 줄 이상 넘어가면 함수로 빼는 것이 바람직하다.
이제 이 함수와 관련하여 더 살펴보자!
사실 변수명 때 내용가 크게 다르지 않다
의도가 드러나게 함수명을 작성하는 것이 중요하고, 적절하게 함수로 만드는 방법을 아는 것이 중요하다.
함수는 모든 프로그램의 구성에서 첫 라인에 해당한다. 따라서 이걸 잘 작성하는 것이 챕터의 제목을 잘 작성하는 것과 같다!
이제 Bad Code에서 Refactoring 과정을 통해 Better Code, Best Code 예시를 통해 어떻게 함수를 작성해야 하는지 자세하게 살펴보자.
public static String testableHtml(PageData pageData, boolean includeSuiteSetup){
WikPage wikiPage = pageData.getWikiPage();
StringBuffer buffer = new StringBuffer();
if(pageData.hasAttribute("Test")){
if(includeSuiteSetup){
WikiPage suiteSetup = PageCrawlerImp.getInheritedPage(...);
if (suiteSetup != null) {
WikiPagePath pagePath = suiteSetup.getFullPath();
String pagePathName = PathParser.render(pagePath);
buffer.append("!include -setup .")
.append(pagePathName)
.append("\n");
}
}
WikiPage setup = PageCrawlerImp.getInheritedPage(...);
if (setup != null) {
WikiPagePath setupPath = setup.getFullPath();
String setupPathName = PathParser.render(setupPath);
buffer.append("!include -setup.")
.append(setupPathName)
.append("\n");
}
}
buffer.append(pageData.getContent());
if (pageData.hasAttribute("Test")) {
WikiPage teardwon = PageCrawlerImpl.getInheritedPage(...);
if (teardown != null) {
WikiPagePath tearDownPath = teardown.getFullPath();
String tearDownPathName = PathParser.render(tearDownPath);
buffer.append("\n")
.append("!inlucde -teardown.")
.append(tearDownPathName)
.append("\n");
}
if (includeSuiteSetup) {
WikiPage suitTeardwon = PageCrawlerImpl.getInheritedPage(...);
if (suiteTeardown != null) {
WikiPagePath pagePath = suiteTeardown.getFullPath();
String pagePathName = PathParser.render(pagePath);
buffer.append("!include -teardown.")
.append(pagePathName)
.append("\n");
}
}
}
pageData.setContent(buffer.toString());
return pageData.getHtml();
}
3분 안에 위 코드를 바로 이해할 수 있나?
아마 아닐 것이다.
왜냐면 ...
이제 조금 리팩터링 한 코드를 살펴보면서 차이점을 보자!
public static String renderPageWithSetupsAndTeardowns (
pageData pageData, boolean isSuite
)
(
boolean isTestPage = pageData.hasAttribute("Test");
if (isTestPage) {
WikiPage testPage = pageData.getWikiPage();
Stirng Buffer newPageContent = new StringBuffer();
includeSetupPages(testPage, newPageContent, isSuite);
newPageContent.append(pageData.getContent());
includeTeardownPages(testPage, newPageContent, isSuite);
pageData.setContent(newPageContent.toString());
}
return pageData.getHtml();
}
✔️ 바뀐 사항
물론 모든 디테일을 모두 파악할 수는 없겠지만, 해당 함수가 setup과 teardown 과정을 test page에 적용하고, 그 페이지를 HTML로 만든다는 것을 알 수 있을 것이다.
그렇다면 여기서 질문!
👉 바로바로~~ function을 SMALL! 작게! 유지하는 것이다!
이것을 유념해서 위 코드를 한 번 더 리팩터링 해보자.
public static String renderPageWithSetupsAndTeardowns(
pageData pageData, boolean isSuite
)
(
if (isTestPage(pageData))
includeSetupAndTeardownPages(pageData, isSuite);
return pageData.getHTML();
}
✔️ 바뀐 사항
👉 if, else, when 절 안에 있는 코드는 한 줄 길이여야 한다!

ONE THING = One level of abstraction
❓ 추상화 단계가 무엇인가
만약 함수가 함수 이름에서 나타낸 것보다 한 단계 아래의 작업만 수행한다면, 그 함수는 한 가지 일만 하고 있는 것이다.
만약 함수가 여러 섹션으로 나뉘어져 있다면, 함수가 한 가지 일보다 더 하고 있다는 반증이다!


코드와 함께 살펴보자!
private String render (boolean isSuite) throws Exception {
this.isSuite = isSuite;
if (isTestPage())
includeSetupAndTeardownPages();
return pageData.getHtml();
}
private boolean isTestPage() throws Exception {
return pageData.hasAttribute("Test");
}
private void includeSetupAndTeardownPages() throws Exception {
includeSetupPages();
includePageContent();
includeTeardownPages();
updatePageContent();
}
private void includeSetupPages() throws Exception {
if (isSuite)
includeSuiteSetupPage();
includeSetupPage();
}
private void includeSuiteSetupPage() throws Exception {
include(SuiteResponder.SUITE_SETUP_NAME, "-setup");
}
private void includeSetupPage() throws Exception {
include("SetUp", "-setup");
}
private void includePageContent() throws Exception {
newPageContent.append(pageData.getContent());
}
private void includeTeardownPages() throws Exception {
includeTeardownPage();
if (isSuite)
includeSuiteTeardownPage();
}
private void includeTeardownPage() throws Exception {
include("TearDown", "-teardown");
}
private void includeSuiteTeardownPage() throws Exception {
include(SuiteResponder.SUITE_TEARDOWN_NAME, "-teardown");
}
private void updatePageContent() throws Exception {
pageData.setContent(newPageContent.toString());
}
private void include(String pageName, String arg) throws Exception {
WikiPage inheritedPage = findInheritedPage(pageName);
if (inheritedPage != null) {
String pagePathName = getPathNameForPage(inheritedPage);
buildIncludeDirective(pagePathName, arg);
}
}
private WikiPage findInheritedPage(String pageName) throws Exception {
return PageCrawlerImpl.getInheritedPage(pageName, testPage);
}
private String getPathNameForPage(WikiPage page) throws Exception {
WikiPagePath pagePath = pageCrawler.getFullPath(page);
return PathParser.render(pagePath);
}
private void buildIncludeDirective(String pagePathName, String arg) {
newPageContent
.append("\n!include ")
.append(arg)
.append(" .")
.append(pagePathName)
.append("\n");
}
함수 이름이 길어지는 것을 두려워하지 말것!
긴 설명적인 이름이 짧고 모호한 이름보다 낫다
예를 들어, testableHTML() 보다는 renderPageWithSetupsAndTearDowns()가 더 좋다
깨끗한 코드를 작성하는 데 절반의 전쟁은 하나의 일을 수행하는 작은 함수에 좋은 이름을 선택하는 것이다! 그러니 이름을 선택하는 데 시간을 들이는 것을 두려워하지 말자!
함수가 작을수록 설명적인 이름을 선택하기가 더 쉬우니 keep functions small!!
함수명을 지을 때 같은 구절, 명사, 동사를 사용해야 한다
예를 들어, include라고 했다가 have라고 했다가 하지말고 include라고만 일관성 있는 표현을 사용하라는 의미이다.
함수 인수의 이상적인 개수 :
0개 (베스트) → 1개 → 2개 → 3개 → 그 이상(최악)
함수 인수의 수가 많아질수록 함수를 이해하기 어려워진다 : ex) includeSetupPage() vs. includeSetupPageInto(newContent)
테스트 관점에서도 인수가 많아지면, 인수 조합의 수가 많아지기 때문에 더 어려워진다.
출력 인수는 입력 인수보다 이해하기 더 어렵다
단일 인수 함수는 세 가지 주요 형태로 나타난다
질문 형태의 함수
이 함수는 인수에 대해 질문을 던지고, 그 결과를 boolean 값으로 반환함
그래서 꼭 if문을 통해 확인을 하는 과정을 거쳐야 한다!!
[ex] boolean doesFileEist(String fileName)
변환을 수행하는 함수
인수를 받아서 해당 인수를 변환하고, 변환된 값을 반환하는 함수
[ex] InputStream openFile(String fileName) : 파일 이름을 받아서 해당 파일을 열고, 파일 스트림을 반환
인수를 사용해 무언가를 수행하는 함수
이 함수는 인수를 기반으로 작업을 수행한다
[ex] void handleWhenPasswordAttemptFailedNTimes(int attempts) : 주어진 인수(시도 횟수)를 기반으로 비밀번호 실패 처리 작업을 수행
이 형태들은 매우 직관적이고, 각 함수는 인수 하나만 사용하기 때문에 가독성과 단순성을 유지할 수 있다. 하지만 단일 인수를 받는 모든 함수가 좋은 구조는 아니다. 특히, 다음과 같은 형태의 함수는 피해야 한다.
void getEmptyPage (StringBuffer page)
..
getEmptyPage (newPage)
이 함수는 StringBuffer 인수를 통해 값을 설정하려고 하지만, 코드를 읽는 사람은 혼란스러울 수 있다. 대신, 아래 StringBuffer getEmptyPage()처럼 반환값을 통해 데이터를 전달하는 것이 더 명확하다
⏬
StringBuffer getEmptyPage()
...
newPage = getEmptyPage()
출력 인수는 함수의 목적을 흐리게 만들고, 코드의 가독성을 해친다. 따라서, 출력 인수보다는 반환값을 사용하는 것이 좋다.
일반적으로 플래그 인수를 사용하는 함수는 하나의 함수가 두 가지 이상의 작업을 수행하게 되는 구조를 만든다. 이는 가독성을 저하시킬 뿐만 아니라, 테스트 및 유지보수를 어렵게 만든다.
따라서 플래그 인수를 사용하는 대신, 조건에 따라 별도의 함수를 작성하여 각 함수가 하나의 작업만 수행하도록 설계하는 것이 좋다.

함수에 두 개의 인수를 받는 경우는, 함수의 역할이 조금 더 복잡해지기 시작하는 지점이다. 두 개의 인수를 사용하는 것이 항상 나쁜 것은 아니지만, 함수의 이해도와 테스트 복잡성이 증가할 수 있다.
Appropriate two arguments example
Point p = new Point(0, 0)Inappropriate two arguments example
void wrtieField(OutputStream outputStream, String name)✔️ 대안 1 : 메서드로 변경
함수를 객체의 메서드로 만든다.
이 방법을 통해 하나의 인수만 처리하도록 변경할 수 있다.
class OutputStream {
void writeField(String name) {
// OutputStream의 맥락에서 name을 처리
}
}
OutputStream outputstream;
...
outputStream.writeField(name);
...
이제 OutputStream이라는 객체 내에서 writeField가 처리되므로, 객체의 상태와 인수(name)의 관계가 명확해진다.
✔️ 대안 2 : 객체의 멤버 변수로 처리
두 번째 인수를 현재 클래스의 멤버 변수로 만든다. 이 방식도 함수가 하나의 인수만 처리하도록 하여 가독성을 높인다
class CurrentClass {
private OutputStream outputStream;
void writeField(String name) {
// use this.outputstream
}
}
이 방법은 함수의 인수를 줄여 함수가 더 명확한 책임을 가지도록 만든다. 또한, 클래스 내의 멤버 변수를 통해 다른 관련된 상태와 함수를 쉽게 처리할 수 있다.
함수에 세 개의 인수가 있을 때는, 복잡성이 더 커진다. 세 개 이상의 인수를 받는 함수는 특히 테스트나 유지보수에서 어려움을 겪을 수 있다.
예) assertEquals(message, expected, actual);
이 함수는 세 개의 인수를 받으며, 각 인수가 무엇을 의미하는지 파악하기 어려울 수 있다. 세 개의 인수가 있으면 각 인수가 무엇을 나타내는지 매번 확인해야 하며, 이로 인해 혼란이 생길 수 있다.
인수가 하나씩 늘어날 때마다 복잡성은 두배 이상으로 커진다. 함수가 처리해야 할 정보가 많아지고, 그에 따른 혼란과 오류의 가능성도 높아진다. 이로 인해 개발자는 코드의 의미를 파악하기 위해 반복해서 확인해야 한다.
세 개 이상의 인수를 가진 함수가 있을 때, 좋은 해결책은 인수들을 객체로 묶는 것이다.
ex. Circle makeCircle (double x, double y, double radius);
⏬
Circle makeCircle (Point center, double radius);
인수들을 객체로 묶는 것은 인수의 순서와 의도를 더 명확하게 표현하는 데 도움을 준다.
단일 인수를 가진 함수 : 함수와 인수는 동사-명사 쌍을 형성해야 한다.
write(name): 굿writeField(name) : 더 나음인수(매개변수)의 순서 : 함수 이름을 키워드 형식으로 사용하여 기억하기 쉽게 만든다
assertEquals(expected, actual) → assertExpectedEqualsActual(expected, actual)이처럼 함수명에 적힌 작업 외의 작업을 포함시켜서 side effect 생기는 일을 방지해야 한다. (ex. 교수님께서 삼성전자 근무했을 때 DB 사건처럼...)
예시 코드를 살펴보자!
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 = crpytographer.decrypt(codedPhrase, password);
if ("Valid Password".equals(phrase)) {
Session.initialize();
return true;
}
}
return false;
}
}
CheckPassword()라는 함수가 있으면 패스워드만 확인해야한다.
위에서도 언급을 했지만, 출력인수는 피하는 것이 좋다. 함수 인수는 보통 입력으로 해석되기 때문에, 출력 인수로 사용하면 이를 이해하는 데 시간이 걸린다.
ex. appendFooter(content) 함수에서 content가 footer로 추가되는지, 아니면 content에 footer가 추가되는지 헷갈릴 수 있다. 함수 시그니처를 보고 나서야 content가 출력 인수임을 알 수 있다.
이를 해결하기 위해, content.appendFotter()와 같은 방식으로 호출하는 것이 더 나은 방법이다.
함수는 명령을 하거나, 질문을 하거나 하나만 수행해야 한다. 두 가지를 동시에 수행하면 혼란을 초래할 수 있으며, 이는 버그로 이어질 수 있다.
예) public boolean set(String attribute, String value) : 이 함수는 속성 값을 설정하고, 동시에 설정이 성공했는지 여부를 반환한다.
if (set("username", "unclebob"))... 와 같이 활용을 한다고 하면 이는 호출자가 "username"이 "unclebob"으로 기존에 설정되어 있었던 것인지, 또는 unclebob으로 설정이 성공했는지를 말하는 것인지 혼란스럽게 만든다

예) if (deletePage(page) == E_OK) { ... }
예시 코드를 더 살펴보자
if (deletePage(page) == E_OK) {
if (registry.deleteReference(page.name) == E_OK) {
if (configKeys.deleteKey(page.name.makeKey()) == E_OK){
logger.log("page deleted");
}
else {
logger.log("configKey not deleted");
}
}
else {
logger.log("deleteReference from registry failed");
}
}
else {
logger.log("delete failed");
return E_ERROR;
}
try {
deletePage(page);
registry.deleteReference(page.name);
configKeys.deleteKey(page.name.makeKey());
} catch (Exception e) {
logger.log(e.getMessage());
}
try-catch문도 더 간단하게 만들 수 있다
public void delete(Page page) {
try {
deletePageAndAllReferences(page);
} catch (Exception e) {
logError(e);
}
}
👉 예외를 사용하는 경우, 예외를 서브 클래스로 확장할 수 있어 기존 클래스들을 재컴파일할 필요가 없다.
중복되는 내용이 있는 것은 소프트웨어에서 악마의 근원이 된다. 코드를 부풀리고, 알고리즘이 바뀌면 많은 변형이 이뤄져야 한다. 하지만, 대부분 조금씩만 차이가 나기 때문에 복제/중복된 부분을 찾아내는 것은 쉽지 않다.
예를 들어, 맨 위에서 들었던 예시 코드(HtmlUtil.java)에서 아래 부분은 4번 반복된다.
...
WikipagePath pagePath = suiteSetup.getFullPath();
String pagePathName = PathParser.render(pagePath);
buffer.append("!include -setup. ")
.append(pagePathName)
.append("\n");
...
여기서 일부 글자만 조금 다르게 반복이 되는데, 아래와 같이 수정할 수 있다
private void buildIncludeDirective (String pagePathName, String arg){
newPageContent.append("\n!include")
.append(arg)
.append(" .")
.append(pagePathName)
.append("\n");
}
Edsger Dijkstra의 구조적 프로그래밍 규칙:
그러나 함수가 작은 경우, 이런 규칙들이 큰 도움이 되지 않을 수 있습니다. 가끔씩 여러 개의 return문이나 break, continue문을 사용하는 것이 더 표현력이 좋을 때도 있습니다.