프로그래밍방법론

클린 아키텍처를 읽고…

이 책을 펴기 전에는 책 내용이 아키텍처의 사전일 거라 예상했었습니다. 하지만 읽어보니 아키텍처 사전보단 변경에 유연하고, 쉽게 배포하고 관리할 수 있는 아키텍처를 만들기 위한 개념과 이유를 중심으로 설명하는 책이었습니다. 이 책은 정말 책 제목대로 깔끔한 아키텍처를 만드는 것에 대한 책입니다.

제 감상은 다음과 같습니다. 먼저 전에 읽은 조엘 온 소프트웨어와 달리 이 책은 유쾌한 것보단 고정관념을 부수는 충격요법을 통해 독자의 흥미를 유발하는 책입니다. ‘프로그래밍 패러다임은 사실 프로그램에 제약을 두는 방법이다.’, ‘마이크로서비스 아키텍처는 모든 아키텍처를 압도하는 건 아니다.’, ‘Java의 패키지를 제대로 쓰려면 무분별한 public class를 막아라.’ 같은 내용을 읽을 때마다 뜨끔한 기억이 있기 때문입니다. 이런 게 없었으면 아마 흔한 소프트웨어 공학책이라고 생각하고 대충 읽었을 거로 생각합니다.

사실 이 책이 말하고자 하는 건 짧습니다. ‘변경 이유를 기준으로 컴포넌트를 나눠라.’, ‘세부 사항이 핵심을 의존하게 해라.’ 같이 이미 다른 아키텍처 책에서도 봤을 내용들을 말하고 있습니다. 하지만 이전까지 본 글에서 깔끔한 아키텍처는 너무 이상적이라 적용하기 어렵다는 생각도 들었습니다.

하지만 이 책은 그런 생각을 덜어 줄 수 있는 책입니다. 왜냐하면 ADP와 안정된 추상화 원칙, 컴포넌트 3원칙, 컴포넌트의 경계를 나누는 데 영향을 미치는 요소들 등등 아키텍처를 설계하는데 중요한 개념들을 설명해주기 때문입니다. 그리고 한 줄도 절약하는 임베디드 환경에선 장치 접근을 로직에서 어떻게 분리하는지, 작은 규모의 프로젝트에선 양방향 의존관계여도 인터페이스를 매번 만들지 않아도 된다든지 같은 현실적인 문제를 위한 조언도 들어있습니다.

그 외에 들었던 생각은 책 내용이 조엘 온 소프트웨어와 달리 2019년도에 나왔기 때문에 오래된 조언이 아니라고 말할 수 있다는 것입니다. 덕분에 책 내용을 친구에게 소개해주니 마이크로서비스 아키텍처 맹신자에게 한 방 먹일 수 있다고 좋아했었습니다. 또한 마지막 부록에 있는 천공 카드 시절부터의 저자의 경험과 거기서 나온 소프트웨어 공학들 내용은 80쪽 넘는 긴 챕터였지만 역사책 읽는 느낌이라 즐거웠습니다.

저는 이 책이 최소 한 번은 읽고 가까운 책장에 두고 있어야 될 책이라고 생각합니다. 적어도 개발하면서 직관적이고 관리하기 쉽게 소스 코드를 구분하는 방법을 알고 싶은 사람이라면 말입니다. 한 번에 이해하지 못했다면 희미하게라도 기억했다가 필요할 때 바로 찾아볼 수 있다면 좋은 아키텍처를 만드는 데 많은 도움이 될 거라고 생각합니다.

조엘 온 소프트웨어

조엘 온 소프트웨어를 읽고…

회사에서 여러 권을 추천 받았는데 그 중 이 책에 대한 평입니다. 사내에서 쓴 글을 공유합니다.

이 책을 첫 번째로 읽었던 이유는 다른 책과 달리 읽기 편한 블로그 글로 작성되어 있어서 시작 도서로 적합해 보였기 때문입니다.

그리고 기대했던 것처럼 이 책은 길어도 20분 내로 읽을 수 있는 45개의 글과 부록으로 이루어져 있습니다. 그리고 내용은 저자가 개발자와 프로젝트 관리자로 일하면서 겪었던 일들과 그에 따른 교훈을 설명해주고 있습니다.

책을 다 읽고 나니 이 책이 많은 개발자에게 추천받은 게 당연하다는 생각이 들었습니다.

먼저 책에서 앞으로 일하면서 접할 수 있는 다양한 경험과 생각에 대해 좋은 힌트를 제시해 줍니다.

예를 들어 일일 빌드와 일정, 명세서 같이 회사에서 처음 접할 수 있는 것들을 잘 활용하는 방법을 소개해줍니다. 또 관리자의 역할이나 목표 시장에 따라 바뀔 수 있는 프로젝트 진행 방법, 기존 것을 없애고 새로 시작하는 게 왜 위험한지 등등 신입인 저는 경험하지 못한 고민도 알 수 있었습니다. 오랫동안 쌓인 경험을 책을 사는 것만으로 배울 수 있다는 게 이 책이 추천받아야 할 이유라고 생각합니다.

그리고 전문 분야를 다루지만, 굉장히 읽기 편한 것도 이 책의 장점입니다.

여기도 여러 가지 이유가 있습니다. 첫 번째로 저자 본인의 경험과 주변 사례를 가져와서 이해하기 쉬웠습니다. 또 전문 용어가 아닌 쉬운 단어와 적절한 비유를 추가해서 읽기 편했습니다. 마지막으로 영미권에서만 이해할 수 있는 표현을 우리 문화에 맞게 옮기거나 적절한 각주를 달아서 저자의 농담도 재밌게 읽을 수 있었습니다.

