본문 바로가기
Book

[독서 기록] 클린 코드 9장, 10장 / 단위 테스트, 클래스

by Renechoi 2022. 11. 9.

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

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

첫째 법칙 : 실패하는 단위 테스트를 작성할 때까지 실제 코드를 작성하지 않는다.

둘째 법칙 : 컴파일은 실패하지 않으면서 실행이 실패하는 정도로만 단위 테스트를 작성한다.

셋째 법칙 : 현재 실패하는 테스트를 통과할 정도로만 실제 코드를 작성한다. 

 

- 155p

 

 

테스트 케이스가 있다면 공포는 사실상 사라진다. 테스트 커버리지가 높을수록 공포는 줄어든다. 아키텍처가 부실한 코드나 설계가 모호하고 엉망인 코드라도 별다른 우려 없이 변경할 수 있다. 아니, 오히려 안심하고 아키텍처와 설계를 개선할 수 있다. 

- 157p 

 

 

깨끗한 테스트 코드를 만들려면? 세 가지가 필요하다. 가독성, 가독성, 가독성. 어쩌면 가독성은 실제 코드보다 테스트 코드에 더더욱 중요하다. 테스트 코드에서 가독성을 높이려면? 여느 코드와 마찬가지다. 명료성, 단순성, 풍부한 표현력이 필요하다. 테스트 코드는 최소의 표현으로 많은 것을 나타내야 한다.

- 158p 

 

 

가독성이 좋은 코드 

public void testGetPageHierarchyAsXml() throws Exception {
	makePages("PageOne", "PageOne.ChildOne", "PageTwo");

	submitRequest("root", "type:pages");

	assertResponseIsXML();
	assertResponseContains(
		"<name>PageOne</name>", "<name>PageTwo</name>", "<name>ChildOne</name>");
}

public void testSymbolicLinksAreNotInXmlPageHierarchy() throws Exception {
	WikiPage page = makePage("PageOne");
	makePages("PageOne.ChildOne", "PageTwo");

	addLinkTo(page, "PageTwo", "SymPage");

	submitRequest("root", "type:pages");

	assertResponseIsXML();
	assertResponseContains(
		"<name>PageOne</name>", "<name>PageTwo</name>", "<name>ChildOne</name>");
	assertResponseDoesNotContain("SymPage");
}

public void testGetDataAsXml() throws Exception {
	makePageWithContent("TestPageOne", "test page");

	submitRequest("TestPageOne", "type:data");

	assertResponseIsXML();
	assertResponseContains("test page", "<Test");
}

- 160p 

 

public String getState() {
    String state = "";
    state += heater ? "H" : "h";
    state += blower ? "B" : "b";
    state += cooler ? "C" : "c";
    state += hiTempAlarm ? "H" : "h";
    state += loTempAlarm ? "L" : "l";
    return state;
}

StringBuffer는 보기에 흉하다. 나는 실제 코드에서도 크게 무리가 아니라면 StringBuffer는 피한다. ... 실제 환경에서는 절대로 안 되지만 테스트 환경에서는 전혀 문제없는 방식이 있다.

- 164P 

 

 

 

나는 '단일 assert문'이라는 규칙이 훌륭한 지침이라 생각한다. 목록 9-5에서 봤듯이, 대체로 나는 단일 assert를 지원하는 해당 분야 테스트 언어를 만들려 노력한다. 하지만 때로는 주저 없이 함수 하나에 여러 assert문을 넣기도 한다. 단지 assert문 개수는 최대한 줄여야 좋다는 생각이다.

- 165p 

 

 

 

테스트 당 개념 하나 

/**
 * addMonth() 메서드를 테스트하는 장황한 코드
 */
public void testAddMonths() {
	SerialDate d1 = SerialDate.createInstance(31, 5, 2004);
	
	// (6월처럼) 30일로 끝나는 한 달을 더하면 날짜는 30일이 되어야지 31일이 되어서는 안된다.
	SerialDate d2 = SerialDate.addMonths(1, d1); 
	assertEquals(30, d2.getDayOfMonth()); 
	assertEquals(6, d2.getMonth()); 
	assertEquals(2004, d2.getYYYY());

	// 두 달을 더하면 그리고 두 번째 달이 31일로 끝나면 날짜는 31일이 되어야 한다.
	SerialDate d3 = SerialDate.addMonths(2, d1); 
	assertEquals(31, d3.getDayOfMonth()); 
	assertEquals(7, d3.getMonth()); 
	assertEquals(2004, d3.getYYYY());

	// 31일로 끝나는 한 달을 더하면 날짜는 30일이 되어야지 31일이 되어서는 안된다.
	SerialDate d4 = SerialDate.addMonths(1, SerialDate.addMonths(1, d1)); 
	assertEquals(30, d4.getDayOfMonth());
	assertEquals(7, d4.getMonth());
	assertEquals(2004, d4.getYYYY());
}

- 166p 

 

 

 

First 규칙

빠르게 Fast

