클린코드 9장 단위테스트

 

TDD 법칙 세 가지

  • 첫째 법칙: 실패하는 단위 테스트를 작성할 때까지 실제 코드를 작성하지 않는다.
  • 둘째 법칙: 컴파일은 실패하지 않으면서 실행이 실패하는 정도로만 단위 테스트를 작성한다.
  • 셋째 법칙: 현재 실패하는 테스트를 통과할 정도로만 실제 코드를 작성한다.

깨끗한 테스트 코드 유지하기

지저분한 테스트 코드라도 있는 것이 좋을 것이라고 판단하기 쉬우나 사실은 테스틑를 안하는 것 보다 더 나쁘다.

실제코드가 진화하면 테스트 코드도 변해야 한다. 그런데 테스트 코드가 복잡하면 실제 코드를 짜는 시간보다 테스트 케이스를 추가하는 시간이 더걸리기 십상이다.

새 버전 출시할 때 마다 팀이 테스트 케이스를 유지하고 보수하는 비용도 늘어 테스트 코드가 점차 개발자 사이에서 가장 큰 불만으로 자리 잡는다.

하지만 테스트 슈트가 없으면 개발자는 자신이 수정한 코드가 제대로 도는지 확인할 방법이 없다. 테스트 슈트가 없으면 시스템 이쪽을 수정해도 저쪽이 안전하다는 사실을 검증하지 못한다. 그래서 결합율이 높아지기 시작한다.

변경하면 듣곱다 해가 크다 생각해 더 이상 코드를 정리하지 않고, 그러면 코드가 망가지기 시작한다. 결국 테스트 슈트도 없고 얼기설기 뒤섞인 코드에 좌절한 고객과 테스트에 쏟아 부은 노력이 허라였다는 살망감만 남는다.

테스트 코드는 실제 코드 못지 않게 중요하다.

테스트는 유연성, 유지보수성, 재사용성을 제공한다.

테스트 코드를 깨끗하게 유지하지 않으면 결국 잃어버린다. 그리고 테스트 케이스가 없으면 실제 코드를 유연하게 만드는 버팀목도 사라진다.

코드에 유연성, 유지보수성, 재사용성을 제공하는 버팀목이 바로 단위 테스트다. 이유는 단순하다. 테스트 케이스가 있으면 변경이 두렵지 않다.

테스트 코드가 없으면 설계를 아무리 잘 해도 모든 변경이 잠정적인 버그다.

실제 코드를 점검하는 자동화된 단위 테스트 슈트는 설계와 아키텍처를 최대한 깨끗하게 보존하는 열쇠다. 테스트는 유연성, 유지보수성, 재사용성을 제공한다.

깨끗한 테스트 코드

깨끗한 테스트 코드를 만들려면 가독성이 필요하다.

가독성을 높이려면 명료성, 단순성, 풍부한 표현력이 필요하다.

테스트 코드는 최소의 표현으로 많은 것을 나타내야 한다.

도메인에 특화된 테스트 언어

public void testGetPageHierarchyAsXml() throws Exception {
	makePages("pageOne", "pageOne.ChiildOne", "PageTwo");
	
	submitRequest("root", "type:pages");
	
	assertResponseIsXml();
	assertResponseContains("<name>PageOne</name>", "<name>PageTwo</name>", "<name>ChildOne</name>");)
}

public void testSymbolicLinkAreNotInXmlPageHierachy() throws Exception {
	WikiPage page = makePage("PageOne");
	makePages("PageOne.ChioldOne", "PageTow");
	
	addLinkTo(page, "PageTwo", "SymPage");
	
	submitRequest("root", "type:pages");
	
	assertResponseIsXml();
	assertResponseContains("<name>PageOne</name>", "<name>PageTwo</name>", "<name>ChildOne</name>");)
	assertResponseDoesNotContain("SymPage");
}

public void testGetDataAXml() throws Exception {
	makePageWithContent("TestPageOne", "test page");
	
	submitRequest("TestPageOne", "type:data");
	
	assertREsponseIsXML();
	assertResponseContains("test page", "<test");
}

이 코드는 도메인에 특화된 언어로 테스트 코드를 구현하는 기법을 보여준다.

흔히 쓰는 시스템 조작 API 를 사용하는 대신 API 위에 함수와 유틸리티를 규현한 후 그 함수와 유틸리티를 사용하므로 테스트 코드를 짜기도 읽기도 쉬워진다.

이렇게 구현한 함수와 유틸리티를 사용하므로 테스트 코드를 짜기도 읽기도 쉬워진다.

이렇게 구현한 함수와 유틸리티는 테스트 코드에서 사용하는 특수 API가 된다. 즉, 테스트를 구현하는 당사자와 나중에 테스트를 일겅볼 독자를 도와주는 테스트 언어가 된다.

이중 표준

테스트 API 코드에 적용하는 표준은 실제 코드에 적용하는 표준과 확실히 다르다.

단순하고 간결하고 표현력이 풍부해야 하지만 실제 코드만큼 효율적일 필요는 없다.

실제 환경이 아니라 테스트 환경에서 돌아가는 코드이기 때문이다. 실제 환경과 테스트 환경은 요구사항이 판이하게 다르다.

아래 코드는 온도가 급격하게 떨어지면 경보 온풍기 송푸기 가 모두 가동되는지 확인하는 코드이다.

@Test
public void turnOnLoTempAlarmAtThreshold() throws Exception {
	hw.setTemp(WAY_TOO_COLD);
	
	controller.tic();
	
	assertTrue(hw.heaterState());
	assertTrue(hw.blowerState());
	assertFalse(hw.collerState());
	assertFalse(hw.hiTempAlarm());
	assertTrue(hw.loTempAlarm());
}

위 코드는 가독성이 좋지 않다, 아래와 같이 변경하면 가독성이 높아진다.

@Test
public void turnOnLoTempAlarmAtThreshold() throws Exception {
	wayTooColde();
	
	assertEquals("HBchL", hw.getState());
}

wayTooColde 함수를 만들어서 tic 함수 를 숨겼다.

“HBchL” 라는 문자열을 만들어 새로운 상태값을 만들었다. (물론 그릇된 정보를 피하라 규칙 위반에 가깝지만 여기에서는 적절해보인다.)

문자는 {heater, blower, cooler, hi-temp-alarm, lo-temp-alarm} 순서이고 대문자는 켜짐 소문자는 꺼짐이다.

