본문 바로가기
Book

[독서 기록] 클린 코드 3장 / 함수

by Renechoi 2022. 11. 8.

클린 코드, 로버트 C. 마틴, 박재호 이해영 옮김, 인사이트

 
Clean Code(클린 코드)
『Clean Code(클린 코드)』은 오브젝트 멘토(Object Mentor)의 동료들과 힘을 모아 ‘개발하며’ 클린 코드를 만드는 최상의 애자일 기법을 소개하고 있다. 소프트웨어 장인 정신의 가치를 심어 주며 프로그래밍 실력을 높여줄 것이다. 여러분이 노력만 한다면. 어떤 노력이 필요하냐고? 코드를 읽어야 한다. 아주 많은 코드를. 그리고 코드를 읽으면서 그 코드의 무엇이 옳은지, 그른지 생각도 해야 한다. 좀 더 중요하게는 전문가로서 자신이 지니는 가치와 장인으로서 자기 작품에 대한 헌신을 돌아보게 된다.
저자
로버트 C 마틴
출판
인사이트
출판일
2013.12.24

 

작게 만들어라

- 42p

 

함수에서 들여쓰기 수준은 1단이나 2단을 넘어서면 안 된다. 

- 44p

 

함수는 한 가지를 해야 한다. 그 한 가지를 잘 해야 한다. 그 한 가지만을 해야 한다.

- 44p

 

 

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

- 45p

 

 

직원 유형에 따라 다른 값을 계산해 반환하는 함수다.

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

 

위 함수에는 몇 가지 문제가 있다. 첫째, 함수가 길다. 새 직원 유형을 추가하면 더 길어진다. 둘째, '한 가지' 작업만 수행하지 않는다. 셋째, SRP를 위반한다. 코드를 변경할 이유가 여럿이기 때문이다. 넷째, OCP를 위반한다. 새 직원 유형을 추가할 때마다 코드를 변경하기 때문이다. 하지만 아마 가장 심각한 문제라면 위 함수와 구조가 동일한 함수가 무한정 존재한다는 사실이다. 예를 들어, 다음과 같은 함수가 가능하다.

 

isPayday(Employee e, Date date);

 

...

 

이 문제를 해결한 코드가 목록 3-5다. 목록 3-5는 switch 문을 추상 팩토리에 꽁꽁 숨긴다. 아무에게도 보여주지 않는다. 팩토리는 switch 문을 사용해 적절한 Employee 파생 클래스의 인스턴스를 생성한다. calculatePay, isPayday, deliverPay 등과 같은 함수는 Employee 인터페이스를 거쳐 호출된다. 그러면 다형성으로 인해 실제 파생 클래스의 함수가 실행된다. 

 

public abstract class Employee{
    public abstract boolean isPayDay();
    public abstract Money calculatePay();
    public abstract void deliveryPay(Money pay);
}
public interface EmployeeFactory{
    public Employee makeEmployee(EmployeeRecord r) throws InvalidEmployeeType;
}
public class EmployeeFactoryImpl implements EmployeeFactory {

    @Override
    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 SalariedEmployee(r);
            default:
                throw new InvalidEmployeeType(r.type);
        }
    }
}

- 47 ~ 48p 

 

 

 

서술적인 이름을 사용해라

함수 인수 (3개는 가능한 피하는 편이 좋다)

- 49 ~ 50p

 

 

인수 객체

인수가 2-3개 필요하다면 일부를 독자적인 클래스 변수로 선언할 가능성을 짚어 본다. 예를 들어, 다음 두 함수를 살펴보자.

 

Circle makeCircle(double x, double y, double radius);
Circle makeCircle(Point center, double radius);

객체를 생성해 인수를 줄이는 방법이 눈속임이라 여겨질지 모르지만 그렇지 않다. 위 예제에서 x와 y를 묶었듯이 변수를 묶어 넘기려면 이름을 붙여야 하므로 결국은 개념을 표현하게 된다.

- 53p

 

 

 

public class UserValidator {
    private Cryptographer cryptographer;
    
    public boolean checkPassword(String userName, String password){
        User user = UserGateWay.findByName(userName);
        if(user != USER.NULL){
            String codePhrase = user.getPhraseEncodedByPassword();
            String phrase = cryptographer.decrypt(codePhrase, password);
            if("Valid Password".equals(phrase)){
                Session.initialize();
                return true;
            }
        }
        return false;
    }
}

여기서, 함수가 일으키는 부수 효과는 Session.initialize() 호출이다. checkPassword 함수는 이름 그대로 암호를 확인한다. 이름만 봐서는 세션을 초기화한다는 사실이 드러나지 않는다. 

- 55p

 

 

목록 3-6은 checkPasswordAndInitializeSession 이라는 이름이 훨씬 좋다. 물론 함수가 '한 가지'만 한다는 규칙을 위반하지만.