독립적으로 Independent

반복가능하게 Repeatable

자가검증하는 Self-Validating

적시에 Timely

 

- 167 ~ 168p 

 

 


 

 

클래스 체계

- 172p 

 

클래스는 작아야 한다. 

- 172p

 

 

단일 책임 원칙Single Responsibility Principle은 클래스나 모듈을 변경할 이유가 하나, 단 하나뿐이어야 한다는 원칙이다.

- 175p 

 

 

큰 클래스 몇 개가 아니라 작은 클래스 여럿으로 이뤄진 시스템이 더 바람직하다. 

- 177p 

 

 

 

 

응집도가 높은 클래스 

public class Stack {
    private int topOfStack = 0;
    List<Integer> elements = new LinkedList<Integer>();

    public int size() { 
        return topOfStack;
    }

    public void push(int element) { 
        topOfStack++; 
        elements.add(element);
    }

    public int pop() throws PoppedWhenEmpty { 
        if (topOfStack == 0)
            throw new PoppedWhenEmpty();
        int element = elements.get(--topOfStack); 
        elements.remove(topOfStack);
        return element;
    }
}

 

'함수를 작게, 매개변수 목록을 짧게'라는 전략을 따르다 보면 때때로 몇몇 메서드만이 사용하는 인스턴스 변수가 아주 많아진다. 이는 십중팔구 새로운 클래스로 쪼개야 한다는 신호다. 응집도가 높아지도록 변수와 메더르를 적절히 분리해 새로운 클래스 두 세개로 쪼개준다.

- 178p 

 

 

 

 

<참고자료>

https://github.com/kwj1270/TIL_CleanCode/blob/master/10%20%ED%81%B4%EB%9E%98%EC%8A%A4.md

 

 

 

 

package literatePrimes;

import java.util.ArrayList;

public class PrimeGenerator {
    private static int[] primes;
    private static ArrayList<Integer> multiplesOfPrimeFactors;

    protected static int[] generate(int n) {
        primes = new int[n];
        multiplesOfPrimeFactors = new ArrayList<Integer>(); 
        set2AsFirstPrime(); 
        checkOddNumbersForSubsequentPrimes();
        return primes; 
    }

    private static void set2AsFirstPrime() { 
        primes[0] = 2; 
        multiplesOfPrimeFactors.add(2);
    }

    private static void checkOddNumbersForSubsequentPrimes() { 
        int primeIndex = 1;
        for (int candidate = 3 ; primeIndex < primes.length ; candidate += 2) { 
            if (isPrime(candidate))
                primes[primeIndex++] = candidate; 
        }
    }

    private static boolean isPrime(int candidate) {
        if (isLeastRelevantMultipleOfNextLargerPrimeFactor(candidate)) {
            multiplesOfPrimeFactors.add(candidate);
            return false; 
        }
        return isNotMultipleOfAnyPreviousPrimeFactor(candidate); 
    }

    private static boolean isLeastRelevantMultipleOfNextLargerPrimeFactor(int candidate) {
        int nextLargerPrimeFactor = primes[multiplesOfPrimeFactors.size()];
        int leastRelevantMultiple = nextLargerPrimeFactor * nextLargerPrimeFactor; 
        return candidate == leastRelevantMultiple;
    }

    private static boolean isNotMultipleOfAnyPreviousPrimeFactor(int candidate) {
        for (int n = 1; n < multiplesOfPrimeFactors.size(); n++) {
            if (isMultipleOfNthPrimeFactor(candidate, n)) 
                return false;
        }
        return true; 
    }

    private static boolean isMultipleOfNthPrimeFactor(int candidate, int n) {
        return candidate == smallestOddNthMultipleNotLessThanCandidate(candidate, n);
    }

    private static int smallestOddNthMultipleNotLessThanCandidate(int candidate, int n) {
        int multiple = multiplesOfPrimeFactors.get(n); 
        while (multiple < candidate)
            multiple += 2 * primes[n]; 
        multiplesOfPrimeFactors.set(n, multiple); 
        return multiple;
    } 
}

 

가장 먼저 눈에 띄는 변화가 프로그램이 길어졌다는 사실이다. 한쪽을 조금 넘겼던 프로그램이 거의 세 쪽으로 늘어났다. 길이가 늘어난 이유는 여러 가지다. 첫째, 리팩터링한 프로그램은 좀 더 서술적인 변수 이름을 사용한다. 둘째, 리팩터링한 프로그램은 코드에 주석을 추가하는 수단으로 함수 선언과 클래스 선언을 활용한다. 셋째, 가독성을 높이고자 공백을 추가하고 형식을 맞추었다.

 

원래 프로그램은 세 가지 책임으로 나눠졌다. PrimPrinter 클래스는 MAIN 함수 하나만 포함하여 실행환경을 책임진다. 호출 방식이 달라지면 클래스도 바뀐다. 예를 들어, 프로그램 SOAP 서비스로 바꾸려면 PrimePrinter 클래스를 고쳐준다.

 

