[Clean Coding] 3. Functions

라을·2024년 10월 14일

Clean Coding

목록 보기
3/10

아래 코드를 보자

Bad code는 모든 수행 작업을 다 풀어 적은 형태기 때문에 독자가 하나하나 코드를 읽어가며 의도를 추측해야 한다.

이전 챕터에서도 줄곧 이야기 했듯이, 의도를 직관적으로 알 수 없으면 좋은 코드라고 할 수 없다. 따라서 의도가 들어나도록, 함수를 만들고 함수명에 의도가 드러나도록 작성을 해야한다.

Good Code를 보면 printBanner()에서 배너를 프린트한다는 것을 바로 알 수 있고, getOutstanding() 역시 Outstanding값을 가져온다는 것을 알 수 있고, 이 값을 print한다는 의도를 printOwing()에서 몇 초만에 확인이 가능하다.

다음 예시를 보자~

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

또 다른 예시를 보면...

if 문 안에서 두 줄 이상의 코드가 적히면 일단 좋지 않다. 특히 세 줄 이상 넘어가면 함수로 빼는 것이 바람직하다.

이제 이 함수와 관련하여 더 살펴보자!
사실 변수명 때 내용가 크게 다르지 않다
의도가 드러나게 함수명을 작성하는 것이 중요하고, 적절하게 함수로 만드는 방법을 아는 것이 중요하다.

Functions

함수는 모든 프로그램의 구성에서 첫 라인에 해당한다. 따라서 이걸 잘 작성하는 것이 챕터의 제목을 잘 작성하는 것과 같다!

이제 Bad Code에서 Refactoring 과정을 통해 Better Code, Best Code 예시를 통해 어떻게 함수를 작성해야 하는지 자세하게 살펴보자.

📍 Bad 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분 안에 위 코드를 바로 이해할 수 있나?
아마 아닐 것이다.

왜냐면 ...

  • 함수가 일단 너무 길다
  • 그리고 너무 많은 일이 서로 다른 추상화 수준에서 동시에 일어나고 있다
  • 플래그로 제어되는 두 번 중첩된 if문 사이에 이상한 문자열과 이상한 함수 호출이 섞여 있다

이제 조금 리팩터링 한 코드를 살펴보면서 차이점을 보자!


📍 Better Code

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

✔️ 바뀐 사항

  • 각각의 중첩 if문을 함수로 만들어 가독성 있게 흐름을 파악할 수 있게 만들었다

물론 모든 디테일을 모두 파악할 수는 없겠지만, 해당 함수가 setup과 teardown 과정을 test page에 적용하고, 그 페이지를 HTML로 만든다는 것을 알 수 있을 것이다.

그렇다면 여기서 질문!

  • 어떤 것이 함수를 쉽게 읽히고 이해하기 쉽게 만들까?
  • 어떻게 함수가 의도를 전달하도록 할 수 있을까?
  • 우리의 함수에 어떤 속성을 부여하면, 일반적인 독자가 그 함수가 속한 프로그램의 종류를 직관적으로 이해할 수 있을까?

👉 바로바로~~ function을 SMALL! 작게! 유지하는 것이다!

  • Lines should not be 150 characters long
  • Function should not be 100 lines long
  • Functions should hardly ever be 20 lines long

이것을 유념해서 위 코드를 한 번 더 리팩터링 해보자.


📍 Best Code

public static String renderPageWithSetupsAndTeardowns(
	pageData pageData, boolean isSuite
)
(
	if (isTestPage(pageData))
    	includeSetupAndTeardownPages(pageData, isSuite);
    return pageData.getHTML();
}

✔️ 바뀐 사항

  • 중첩 if문에 있던 내용을 한 함수로 묶고, 그 전체 과정을 아우를 수 있는 함수명을 작성해서 의도를 한 번에 전달하였다

👉 if, else, when 절 안에 있는 코드는 한 줄 길이여야 한다!

  • 그 한 줄 코드는 함수 호출을 하는 코드일 것!
  • 이것이 코드를 짧게 유지시켜준다
  • 더 나아가, 이는 함수 이름이 곧 주석이 되어 문서적 가치도 추가한다

📌 중첩 구조 Nested Structures

  • 중첩 구조는 함수 호출을 통해 대체될 수 있다
    • 함수의 들여쓰기 레벨은 하나나 둘을 넘지 않아야 한다
    • 이렇게 하면 함수가 더 읽기 쉽고 이해하기 쉬워진다