하지만 완벽한 책은 아닙니다. 나온 지 오래된 책이라 넷스케이프나 Windows 3.0 같은 지금은 볼 수 없는 것들이 나오는데 이런 내용이 오히려 이해하기 어렵게 만든다고 생각했습니다. 하지만 2022년의 조엘 스폴스키가 나오지 않는 이상 해결할 순 없는 것이니 아쉬웠습니다.

그리고 ‘순수한’ 개발자들에겐 호불호가 갈릴 책이라고 생각합니다. 왜냐하면 이 책은 프로그램 관리자였던 조엘의 경험이 대부분이라 현실적이고 경제적인 조언이 많이 있습니다. 그래서 자유 소프트웨어를 지지하거나 기술만을 추구하는 분들을 자극하는 내용들이 많이 있습니다. 제 편견일 수 있지만 그럴 수 있을 것 같다는 생각이 계속 들었습니다.

그래도 저는 이 책을 추천합니다. 저자의 모든 말이 맞는 건 아니겠지만 제 업무에 많은 도움이 될 거로 생각한 책이기 때문입니다. 그래서 만약 나중에 개발자가 되든, 관리자가 되든, 몇 년 후에 읽게 된다면 책에서 한 말이 얼마나 맞았는지 알고 싶은 책이라고 생각합니다.

프로그래밍방법론

Circuit Breaker

https://martinfowler.com/bliki/CircuitBreaker.html

서킷 브레이커는 장애가 난 외부 시스템에 요청을 낭비하지 않기 위해 사용하는 패턴이다.

배경

어떤 외부 시스템에 원격 호출을 하는 소프트웨어를 만들었다고 하자. 만약 이 외부 시스템에서 장애가 발생했다면 우리 입장에선 요청이 타임아웃 되는 걸 통해 알 수 있을 것이다.

하지만 단시간 내에 클라이언트가 우리 시스템을 수백번 넘게 실행한다면, 수백번의 요청이 타임아웃 날 것이다. 단지 몇 번의 재시도만 해도 외부 시스템의 장애를 알 수 있는데, 수백번의 요청들이 각각 외부 시스템이 장애가 난 걸 알 때 까지 기다리는 건 자원 낭비다. 이 때문에 서킷 브레이커라는 패턴이 탄생하게 되었다.

기본 이론

서킷 브레이커의 이론은 간단하다. 원격 호출하는 부분을 서킷 브레이커라는 장애를 확인하는 객체로 감싸자는 것이다. 서킷 브레이커를 통해 원격 호출을 했다가 장애를 만나면 서킷 브레이커가 이를 기억한다. 정해진 횟수를 초과하면 이후에 이 원격 호출을 시도하려고 하면 서킷 브레이커에 의해 호출하지 않고 예외를 발생시킨다.

위의 그림은 서킷 브레이커의 상태 머신을 나타낸다. 여기서 closed는 원격 호출이 가능한 상태를 의미한다. 장애가 누적되면 open 상태로 바뀌고 호출은 차단된다. 추가로 half_open이란 상태도 있는데 이는 다음과 같다. 서킷 브레이커는 장애가 발생한 상황 뿐만 아니라 장애가 복구된 상황도 고려했다. 마지막으로 원격 호출을 시도한 시점부터 정해진 시간이 지난 후에 다시 시도한다면, 서킷 브레이커는 원격 호출을 한다. 만약 호출이 성공했다면 장애가 복구되었다고 판단한다. 실패 했다면 다시 차단 상태로 바꾼다. 이와 같이 장애가 복구되었는지 판단하는 상태를 half_open이라고 한다.

라이브러리화

결국 차단할 지 나타내는 상태 변수와, 시도 횟수, 마지막 시도 시점, 반복문을 조합하면 간단한 서킷 브레이커가 완성된다. 하지만 이걸 매번 만드는 건 귀찮은 일이다. 또한 예로 들어, 차단할 조건을 몇 분 이내에 횟수를 초과했을 경우로 한다든지, 복구 조건도 10번 시도해서 50% 이상 성공하면 복구 시켜준다든지, 차단 시 오류 대신 단계를 넘어가게 한다든지 등 다양한 방법으로 개조할 수도 있다. 이런 번거로움을 없애기 위해 라이브러리로 보통 사용한다.

다른 예시들

위에선 예시로 원격 호출을 들었지만 뭐가 됐든 자원이 있는지 확인하는데 오래걸리거나 자원이 복구될 때까지 오래 걸리는 것이면 서킷브레이커를 활용하기 좋다. 예를 들어 쓰레드 풀에서 쓰레드를 가져다가 반납을 늦게 하는 환경이라면 언제 반납할 지도 모르는 걸 기다리지 말고 서킷브레이커로 차단시키는 방법도 있다. 또는 producer-consumer 관계에서 consumer가 소비하는 게 느리다면, 큐가 가득 찼을 때 서킷브레이커로 차단하도록 구성할 수도 있다.

프로그래밍방법론

당신이 소유하지 않은 걸 mocking하지 마시오

https://github.com/testdouble/contributing-tests/wiki/Don’t-mock-what-you-don’t-own

이 글에선 사람들이 테스트에서 가져오기 힘든 실제의 것을 mocking 해서 쓰는 행동이 잘못되었다고 지적하고 있다. 테스트 더블을 사용하는 근본적인 이유는 테스트 대상이 의존하고 있는 인터페이스와 적절하게 소통하고 있는지 확인하기 위해서 이기 때문이다.

