Clean Code that Works.

오랜만에 간단 번역.

이번엔 dzone.com의 TOP POST 2013: There are only 2 Roles on code. 를 번역해 보았습니다.
전체적인 내용은 객체지향 프로그래밍 및 설계의 다섯가지 기본 원칙(SOLID)중에 단일 책임 원칙(Single responsibility priciple)을 좀 더 자세히 설명한 것 같은 느낌이네요. 

중간에 TTD와 단위 테스트를 진행 하는 것보다 더 중요한 것은 한가지 역할에 집중하는 코드를 만드는 것이라 라는 내용이 나오는데, 이게 가장 중요한 내용 같습니다. 가끔 테스트 코드를 살펴보면 mock으로 도배되다 싶이 한 코드가 있는데.. 이걸 보면 이 코드를 어떻게 이해해야되고 수정해야 되는지 난감한데요. 이 포스트에 등록된 내용을 항상 마음속에 곱씹으며 개발하는 습관을 들이면 좀 더 좋은 날이 올것 같습니다. ㅎㅎ

코드를 테스트 하는 화려한 테스트 코드를 작성하는 것보다 단일 기능에 집중하는 클래스 설계를 진행 한다면, 단위 테스트에 작성되는 코드량과 시간을 감소 시킬 수 있고, 후에 유지보수도 쉽게 진행할 수 있습니다.

여기서 알고리즘은 프로그램 내에 있는 비즈니스 로직(핵심 기능)으로 생각하셔도 될 것 같습니다.

========================================================================

모든 코드는 두가지 역할로 분류될 수 있다. 알고리즘 처럼 작업을 하는 것과 작업을 조절 하는 것.

실제 환경에서 코드 베이스들의 복잡성은 한 곳에 이런 역할들을 같이 두기 때문에 발생한한다.

내가 작성했던 코드들의 90%정도가 알고리즘과 작업을 적절히 구분하지 못했던 것에 스스로 죄책감을 느끼고 있다.

일을 좀 더 명확하게 정의 하기.

왜 코드들을 알고리즘과 코디네이터로 구분을 해야 할까. 
알고리즘과 코디네이터들이 의미하는게 무엇인지 먼저 알아 보기로 하자.

우리 대부분은 common algorithms in Computer Science에 나오는 버블 소트나 이진 검색 같은것을 자주 들어서 익숙하지만 우리가 작성한 코드들이 알고리즘을 포함하여 동작하고 있다는 것을 때때로 깨닫지 못하고 있다. 

어떤 문제를 해결하거나 어떤 작업이 수행되는 명령 또는 단계들이 있고 이 단계들은 데이터를 가지고 동작하고 외부로부터 독립되어있다. (버블 소트처럼 정렬이 되는것, 데이터만 제공 하면 정렬이 됨)

우리가 작성하는 모든 코드들은 본질적으로 테스트가 가능 해야 되고 , 우리가 알고 있는 일반적인 정렬 알고리즘 처럼 잠재적으로 독립되어 동작되어야 한다. 

알고리즘들을 프로젝트로부터 제거한다면 프로그램에 남아 있는 것들은 단순히 알고리즘들을 연결하는 코드뿐일 것이다.

코드에서 알고리즘과 코디네이터를 분리하는것이 왜 중요한가

코드들을 잠재적으로 두 큰 카테고리로 분리 해야 된다는 것을 알았다. 
다음 단계는 왜 이렇게 분리해야되고 어떻게 하면 분리 할 수 있는지 확인하는 것이다.

알고리즘을 다른 코드를 조작하는 것들과 분리하면서 얻게 되는 가장 큰 이점은 알고리즘 코드가 독립적으로 동작 한다는 것이다.

알고리즘 코드를 독립적으로 관리한다면 아래 3가지 내용을 즉시 확인 할 수 있다.
1. 단위 테스트를 하기 쉬어진다.
2. 재사용이 쉬어진다.
3. 복잡도가 감소된다.

Mock 방식을 사용하지 않고 IoC 컨테이너가 드물게 사용되고 있었을때는 TTD는 정말 완전 어려웠다.
내가 처음 일을 시작할 때는 TDD를 활용해서 코드 커버리지를 100%로 맞출수 있을꺼라 생각했지만, 그때는 mock 프레임워크나 IoC 컨테이너가 존재 하지 않을 때라서 지금 생각해 보면 미친짓이었다.

만약 당신이 작성하는 코드를 TDD방식으로 하고 싶으면 알고리즘과 관련된 로직들을 분리해내야 된다.
만약 신뢰할만한 단위 테스트를 진행 하고 싶으면 클래스를 작성할때 최소한의 의존성을 가지고 있도록 해야된다.

많은 개발자들이 TDD를 어려워 하는 이유는 실제 코드를 작성하다 보면 다른 코드에 많은 의존성들이 생기기 때문이다. 이 의존성들의 문제들을 해결하기 위해 이 코드들에 대해서 가짜 버전을 만들 필요가 있어졌다.
이를 해결하기 위해 의존성이 필요한 부분을 Mock으로 대체 하는 방법이 고안되고 IoC 컨테이너를 사용하는 아키텍쳐가 인기 있게 되었다.