이렇게 만들면 아래와 같이 테스트 코드를 확장할 수도 있다

@Test
public void turnOnCoolerAndBlowerIfTooHot() throws Exception {
	tooHot();
	
	assertEquals("hBChl", hw.getState());
}

@Test
public void turnOnHeaterAndBlowerIfTooCold() throws Exception {
	tooHot();
	
	assertEquals("HBchl", hw.getState());
}

getState 함수를 살펴보면 아래와 같이 작성했다.

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";
	retyrb state;
}

StriugBufer는 보기에 흉하다. StriugBufer 를 안 써서 치르는 대가가 미미하다. 하지만 이 프로그램은 임베디드 시스템이기 때문에 자원과 메모리가 제한적일 가능성이 높다. 하지만 테스트 환경의 자원은 제한적이지 않다

이것이 이중 표준의 본질이다. 실제 환경에서는 절대로 안 되지만 테스트 환경에서는 전혀 문제가 없는 방식이 있다. 대게 메모리나 CPU 효율과 관련 있는 경우다. 코드의 깨끗함과는 무관하다.

테스트 당 assert 는 하나

JUnit 으로 테스트 코드를 짤 때는 함수마다 assert 문을 단 하나만 사용해야 한다고 주장하는 학파가 있따. 가혹한 규칙이라 여길지도 모르지만 확실히 장점은 있따. assert 문이 단 하나인 함수는 결론이 하나라서 코드를 이해하기 쉽고 빠르다.

하지만 출력이 xml 인 경우 assert 문과 특정 문자열 포함한다는 assert 문을 하나로 병합하는 방식이 불합리해 보인다.

public void testGetPageHierarchyAsXml() throws Exception {
	makePages("pageOne", "pageOne.ChiildOne", "PageTwo");
	
	submitRequest("root", "type:pages");
	
	assertResponseIsXml();
	assertResponseContains("<name>PageOne</name>", "<name>PageTwo</name>", "<name>ChildOne</name>");)
}
public void testGetPageHierachAsXml() throws Exception {
	givenPages("pageOne", "pageOne.ChiildOne", "PageTwo");
	
	whenRequestIsIssued("root", "type:pages");
	
	assertResponseIsXml();
}

public void testGetPageHierachyHasRightTags() throws Exception {
	givenPages("pageOne", "pageOne.ChiildOne", "PageTwo");
	
	whenRequestIsIssued("root", "type:pages");
	
	assertResponseContains("<name>PageOne</name>", "<name>PageTwo</name>", "<name>ChildOne</name>");)
}

바꾸면서 given-when-then 이라는 관례를 사용했다 그러면 읽기가 쉬워진다.

불행하게도 위에서 보듯이 테스트를 분리하면 중복되는 코드가 많아진다.

TEMPLATE MTEHOD 패턴을 사용하면 중복을 제거할 수 있다.

given/when 부분을 부모 클래스에 두고 then 부분을 자식 클래스에 두면 된다. 아니면 완전히 독자적인 테스트 클래스를 만들어 @before 함수에 given/when 부분을 넣고 @Test 부분에 then 을 넣어도 된다.

이것 저것 감안해보면 assert 문을 여러개 사용하는 편이 좋다고 생각한다.

단일 assert 문 이라는 규칙은 훌륭한 지침이라 생각한다. 하지만 때로는 주저 없이 함수 하나에 여러 assert 문을 넣기도 한다. 단지 assert 문 개수를 최대한 줄여야 좋다고 생각한다.

테스트 당 개념은 하나

어쩌면 테스트 함수마다 한 개념만 테스트하라는 규칙이 더 낫다. 이것저것 잡다한 개념을 연속으로 테스트 하는 긴 함수를 피한다.

아래 코드는 여러 개념을 하나의 테스트로 진행하기 때문에 좋지 않다.

public void testAddMonth() {
	SerialDate d1 = SeialDate.createInstance(31, 5, 2004);
	
	SerialDate d2 = SerialDate.addMonths(1, d1);
	assertEquals(30, d2.getDayOfMonth());
	assertEquals(6, d2.getMonth());
	assertEquals(2004, d2.getYYYY());
	
	SerialDate d3 = SerialDate.addMonths(2, d1);
	assertEquals(31, d3.getDayOfMonth());
	assertEquals(7, d3.getMonth());
	assertEquals(2004, d3.getYYYY());
	
	SerialDate d4 = SerialDate.addMonths(1, SerialDate.addMonths(1, d1));
	assertEquals(30, d4.getDayOfMonth());
	assertEquals(7, d4.getMonth());
	assertEquals(2004, d4.getYYYY());
}

F.I.R.S.T

깨끗한 테스트는 다음 다섯 가지 규칙을 따르게 된다.

빠르게 (First): 테스트는 빨라야 하낟.

독립적으로 (Independent): 각 테스트는 서로 의존하면 안 된다.

반복가능하게 (Repeateable): 테스트는 어떤 환경에서도 반복 가능해야 한다.

자가검증하는 (Self-Validating): 테스트는 부울 값으로 결과를 내야 한다.

적시에 (Timely): 테스트는 적시에 작성해야 한다.

결론

이 장은 주제를 수박 겉핡기 정로도만 훝었따. 사실상 꺠긋한 테스트 코드 라는 주제는 책 한권을 할애해도 모자랄 주제다. 테스트 코드는 실제 코드만큼이나 프로젝트 건강에 중요하다. 어쩌면 실제 코드보다 더 중요할지도 모르겠다. 테스트 코드는 실제 코드의 유연성, 유지보수성, 재사용성을 보존하고 강화하기 떄문이다. 그러므로 테스트 코드는 지속저긍로 깨끗하게 관리하자. 표현력을 높이고 간결하게 정리하자. 테스트 API 구현해 도메인 특화 언어를 만들자 그러면 그만큼 테스트 코드를 짜기가 쉬워진다. 테스트 코드가 방치되어 망가지면 실제 코드도 망가진다. 테스트 코드를 깨끗하게 유지하자.

클린코드 8장 경계

 시스템에 들어가는 모든 소프트웨어를 직접 개발하는 경우는 드물다 (오픈소스를 쓰거나, 다른 팀의 컴포넌트를 쓰거나..)

어떤식으로든 외부 코드를 우리 코드에 깔금하게 통합해야 한다.