테스트 더블은 아직 구현되지 않은 것을 의존하는 테스트 대상을 위해서 안전하고 ‘쉽게 수정할 수 있는’ 테스트 용 객체를 제공하기 위해 탄생했다. 그리고 왜 쉽게 수정할 수 있어야 하냐면, 테스트 더블의 최종 목적은 의존하고 있는 인터페이스의 설계가 제대로 되었는지 알려주기 위해서 이기 때문이다. 이는 테스트 더블을 가지고 테스트 대상에서 의존하는 코드를 짤 때 코드 스멜을 통해 설계에 문제가 있음을 알 수 있다.

그래서 서드파티 의존성을 테스트 더블로 교체하는 것은 이런 본래 목적에 맞지 않다. 만약 서드파티 의존성을 가짜로 만들어서 테스트하는데 문제가 없었다면, 서드파티 의존성과의 결합도가 너무 높은 것일 수 있다. 반대로 의존성을 가짜로 만들어서 테스트하는게 힘들었다면, 그만큼 서드파티 의존성을 재현하려고 고생해서 만들었을 텐데 그렇게 만든 테스트 더블은 사실 별 가치가 없다. 의존하는 부분의 설계가 잘 된 걸 알려주지 않기 때문이다.

그래서 이 글에선 실제의 것을 바로 mocking하지 말고 adapter, wrapper, shim 같은 걸 직접 만들어서 그걸로 서드파티 의존성을 감싸라고 말한다. 이러면 서드파티 의존성을 쉽고 일관적인 방법으로 사용할 수 있다. 예로 들어 의존성을 method chaining 구조로 만들어야 한다든지, 여러 단계로 호출해야 한다든지, 반복적인 기본값 설정을 줘야 된다든지 하는 부분을 adapter로 감싸면, 이 adapter를 사용하는 입장에선 매우 편해진다.

이렇게 adapter를 쓰는 건 다른 이점도 있다. 만약 의존성이 수많은 기능을 제공하는데 그 중 일부만 사용한다면, adapter에도 이 일부 기능만 구현할 것이다. 그러면 다른 의존성으로 교체할 때도, adapter에서 제공하는 이 몇몇 기능만 열어주면 쉽게 교체할 수 있다. 게다가 기능에 관련된 설정들을 모아서 관리할 수도 있어서 편하다.

architecture patterns with python

Architecture Patterns with Python – 8

이 책의 영문 버전은 무료로 읽을 수 있습니다.
http://www.cosmicpython.com/book/preface.html

책의 실습 코드들
https://github.com/cosmicpython

Aggregate 패턴과 일관성 경계

이 챕터에선 도메인 모델에서 컬렉션에 대해 일관성을 유지하는 aggregate 패턴과 그와 관련된 용어들을 설명한다.

제약과 불변 조건, 일관성, 락

의자를 -350개를 주문하거나 100억 개 이상의 비정상적인 주문이 생기지 않으려면, 도메인 로직에서 이런 제약을 검증할 필요가 있다.

비슷하게 쓰이지만 제약은 도메인 모델이 가질 수 있는 상태에서 제약을 두는 것이고, 불변 조건은 도메인 모델이 항상 참이어야 하는 조건을 의미한다. 이들은 경우에 따라서 ‘잠시’ 지켜지지 않을 수 있지만, 최종적으론 지켜져야 한다. 만약 어떻게 해도 이 제약과 불변 조건을 지키면서 새로 온 입력을 넣을 수 없다면, 오류를 출력하고 입력이 오기 전으로 되돌려야 한다.

이런 일관성은 특히 동시에 접근하는 사람들이 많아질수록 어려워진다. 그러기 위해 락을 쓰지만 사용자가 많으면 한계가 있다. 수십만명의 사용자 요청이 각각 테이블 전체를 락을 걸면 데드락이 생기든 성능에 문제가 생길 수 밖에 없기 때문이다.

책 내용 아님: 잠시 지켜지지 않는 말은 NoSQL의 BASE 모델과 연관된 말인 것 같다. NoSQL은 RDB처럼 매 트랜잭션마다 정합성을 보장하지 않고 필요한 순간에 정합성을 보장할 수 있으면 당장은 안 지켜도 되기 때문이다.

Aggregate와 일관성 경계

Aggregate는 같은 목적을 가지고 변경해야 하는 데이터들의 부분 집합을 만들어서 이 집합이 동시성 제어를 하고 단일 진입점 역할을 하는 걸 말한다. 앞서 말한 동시성은 보장하고 싶지만 매 row마다 수많은 락이 생기는 걸 방지하고 싶을 때 사용한다.

책에 나오는 할당 예제에선 OrderLine을 할당할 Batch를 찾는건 같은 sku를 가진 Batch로 한정된다. 즉, 의자를 주문하면 의자의 재고에서만 고려한다는 말이다. 그럼 의자 Batch들을 하나의 Aggregate로 묶어서 의자를 주문하는 요청들끼리는 같은 락으로 의자 Batch에 대해 일관성을 유지할 수 있다. 그리고 침대나 다른 가구를 주문하는 요청들은 의자 주문에 의해 락이 걸리지 않는다.

일관성 경계는 위에서의 가구 종류처럼 논리적으로 락을 분리해야하는 되는 경계를 의미한다.

만약 위와 같이 aggregate 패턴을 적용하기로 했다면 기존의 Batch는 단일 엔티티는 접근할 수 없게 해야 한다. 오직 aggregate로만 접근해야 한다.

