우당탕탕 우리네 개발생활

[클린코드] 7~9장 정리(오류 처리, 경계, 단위 테스트) 본문

tech

[클린코드] 7~9장 정리(오류 처리, 경계, 단위 테스트)

미스터카멜레온 2024. 6. 6. 17:40
<클린코드> 책을 통해 공부한 내용을 정리하기 위해 작성하였습니다. 제 개인적인 각색과 의견이 첨가되어 있어 실제 책의 내용과는 차이가 있을 수 있습니다.

7장. 오류 처리

- 먼저 강제로 예외를 일으키는 테스트 케이스를 작성한 후 테스트를 통과하게 코드를 작성하는 방법을 권장한다. 그러면 자연스럽게 try 블록의 트랜잭션 범위부터 구현하게 되므로 범위 내에서 트랜잭션 본질을 유지하기 쉬워진다.

 

- 확인된 예외는 OCP를 위반한다. 확인된 예외는 캡슐화를 깨버린다.

-> (부연 설명) OCP란 기능 추가에는 열려있고, 기능 수정에는 닫혀있어야 함을 나타내는 객체지향의 대표적인 원칙이다. 매번 어떤 요구사항이 있을 때마다 객체의 특정 메서드를 수정해야한다면 그건 OCP를 위반한다고 볼 수 있다. 기존 기능들을 그대로 유지하면서 새로운 기능을 객체에 추가할 수 있다면 그건 OCP를 잘 지키고 있다는 의미이다. 어느 메서드의 기능이 어쩔수 없이 수정됨에 따라 확인된 예외를 늘리게 됐을 경우 그 메서드를 호출한 어느 메서드는 관련된 코드수정이 하나도 일어나지 않더라도 선언부에 확인된 예외는 추가해줘야 한다. 이래서 OCP 위반이다. 또한 이러한 예시는 캡슐화와도 연결되는데, 확인된 예외라는 거 자체가 종속성 객체의 내부 예외 경우를 알게되는 것과 같아서 이는 올바른 캡슐화가 된 경우가 아니다.

 

- 실제로 외부 API를 사용할 때는 감싸기 기법이 최선이다. 외부 API를 감싸면 외부 라이브러리와 프로그램 사이에서 의존성이 크게 줄어든다. 특정 업체가 API를 설계한 방식에도 발목을 잡히지 않을 수 있다.

 

- 특수 사례 패턴(Special case pattern)이라는 것이 있다. 클래스를 만들거나 객체를 조작해 특수 사례를 처리하는 방식이다. 그러면 클라이언트 코드가 예외적인 상황을 처리할 필요가 없어진다. 클래스나 객체가 예외적인 상황을 캡슐화해서 처리하므로.

 

- 메서드에서 null을 반환하고픈 유혹이 든다면 그 대신 예외를 던지거나 특수 사례 객체를 반환한다.

-> (내 생각) 객체 지향의 코드를 작성하는 습관없이 작업을 했을 때 null을 정당화하여 많이 사용했다. null은 확연히 객체와는 성격이 다르기 때문에 null을 위한 조건처리가 따로 들어가야 한다. 이 말은 결국 책임이 복잡해진다는 뜻이다. 예외를 던지거나 특수 사례 객체를 반환하는 연습을 해야겠다.

 

리스트를 반환하는 메서드의 경우 null보단 빈 리스트를 반환하자.

8장. 경계

public class Sensors {
    private sensors = new Map<string, Sensors>();

    public getById(id: string): Sensors {
        return sensors.get(id);
    }
    
    //...
}

- 경계 인터페이스인 Map을 Sensors 안으로 숨긴다. 따라서 Map 인터페이스가 변하더라도 나머지 프로그램에는 영향을 미치지 않는다. 제네릭스를 사용하든 하진 않든 더 이상 문제가 안 된다. Sensors 클래스 안에서 객체 유형을 관리하고 변환하기 때문이다. Sensors 클래스는 프로그램에 필요한 인터페이스만 제공한다. 코드는 이해하기 쉽지만 오용은 어렵다. Map과 같은 경계 인터페이스를 이용할 때는 이를 이용하는 클래스나 클래스 계열 밖으로 노출되지 않도록 주의한다. Map 인스턴스를 공개 API의 인수로 넘기거나 반환값으로 사용하지 않는다.

 

- 학습 테스트란 우리쪽 코드를 작성해 외부 코드를 호출하는 대신 먼저 간단한 테스트 케이스를 작성해 외부 코드를 익히는 것을 말한다. 학습 테스트는 프로그램에서 사용하려는 방식대로 외부 API를 호출한다. 

 

- log4j 익히기. log4j에 대해 구글을 뒤지고, 문서를 읽어보고, 테스트를 돌린 끝에 실제 코드에서 원하는 기능에 맞는 테스트 코드들을 얻었다. 그동안 log4j가 돌아가는 방식을 상당히 많이 이해했다. 이제 이를 기반으로 모든 지식을 독자적인 로거 클래스로 캡슐화한다. 그러면 나머지 프로그램은 log4j 경계 인터페이스를 몰라도 된다.

- (내 생각) prisma, redis 등등 모두 캡슐화해서 사용하자! repository를 생성할 때 prisma나 redis를 바로 사용하는 것이 아닌 인터페이스를 하나 더 둬서 repository에서 이를 사용하도록 할 것!

 

- 학습 테스트는 공짜 이상이다. 투자하는 노력보다 얻는 성과가 더 크다. 패키지 새 버전이 나온다면 학습 테스트를 돌려 차이가 있는지 확인한다.