외부 코드 사용하기

인터페이스 제공자의 경우 더 많은 환경에서 돌아가기 위해서 적용성을 최대한 넓히려고 애를 쓴다 → 그로 인하여 시스템 경계에서 문제가 생길 소지가 많다.

예를 들어 java.util.Map 을 살펴보면 Map 은 다양한 인터페이스로 수많은 기능을 제공한다. 제공하는 기능성과 유연성은 확실히 유용하지만 그만큼 위험하다 → 프로그램에서 Map 을 만들어서 넘긴다고 가정하면, 넘기는 쪽에서 아무도 Map 을 삭제하지 않으리라 믿을지도 모르지만, Map 내용을 지울 권한은 누구에게나 있다.

Map<String, Sensor> sensors = new HashMap<>();
...
Senser s = sensors.get(sensorId);

위 코드 보다 아래 코드와 같으 사용하는 것이 바람직하다.

public class Sensors {
	private Map<String, Sensor> sensors = new HashMap<>();
	
	public Sensor getById(String id) {
		return sensors.get(id);
	}
}

경계 인터페이스인 Map을 Sensors 안으로 숨긴다. 따라서 Map 인터페이스가 변하더라도 나머지 프로그램에는 영향을 미치지 않는다. 또한 Sensors 클래스는 필요한 인터페이스만 제공할 수 있다.

Map 을 사용할 때마다 캡슐화를 해서 사용하라는 것이 아니라 Map 을 사용하여 여기저기 넘기지 말라는 말이다. (파라미터나 반환값으로 쓰지 말라는 것)

경계 살피고 익히기

외부 코드를 사용하면 적은 시간에 더 많은 기능을 출시하기가 쉬워진다.

외부 패키지 테스트는 우리의 책임은 아니지만, 우리 자신을 위해 우리가 사용할 코드를 테스트 하는 것이 바람직하다.

타사 라이브러리를 가져왔고 사용법이 분명하지 않다고 가정하면 대개는 하루나 이틀 문서를 읽으며 사용법을 결정한다. 그런 다음 우리 쪽 코드를 작성하여 예상대로 동작하는지 확인한다. 때로는 우리 버그인지 라이브러리 버그인지 찾아내느라 오랜 디버깅으로 골치를 앟는다.

외부 코드는 익히기 어렵다. 외부 코드를 통합하기도 어렵다. 두가지를 동시에 하는건 더 어렵다.

그렇기 때문에 다르게 접근할 필요가 있다. 곧바로 우리쪽 코드를 작성해 외부 코드를 호출하는 대신 먼저 간단한 테스트 케이스를 작성해 외부 코드를 익히면 어떨까.. (이를 학습테스트 라고 부른다.)

학습 테스트는 프로그램에서 사용하려는 방식대로 외부 API를 호출한다 .통제된 환경에서 API를 제대로 이해하는지를 확인하는 셈이고, 학습테스트는 API를 사용하려는 목적에 초점을 맞춘다.

학습 테스트는 공짜 이상이다

학습 테스트에는 비용이 들지 않는다. (어째든 익혀야 하니까.. 오히려 필요한 지식만 확보하는 손쉬운 방법이다)

학습 테스트는 이해도를 높여주는 정확한 실험이다.

학습 테스트는 공짜 이상이다. 투자하는 노력보다 성과가 크다.

패키지가 새로운 버전이 나온다면 학습 테스트를 돌려 차이가 있는지 확인할 수 있다.

학습 테스트는 패키지가 예상대로 도는지 검증한다. 일단 통합한 이후라고 하더라도 패키지가 우리 코드와 호환이되리라는 보장은 없다.

학습 테스트를 이용한 학습이 필요하든 그렇지 않든, 실제 코드와 동일한 방식으로 인터페이스를 사용하는 테스트 케이스가 필요하다. 이런 경계 테스트가 있다면 패키지의 새 버전으로 이전하기 쉬워진다. (그렇지 않다면 낡은 패키지를 오랫동안 사용하게 된다.)

아직 존재하지 않는 코드를 사용하기

경계와 관련해 또 다른 유형은 아는 코드와 모르는 코드를 분리하는 경계이다.

때로 우리 지식이 경계를 너머 미치지 못하는 코드 영역도 있다. 떄로는 알려고 해도 알 수 가 없다. 때로는 더이상 내다보지 않기로 결정한다.

다른 팀에 API를 설계하지 않았으므로 구체적인 방법은 모를 수 있기에 구현을 너중으로 미루고, 자체적인 인터페이스를 정의했다.

우리가 바라는 인터페이스를 구현하면 우리가 인터페이스를 전적으로 통제한다는 장점이 생긴다. 또한 코드 가독성도 높아지고 코드 의도도 분명해진다.

그런 다음 나중에 다른 팀이 구현한 API를 해당 인터페이스를 통하여 제공을 하면 된다.

깨끗한 경계

경계에서는 흥미로운 일이 많이 벌어진다. 변경이 대표적인 예다.

소프트웨어 설계가 우수하다면 변경하는데 많은 투자와 재작업이 필요하지 않다.

엄청난 시간과 노력과 재작업을 요구하지 않는다.

통제하지 못하는 코드를 사용할 떄는 너무 많은 투자를 하거나 햐ㅕㅇ후 변경 비용이 지나치게 커지지 않도록 각별히 주의해야 한다.

경계에 위치하는 코드는 깔끔히 분리한다. 또한 기대치를 정의하는 테스트 케이스도 작성한다.

외부 패키지를 호출하는 코드를 가능한 줄여 경계를 관리하자. Map 에서 봤듯이 새로운 클래스로 경계를 감싸거나 아니면 ADAPTER 패턴을 사용해 우리가 원하는 인터페이스를 패키지가 제공하는 인터페이스로 변환하자.

클린코드 7장 오류 처리

오류 처리는 프로그램에 반드시 필요한 요소 중 하나.

여기 저기 흩어진 오류 처리 코드 때문에 실제 코드가 하는 일을 파악하기가 거의 불가능하다.

오류 처리는 중요하지만 해당 코드로 인해 프로그램 논리를 이해하기 어려워진다면 깨끗한 코드라 부르기 어렵다.

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

함수를 호출한 즉시 오류를 확인해야 하는 것은 잊어버리기 쉽다. 그래서 오류가 발생하면 예외를 던지는 편이 더 낫다. 그러면 호출자 코드가 더 깔끔해진다. (논리가 오류 처리 코드와 뒤섞이지 않으니까)