성능에 대한 우려

aggregate를 사용하면 하나의 배치를 원하더라도 모든 배치를 읽어오게 된다. 이 방식이 비효율적일 수 있지만 이런 점도 고민해보자.

  1. aggregate는 최대한 질의 한번과 변경을 한번에 반영하려는 패턴이다. 이 패턴을 쓰지 않고 개별적으로 질의한다면 데이터가 많아질수록 트랜잭션은 느려지고 복잡해진다.
  2. 데이터 구조를 최소한으로 사용하며 한 행당 소수의 문자열과 정수만 만들면 많이 불러오더라도 오래 걸리지 않는다.
  3. 배치를 다 쓰면 그 배치를 계산에서 제외하면 되기 때문에 조회할 데이터 양이 예상을 벗어날 일은 없다. (이 부분은 뭐에 대한 우려인지 모르겠다.)

그래도 우려가 된다면 지연 읽기를 적용해서 한번 읽어오는 양을 줄이는 것도 요청 수는 많아지겠지만 좋은 방법이다. 특히 조건에 맞는 행 하나만 찾는 거라면 조금씩 읽다가 조건이 맞으면 더 이상 안 읽어도 된다.

낙관적 동시성

만약 시스템에서 두 개의 변경 사항이 충돌하는 일이 적다고 판단된다면, 충돌 가능성이 있는 곳에 전부 락을 거는 비관적 동시성보단 일단 변경을 수행하고 충돌이 발생했을 때 조치를 취하는 낙관적 동시성을 시도해볼 만하다.

전제 조건은 실패를 감지할 수 있어야 한다. 책에서는 버전 번호를 도메인 객체에 추가하는 걸 권장한다. 버전 번호를 읽고 번호를 증가시킨 후 커밋하는 것이다. 만약 새 번호로 변경을 시도 했는데 이미 저장소에 올라간 번호가 저장되어 있다면, 충돌했다고 판단하고 재시도를 하든 다른 조치를 취한다.

버전 번호라는 게 도메인에 필요한 속성은 아니다. 하지만 서비스나 저장소 객체가 맡기엔 도메인 객체의 변경을 기록하는 책임과 관심사가 맞지 않기 때문에 결국 도메인 객체에 붙이는 게 낫다.

요약

애그리게이트와 일관성 경계에 대해 다음을 기억하자.

  • 애그리게이트는 도메인 모델에 대한 진입점이다. 도메인을 직접 제어할 수 없게 해서 도메인의 일부를 감출 수 있다.
  • 애그리게이트는 일관성 경계를 책임진다. 여러 개의 객체를 묶어서 불변 조건을 검사하고 이를 위배하는 변경(낙관적 동시성의 예시)을 거부할 수도 있다.
  • 애그리게이트를 쓰는 건 개념적인 설계 뿐만 아니라 동시성과 성능에 대한 고민도 포함된다.

1부 마무리: DDD

여기까지가 이 책에서 DDD에 대한 설명이다. 2부부터는 일관성 경계를 넘어서 제어하기 위해 이벤트 기반 아키텍쳐(EDA)에 대해 다룰 예정이다.

architecture patterns with python, Python

Architecture Patterns with Python – 7

이 책의 영문 버전은 무료로 읽을 수 있습니다.
http://www.cosmicpython.com/book/preface.html

책의 실습 코드들
https://github.com/cosmicpython

작업 단위 패턴

작업 단위 패턴(Unit of work)은 atomic operation에 대한 추상화다. 그래서 예로 들어 저장소에 대한 작업 단위를 만든다고 하면, 저장소에 연결해서 상호작용 한 후에 커밋하거나 롤백 후 연결을 종료하는 과정을 작업 단위 객체가 처리하게 하는 것이다.

아래는 작업 단위 패턴을 사용하는 예시다. 이러한 동작을 할 수 있도록 작업 단위를 구현하면 된다.

with uow:  # uow를 컨텍스트 매니저로서 시작한다.
           # 만약 uow가 실제 DB를 사용한다면 DB와 연결한다.
        batches = uow.batches.list()  # uow의 저장소와 상호작용 한다.
        ...
        batchref = model.allocate(line, batches)
        uow.commit()  # 오류 없이 끝났다면 마지막에 직접 커밋한다.
                      # 만약 오류가 발생하면 uow는 저장소에 변경사항을 커밋해선 안된다.

batches는 uow가 가지고 있는 저장소 객체를 가리킨다.

따라서 원래 저장소 객체와 세션 객체를 API에서 서비스 계층으로 주입했기 때문에 API와 서비스 계층은 저레벨인 저장소를 직접 제어하고 있었다.

하지만 uow 로 추상화하면 API는 더 이상 저장소를 직접 알 필요가 없다. 그리고 서비스 계층도 DB가 실제로 연결되는 과정을 알 필요가 없다.

명시적 커밋과 암묵적 커밋

저자들의 경우는 명시적 커밋을 선호한다. 저장소를 변경하는 데 성공한 경우를 한 개로 줄일 수 있어서 코드 추론이 쉬워지고, 혹시나 놓친 오류가 발생해도 명시적 커밋의 기본값은 변경되지 않는 것이므로 상대적으로 안전하다.

명시적 커밋을 한다면 커밋을 잊어버린 경우 롤백 되는지, 데이터 수정 중에 예외가 발생해도 롤백되는지 테스트 하면 좋다.

SQLAlchemy의 Session과 작업 단위