- 56p 

 

 

명령과 조회를 분리하라

- 56p 

 

 

public boolean set(String attribute, String value);

이 함수는 이름이 attribute인 속성을 찾아 값을 value로 설정한 후 성공하면 true를 반환하고 실패하면 false를 반환한다. 

 

... 

 

진짜 해결책은 명령과 조회를 분리해 혼란을 애초에 뿌리뽑는 방법이다. 

if (attributeExists("userName")){
    setAttribute("userName", "goodGid");
}

- 57p 

 

 

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

- 57p 

 

 

 

오류 처리고 한 가지 작업이다.

- 59p 

 

 

중복은 소프트웨어에서 모든 악의 근원이다.

- 60p 

 

 

소프트웨어를 짜는 행위는 여느 글짓기와 비슷하다. 논문이나 기사를 작성할 때는 먼저 생각을 기록한 후 읽기 좋게 다듬는다. 초안은 대개 서투르고 어수선하므로 원하는 대로 읽힐 때까지 말을 다듬고 문장을 고치고 문단을 정리한다.

 

내가 함수를 짤 때도 마찬가지다. 처음에는 길고 복잡하다. 들여쓰기 단계도 많고 중복된 루프도 많다. 인수 목록도 아주 길다. 이름은 즉흥적이고 코드는 중복된다. 하지만 나는 그 서투른 코드를 빠짐없이 테스트하는 단위 테스트 케이스도 만든다. 

 

그런 다음 나는 코드를 다듬고, 함수를 만들고, 이름을 바꾸고, 중복을 제거한다. 메서드를 줄이고 순서를 바꾼다. 때로는 전체 클래스를 쪼개기도 한다. 이 와중에도 코드는 항상 단위 테스트를 통과한다.

 

최종적으로는 이 장에서 설명한 규칙을 따르는 함수가 얻어진다. 처음부터 딱 짜내지 않는다. 그게 가능한 사람은 없으리라. 

 

- 61 ~ 62p 

 

 

대가 프로그래머는 시스템을 (구현할) 프로그램이 아니라 (풀어갈) 이야기로 여긴다. 프로그래밍 언어라는 수단을 사용해 좀 더 풍부하고 좀 더 표현력이 강한 언어를 만들어 이야기를 풀어간다. 시스템에서 발생하는 모든 동작을 설명하는 함수 계층이 바로 그 언어에 속한다. 재귀라는 기교로 각 동작은 바로 그 도메인에 특화된 언어를 사용해 자신만의 이야기를 풀어간다.

-62p

 

 

정말 아름답다. 

 

 

 

package fitness.html;
 
import fitnesse.reponders.run.SuiteResponder;
import fitnesse.wiki.*;
 
puclib class SetupTeardownIncluder {
 
    private PageData pageData;
    private boolean isSuite;
    private WikiPage testPage;
    private StringBuffer newPageContent;
    private PageCrawler pageCrawler;
 
    public static String render ( Page Data pageData ) throws Exception {
        return render(pageData, false);
    }
 
    public static String render ( PageData pageData, boolean isSuite ) throws Exception {
        return new SetupTeardownIncluder ( pageData ) .render( isSuite);
    }
 
    private SetupTeardownIncluder ( PageData pageData ) {
        this.pageData = pageData;
        testPage = pageData.getWikiPage();
        pageCrawler = testPage.getPageCrawler();
        newPageContent = new String buffer();
    }
 
    private String render ( boolean isSuite ) throws Exception {
        this.isSuite = isSuite;
        if ( isTestPage() ) 
        includeSetipAndTeardownPages();
        return pageData.getHtml();
    }
 
    private boolean isTestPage(0 throw Exception {
        return pageData.hasAttribute("Test");
    }
 
    private void includeSetipAndTeardownPages() throws Exception {
        includeSetupPages();
        icludePageContent();
        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 ((is Suite)
            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 = findInheeritedPage( pageName);
        if ( inheritedPage != null ) {
            String pagePathName = getPathNameForPage ( inheritedPage );
            buildIncludeDirective( pagePathName, arg );
        }
    }
 
    private WikiPage indInheritedPage( String pageName ) throws Exception {
        return PageCrawlerImpl.getInheritedPage( pageName, testPage );
    }
 
    private String getPathNameForPage( WikiPage page ) throws Excpetion {
        WikiPagePath pagePath = pageCrawler.getFullPath( page );
        return PathParser.render( pagePth) ;
    }
 
    private void buildIncludeDirective( String pagePathName, String arg ) {
        newPageContent
            .append("\n!include ")
            .append(arg)
            .appent(" .")
            .append(pagePathName)
            .append("\n");
    }
}

- 62 ~ 65p

반응형