Search
Duplicate

[클린코드] 함수는 작고 간결하게

간단소개
함수를 작성하고 리펙토링 해보기
팔만코딩경 컨트리뷰터
ContributorNotionAccount
주제 / 분류
Java
개발방법론
개발지식
Scrap
태그
Object Oriented Programming
clean
설계
Java
9 more properties

작게 만들어라!

간결한 함수 작성하기

[code1]
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 = wikiPage.getPageCrawler().getFullPath(teardown); String tearDownPathName = PathParser.render(tearDownPath); buffer.append("\n") .append("!include -teardown .") .append(tearDownPathName) .append("\n"); } if (includeSuiteSetup) { WikiPage suiteTeardown = PageCrawlerImpl.getInheritedPage( SuiteResponder.SUITE_TEARDOWN_NAME, wikiPage ); if (suiteTeardown != null) { WikiPagePath pagePath = suiteTeardown.getPageCrawler().getFullPath(suiteTeardown); String pagePathName = PathParser.render(pagePath); buffer.append("!include -teardown .") .append(pagePathName) .append("\n"); } } } pageData.setContent(buffer.toString()); return pageData.getHtml(); }
Java
복사
⇒ 딱 봐도 함수가 길다.
[code2]
다음은 위 코드를 9줄로 리펙터링 한 내용이다.
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(); }
Java
복사
→isTestPage 는 pageData에서 Test라는 속성을가져와서, isTestPage 가 true 일때, Wikipage 를pageData에서 가져와서 includeSetupPages 에 전달해주고… newPageContent 에 내용을 append 해주는 내용, includeSetpuPage나, includeTeardownPages 등등 이것도 하는 일이 많다.
되도록 한 함수당 3~5줄 이내로 줄이는 것을 권장한다.
[code3]
public static String renderPageWithSetupsAndTeardowns( PageData pageData, boolean isSuite) throws Exception { if (isTestPage(pageData)) includeSetupAndTeardownPages(pageData, isSuite); return pageData.getHtml(); }
Java
복사
⇒작게 쪼개서 함수 내 추상화 수준을 동일하게 맞춘다.
⇒페이지가 테스트 페이지인지 확인한 후 테스트 페이지라면 설정 페이지와 해제 페이지를 넣는다. 테스트 페이지든 아니든 페이지를 HTML로 렌더링한다.

한 가지만 해라!

페이지를 가져오고, 상속된 페이지를 검색하고, 경로를 렌더링하고, 문자열을 붙이고, HTML을 생성한다.
반면에 [code3] 의 함수는 한 가지만 처리한다. 설정 페이지와 해제 페이지를 테스트 페이지에 넣는다.

추상화 수준을 맞춰라

[code1] 은 추상화 수준이 맞지 않는다.
getHtml()은 추상화 수준이 매우 높다
String pagePathName = PathParser.render(pagepath); 는 추상화 수준이 중간이다.
.append(”\n”) 과 같은 코드는 추상화 수준이 아주 낮다.
⇒한 함수 내에서 추상화 수준을 섞으면 코드를 읽는사람이 헷갈린다.

내려가기 규칙

함수가 ‘한가지’ 작업만 하려면 함수 내 모든 문장의 추상화 수준이 동일해야 된다. 만약 한 함수 내에 추상화 수준이 섞이게 된다면 읽는 사람이 헷갈린다.
위에서 아래로 코드 읽기
코드는 위에서 아래로 이야기처럼 읽혀야 좋다.함수 추상화 부분이 한번에 한단계씩 낮아지는 것이 가장 이상적이다.(내려가기 규칙)

중첩구조

중첩구조(while, if / else 등) 에 들어가는 블록은 한 줄이어야 한다.
각 함수 별 들여쓰기 수준이 2레벨을 넘어가지 않고 각 함수가 명확하다면 함수는 더욱 이해하기 쉬워진다.