책에선 Session 객체를 UoW로 감췄지만 사실 Session이 UoW 패턴을 사용해서 만들어진 것이다. 즉, UoW를 UoW로 감춘 셈인데, 이에 대해 저자들은 다음과 같이 설명한다.

사실 Session만 써도 원래 UoW에서 원했던 atomic operation을 쉽고 안전하게 사용한다는 주 목표는 달성한 셈이다. 하지만 Session은 직접 구현한 UoW보다 다양한 영속성 제어를 지원한다. 그래서 DB 접근 코드가 무분별하게 생성되는 걸 제어 할 수 없다. 또한 테스트를 위한 가짜 세션을 만들 때도 Session과 SQLAlchemy의 복잡한 구조에 결합하게 된다. 이 때문에 외부 객체의 mock을 만들 때는 wrapper를 만들라는 말도 있다.

게다가 API 입장에서 UoW를 직접 만들면 저장소 패턴 객체와 세션 객체를 같이 감출 수 있으니 이 점에서 더 좋다고 생각한다.

참고 글: Don’t mock what you don’t own

프로그래밍방법론

FIRST와 Right BICEP

테스트에서 유명한 원칙이므로 알아둘 필요가 있어서 정리해둠.

FIRST

단위 테스트가 지켜주면 좋은 속성들을 앞 글자를 따서 정리함.

  • Fast: 단위 테스트는 빨라야 함. 반대로 말하면 테스트를 느리게 하는 DB나 File IO 같은 것에 의존하지 않는 게 좋다고 말함.
  • Independent/Isolated: 각각의 단위 테스트는 격리되어야 함. 위에서 언급한 DB 같은 외부 자원이나 테스트 끼리 공유하는 자원이 있다면, 다른 테스트에 의존할 수 있음.
  • Repeatable: 단위 테스트는 반복되어도 같은 결과를 내야 함.
  • Self validating: 단위 테스트는 스스로 성공 또는 실패를 반환해야 함. 사람이 일일이 판단하게 만들면 안된다는 의미.
  • Timely/Thorough: 두 가지 단어로 적혀있던데 여러 정리 글을 읽어보고 나서 “단위 테스트는 구현을 개발하는 시점에 맞춰서 준비되어야 구현한 것에 대해 확신을 줄 수 있다”는 말로 요약했다.

Right-BICEP

주로 테스트 하는 것들을 모아둠.

  • Right: 결과가 올바른가?
  • Boundary: 경계 조건 테스트
  • Inverse relationship: 역 관계 테스트
  • Cross-check: 다른 수단으로 교차 검증하기
  • Error condition: 오류 조건이 발생했을 때의 테스트
  • Performance: 성능 조건에 대한 테스트. ex) 리팩토링 후에 기존 것과 비교

경계 조건 테스트는 범위가 넓어서 이에 대한 가이드라인도 있다.

출처: 자바와 JUnit을 활용한 실용주의 단위 테스트 – 제프 랭어, 앤디 헌트, 데이브 토마스

주의: 이 글은 책을 읽은 다른 사람들의 정리 글을 본 내용입니다.

  • Conformance: 정해진 형식이 있으면 그 형식에 맞는지 테스트
  • Ordering: 순서 있는 데이터를 다룬다면 기대한 것처럼 정렬되었는지 테스트
  • Range: 범위가 정해진 값이면 범위 안에 있는지 테스트
  • Reference: 사전/사후 조건과 외부 의존성이 준비되었는지 테스트.
    (다만 좋은 테스트는 이런 외부 의존성의 준비와 상관없이 동작해야 함.)
  • Existence: null check하기
  • Cardinality: 컬렉션은 반드시 충분한 값이 있는지 테스트하기.
    ex) 0개일 때, 1개일 때, n개일 때
  • Time: 순서와 시간의 특성(DST, timezone 등), 동시성 문제를 고려해야 함.

기타 자료

https://stackoverflow.com/a/43129899

이 답변에선 FIRST 대신 FASRCS라는 것도 얘기하는데 추가 내용은 다음과 같다.

“테스트는 최대한 단순하고, 요구사항을 기반해야 하며, 테스트 내용을 명확하게 파악할 수 있게 구현해야 한다.”

architecture patterns with python, Python

Architecture Patterns with Python – 6

이 책의 영문 버전은 무료로 읽을 수 있습니다.
http://www.cosmicpython.com/book/preface.html

책의 실습 코드들
https://github.com/cosmicpython

TDD의 고단 기어와 저단 기어

이전 장까지 단위 테스트와 서비스 계층 테스트, E2E 테스트 에 대한 설명을 했다. 하지만 책에서 만들었던 테스트 코드들은 도메인 계층에 많이 의존하고 있다. 만약 서비스 계층을 테스트하는 거라면 도메인 계층에 의존해야 할까? 도메인 계층 위주의 세분화된 테스트는 E2E나 서비스 계층 테스트 보다 무조건 좋을까? 이번 장에선 테스트 대상 외의 의존을 줄이는 방법과 테스트 피라미드에 대해서 배운다.

테스트 피라미드

책에선 지금까지 짠 테스트 개수를 단위, 통합, E2E 테스트로 범주를 나눠서 확인하고, 테스트 피라미드 상 건강하다고 본다. 여기서 테스트 피라미드는 이 세 범주가 균형이 잡혀있는지 확인하는 것이다.

이러한 개념이 나온 배경은 다음과 같다. 위 그림에선 E2E 테스트 대신 UI 테스트를 예로 들었는데, UI 테스트는 적절한 GUI 테스팅 도구만 있다면 작성하기 쉽다. 하지만 UI 테스트를 남용하면 안되는데, 그 이유는 GUI 도구들의 구매 비용, 오래 걸리는 테스트, 그리고 대개 비결정적 테스트이기 때문에 버그를 찾는데 어렵기 때문이다.