DO ONE THING❗

  • Functions should do one thing
  • They should do it well
  • They should do it only

ONE THING = One level of abstraction

추상화 단계가 무엇인가

만약 함수가 함수 이름에서 나타낸 것보다 한 단계 아래의 작업만 수행한다면, 그 함수는 한 가지 일만 하고 있는 것이다.

Sections with Functions

만약 함수가 여러 섹션으로 나뉘어져 있다면, 함수가 한 가지 일보다 더 하고 있다는 반증이다!

✔️ SRP (Single Responsibility Principle)

  • 모든 함수는 하나의 책임만을 가져야한다는 의미다.
  • 함수 혹은 클래스는 변경해야 할 이유가 하나만 있어야 한다!!

📌 The Stepdown Rule

  • 모든 함수는 다음 추상화 수준의 함수로 이어져야한다
  • 즉, 높은 수준의 추상화 → 낮은 수준의 추상화로 이어져야 한다

코드와 함께 살펴보자!

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

📌 Designing Names

✔️ Use Descriptive Names

함수 이름이 길어지는 것을 두려워하지 말것!
긴 설명적인 이름이 짧고 모호한 이름보다 낫다

예를 들어, testableHTML() 보다는 renderPageWithSetupsAndTearDowns()가 더 좋다

깨끗한 코드를 작성하는 데 절반의 전쟁은 하나의 일을 수행하는 작은 함수에 좋은 이름을 선택하는 것이다! 그러니 이름을 선택하는 데 시간을 들이는 것을 두려워하지 말자!

함수가 작을수록 설명적인 이름을 선택하기가 더 쉬우니 keep functions small!!


✔️ Be Consistent in Your Names

함수명을 지을 때 같은 구절, 명사, 동사를 사용해야 한다

예를 들어, include라고 했다가 have라고 했다가 하지말고 include라고만 일관성 있는 표현을 사용하라는 의미이다.


📌 Function Arguments

함수 인수의 이상적인 개수 :
0개 (베스트) → 1개 → 2개 → 3개 → 그 이상(최악)

  • 함수 인수의 수가 많아질수록 함수를 이해하기 어려워진다 : ex) includeSetupPage() vs. includeSetupPageInto(newContent)

  • 테스트 관점에서도 인수가 많아지면, 인수 조합의 수가 많아지기 때문에 더 어려워진다.

  • 출력 인수는 입력 인수보다 이해하기 더 어렵다

    • 우리는 보통 반환값을 통해 정보가 나가는 것을 기대하지, 인수를 통해 나가는 것을 기대하지 않기 때문이다

📍 ONE

🔻Common Monadic Forms

  • Monadic Functions : 단일 인수를 받는 함수
    • 인수의 수는 적을수록 좋다는 원칙에 따라, 단일 인수를 받는 함수는 매우 자주 사용된다
    • 인수가 하나인 함수는 여러 가지 형태로 나타날 수 있으며, 이러한 형태를 이해하는 것이 깨끗한 코드를 작성하는 데 중요!

단일 인수 함수세 가지 주요 형태로 나타난다

  1. 질문 형태의 함수

    이 함수는 인수에 대해 질문을 던지고, 그 결과를 boolean 값으로 반환
    그래서 꼭 if문을 통해 확인을 하는 과정을 거쳐야 한다!!

    [ex] boolean doesFileEist(String fileName)

  2. 변환을 수행하는 함수

    인수를 받아서 해당 인수를 변환하고, 변환된 값을 반환하는 함수

    [ex] InputStream openFile(String fileName) : 파일 이름을 받아서 해당 파일을 열고, 파일 스트림을 반환

  3. 인수를 사용해 무언가를 수행하는 함수

    이 함수는 인수를 기반으로 작업을 수행한다

    [ex] void handleWhenPasswordAttemptFailedNTimes(int attempts) : 주어진 인수(시도 횟수)를 기반으로 비밀번호 실패 처리 작업을 수행

이 형태들은 매우 직관적이고, 각 함수는 인수 하나만 사용하기 때문에 가독성단순성을 유지할 수 있다. 하지만 단일 인수를 받는 모든 함수가 좋은 구조는 아니다. 특히, 다음과 같은 형태의 함수는 피해야 한다.

  • 출력 인수를 사용하는 함수 : 함수가 인수를 출력값으로 사용하는 경우, 혼란을 일으킬 수 있다. 일반적으로 함수는 입력값을 받는다고 가정하기 때문에, 인수를 통해 출력을 하는 방식은 직관적이지 않다.