TDD와 유닛 테스트는 어디에서나 동작 할 수 있는 코드로 작성 되어야 하지만 TTD보다 더 중요한 점은 알고리즘 코드를 이와 관련 없는 코드로 부터 분리해내야 된다는 것이다.

더 좋은 방법!

이 문제를 해결할 수 있는 방법은 꾸준한 노력을 기울이는 것이다. 
이를 수행하기 위해 필요한 최소한의 노력은 IoC 컨테이너를 사용하고 시간이 날때마다 단위 테스트를 작성하고 조금씩 리팩토링을 진행 하도록 하는것이다. 

아래에서 간단한 예제를 볼 것이다. 
이 예제에서 가장 중요한 점은 코드들이 의존성을 제거하는 리팩토링을 진행하고 로직을 명확하게 분류해 내는것을 이해 하는 것이다.

Calculator 클래스를 살펴보자.

01.public class Calculator
02.{
03.private readonly IStorageService storageService;
04.private List<int> history = new List<int>();
05.private int sessionNumber = 1;
06.private bool newSession;
07. 
08.public Calculator(IStorageService storageService)
09.{
10.this.storageService = storageService;
11.}
12. 
13.public int Add(int firstNumber, int secondNumber)
14.{
15.if(newSession)
16.{
17.sessionNumber++;
18.newSession = false;
19.}
20. 
21.var result = firstNumber + secondNumber;
22.history.Add(result);
23. 
24.return result;
25.}
26. 
27.public List<int> GetHistory()
28.{
29.if (storageService.IsServiceOnline())
30.return storageService.GetHistorySession(sessionNumber);
31. 
32.return new List<int>();
33.}
34. 
35.public int Done()
36.{
37.if (storageService.IsServiceOnline())
38.{
39.foreach(var result in history)
40.storageService.Store(result, sessionNumber);
41.}
42.newSession = true;
43.return sessionNumber;
44.}
45.}

이 클래스는 간단한 덧셈 계산을 하고 결과를 storage 서비스를 통해 저장하는 일은 하는 것이다.
아주 복잡한 코드는 코드는 아니다. Calcalator 클래스는 storage service를 필요로 한다.

이 클래스는 로직을 추출해서 클래스를 재작성하는 작업을 통해 의존성이 제거된 클래스를 만들수 있고, 조정클래스는 로직을 전혀 가지고 있지 않은 것을 볼 수 있다.

01.public class Calculator_Mockless
02.{
03.private readonly StorageService storageService;
04.private readonly BasicCalculator basicCalculator;
05. 
06.public Calculator_Mockless()
07.{
08.this.storageService = new StorageService();
09.this.basicCalculator = new BasicCalculator();
10.}
11. 
12.public int Add(int firstNumber, int secondNumber)
13.{
14.return basicCalculator.Add(firstNumber, secondNumber);
15.}
16. 
17.public List<int> GetHistory()
18.{
19.return storageService.
20.GetHistorySession(basicCalculator.SessionNumber);
21.}
22. 
23.public void Done()
24.{
25.foreach(var result in basicCalculator.History)
26.storageService
27..Store(result, basicCalculator.SessionNumber);
28. 
29.basicCalculator.Done();
30.}
31.}
32. 
33.public class BasicCalculator
34.{
35.private bool newSession;
36. 
37.public int SessionNumber { get; private set; }
38. 
39.public IList<int> History { get; private set; }
40. 
41.public BasicCalculator()
42.{
43.History = new List<int>();
44.SessionNumber = 1;
45.}
46.public int Add(int firstNumber, int secondNumber)
47.{
48.if (newSession)
49.{
50.SessionNumber++;
51.newSession = false;
52.}
53. 
54.var result = firstNumber + secondNumber;
55.History.Add(result);
56. 
57.return result; ;
58.}
59. 
60.public void Done()
61.{
62.newSession = true;
63.History.Clear();
64.}
65.}

BasicCalculator 클래스를 보면 아무런 외부 의존성을 가지고 있지 않고 이는 단위 테스트를 쉽게 진행 할 수 있다는 것을 의미한다. 
실제 로직도 전부 포함 하고 있기 때문에 더 이해 하기가 쉽고 Calculator_Mockless 클래스는 코드들을 조정하는 역할 만 한다.

위 예는 기본적인 예제이긴 하지만 인위적이진 않다. 무슨말이냐면 의도적으로 작성한 코드는 아니고 실제 운영되고 있는 서비스에서도 볼 수 있는 그런 예이다.

마지막 조언.
목 객체를 전부 제거하거나 목 객체를 사용할 생각이 전혀 없다면 작성하는 코드들을 명확히 알고리즘과 코드 조합 부분을 구분해서 작성 해야 된다.

이것은 매우 어렵기 때문에 나 역시 이 방법이 익숙해 지도록 매번 노력하고 있다. 하지만 이 방법을 잘 수행된다면 반드시 큰 이익이 있을 것이라 생각한다. 

코드들로 부터 알고리즘을 분리해 낸다면 전체 시스템 구조를 이해하는데도 큰 도움이 된다.