그래서 비록 단위 테스트가 작성하기 어렵지만 테스트 시간과 비용, 정밀한 오류 추적을 위해 충분히 작성할 필요가 있다. 하지만 UI 테스트 또한 단위 테스트를 실수로 잘못 짰을 때를 위한 2차 방어선 역할을 해줄 수 있다. 그리고 통합 테스트는 가운데서 단위 테스트와 UI 테스트를 보완해 줄 수 있다. 이에 대한 자세한 설명은 다음에 나온다.

더 자세한 설명은 다음 글에서 확인할 수 있다. https://martinfowler.com/bliki/TestPyramid.html

고단 기어와 저단 기어

책에선 마치 자전거가 속도를 받으면 기어를 바꾸는 것처럼 테스트도 경우에 따라 도메인 모델의 의존도를 줄여야 된다고 말한다.

예로 들어 프로젝트 초기에 도메인 모델을 새로 만들어야 할 때, 도메인 모델을 대상으로 하는 테스트는 도메인 모델에 대한 정보를 쉽게 보여주고, 빠르고 상세한 피드백을 줄 수 있다.

하지만 시간이 지나면 서비스 계층 테스트를 추가한다. 더 넓은 범위를 테스트 할 수 있고, 도메인 모델과의 결합을 줄여서 도메인 모델을 변경 할 때 같이 수정할 일이 줄어든다.

(기존의 도메인 모델 테스트를 수정하는 게 아니고 기존 테스트들을 포함하고 도메인 모델에 의존하지 않는 서비스 계층 테스트를 만든다. 그러면 나중에 도메인 모델이 변경될 때, 굳이 기존 테스트를 수정하지 않고 버려도 된다.)

하지만 진행 도중 큰 문제에 부딪히면 다시 우리의 의도가 맞는지 깊게 확인해야 하고, 더 상세한 피드백이 필요해진다. 그러면 단위 테스트를 만들 때다.

서비스 계층 테스트에서 도메인 결합 떼기

이전 장 까지의 책의 예제에선 서비스 계층 테스트가 도메인 모델을 의존하고 있었다. 그래서 결합을 떼내는 방법을 설명한다.

  • 인자로 받는 모델은 primitive 타입의 변수들로 바꾸기
  • 내부에서 테스트 데이터로 생성할 땐 helper function이나 fixture로 감추기
    (이 helper function도 primitive 타입으로 받기)
  • 필요하면 새로운 서비스를 만들어서 호출하기
    ex) 할당을 위한 Batch가 DB에 있어야 하면 DB에 Batch를 추가하는 서비스 구현

책 내용 밖의 생각: Python의 경우 타입을 명시하지 않는 언어라서 변수 선언의 우변만 위의 방법처럼 수정하면 된다. 하지만 타입을 명시하는 언어면 좌변에도 타입이 있는데 이게 될 지는 모르겠다.

E2E 테스트도 개선하기

E2E 테스트도 서비스 계층 테스트와 마찬가지로 도메인 모델과의 결합을 제거하는 게 도메인 모델을 수정 시의 변경 비용을 줄일 수 있다. 책에선 앞서 만든 서비스를 API까지 만들어서 기존의 E2E 테스트에서 직접 DB에 접근하는 코드를 수정했다.

결론

여러 종류의 테스트를 넣는 규칙

  1. 기능 하나에 E2E 테스트 하나는 있어야 한다.
  2. 테스트의 대부분은 서비스 계층을 대상으로 만들자
  3. 도메인 모델을 위한 테스트는 핵심적인 것들은 유지하자
  4. 실패하는 경우는 오류 처리 방법을 기준으로 기능을 정한다.
    (이 어플리케이션에선 모든 오류들이 HTTP 400 하나로 처리된다. 따라서 각각의 기능마다 정상 케이스를 테스트하지만 모든 실패 케이스는 하나의 E2E 테스트로 묶는다)

그 외에

  • 서비스 계층에선 도메인 객체를 원시 타입으로 표현하기
  • 이상적으론 서비스 계층에선 서비스 만으로 테스트 해야 한다. 저장소를 직접 접근하지 말아야 한다. E2E도 마찬가지다.
프로그래밍방법론

비결정적 테스트

https://martinfowler.com/articles/nonDeterminism.html

위 글의 내용이 길어서 짧게 요약한다. 그러니 아래 내용은 원 저자의 경험이다.

비결정적 테스트란?

말 그대로 테스트 대상을 수정하지 않았는데도 테스트할 때마다 통과할 수도 실패할 수도 있는 테스트를 말한다.

비결정적 테스트는 왜 문제가 되는가?

비결정적 테스트가 있으면 테스트가 실패했을 때, 이게 코드를 잘못 수정해서 발생한 것인지 코드는 정상인데 테스트가 실패한 것인지 알 수 없다. 이렇게 되면 테스트를 신뢰할 수 없다.

만약 비결정적 테스트라는 걸 발견했다면?

격리하라. 메인 테스트 파이프라인에서 해당 테스트를 제외해야 한다.