Try-Catch-Finally 문 부터 작성하라

try catch finally 는 트랙잭션과 비슷하다. try 에서 무슨일이 생기든지 catch 블록은 일관성 있게 유지해야 한다.

미확인 예외를 사용하라

처음에는 확인된 예외가 멋진 아이디어라고 생각했다. 하지만 대부분의 경우 불필요하다.

확인된 예외는 OPC 를 위반한다. 메서드에서 확인된 예외를 던졌는데 catch 블록이 세 단계 위에 있다면 그 사이 메서드는 모두가 메서드 선언부를 고쳐줘야 한다. 모듈과 관련된 코드가 전혀 바뀌지 않았는데 메서드 선언분를 전부 고쳐야한다는 말이다.

예외에 의미를 제공하라

예외를 던질 때는 전후 상황을 충분히 덧 붙인다. 그러면 오류가 발생한 원인과 위치를 찾기 쉬워진다.

자바는 모든 예외 호출 스택을 제공한다. 하지만 실패한 코드의의도를 파악하려면 호출 스택만으로는 부족하다. 오류 메시지에 정보를 담아 예외와 함께 던진다. 실패한 연산 이름과 실패 유형도 언급한다. 애플리케이션이 로깅 기능이을 사용하려면 catch 블록에서 오류를 기록하도록 충분한 정보를 넘겨준다.

호출자를 고려해 예외 클래스를 정의하라

같은 처리를 필요로하는 예외가 있으면 하나 감사서 같은 예외를 throw 하는 것이 바람직하다. 그래야 호출자가 예외에 대해서 별도로 catch 하지 않아도 된다.

정상 흐름을 정의하라