void getEmptyPage (StringBuffer page)
..
getEmptyPage (newPage)

이 함수는 StringBuffer 인수를 통해 값을 설정하려고 하지만, 코드를 읽는 사람은 혼란스러울 수 있다. 대신, 아래 StringBuffer getEmptyPage()처럼 반환값을 통해 데이터를 전달하는 것이 더 명확하다

StringBuffer getEmptyPage()
...
newPage = getEmptyPage()

출력 인수는 함수의 목적을 흐리게 만들고, 코드의 가독성을 해친다. 따라서, 출력 인수보다는 반환값을 사용하는 것이 좋다.

🔻Flag Arguments

  • 플래그 인수 → 두 가지 이상의 서로 다른 동작을 수행할 수 있다는 신호

일반적으로 플래그 인수를 사용하는 함수는 하나의 함수가 두 가지 이상의 작업을 수행하게 되는 구조를 만든다. 이는 가독성을 저하시킬 뿐만 아니라, 테스트 및 유지보수를 어렵게 만든다.

따라서 플래그 인수를 사용하는 대신, 조건에 따라 별도의 함수를 작성하여 각 함수가 하나의 작업만 수행하도록 설계하는 것이 좋다.


📍 TWO

함수에 두 개의 인수를 받는 경우는, 함수의 역할이 조금 더 복잡해지기 시작하는 지점이다. 두 개의 인수를 사용하는 것이 항상 나쁜 것은 아니지만, 함수의 이해도와 테스트 복잡성이 증가할 수 있다.

  • Appropriate two arguments example

    • Point p = new Point(0, 0)
    • 하지만 이 경우 (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
    }
}

이 방법은 함수의 인수를 줄여 함수가 더 명확한 책임을 가지도록 만든다. 또한, 클래스 내의 멤버 변수를 통해 다른 관련된 상태와 함수를 쉽게 처리할 수 있다.

📍 THREE

함수에 세 개의 인수가 있을 때는, 복잡성이 더 커진다. 세 개 이상의 인수를 받는 함수는 특히 테스트나 유지보수에서 어려움을 겪을 수 있다.

예) assertEquals(message, expected, actual);
이 함수는 세 개의 인수를 받으며, 각 인수가 무엇을 의미하는지 파악하기 어려울 수 있다. 세 개의 인수가 있으면 각 인수가 무엇을 나타내는지 매번 확인해야 하며, 이로 인해 혼란이 생길 수 있다.

인수가 하나씩 늘어날 때마다 복잡성은 두배 이상으로 커진다. 함수가 처리해야 할 정보가 많아지고, 그에 따른 혼란과 오류의 가능성도 높아진다. 이로 인해 개발자는 코드의 의미를 파악하기 위해 반복해서 확인해야 한다.

🔻 Argument Objects

세 개 이상의 인수를 가진 함수가 있을 때, 좋은 해결책은 인수들을 객체로 묶는 것이다.

ex. Circle makeCircle (double x, double y, double radius);

Circle makeCircle (Point center, double radius);

✔️ Verbs and Keywords

인수들을 객체로 묶는 것은 인수의 순서와 의도를 더 명확하게 표현하는 데 도움을 준다.

  • 단일 인수를 가진 함수 : 함수와 인수동사-명사 쌍을 형성해야 한다.

    • 예) write(name): 굿
      writeField(name) : 더 나음
  • 인수(매개변수)의 순서 : 함수 이름을 키워드 형식으로 사용하여 기억하기 쉽게 만든다

    • 예) assertEquals(expected, actual)assertExpectedEqualsActual(expected, actual)

📌 Side Effects가 생기지 않게 주의하라!

  • 사람들이 함수명을 봤을 때 기대하는 작업까지만 해야한다.

이처럼 함수명에 적힌 작업 외의 작업을 포함시켜서 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()라는 함수가 있으면 패스워드만 확인해야한다.

  • 부작용 : Session.initialize() 호출하는 부분
    • 함수 이름인 checkPassword()는 명시적으로는 비밀번호만 확인하는 것으로 보이지만, 사실 세션을 초기화하는 숨겨진 동작이 포함되어 있다
    • 이름은 세션 초기화를 포함한다는 사실을 전혀 나타내지 않았다.
  • 문제점 : checkPassword() 함수는 세션을 초기화하는 것이 안전할 때만 호출될 수 있다.
    • 하지만 호출자가 어떻게 이 사실을 알 수 있을까? 이것은 매우 버그를 유발할 가능성이 크다!
  • 해결책 : Session까지 초기화한다면 checkPasswordAndInitializeSession이라고 함수명을 바꿔야 한다.