비결정적 테스트의 원인을 파악하는 건 어려운 작업이다. 그래서 매번 발생할 때마다 테스트를 고치려고 당장 달려드는 건 원래 하던 일에 차질을 줄 수 있다. 그래서 이런 테스트의 원인을 당장 파악하기 어려우면 수정하는 걸 미뤄도 된다. 단 규칙을 정하자. 몇 개 이상 쌓이거나 며칠 이상이 지나면 더 이상 다른 할 일을 받지 말고 테스트를 수정하는 걸 권장한다. 개수와 타임아웃은 본인이 속한 팀의 재량이지만 테스트를 건강하게 유지할 수 있는 정도로 정하자.

비결정적 테스트의 원인들

테스트 환경을 분리하지 않음

예로 들어 DB에서 같은 테이블에 대해 여러 개의 테스트를 실행한다고 하자. 만약 앞의 테스트가 남긴 찌꺼기 데이터가 DB에 남아있다면 뒤의 테스트가 실패할 수도 있다. 이렇게 테스트마다 환경이 분리되지 않아서 테스트가 정확하지 않을 수 있다.

위에선 DB를 예로 들었지만 인 메모리에서도 일어날 수 있다. 예로 들어 ‘현재 로그인 유저’ 같은 싱글톤 객체가 대표적이다.

보통 이런 경우의 가장 권장되는 해결책은 매 테스트마다 환경을 초기화 하는 것이다. 하지만 테스트 환경을 만드는 비용이 크거나 시간이 오래 걸리면, 조금 위험하지만 ‘수정되지 않는 원본 환경’을 만들어두는 것도 해결책이 될 수 있다.

그리고 tear-down을 꼼꼼히 확인해보자. 만약 tear-down에서 오류가 발생하면 tear-down을 실행한 테스트는 통과했겠지만, 그 이후의 테스트가 실패할 수 있기 때문이다.

첨언으로, 만약 DB를 쓴다면 일부러 트랜잭션 내에서 테스트하고 마지막에 롤백 시키는 테크닉도 있다.

비동기 작업

비동기 작업은 언제 결과가 돌아올 지 모르기 때문에 테스트가 까다롭다. 비동기 요청 동안은 대기하고 있다가 응답이 들어왔을 때 테스트가 응답을 확인하는 게 이상적이기 때문에 이 글에선 두 가지 방법을 추천한다.

첫 번째는 비동기 호출에 콜백 추가하기 다. 애초에 비동기 호출된 곳에 작업이 끝나면 실행해줬으면 하는 걸 요청할 수 있다면, 언제 응답할지 걱정할 필요가 없다. 게다가 이후 소개할 두 번째 방법과 달리 테스트 코드만 짜면 된다. 편하다.

하지만 첫 번째 방법은 비동기 호출된 곳을 수정할 수 없다면 쓸 수 없는 방법이다. 예로 들어 외부 서비스의 API인 경우다. 그래서 두 번째 방법은 Polling을 하는 것이다. 응답을 짧은 인터벌을 두고 루프를 돌면서 확인하는 것이다. 그리고 미리 본인이 정한 타임아웃을 넘기면 테스트 실패를 알리면 된다.

이러면 외부 서비스를 쓰더라도 응답이 돌아온 즉시 테스트를 할 수 있다. 그리고 비동기 작업에 오류가 생겨서 테스트가 영원히 기다리는 일도 없다. 하지만 테스트 실패에 대해서 타임아웃 외에는 아는 게 없다는 문제가 있다. 그래서 콜백을 쓸 수 있도록 상대방을 설득해보라고 한다.

여기까지가 비동기 작업을 직접 테스트 한다면 취할 수 있는 방법이다. 하지만 시간이 오래 걸린다는 문제 때문에 직접 호출을 회피하는 방법도 소개한다. 예로 들어 mock 객체 같은 테스트 더블을 쓰는 방법이 있다. 그리고 본격적인 테스트 전에 외부 서비스가 작동하는지 점검하는 것도 쓸데없는 시간 낭비를 막아줄 수 있다. 이를 smoke test라고 한다. 마지막으로 비동기 환경이 필요없는 로직을 최대한 포함하지 않게 비동기 작업 코드를 수정한다면, 비동기 테스트를 해야 될 범위를 줄일 수 있다.
(이 문단의 내용은 다음에 설명할 ‘원격 서비스’와도 내용이 비슷하다.)

원격 서비스

원격 서비스는 우리 팀이 제어할 수 없는 시스템과 연동해야 할 경우를 말한다. 그래서 상대 쪽에서 시스템을 수정하면 우리 쪽 테스트가 실패할 수 있는 건 당연하다.

이에 대한 해결책은 ‘비동기 작업’에서도 말했던 테스트 더블을 쓰는 것이다. 단, 외부 서비스를 흉내내는 것인 만큼 실제 외부 서비스와 동일한 값을 반환해줘야 한다. 그래서 테스트 더블을 안전하게 쓰는 좋은 팁 두 개가 있다.

하나는 실제 서비스의 응답과 테스트 더블의 응답을 비교하는 Contract Test를 구현하는 것이다. 이 테스트는 가끔씩 실행해서 우리의 테스트 더블이 틀리지 않았는지만 확인해주면 된다.

나머지는 테스트 더블을 만들 때 실제로 왔던 응답을 가져다 쓰는 것이다. 이걸 self initializing fake 라고 부르는데, 말 그대로 첫 호출 때 실제 서비스의 응답을 저장해뒀다가 테스트할 땐 저장된 응답을 쓰는 거다. 매우 쉽고 당연하지만 좋은 방법이라 강조한다.

시간

테스트에서 시스템 시각을 그대로 사용해서 발생하는 문제다. 테스트 할 때마다 시각이 바뀌니 결과도 다를 수 밖에 없다. 그래서 해결책은 테스트 더블을 써서 고정된 시각을 받아오도록 테스트를 수정하면 해결된다.