SRP(한가지만 하면서),OCP (변경에 닫게 만들기)

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); } }
Java
복사
계산도하고, Money도 생성한다 ⇒ 두 가지 기능이 보인다.
만약 새로운 직원 타입이 추가된다면?
public abstract class Employee { public abstract boolean isPayday(); public abstract Money calculatePay(); public abstract void deliverPay(Money pay); } public interface EmployeeFactory { public 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) ; case HOURLY: return new HourlyEmployee(r); case SALARIED: return new SalariedEmploye(r); default: throw new InvalidEmployeeType(r.type); } } }
Java
복사
계산과 타입을 분리
⇒ 타입에 대한 처리는 최대한 Factory 에서만 한다.
Empoly 라는 클래스 안에 필요한 메소드 들을 abstact 추상으로 선언
public abstract boolean isPayday(); public abstract Money calculatePay(); public abstract void deliverPay(Money pay);
Java
복사
⇒계산하는 과정들이 선언되어있다.
EmployeeFactory 를 통해 Employee 들이 생성되는 과정을 분리해놓았다.
EmployeeFactoryImpl 클래스
EmployeeFactory 를 구현하였다.
Employee makeEmployee(EmployeeRecord r)
Employee 의 레코드를 가져와서 type (COMMISONED, HOURLY, SALARIED) 에 따라서 Empolyee 자체를 생성해준다.
Money는 Employee 자체에 calculatePay() 메소드를 통해서 계산을 하도록 처리한다.
⇒ 계산을 하는곳과 Type 에대한 처리를 하도록 분리가 되었다.
⇒ 분기를 나누어야할때는 객체를 생성하는 부분에서 분기를 나누어주고, 다른부분들은 다형성을 사용할 수 있도록 처리한다.

함수 인수

인수의 갯수는 0~2개가 적당하다.
[BAD]
Circle makeCircle(double x, double y, double radius);
Java
복사
[GOOD]
Circle makeCircle(Point center, double radius);
Java
복사
가변인자를 넘기는경우
String.format(String format, Object... args);
Java
복사
⇒특별한 경우가 아니면 잘 사용하지 않는다. 객체를 통해서 인자를 넘기는것이 일반적이다.

부수효과(Side Effect)를 일으키지 마라!

부수효과
값을 반환하는 함수가 외부 상태를 변경하는 경우
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; } }
Java
복사
1.
checkPassword 비밀번호를 확인하는 함수겠구나
2.
User 객체를 전달인자 userName 으로 찾아서 가져온다
3.
User객체의 codedPhrase 를 가지고 와서, decrypt 한다 (암호화 복호화)
4.
if 유효한 패스워드 일경우에 Session 을 초기화한다.
세션이라는 것은 서버 어플리케이션의 전역에 접근한다.
checkPassword 에서 수행하기에는 범위를 벗어난 행동이다.
5.
리턴 true 혹은 false
⇒함수와 관계없는 외부 상태를 변경시킨다. 따라서 함수 이름과 맞지 않는다.

함수 일단 작성해보기

처음부터 잘 짜지기는 정말 힘들다.
처음에는 길고 복잡하고, 들여쓰기 단계나 중복된 루프도 많다. 인수목록도 길지만, 이 코드들을 빠짐없이 테스트하는 단위 테스트 케이스도 만들고, 코드를 다듬고, 함수를 만들고, 이름을 바꾸고, 중복을 제거한다.
1.
기능을 구현하는 함수를 일단 작성
길고, 복잡하고 중복되는 내용이 있다.
2.
테스트 코드를 작성
함수 내부의 분기와 엣지값마다 전부 테스트하는 코드를 짜본다. 여기까지하면 기능에 대한 구현이 된 것 이다.
3.
리팩터링
코드를 다듬고, 함수를 쪼개고, 이름을 바꾸고, 중복을 제거한다.
현재 아웃풋과 동일하다는 전재하에, 가독성을 높이는 작업이다. 따라서 테스트 코드를 돌려보면서 가독성을 높이며 아웃풋이 달라지지 않았는지 확인하는 작업이 필요하다.