📌 Output Arguments에 관하여

위에서도 언급을 했지만, 출력인수는 피하는 것이 좋다. 함수 인수는 보통 입력으로 해석되기 때문에, 출력 인수로 사용하면 이를 이해하는 데 시간이 걸린다.

ex. appendFooter(content) 함수에서 content가 footer로 추가되는지, 아니면 content에 footer가 추가되는지 헷갈릴 수 있다. 함수 시그니처를 보고 나서야 content가 출력 인수임을 알 수 있다.

이를 해결하기 위해, content.appendFotter()와 같은 방식으로 호출하는 것이 더 나은 방법이다.

🔻 Comand Query Separation

함수는 명령을 하거나, 질문을 하거나 하나만 수행해야 한다. 두 가지를 동시에 수행하면 혼란을 초래할 수 있으며, 이는 버그로 이어질 수 있다.

예) public boolean set(String attribute, String value) : 이 함수는 속성 값을 설정하고, 동시에 설정이 성공했는지 여부를 반환한다.

if (set("username", "unclebob"))... 와 같이 활용을 한다고 하면 이는 호출자가 "username"이 "unclebob"으로 기존에 설정되어 있었던 것인지, 또는 unclebob으로 설정이 성공했는지를 말하는 것인지 혼란스럽게 만든다

  • 해결책 : 명령과 질문을 분리한다

🔻 ERROR CODES

  • 명령 함수에서 오류 코드를 반환하는 것을 피하라.이는 명령-쿼리 분리 원칙을 위반하는 미묘한 사례다!
  • if 문에서 명령을 조건으로 사용하는 방식은 코드의 중첩 구조를 증가시키고 복잡성을 높인다
    • 모든 함수가 오류 코드를 반환할 때, 호출자는 즉시 오류를 처리해야 하므로 깊이 중첩된 구조가 발생하는 것!

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

The Error.java Dependency Magnet

  • 오류 코드를 반환하는 방식은 Error 클래스나 열거형에 정의된 모든 오류 코드를 참조해야한다는 것을 의미한다. 이는 의존성 자석이 되어, 많은 클래스가 이를 참조하게 된다
  • Error 열거형이 변경되면 모든 관련 클래스가 재컴파일되어야 하므로 새로운 오류 코드를 추가하는 것이 부담스러워진다

👉 예외를 사용하는 경우, 예외를 서브 클래스로 확장할 수 있어 기존 클래스들을 재컴파일할 필요가 없다.


📌 DRY(Don't Repeat Yourself) Principle

중복되는 내용이 있는 것은 소프트웨어에서 악마의 근원이 된다. 코드를 부풀리고, 알고리즘이 바뀌면 많은 변형이 이뤄져야 한다. 하지만, 대부분 조금씩만 차이가 나기 때문에 복제/중복된 부분을 찾아내는 것은 쉽지 않다.

예를 들어, 맨 위에서 들었던 예시 코드(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");
 }

  • DRY 원칙: 각 지식은 단일한 표현을 가져야 한다!
    • 코드에서 같은 지식이나 로직이 여러 번 반복되면 유지보수가 어려워지고 수정 시 오류 발생 가능성이 높아진다

📌 Structured Programming

  • Edsger Dijkstra의 구조적 프로그래밍 규칙:

    • 함수와 함수 내의 블록은 하나의 진입점과 하나의 종료점만을 가져야 한다.
    • 함수는 하나의 return문만 가져야 하며, break나 continue는 반복문에서 사용하지 않는 것이 좋다.
    • goto문은 절대 사용하지 말아야 합니다.
  • 그러나 함수가 작은 경우, 이런 규칙들이 큰 도움이 되지 않을 수 있습니다. 가끔씩 여러 개의 return문이나 break, continue문을 사용하는 것이 더 표현력이 좋을 때도 있습니다.

  • 소프트웨어 작성 = 글쓰기와 비슷!
    • 글을 쓸 때, 먼저 생각을 적어내고(초안 작성), 그 후 구조를 재정비하여 읽기 쉽게 만든다.
    • 함수도 비슷하다. 처음에는 길고 복잡할 수 있지만, 나중에 리팩토링하면서 함수들을 쪼개고, 이름을 바꾸고, 중복을 제거하여 개선할 수 있다.
  • 최종적으로 좋은 함수가 만들어집니다.
profile
욕심 많은 공대생

0개의 댓글