만약 테스트에서 쓴 시각이 너무 오래 전에 정한거라 다른 시간적 요인이랑 충돌해서 실패할 수 있다. 이럴 때는 시각을 수정해도 된다. 단, 테스트 내에서 수정된 통제변인이 ‘시각’ 하나만 있어야 한다. 여러 통제변인이 변한다면 이 테스트가 실패하는 원인이 시각 때문인지 다른 변인 때문인지 알 수 없다.

정말 시스템 시간을 그대로 썼다가 프로그램에 문제가 생긴 걸 많이 봤다. 그러니 빌드 전에 시간을 직접 호출하는 부분이 있는지 코드 분석을 다시 해보자.

자원 누수

프로그램에서 자원 누수가 생기면 테스트가 임의로 실패할 수 있다. 만약 테스트가 비결정적 테스트가 아닌게 확실한데도 테스트가 실패한다면 자원 누수를 확인해보는 것도 좋다.

이런 누수는 보통 C++같이 메모리 관리가 없는 환경이거나 DB 커넥션이나 소켓 같은 걸 안 닫아서 생긴다. 그래서 이런 자원을 무분별하게 늘리고 있는지 확인하려면 크기가 1인 Resource pool을 통해 받도록 하면 된다. 그러면 받은 자원을 해제하지도 않았는데 추가로 받으려는 순간 바로 알 수 있어서 문제를 찾기 쉽다.

이 아이디어는 크게 보면 제한을 세게 줘서 오류가 발생하기 쉽게 하는 것이다. 그래서 이 원리를 쓰면 테스트 단계에서 오류를 막을 수 있다. 그래서 다른 예를 소개하자면, 임의의 파일명으로 임시 파일을 만드는 프로그램에서 누수를 빨리 발견한 사례가 있다. 이 때는 단일 파일명으로 임시 파일을 생성하도록 수정해서 테스트 하는 방법을 통해 이 원리를 활용했다.

프로그래밍방법론, 학습노트

읽기 좋은 코드가 좋은 코드다 – 2

주석으로 담아야 될 대상

주석은 코드를 읽는 사람이 작성한 사람만큼의 이해도를 갖출 수 있게 도와주는 역할

코드를 작성할 때의 의도를 기록하자. 코드에서 해결되지 않은 결함, 어떤 상수가 결정된 이유 같은 것들.

타인이 물어볼 만한 질문의 답 적기, 코드를 사용할 때의 유의점 적기

주석도 결국 글이다. 처음부터 완벽하지 않아도 됨. 점점 다듬어 나가면 좋은 주석이 됨.

명확하고 간결한 주석

  • 엉터리 문장 다듬기
  • 코드의 의도 명시하기
  • 함수 파라미터에 적어두는 주석

루프와 논리를 단순화하기

읽기 쉽게 흐름 제어 만들기

  • 조건문에서 인수의 순서를 생각하기
  • 요다 표기법 쓰지 않기 ex) if (NULL == obj)
  • if/else 블록의 순서
    • 긍정을 선호하자
    • 간단한 블록을 먼저 쓰자
    • 더 주의를 기울여야 하는 것을 먼저 다루어라
  • 중첩 최소화하기

거대한 표현을 잘게 쪼개기

  • 복잡한 값, 동작을 변수, 함수, 매크로 등으로 묶어주자
  • 재사용성도 가독성도 좋아진다

변수와 가독성

  • 임시 변수 제거하기
  • 변수의 범위를 좁혀라
  • 변수의 선언을 사용 직전에 해라
  • 값을 한 번만 할당하는 변수를 선호해라

코드 재작성하기

상관 없는 하위문제 추출하기

  1. 상위 수준에서 본 이 코드의 목적은 무엇인가
  2. 이 코드는 목적과 직접적으로 상관없는 하위 문제를 해결하고 있는가?
  3. 2번에 해당하는 코드가 길면, 별도의 함수로 추출하라
  4. 지나치게 추출하는 것도 좋지는 않다

한 번에 하나씩

코드가 수행하는 모든 ‘작업’을 나열한다

이러한 작업을 분리하여 서로 다른 함수로 만들어라, 아니면 적어도 논리적으로 구분이 가능하게 수정해라

생각을 코드로 만들기

코드를 더 명확하게 만드는 과정

  1. 코드가 할 일을 동료에게 말하듯 자연어로 묘사하기
  2. 이 설명에 들어가는 핵심적인 단어 포착
  3. 설명과 부합하는 코드 작성

코드 분량 줄이기

  • 요구 사항을 잘 분석하면 최대한 구체적인 문제를 정의할 수 있어서 코드 분량을 줄일 수 있다.
  • 코드베이스를 작게 유지하기
  • 라이브러리와 친숙해져라

테스트와 가독성

  • 가독성, 유지보수 하기 좋은 테스트를 만들어라
  • 하나의 테스트 케이스를 추가하는데 필요한 코드가 짧은 것이 이상적이다.
  • 의미 있는 assert 메세지 만들기
  • 테스트 입력값도 의미가 있어야 한다. 큰
    음수를 정의할거면 integer.neg_inf 같은걸 쓰지 -1000 같은 이상한 수 쓰지 말기
  • 테스트 함수에 이름은 자세하게
  • 지나친 테스트는 실제 코드의 가독성과 개발 비용에 영향을 줌.
  • 테스트가 프로젝트에서 얼마나 중요한지 생각해보기(우주선 장비 개발이면 지나쳐도 당연함)