try {
	MealExpense expenses = expenseReportDAO.getMeals(employee.getId());
	m_total += expenes.getTotal();
} catch(MealExpenseNotFound(e) {
	m_total += getMealPerDiem();
}

과 같이 작성하지 않고..

DAO 클래스를 고쳐서 언제나 MealExpense 객체를 반환하도록 처리하도록 하는 것이 좋다

MealExpense expenses = expenseReportDAO.getMeals(employee.getId());
m_total += expenes.getTotal();
public class PerDiemMealExpenses implements MealExpenases {
	public int getTotal() {
		// 기본 값으로 일일 기본 식비를 반환한다.
	}
}

null 을 반환하지 마라

null 을 환환하는 습관은 좋지 않다. 한 줄 건너 하나씩 null 을 확인해야하는 코드로 가득한 애플리케이션이 된다.

null 을 반환하면 호출자는 매번 null 을 체크하고, 하지 않으면 오류가 발생할 가능성이커진다.

메서드에서 null을 반환하고 싶은 유혹이 든다면 그 대신 예외를 던지거나 특수 사례 객체를 반환한다. (위에 PerDiemMealExpenses 같은..)

List<Employee> employees = getEmployees(); 
if (employees != null) {
	for (employee e: employees) {
		totalPay += e.getPay();
	}
}

getEmployees 가 null 을 반환하기 때문에 호출자에서 체크해줘야 한다.

하지만 null 이 아닌 빈 객체를 반환하면.. 호출자가 null 체크할 필요가 없어진다

public List<Employees> getEmlpoyees() {
	if (..직원이 없으면..) {
		return Collections.emptyList();
	}
	...
}

null 을 전달하지 마라

null 을 반환하는 것도 나쁘지만 null 을 인수로 받는 방식은 더 나쁘다.

public class MetricsCalculator {
	public double xProjection(Point p1, Point p2) {
		return (p2.x - p1.x) * 1.5;
	}
	...
}

위 코드에서 null 을 전달하면 NullPointException 이 발생한다.

public class MetricsCalculator {
	public double xProjection(Point p1, Point p2) {
		if (p1 == null || p2 == null) { 
			throw InvaildArgumentException("${Message}");
		}
		return (p2.x - p1.x) * 1.5;
	}
	...
}

위 코드 처럼 처리할 수 있지만, InvalidArgumentException 을 잡아내는 처리기가 필요하다.

assert 문을 사용하는 방법도 있다. (개인적으로 별로 좋아보이진 않는다..)

public class MetricsCalculator {
	public double xProjection(Point p1, Point p2) {
		assert p1 != null : "p1 should not be null";
		assert p2 != null : "p2 should not be null";
		return (p2.x - p1.x) * 1.5;
	}
	...
}

문서화가 잘 되어 코드 읽기 편하지만 문제를 해결하지는 못한다. 누군가 null 을 전달하면 여전히 실행 오류가 발생한다. 대다수 프로그래밍 언어는 호출자가 실수로넘기는 null 을 적절히 처리하는 방법이 없다. 그러면 애초에 null 을 넘기지 못하도록 금지하는 정책이 합리적이다. 즉, 인수로 null 이 넘어오면 코드에 문제가 있다는 말이다. 이런 정책을 따르면 그 만큼 부주의 한 실수를 저지를 확률도 작아진다.

→ NonNull annotaiton 같은것을 활용.. 하면 IDE에서 알려줄 수 있음.

결론

깨끗한 코드는 읽기도 좋아야 하지만 안정성도 높아야 한다. 이 둘은 상충하는 목표가 아니다. 오류 처리를 프로그램 논리와 분리해 독자적인 사안으로 고려하면 튼튼하고 깨끗한 코드를 작성할 수 있다. 오류 처리를 프로그램 논리와 분리하면 독립적인 추론이 가능해지며 코드 유지보수성도 크게 높아진다.

클린코드 6장 객체와 자료 구조

 변수를 private로 정하는 이유가 있음. 남들이 변수에 의존하지 않게 만들고 싶어서 임.

그렇다면 왜 모든 변수에 대해서 get / set 함수를 만들까

자료 추상화

변수를 private 로 하더라도 get / set 함수를 제공하면 구현을 외부로 노출하는 셈이 됨.

구현을 감추려면 추상화가 필요함.

아래 코드를 보면 연료 상태를 구체적인 숫자값으로 알려주는 인터페이스와 백분율이라는 추상적인 개념으로 알려주는 인터페이스가 있다. 그 중에 더 좋은 것은 추상적인 개념으로 알려주는 인터페이스다. → 자료를 세세하게 공개하는 것보다 추상적인 개념으로 표현하는 좋다.

인터페이스와 조회/설정 함수만으로는 추상화가 이루어지지 않기 때문에 객체가 포함하는 자료를 표현할 가장 좋은 방법을 심각하게 고민해야한다.

구체적인 Vehicle 클래스

public interface Vehicle {
	double getFuelTankCapacityInGallons();
	double getGallonsOfGasoline();
}

추상적인 Vehicle 클래스

public interface Vehicle {
	double getPercentFuelRemaining();
}

자료/객체 비대칭

객체는 추상화 뒤로 자료를 숨긴 채 자료를 다루는 함수만 공개한다.

자료구조는 자료를 그대로 공개하며 별다른 함수는 제공하지 않는다.

둘은 상반대 되는 개념이다.. 객체 지향 코드에서 어려운 변경은 절차적인 코드에서 쉬우며, 절차적인 코드에서 어려운 변경은 객체 지향 코드에서 쉽다.

복잡한 시스템을 짜다 보면 새로운 함수가 아니라 새로운 자료 타입이 필요한 경우가 생긴다 → 클래스와 객체 지향 기법이 가장 적합하다.

반면, 새로운 자료 타입이 아니라 새로운 함수가 필요한 경우도 생긴다. → 절차적인 코드와 자료 구조가 좀 더 적합하다.

  • 분별 있는 프로그래머는 모든 것이 객체라는 생간이 미신임을 안다. 때로는 자료 구조와 절차적인 코드가 가장 적합한 상황도 있다.

디미터(데메테르) 법칙

데메테르의 법칙은 노스이스턴 대학교에서 개발되었습니다. 이 법칙은 그리스의 수확의 여신 데메테르의 이름을 딴 데메테르 프로젝트의 이름을 따서 명명되었습니다. 발음에 대해서는 의견이 분분하지만, 정확한 발음은 두 번째 음절을 강조하는 것이니 저희를 믿으셔도 됩니다.

디미터 법칙(Law of Demeter)은 데메테르 법칙이라고도 불리며 줄여서 LoD라고도 불립니다. 이 법칙은 "최소한의 지식 원칙(The Principle of Least Knowledge)으로 알려져 있으며, 모듈은 자신이 조작하는 객체의 속사정을 몰라야 한다는 것을 의미합니다.

실제로 Demeter라는 프로젝트를 진행하던 개발자들은 어떤 객체가 다른 객체에 대해 지나치게 많은 정보를 알고 있다 보니, 서로에 대한 결합도가 높아지고 이로 인해 좋지 못한 설계를 야기한다는 사실을 발견하게 됩니다. 그래서 이를 개선하고자 다른 객체에게 어떠한 자료(내부 구조)를 가지고 있는지 숨기고 함수를 통해 공개하게 만들었는데 이것이 바로 디미터 법칙입니다.

즉, 간단하게 말하면 여러개의 .(dot)을 최대한 사용하지 말라는 법칙으로 말할 수 있음

기차 충돌

final String outputDir = ctxt.getOptions().getScratchDir().getAbsolutionPath();

여러 객체가 한 줄로 이어진 기차처럼 보이기 때문에 기차 충돌이라 불린다.

조잡한 구조기 때문에 피하는 것이 좋다.

아래와 같이 나누는 것이 좋다.

Options opts = ctxt.getOptions();
File scratchDir = opts.getScratchDir();
final String outputDir = scratchDir.getAbsolutionPath();

디미터 법칙을 위반하는지 여부는 ctxt, Options, ScratchDir 이 객체인지 자료구지인지에 따라 달렸다. 객체라면 내부 구조를 숨겨야 하므로 확실히 디터 법칙을 위반한다. 자료구조라면 위반한 것이 아니다.

** ctxt가 객체인 경우는 밑에서 설명

잡종 구조

때때로는 절반은 객체, 절반은 자료 구조인 잡종 구조가 나온다.

잡종 구조는 중요한 기능을 수행하는 함수도 있고, 공개 변수나 공개 조회설정 함수도 있다.

공개 조회/설정 함수는 비공개 변수를 그대로 노출한다. 덕택에 다른 함수가 절차적인 프로그래밍의 자료 구조 접근 방식처럼 비공개 변수를 사용하고 싶은 유혹에 빠지기 쉽상이다.

이런 잡종 구조는 새로운 함수는 물론이고 새로운 자료 구조도 추가하기 어렵다. 양쪽 단점만 모아놓은 구조다.

프로그래머가 함수나 타입을 보호할지 공개할지 확신하지 못해(무지해서..) 어중간하게 내놓은 설계에 불과하다.

구조체 감추기

만약 ctxt, options, scratchDir 이 진짜 객체라면 줄줄이 사탕으로 엮어서는 안된다. (객체 내부 구조를 감춰야 하니까)

ctxt.getAbsolutePathOfScratchDirectoryOption();
ctxt.getScratchDirectoryOption().getAbsolutePath();

위 코드를 봤을 때 첫번째 방법은 ctxt 에서 공개해야하는 메서드가 너무 많아진다. 두번째 방식은 getScrachDirctoryOption() 이 객체가 아니라 자료구조를 반환한다고 가정한다. 어느 방법도 썩 내키지 않는다.

ctxt가 객체라면 뭔가를 하라고 말해야지 속을 드러내면 안된다. 임시 디렉터리의 정대 경로가 왜 필요한지. 어디에 쓸려고 하는지 알아야 한다.

목적이 “임시 파일을 생성하기 위해서” 라면 아래 와 같이 ctxt 임시 파일을 생성 시키면 어떨까?

BufferendOutputStream bos = ctxt.createScratchFileStream(classFileName);

객체 내부 구조를 드러내지 않으며, 해당 함수는 자신이 몰라야 하는 여러 객체를 탐색할 필요가 없다.

자료 전달 객체

자료 구조체의 전형적인 형태는 공개 변수만 있고 함수가 없는 클래스다. 이런 자료 구조체를 때로는 자료 전달 객체 (Data TRansfer Object, DTO) 라고 한다.

활성 레코드

활성화 레코드는 DTO 의 특수한 형태다. 공개 변수가 있거나 비공개 변수에 조회/설정 함수가 있는 자료 구조지만, 대개 save / find 와 같은 탐색 함수도 제공한다. 활성 레코드는 데이터베이스 테이블이나 다른 소스에서 자료를 직접 변환한 결과다.

불행히도 활성 레코드에 비즈니스 규칙 메서드를 추가해 이런 자료 자료구조를 객체로 취곱하는 개발자가 흔하다. 하지만 이는 바람직하지 않다. 이러면 자료구조도 아니고 객체도 아닌 잡종 구조가 나오기 때문이다.

해결책은 활성 레코드는 자료 구조로 취급하고, 비즈니스 규칙을 담으면서 내부 자료를 숨기는 객체는 따로 생성한다. (여기에서 내부 자료는 활성 레코드의 인스턴스일 가능성이 높다.)

결론

객체는 동작을 공개하고 자료를 숨긴다. 그래서 기존 동작을 변경하지 않으면서 새 객체 타입을 추가하기 쉬운 반면, 기존 객체에 새 동작을 추가하기는 조금 어렵다. 자료 구조는 별다른 동작 없이 자료를 노출한다. 그래서 기존 자료 구조에 새 동작을 추가하기는 쉬우나, 기존 함수에 새 자료 구조를 추가하기 어렵다.

시스템을 구현할 때, 새로운 자료 타입을 춧가하는 유연성이 필요하면 객체가 더 적합하고, 다른 경우로 새로운 동작을 추가하는 유연성이 필요하면 자료 구조와 절차적인 코드가 더 적합하다. 우수한 소프트웨어 개발자는 편견 없이 이 사실을 이해해 직면한 문제에 최적인 해결책을 선택한다.

클린코드 5장 형식 맞추기

 프로그래머라면 형식을 깔끔하게 맞춰 코드를 짜야 한다.

코드 형식을 맞추기 위한 간단한 규칙을 정하고 그 규칙을 착실히 따라야 한다. 팀으로 일한다면 팀이 합의해 규칙을 정하고 모두가 그 규칙을 따라야 한다.

형식을 맞추는 목적

코드의 형식은 중요하다. 너무 중요해서 무시하기 어렵다.

하지만 융통성 없이 맹목적으로 따르면 안된다.

‘돌아가는 코드’가 전문 개발자의 일차적인 의무가 아니다. 오늘 구현한 기능이 다음 버전에서 바뀔 확률은 매우 높다. 오늘 구현한 코드 코드의 가독성은 앞으로 바뀔 코드 품질에 매우 지대한 영향을 미친다. 코드는 사라질지라도 개발자의 스타일과 규율은 사라지지 않는다.

적절한 행길이를 유지하라

소스 코드는 얼마나 길어야 적당할까?

대다수 클래스의 경우 500 줄을 넘지 않고 대부분 200줄 정도인 파일로도 커다란 시스템을 구축 할 수 있다.

반드시 지킬 엄격한 규칙은 아니지만 바람직한 규칙으로 삼으면 좋다.

일반적으로 큰 파일보다 작은 파일이 이해하기 쉽다.

신문 기사 처럼 작성하라

독자는 위에서 아래로 기사를 읽는다. 최상단에 기사를 몇 마디로 요약하는 표제가 나온다. 독자는 표제를 보고서 기사를 읽을지 말지 결정한다. 첫 문단은 전체 기사 내용을 요약한다.

소스 파일도 신문 기사와 비슷하게 작성한다. 이름은 간단하면서도 설명이 가능하게 짓는다.

이름만 보고도 올바른 모듈을 살펴보고 있는지 아닌지를 판단할 정도로 신경써서 짓는다. 소스 파일 첫 부분은 고차원 개념과 알고리즘을 설명한다. 아래로 내려갈수록 의도를 세세하게 묘사한다.

개념은 빈 행으로 분리하라

거의 모든 코드는 왼쪽에서 오른쪽으로 그리고 위에서 아래로 읽힌다.

각 행은 수식이나 절을 나타내고, 일련의 행 묶음은 완결된 생각 하나를 표현한다. 생각 사이에는 빈 행을 넣어 분리해야 마땅하다.

package com.jobkorea.service

import java.util.regex.*;
import com.jobkorea.model.Board;

public class BoardService {
	public void save(Board board) {
		...
	}

	public void delete(long boardId) {
		...
	}
}

세로 밀집도

줄바꿈이 개념을 분리한다면 세로 밀집도는 연관성을 의미한다.

즉, 서로 밀집한 코드 행은 세로로 가까이 놓여야 한다는 뜻이다.

public class ReportConfig {
	/**
	 * 리포터 리스너의 클래스 이름
	*/
	private String m_className;

	/**
	 * 리포터 리스너의 속성
	*/
	private List<Property> m_properties = new ArrayList<Property>();
	public void addProperty(Property property) {
		m_properties.add(property);
	}
}

위의 코드 보다는 아래 코드가 더 잘 읽힌다.

public class ReportConfig {
	private String m_className;
	private List<Property> m_properties = new ArrayList<Property>();

	public void addProperty(Property property) {
		m_properties.add(property);
	}
}

수직 거리

함수 연관 관계와 동작 방식을 이해하려고 이 함수에서 저 함수로 오가며 소스 파일을 위 아래로 뒤지는 등 뺑뺑이를 돌았으나 결국은 미로 같은 코드 때문에 혼란만 가중된 경험이 있을 것이다.

함수나 변수나 정의된 코드를 찾으려 상속 관계를 줄줄이 거슬러 올라간 형험도 있을 것이다.

서로 밀접한 개념은 세로로 가까이 둬야 한다. (물론 두 개념이 서로 다른 파일에 속한다면 규칙이 통하지 않는다. 하지만 타당한 근거가 없다면 서로 밀접한 개념은 한 파일에 속해야 마땅하다. 이게 바로 protected 변수르 피해야하는 이유 중 하나다.)

연관성이 깊은 두 개념이 멀리 떨어져 있으면 코드를 읽는 사람이 소스파일과 클래스를 여기저기 뒤지게 된다.

지역 변수:

사용하는 위치에 최대한 가까이 선언한다.

인스턴스 변수:

인스턴스 변수는 클래스 맨 처음에 선언한다.

잘 설계한 클래스는 클래스의많은 메서드가 인스턴스 변수를 사용하기 때문이다.

종속 함수:

한 삼수가 다른 함수를 호출한다면 두 함수는 세로로 가까이 배치한다. 또한 가능하다면 호출하는 함수를 호출되는 함수보다 먼저 배치한다. 그러면 프로그램이 자연스럽게 읽힌다. 모듈 전체의 가독성도 높아지고 호출되는 함수를 찾기도 좋아 진다.

개념적 유사성:

어떤 코드는 서로 끌어당긴다. 개념적인 친화도가 높기 때문이다. 친화도가 높을수록 코드를 가까이 배치한다.

종속 함수일 수도 있고, 개념적으로 비슷한 함수일 수 도 있고, 비슷한 동작을 하는 함수일 수도 있다.

세로 순서

일반적으로함수 호출 종속성은 아래 방향으로 유지한다.

호출 되는 함수를 호출하는 함수보다 나중에 배치한다.

가로 형식 맞추기

한 행은 가로로 얼마나 길어야할까?

대 다수가 20-60 자 사이고 80자가 넘어가는 것은 급격하게 감소한다.

옛날에는 80자 제한을 권고했지만 이것은 인위적이고 모니터가 커짐에 다라 100자나 120자에 달해도 나쁘지 않다. 그 이상은 솔직히 주의부족이다.

요즘은 모니터가 넓어지면서 200자 까지도 한 화면에 들어오지만 저자의 경우 120자가 기준이 되는 것이 바람직하다고 한다.

가로 공백과 밀집도

  • 변수에 할당 연산자에는 앞 뒤로 공백을 준다.
    • User user = userService.findByName(”xxxx”)
  • 함수 이름과 이어지는 괄호 사이에는 공백을 주지 않는다. → 함수와 인수는 서로 밀접하기 떄문…
    • public User findByName(String name) {
  • 함수 호출 시 인수 사이에는 공백을 준다.
    • boardRepository.save(”title”, “content”, “author”);
  • 연산자 우선순위에 따라 공백을 추가하는 경우도 있다. → 하지만 툴이 대부분 지원하지 않는다.
    • return bb - 4a*c;
    • 사견) 개인적으로는 괄호로 표현하는게 더 직관적일 것이라 생각함. return (b * b) - (4 * a * c);

가로 정렬

아래 코드 처럼 억지로 맞추는 것은 좋지 않다. 코드가 어뚱한 부분을 강조해 진짜 의도가 가려지기 때문이다.

Strign firstName = "수열";
String lastName  = "정"; 
String address   = "서울시 강서구";
String zipcode   = "15243";
Date birth       = new Date(1986, 12, 18);

그냥 아래 처럼 적어도 가독성에 문제가 생기지 않는다.

정렬이 필요할 정도로 목록이 길다면 문제는 목록 일이지 정렬 부족이 아니다. 코드 선언 부분이 길다면 클래스를 쪼개야 한다는 의미다.

Strign firstName = "수열";
String lastName = "정"; 
String address = "서울시 강서구";
String zipcode = "15243";
Date birth = new Date(1986, 12, 18);

들여쓰기

scope 로 이뤄진 계층을 표현하기 위해 들여쓰기를 한다.

들여쓰기한 파일은 구조가 한눈에 들어온다. 변수, 생성자 함수, 메서드가 금방 보인다.

들여쓰기 무시하기

때로는 1줄 짜리 if , while , for 같은 문일 때 들여쓰기를 하지 않는 유혹에 빠질 때가 있지만 원점으러 돌아가 들여쓰기로 표현한다.

if (count < 5) { return false; }
for (int i = 0; i < count; i++) sum(value, i);

보다는..

if (count < 5) {
	return false;
}
for (int i = 0; i < count; i++) {
	sum(value, i);
}

가짜 범위

때로는 빈 while 문이나 for 문 을 접한다. 이런 경우를 피하지 못할 때는 빈 블록을 오바로 들여쓰고 괄호로 감싼다. 아래와 같이 while 문 끝에 세미콜론(;) 하나를 살짝 덧붙인 코드로 수없이 골탕을 먹는다. 세미콜론(;_ 은 새 행에다 제대로 들여써서 넣어준다.

while (dis.read(buf, 0, readBufferSize) != -1);

보다는

while (dis.read(buf, 0, readBufferSize) != -1)
;

팀 규칙

팀 규칙이라는 제목은 말 장난이다.

프로그래머라면 각자 선호하는 규칙이 있따. 하지만 팀에 속한다면 자신이 선호해야 할 규칙은 바로 팀 규칙이다.

팀은 한가지 규ㅣㄱ에 합의해야 한다. 그리고 모든 팀원은 그 규칙을 따라야 한다. 그래야 소프트웨어가 일관적인 스타일을 보인다 .개개인이 따로국밥처럼 맘대로 짜대는 코드는 피해야 한다.

어디에 괄호를 넣을지, 들여쓰기는 몇 자로 할지, 클래스와 변수와 메서드 이름은 어떻게 지을지 등등을 결정해야 한다.

좋은 소프트웨어 시슽셈은 읽기 쉬운 ㅁ누서로 이뤄진다는 사실을 기억하기 바란다. 스타일은 일관적이고 매끄러워야 한다. 한 소스 파일에서 봤떤 형식이 다른 소스 파일에도 쓰이리라는 신뢰감을 독자에게 줘야 한다. 온갖 스타일을 뒤섞어 소스 코드르 필요 이상으로 복잡하게 만드는 실수는 반드시 피한다.

클린코드 4장 주석

 잘 달린 주석은 유용하다

하지만 경솔하고 근거 없는 주석은 코드를 이해하기 어렵게 만든다.

오래되고 조잡한 주석은 거짓과 잘못된 정보를 퍼뜨려 해악을 미친다.

우리에게 프로그래밍 언어를 치밀하게 사용해 의도를 표현할 능력이 있다면 주석은 거의 필요하지 않다.

→ 코드로 의도를 표현하지 못해, 실패를 만회하기 위해 주석을 사용한다.

코드는 변화하고 진화한다. 불행하게도 주석은 언제나 코드를 따라가지 않는다.

부정확한 주석은 아예 없는 주석보다 훨씬 더 나쁘다.

주석은 나쁜 코드를 보완하지 못한다.

코드에 주석을 추가하는 일반적인 이유는 코드 품질이 나쁘기 떄문이다.

코드로 의도를 표현하라

// 직원에게 복지 혜택을 받을 자격이 있는지 검사한다.
if (employee.flags & HOURLY_FLAG) && (employee.agr > 65))

위 코드 보다 아래 코드가 보기가 쉽다.

if (employee.isEligibleForFullBenfits())

좋은 주석

정말 좋은 주석은, 주석을 달지 않을 방법을 찾아낸 주석이다.

법적인 주석

때로는 회사가 정리합 구현 표준에 맞춰 법적인 이유로 특정 주석을 넣으라고 명시한다. (예를 들어 소스 코드 상단에 저작권 정보와 소유권 정보는 필요하고 타당하다)

정보를 제공하는 주석

아래와 같이 정규 표현식이 시각ㄱ과 날짜를 뜻한다고 설명한다. → 하지만 시각과 날짜를 변환하는 클래스를 만들어 코드를 옮겨주면 더 좋고 더 깔끔해지고 주석도 없어질 수 있다.

// kk:mm:ss EEE, MMM dd, yyyy 형식이다.
Pattern timeMatcher = Pattern.compile("\\\\d*:\\\\d*:\\\\d* \\\\w*, \\\\w* \\\\d*, \\\\d");

의도를 설명하는 주석

구현을 이해하게 도와주는 선을 넘어 결정에 깔린 의도까지 설명해주는 주석.

예를 들면 특정 정책에 의해서 결정된 의도인 경우 설명을 하면 좋다.

의미를 명료하게 밝히는 주석

때땔 모호한 인수나 반환값은 그 의미를 읽기 좋게 표현하면 이해하기 쉬워진다. 하지만 변경하지 못하는 코드에 속한다면 의미를 명료하게 밝히는 주석이 유용하다. → 하지만 잘못된 주석을 달아놓을 위험성이 놓다.

결과를 경고하는 주석

때로는 다른 프로그래머에게 결과를 경고할 목적으로 주석을 사용한다.

// 시간이 오래 걸리니.. 시간이 충분하지 않으면 실행하지 마세요.
public void veryLongTimeExecuteTest() {

TODO 주석

앞으로 할 일을 TODO 주석으로 남겨두면 편하다

void makeVersion() {
	// TODO: 현재 필요하지 않지만 추후 필요할 수 있다.
}

중요성을 강조하는 주석

중요하지 않은 무엇인가를 강조하기 위해서 주석을 사용한다.

String listItemContent = match.group(3).trim();
// 여기에서 문자열에 시작 공백이 있으면 다른 문자열료 인식되기 떄문에.. trim 은 정말 중요하다. 
new ListItemWidget(this, listItemContent, this.level + 1);

공개 API 에서 Javadocs

설명이 잘 된 공개 API 는 참으로 유용하고 만족스럽다. 표준 라이브러리에서 사용한 Javadocs 가 좋은 예다.

하지만 어느 주석과 마찬가지로 독자를 오도하거나, 잘못 위치하거나, 그릇된 정보를 전다할 가능성이 있다.

나쁜 주석

대다수의 주석이 이 범주에 속한다.

대다수 주석은 허술한 코드를 지탱하거나, 엉성한 코드를 변명하거나, 미숙한 결정을 합리화하는 등 프로그래머가 주절거리는 독백에서 크게 벗어나지 못한다.

주절 거리는 주석

특별한 이유 없이 의무감으로 혹은 프로세스에서 하라고 하니까 마지 못해 주석을 단다면 전적으로 시간 낭비다.

같은 이야기를 반복하는 주석

코드 내용을 그대로 중복한 주석

오해할 여지가 있는 주석

때로는 의도는 좋았으나 프로그래머가 딱 맞을 정도로 엄밀하게 주석을 달지 못하기도 한다.

의무적으로 다는 주석

모든 함수에 Javadocs 를 달거나 모든 변수에 주석을 달아야 한다는 규칙은 어리석기 그지 없다.

이런 주석은 코드를 복잡하게 만들며, 거짓말을 퍼뜨리고, 혼동과 무질서를 초래한다.

이력을 기록하는 주석

때때로 사람들은 모듈을 편집할 때 모듈 첫머리에 주석을 추가한다.

예전에는 모듈 첫머리에 변경 이력을 기록하고 관리하는 관례가 바람직 했지만 당시에는 소스 코드 관리시스템이 없었기 때문이다. 이제는 혼란만 가중할 뿐이다. 완전히 제거하는 것이 좋다.

있으나 마나 한 주석

지나친 참견을하는 주석은 개발자가 주석을 무시하는 습관에 빠지게 한다.

무서운 잡음

때로는 Javadocs 도 잡음이다.

함수나 변수로 표현할 수 있다면 주석을 달지 마라

위치를 표시한 주석

소스 파일에서 특정 위치를 표시하려고 주석을 사용한다.

닫는 괄호에 다는 주석

try {
	while (...) {
		...
	} // while
} // try 
catch (Exception e) {
	...
} // catch

공로를 돌리거나 저자를 표시하는 주석

소스 관리 시스템이 귀신 기억해준다..

// 릭이 추가 함.
public void execute() {
	...
}

주석으로 처리한 코드

주석으로 처리된 코드는 다른 사람들이 지우기를 주저한다.

이유가 있어 남겨 뒀을꺼라고 생각히 때문에…

어차피 소스 관리 시스템이 이력은 잘 관리하고 있다.

HTML 주석

소스 코드에서 html 주석은 혐오 그 자체이다.

/**
 * 적합성 테스트
 * <pre>
 * &lt;taskdef name=&quot;execute-fitnesse-test*quot;
 * </pre>
**/

전역 정보

주석을 달아야 한다면 근처에 있는 코드만 기술하라.

너무 많은 정보

주석에다 흥미로운 역사나 관련 없는 정보를 장황하게 늘어놓지 마라.

모호한 관계

주석과 주석이 설명하는 코드는 둘 사이 관계가 명백해야 한다.

함수 헤더

짧은 함수는 긴 설명이 필요 없다. 짧고 한가지만 수행하며 이름을 잘 붙인 함수가 주석으로 헤더를 추가한 함수보다 좋다.

비공개 코드에서 Javadocs

공개하지 않을 코드에는 의미가 없다.

도서 내용 정리 - 스태프 엔지니어

개요 네이버 도서 정보:  https://bit.ly/3JzLvQr 개발자는 관리자로만 커리어를 쌓아야 하는가? 관리자가 아닌 기술 리더로 성장하는 길은 없을까? IT 업계가 계속 성장하면서 전에 없던 팀과 조직의 경계를 넘어서는 큰 문제를 다루게 되...