- (내 생각) 낯선 API들엔 테스트를 짜는 습관을 항상 들이자!

 

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

 

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

 

- 경계에서는 흥미로운 일이 많이 벌어진다. 변경이 대표적인 예다. 소프트웨어 설계가 우수하다면 변경하는데 많은 투자와 재작업이 필요하지 않다.

 

- 경계에 위치하는 코드는 깔끔하게 분리한다. 또한 기대치를 정의하는 테스트 케이스도 작성한다. 이쪽 코드에서 외부 패키지를 세세하게 알아야 할 필요가 없다. 통제가 불가능한 외부 패키지에 의존하는 대신 통제가 가능한 우리 코드에 의존하는 편이 훨씬 좋다. 자칫하면 오히려 외부 코드에 휘둘리고 만다.

 

- 외부 패키지를 호출하는 코드를 가능한 줄여 경계를 관리하자

  • 새로운 클래스로 경계를 감싸기
  • ADAPTER 패턴을 사용해 우리가 원하는 인터페이스를 패키지가 제공하는 인터페이스로 변환하기

9장. 단위 테스트

- TDD 법칙 세 가지는 아래와 같다.

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

- 이렇게 일하면 매일 수십 개, 수백 개, 매년 수천 개에 달하는 테스트 케이스가 나온다. 이렇게 일하면 실제 코드를 사실상 전부 테스트하는 테스트 케이스가 나온다. 하지만 실제 코드와 맞먹을 정도로 방대한 테스트 코드는 심각한 관리 문제를 유발하기도 한다.

 

- 깨끗한 테스트 코드 유지하기. 팀은 지저분한 테스트 코드를 내놓으나 테스트를 안 하나 오십보 백보라는, 아니 오히려 더 못하다는 사실을 깨닫지 못했다. 문제는 실제 코드가 진화하면 테스트 코드도 변해야 한다는 데 있다. 그런데 테스트 코드가 지저분할수록 변경하기 어려워진다.

 

- 내 이야기가 전하는 교훈은 다음과 같다. 테스트 코드는 실제 코드 못지 않게 중요하다. 테스트 코드는 이류 시민이 아니다. 테스트 코드는 사고와 설계와 주의가 필요하다. 실제 코드 못지 않게 깨끗하게 짜야 한다.

 

- 코드에 유연성, 유지보수성, 재사용성을 제공하는 버팀목이 바로 단위 테스트다. 이유는 단순하다. 테스트 케이스가 있으면 변경이 두렵지 않으니까! 테스트 케이스가 없다면 모든 변경이 잠정적인 버그다.

 

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

 

- 테스트 코드는 본론에 돌입해 진짜 필요한 자료 유형과 함수만 사용한다.

 

- 숙련된 개발자라면 자기 코드를 좀 더 간결하고 표현력이 풍부한 코드로 리팩터링해야 마땅하다.

 

- 테스트 환경은 자원이 제한적일 가능성이 낮다. 이것이 이중 표준의 본질이다. 실제 환경에서는 절대로 안 되지만 테스트 환경에서는 전혀 문제없는 방식이 있다. 대개 메모리나 CPU 효율과 관련 있는 경우다. 코드의 깨끗함과 철저히 무관하다.

 

- TEMPLATE METHOD 패턴을 사용하면 중복을 제거할 수 있다. given/when 부분을 부모 클래스에 두고 then 부분을 자식 클래스에 두면 된다. 아니면 완전히 독자적인 테스트 클래스를 만들어 @Before 함수에 given/when 부분을 넣고 @Test 함수에 then 부분을 넣어도 된다. 하지만 모두가 배보다 배꼽이 더 크다. 이것저것 감안해 보면 결국 assert 문을 여럿 사용하는 편이 좋다고 생각한다. 결국 포인트는 assert 문 개수는 최대한 줄여야 좋다는 생각이다.

 

- 어쩌면 "테스트 함수마다 한 개념만 테스트하라"는 규칙이 더 낫겠다.

 

- 가장 좋은 규칙은 "개념 당 assert 문 수를 최소로 줄여라"와 "테스트 함수 하나는 개념 하나만 테스트하라"라 하겠다.

 

- F.I.R.S.T 깨끗한 테스트는 아래와 같은 다섯 가지 규칙을 따르는데, 각 규칙에서 첫 글자를 따오면 FIRST가 된다.

  • 빠르게(Fast): 테스트는 빨리 돌아야 한다. 테스트가 느리면 자주 돌릴 엄두를 못 낸다.
  • 독립적으로(Independent): 각 테스트는 서로 의존하면 안 된다. 한 테스트가 다음 테스트가 실행될 환경을 준비해서는 안 된다.
  • 반복가능하게(Repeatable): 테스트는 어떤 환경에서도 반복 가능해야 한다. 버스를 타고 집에 가는 길에 사용하는 (네트워크에 연결되지 않는) 노트북 환경에서도 실행할 수 있어야 한다.
  • 자가검증하는(Self-Validating): 테스트는 부울(bool)값으로 결과를 내야 한다. 성공 아니면 실패다. 통과 여부를 알려고 로그 파일을 읽게 만들어서는 안 된다.
  • 적시에(Timely): 테스트는 적시에 작성해야 한다. 단위 테스트는 테스트하려는 실제 코드를 구현하기 직전에 구현한다.

- 테스트 코드가 방치되어 망가지면 실제 코드도 망가진다. 테스트 코드를 깨끗하게 유지하자.