RowColumnPagePrinter 클래스는 숫자 목록을 주어진 행과 열에 맞춰 페이지에 출력하는 방법을 안다. 출력하는 모양새를 바꾸려면 RowColumnPagePrinter 클래스를 고쳐준다.

 

PrimeGenerator 클래스는 소수 목록을 생성하는 방법을 안다. 코스를 살펴보면 알겠지만, 객체로 인스턴스화하는 클래스가 아니다. 단순히 변수를 선언하고 감추려고 사용하는 유용한 공간일 뿐이다. 소수를 계산하는 알고리즘이 바뀐다면 Primgenerater 클래스를 고쳐준다.

 

재구현이 아니다! 프로그램을 처음부터 다시 짜지 않았다. 실제로 두 프로그램을 자세히 살펴보면 알고리즘과 동작 원리가 동일하다는 것을 눈치채리라.

 

가장 먼저, 원래 프로그램의 정확한 동작을 검증하는 테스트 슈트를 작성했다. 그런 다음, 한 번에 하나씩 수 차례에 걸쳐 조금씩 코드를 변경했다. 코드를 변경할 때마다 테스트를 수행해 원래 프로그램과 동일하게 동작하는지 확인했다. 조금씩 원래 프로그램을 정리한 결과 최종 프로그램이 얻어졌다. 

 

- 185p 

 

 

 

.
.
.

public class SelectWithMatchSql extends Sql { 
	public SelectWithMatchSql(String table, Column[] columns, Column column, String pattern) 
	@Override public String generate()
}

public class FindByKeySql extends Sql (
	String table, Column[] columns, String keyColumn, String keyValue) 
	@Override public String generate()
}

public class PreparedInsertSql extends Sql {
	public PreparedInsertSql(String table, Column[] columns) 
	@Override public String generate() {
	private String placeholderList(Column[] columns)
}

public class Where {
	public Where(String criteria) public String generate()
	public String generate() {
}

public class ColumnList {
	public ColumnList(Column[] columns) public String generate()
	public String generate() {
}

 

각 클래스는 극도로 단순하다. 코드는 순식간에 이해된다. 함수 하나를 수정했다고 다른 함수가 망가질 위험도 사실상 사라졌다. 

 

- 187 ~ 188p 

 

 

 

상세한 구현에 의존하는 코드는 테스트가 어렵다.예를 들어, Portfolio 클래스를 만든다고 가정하자. 그런데 Portfolio 클래스는 외부 TokyoStockExchange API를 사용해 포트폴리오 값을 계산한다. 따라서 우리 테스트 코드는 시세 변화에 영향을 받는다. 5분마다 값이 달라지는 API로 테스트 코드를 짜기란 쉽지 않다.

 

Portfolio 클래스에서 TojyoStockExchange API를 직접 호출하는 대신 Stock Exchange라는 인터페이스를 생성한 후 메서드 하나를 선언한다. 

 

public insterface StockExchange {
	Money currentPrice(String symbol);
}

 

다음으로 StockExchange 인터페이스를 구현하는 TokyoStockExchange 클래스를 구현한다. 또한 Portfolio 생성자를 수정해 StockExchange 참조자를 인수로 받는다. 

public Portfolio { 
	private StockExchange exchange;
	public Portfolio(StockExchange exchange) {
		this.exchange = exchange;
	}
	// ...
}

이제 TojyoStockExchange 클래스를 흉내내는 테스트용 클래스를 만들 수 있다. 테스트용 클래스는 StockExchange 인터페이스를 구현하며 고정된 주가를 반환한다. 테스트에서 마이크로소프트 주식 다섯주를 구입한다면 테스트용 클래스는 언제나 100불을 반환한다. 우리 테스트용 클래스는 단순히 미리 정해놓은 표 값만 참조한다. 그러므로 우리는 전체 포트폴리오 총계가 500불인지 확인하는 테스트 코드를 작성할 수 있다. 

 

public class PortfolioTest {
	private FixedStockExchangeStub exchange;
	private Portfolio portfolio;

	@Before
	protected void setUp() throws Exception {
		exchange = new FixedStockExchangeStub();
		exchange.fix("MSFT", 100);
		portfolio = new Portfolio(exchange);
	}

	@Test
	public void GivenFiveMSFTTotalShouldBe500() throws Exception {
		portfolio.add(5, "MSFT");
		Assert.assertEquals(500, portfolio.value());
	}
}

-188 ~ 190p 

 

 

우리가 개선한 Portfolio 클래스는 TokyoStockExchange라는 상세한 구현 클래스가 아니라 StockExchange 인터페이스에 의존한다. StockExchange 인터페이스는 주식 기호를 받아 현재 주식 가격을 반환한다는 추상적인 개념을 표현한다. 이와 같은 추상화로 실제로 주가를 얻어오는 출처나 얻어오는 방식 등과 같은 구체적인 사실을 모두 숨긴다. 

- 190p 

 

 

반응형