TDD는 소프트웨어 개발 방법론 중 하나로, 개발자가 코드를 작성하기 전에 해당 코드의 테스트 케이스를 먼저 작성하게 한 후 해당 테스트를 통과할 수 있는 코드를 작성하는 방식으로 소프트웨어를 개발합니다. 여기서 파생한 개념으로 BDD가 있습니다. BDD는 소프트웨어 개발 과정에서 비즈니스 요구사항과 소프트웨어의 행동을 강조하는 개발 방법론입니다. 개발자는 비즈니스 의도를 명확하게 이해해야 하고, 스펙을 테스트 가능한 형태로 작성할 수 있어야 합니다. 개발팀과 비즈니스팀 간의 빈번한 의사소통 강조, 테스트 케이스 명세 시 Given-When-Then 같은 자연어로 구성된 시나리오를 사용하는 것을 권장합니다.
TDD, BDD는 코드의 안정성과 유연성을 높여 소프트웨어의 품질을 향상할 수 있는 가장 현대적인 개발 방법론입니다. 이번 장에서는 짧게 알아보죠.
17.1 TDD
TDD를 이용하기로 한 경우, 개발자는 Red, Green, Refactor라고 하는 세 단계를 거쳐 소프트웨어를 개발하게 됩니다.
1) Red : 아직 구현되지 않은 기능을 테스트하는 케이스를 작성합니다. 이 시점에서 테스트는 실패합니다. 왜냐하면 아직 해당 기능이 구현돼 있지 않기 때문이죠. 그래서 이 단계는 테스트가 실패한다는 의미를 담아 Red 단계라고 부릅니다.
2) Green : '최소한'의 코드 작성으로 테스트가 성공하게 만드는 것입니다. 코드 품질에 신경 쓰지 않고, 요구사항에 맞는 최소한의 기능을 개발해서 테스트를 통과시키는 데만 집중합니다.
3) Refactor : Green 단계에서 리팩터링합니다. Blue 단계라고 부르기도 하는데, 코드의 가독성과 유지보수성, 성능을 높이는 데 집중합니다. 리팩터링으로 인해 기능의 동작 방식이 변경돼서는 안 됩니다. 리팩터링은 '기능은 그대로 유지된 상태에서 코드의 구조만 변경하는 작업'이기 때문이죠. 그래서 테스트가 보장된 상태에서 이뤄져야 합니다.
여기서 Green 단계의 이유를 알 수 있습니다. 코드 변경으로 기능의 동작 방식이 변하지는 않았는지 확인하는 데 Green 단계에서 통과시킨 테스트를 사용하기 위함입니다. Refactor 단계에서는 Green 단계에서 만든 테스트 코드를 이용해 코드를 변경하고, 테스트를 실행하는 과정을 계속 반복합니다. TDD에서는 Green 단계를 따로 만들어 버그를 감지할 수 있는 테스트를 최대한 빨리 확보하는 데 집중합니다.
두 개의 모자 -> 개발자는 기능을 추가할 때, 리팩터링할 때 다른 유형의 뇌를 사용하죠. 요구되는 역량이 다르다는 의미고, 개발할 때 한번에 하나의 모자만 선택해 개발해야 한다고 켄트 백이 말한 내용이 있습니다. TDD에서 위 3가지 단계를 반복하며, '테스트를 먼저 작성한다', '기능을 구현한다', '리팩터링한다'가 TDD 전부입니다. 테스트를 중요시하는 개발 방법론이기 때문에 테스트의 장점이 곧 TDD의 장점이죠.
단점으로는 'TDD를 적용하는 것이 어렵다'라는 점입니다. 즉, TDD를 적용하기 이전에 모든 팀원이 테스트를 잘 작성할 수 있어야 한다는 뜻입니다. TDD 찬양론자의 말로 개발 속도가 빨라지는 이유를 설명하는데, 아래와 같습니다.
1) TDD를 이용하면 디버깅 시간이 단축됩니다.
-> TDD의 피드백 루프는 굉장히 짧고 빠릅니다. 긴 개발을 마치고 나서야 문제가 생긴 것을 파악할 일이 생기지 않습니다. 테스트가 실패한 상황에 케이스를 읽으며 바로 확인할 수 있고 문제를 재현하기도 쉽습니다.
2) 테스트가 있는 프로젝트에서 개발자는 코드 변경에 주저하지 않게 됩니다.
-> 코드 변경이 시스템에 부정적인 영향을 미치지 않았는지 바로 확인할 수 있고, 개발자가 자신감을 가지고 코드를 수정할 수 있게 된다는 것입니다.
3) 테스트는 코드의 문서 역할을 합니다.
-> 테스트는 각 기능이 어떻게 동작하는지 명확하게 설명합니다. 테스트를 통해 다른 팀과 소통이 가능하고, 덕분에 시스템 확장이 병렬로 이뤄질 수 있습니다.
이러한 이유로 프로젝트에서 공격적으로 기능을 확장할 수 있습니다. TDD 방법론을 적용하면 소프트웨어의 개발 속도는 장기적으로 그렇지 않은 소프트웨어보다 빠르죠. 하지만 조금 이상한 점으로 TDD의 효용가치가 전통적인 개발 방법론을 넘어서려면 A 지점을 넘어야 합니다. 요구사항이 명확하지 않은 상황에서 TDD는 오히려 독이 됩니다. TDD가 코드의 수정, 확장하기에 좋은 방법론은 맞지만 요구사항도 어느 정도 정해진 상태여야 한다는 것입니다. 초기 도메인 문제가 정확히 정해지지 않은 상황에서는 여구사항이나 인터페이스가 극단적으로 변경될 수도 있습니다.
17.2 BDD
BDD는 TDD에 '사용자 행동'이라는 가치를 덧붙이고 이를 강조합니다. 그래서 사용자 행동을 '행동 명세' 같은 요구사항으로 먼저 만듭니다. 그리고 이것이 테스트로 표현될 수 있게 만듭니다. 즉, 테스트가 요구사항 문서이자 기획 문서가 될 수 있게 만드는 것입니다. 테스트의 단위가 사용자의 행동이며, 이에 맞춰 애플리케이션을 설계할 것을 강조합니다.
TDD의 이론적 배경은 완벽에 가까워 보이지만 시스템 설계를 하기에는 부족합니다. TDD가 무엇을, 어떻게 테스트해야 하는지 설명하지 않기 때문이죠. 기술적인 측면의 검증에만 집중하는 테스트가 만들어지고 테스트 작성자만 아는 형태의 테스트가 만들어지죠.
그럼 BDD란 뭘까요? 우선 TDD의 특징을 이해해야 합니다. TDD를 따라 만들어진 코드는 객체지향적일까요? 아닙니다. 테스트를 먼저 작성하고 구현체를 만드는 게 TDD이기 때문이죠. 오해하면 안되는 것이 모든 프로그램이 객체지향으로 만들어져야 할 필요는 없습니다. 오히려 모든 프로젝트에 사용할 만큼 범용적인 이론이라 평가하는 것이 맞죠. 이것이 TDD의 한계인데, TDD를 하며 객체지향을 보장하는 방법이 없을까요?
이 지점에서 DDD가 등장합니다. DDD는 객체지향 설계 이론이고, TDD는 설계 이론입니다. 장점만 모아서 합쳐본다. 즉, 도메인 분석 단계에서 사용자 위주의 스토리를 만들고(DDD), 이를 바탕으로 테스트 코드를 작성해보는 것(TDD)입니다. 이 설명이 모두 BDD의 모든 것은 담고 있죠.
TDD와 DDD는 상호보완적입니다. TDD는 기능을 테스트하고 구축함으로써 안정성과 유연성을 확보하는데 중점을 두었고, DDD는 도메인 모델을 중심으로 비즈니스 요구사항을 이해하고 설계하는 데 초점을 두어 객체지향을 추구할 수 있는 반면, 소프트웨어의 안정성을 확보하기 위한 시스템적인 해결책은 아닙니다. 그래서 이 둘은 찰떡입니다.
이러한 배경에서 BDD가 탄생했습니다. TDD에 DDD를 끼얹은 것이 BDD입니다. 개발자와 비개발자 협업을 강조, 테스트 코드를 문서로써 비개발자들이 열람할 수 있게 하고 유비쿼터스 언어를 만들어 의사소통에 문제가 없도록 해야 한다고 말하죠. 또한, 이를 통해 팀 전체가 공통된 언어와 이해를 토대로 프로젝트를 진행할 수 있어야 한다고 말합니다. 추가로 공통된 언어를 바탕으로 요구사항 문서가 사용자 스토리 기반으로 작성돼야 한다는 것을 강조합니다. 여기서 '사용자 스토리 기반의 요구사항'이란 요구 사항 문서를 행동 명세에 맞게 작성하는 것을 의미하죠.
행동 명세는 '어떤 사용자가 어떤 상황에서(given), 어떤 행동을 할 때(when), 그러면(then) 어떤 일이 발생한다'를 기술합니다. 이는 요구사항을 뚜렷하게 명시하면서도 테스트 코드로 작성하기가 편하다는 점이죠. 결과적으로 행동 명세를 작성하면서 개발자와 비개발자의 협업 결과물로 나오는 요구사항은 요구사항 명세 같은 기획 문서가 아닌 테스트까지 연결될 수 있습니다. 정리하면 크게 4가지를 강조합니다.
1) 개발자 비개발자 사이의 협업
2) 행동 명세(사용자 스토리 기반의 요구사항) 작성
3) 행동 명세의 테스트화
4) 테스트의 문서화
이러한 흐름을 통해 개발자가 작성하는 테스트는 사용자의 행동과 행동에 따른 결과를 검증하는 테스트가 됩니다. 즉, 테스트가 알고리즘을 테스트하는 것이 아닌, 행동과 역할을 검증하도록 변하는 것입니다. 이러한 테스트를 기반으로 설계가 뒤따라올 때 결과물은 객체지향에 좀 더 가까워질 수 있습니다.
실무적인 관점에서는 근래에는 BDD가 '사용자 위주의 시나리오 검증'이라는 원래 BDD의 철학과 다르게 '테스트 대상의 행동에 따른 상태 검증'이라는 방향으로 더 활용되는 경향이 강합니다. 참고하시면 좋겠습니다.
TDD는 소프트웨어 개발 방법론 중 하나로, 개발자가 코드를 작성하기 전에 해당 코드의 테스트 케이스를 먼저 작성하게 한 후 해당 테스트를 통과할 수 있는 코드를 작성하는 방식으로 소프트웨어를 개발합니다. 여기서 파생한 개념으로 BDD가 있습니다. BDD는 소프트웨어 개발 과정에서 비즈니스 요구사항과 소프트웨어의 행동을 강조하는 개발 방법론입니다. 개발자는 비즈니스 의도를 명확하게 이해해야 하고, 스펙을 테스트 가능한 형태로 작성할 수 있어야 합니다. 개발팀과 비즈니스팀 간의 빈번한 의사소통 강조, 테스트 케이스 명세 시 Given-When-Then 같은 자연어로 구성된 시나리오를 사용하는 것을 권장합니다.
TDD, BDD는 코드의 안정성과 유연성을 높여 소프트웨어의 품질을 향상시킬 수 있는 가장 현대적인 개발 방법론입니다. 이번 장에서는 짧게 알아보죠.
17.1 TDD
TDD를 이용하기로 한 경우, 개발자는 Red, Green, Refactor라고 하는 세 단계를 거쳐 소프트웨어를 개발하게 됩니다.
1) Red : 아직 구현되지 않은 기능을 테스트하는 케이스를 작성합니다. 이 시점에서 테스트는 실패합니다. 왜냐하면 아직 해당 기능이 구현돼 있지 않기 때문이죠. 그래서 이 단계는 테스트가 실패한다는 의미를 담아 Red 단계라고 부릅니다.
2) Green : '최소한'의 코드 작성으로 테스트가 성공하게 만드는 것입니다. 코드 품질에 신경 쓰지 않고, 요구사항에 맞는 최소한의 기능을 개발해서 테스트를 통과시키는 데만 집중합니다.
3) Refactor : Green 단계에서 리팩터링합니다. Blue 단계라고 부르기도 하는데, 코드의 가독성과 유지보수성, 성능을 높이는 데 집중합니다. 리팩터링으로 인해 기능의 동작 방식이 변경돼서는 안됩니다. 리팩터링은 '기능은 그대로 유지된 상태에서 코드의 구조만 변경하는 작업'이기 때문이죠. 그래서 테스트가 보장된 상태에서 이뤄져야 합니다.
여기서 Green 단계의 이유를 알 수 있습니다. 코드 변경으로 기능의 동작 방식이 변하지는 않았는지 확인하는 데 Green 단계에서 통과시킨 테스트를 사용하기 위함입니다. Refactor 단계에서는 Green 단계에서 만든 테스트 코드를 이용해 코드를 변경하고, 테스트를 실행하는 과정을 계속 반복합니다. TDD에서는 Green 단계를 따로 만들어 버그를 감지할 수 있는 테스트를 최대한 빨리 확보하는 데 집중합니다.
두 개의 모자 -> 개발자는 기능을 추가할 때, 리팩터링할 때 다른 유형의 뇌를 사용하죠. 요구되는 역량이 다르다는 의미고, 개발할 때 한번에 하나의 모자만 선택해 개발해야 한다고 켄트 백이 말한 내용이 있습니다. TDD에서 위 3가지 단계를 반복하며, '테스트를 먼저 작성한다', '기능을 구현한다', '리팩터링한다'가 TDD 전부입니다. 테스트를 중요시하는 개발 방법론이기 때문에 테스트의 장점이 곧 TDD의 장점이죠.
단점으로는 'TDD를 적용하는 것이 어렵다'라는 점입니다. 즉, TDD를 적용하기 이전에 모든 팀원이 테스트를 잘 작성할 수 있어야 한다는 뜻입니다. TDD 찬양론자의 말로 개발 속도가 빨라지는 이유를 설명하는데, 아래와 같습니다.
1) TDD를 이용하면 디버깅 시간이 단축됩니다.
-> TDD의 피드백 루프는 굉장히 짧고 빠릅니다. 긴 개발을 마치고 나서야 문제가 생긴 것을 파악할 일이 생기지 않습니다. 테스트가 실패한 상황에 케이스를 읽으며 바로 확인할 수 있고 문제를 재현하기도 쉽습니다.
2) 테스트가 있는 프로젝트에서 개발자는 코드 변경에 주저하지 않게 됩니다.
-> 코드 변경이 시스템에 부정적인 영향을 미치지 않았는지 바로 확인할 수 있고, 개발자가 자신감을 가지고 코드를 수정할 수 있게 된다는 것입니다.
3) 테스트는 코드의 문서 역할을 합니다.
-> 테스트는 각 기능이 어떻게 동작하는지 명확하게 설명합니다. 테스트를 통해 다른 팀과 소통이 가능하고, 덕분에 시스템 확장이 병렬로 이뤄질 수 있습니다.
이러한 이유로 프로젝트에서 공격적으로 기능을 확장할 수 있습니다. TDD 방법론을 적용하면 소프트웨어의 개발 속도는 장기적으로 그렇지 않은 소프트웨어보다 빠르죠. 하지만 조금 이상한 점으로 TDD의 효용가치가 전통적인 개발 방법론을 넘어서려면 A 지점을 넘어야 합니다. 요구사항이 명확하지 않은 상황에서 TDD는 오히려 독이 됩니다. TDD가 코드의 수정, 확장하기에 좋은 방법론은 맞지만 요구사항도 어느 정도 정해진 상태여야 한다는 것입니다. 초기 도메인 문제가 정확히 정해지지 않은 상황에서는 여구사항이나 인터페이스가 극단적으로 변경될 수도 있습니다.
마치며
소프트웨어 개발은 매력적입니다. 생산적인 활동을 하면서 생기는 고민과 해결책을 공유해 업계 전체가 더 나은 방향으로 나아가고 있으니까요. 이 독특한 문화는 '소프트웨어 장인정신'이라는 철학에서 나온 것입니다.
1부에서는 객체지향 프로그래밍의 핵심 원칙으로 출발해서 시스템에 객체지향 원칙을 반영하기 위해 어떤 것에 집중해야 하는지 알아봤습니다. SOLID 이야기를 하며, 의존성의 중요성을 학습하고 순환 참조를 경계해야 한다는 것을 배웠습니다.
2부에서는 스프링을 사용할 때 많이 발생하는 안티패턴을 살펴보았죠. 그리고 더 나은 아키텍처를 적용하는 방법에 관해 얘기했으며, 특히 의존성 역전을 아키텍처에 어떻게 녹여야 하는지 이야기했습니다.
3부에서는 테스트에 관해 학습했습니다. 회귀 버그와 테스트 대역 같은 개념을 살펴보고, 테스트 가능성이 무엇인지 이해하고 테스트와 설계가 어떤 상관관계를 갖는지 알아봤죠.
시스템을 개발하는 이유는 사용자에게 가치를 전달하기 위함입니다. '설계는 완벽하지만 돌아가지 않는다' 같은 평가가 나와서는 안됩니다. 설계를 위해 소프트웨어를 개발해서는 안된다는 말입니다. 아키텍처와 테스트 코드를 고민하는 것은 이러한 기본 전제가 성립된 상태에서 이뤄져야 합니다.
부록A - 포트 어댑터 패턴
의존성 역전 원칙은 포트-어댑터 패턴이라 부르기도 합니다. 엄밀히 말해 다른 개념이지만, 두 개념이 만들어진 이유와 결과가 상당히 비슷하니 그렇게 부를 수 있다는 말이죠.
각 객체를 가리키는 명칭만 다를 뿐입니다. 포트 어댑터 패턴은 두 시스템이 상호 작용할 때 가운데 추상(인터페이스)을 두고 통신하도록 만들어 시스템 간의 종속을 피하도록 만드는 것을 의미합니다. Port는 시스템 가운데에 존재하는 인터페이스를 나타내며, Adapter는 포트에 의존하는 객체들을 의미합니다.
포트-어댑터 패턴에서 포트를 사용하는 방향에 따라 이를 구분해서 부르는데 하나는 '출력 포트-출력 어댑터', 다른 하나는 '입력 포트-입력 어댑터'입니다.
A.1 출력 포트-출력 어댑터
포트를 출력 포트라 부르고 포트를 구현하는 구현체는 출력 어댑터라고 부를 수 있습니다. 출력이라는 용어가 붙은 이유는 실행 객체 입장에서 포트에 메시지를 전달하는 행위가 어떤 새로운 결과를 출력하기 위함이기 때문이죠. 그래서 포트는 결과를 만드는 '출력 포트'로 볼 수 있고, 어댑터는 출력 포트를 구현한 '출력 어댑터'로 볼 수 있습니다.
A.2 입력 포트-입력 어댑터
포트는 외부에 포트의 구현체들을 사용하는 방법을 알려주는 규격입니다. 실행 객체 입장에서 포트는 결과를 만들기 위한 입력 포트입니다. 모든 입력은 포트를 통해 이뤄지기 때문입니다. 그래서 이는 '입력 포트-입력 어댑터' 관계라 볼 수 있습니다. 구현체 입장에서 입력 포트를 통해 메시지를 전달하는 객체는 마치 출력 포트와 출력 어댑터 관계에서 그랬던 것처럼 시시각각 변하는 어댑터라 볼 수 있습니다. 마찬가지로 어댑터의 변경이 자유롭기 때문이죠.
A.3 정리
포트 어댑터 패턴은 내부 시스템이나 컴포넌트가 외부 시스템과 통신하는 방식을 추상화합니다. 외부 시스템이 변경돼도 내부 시스템에 영향을 주지 않게 만듭니다. 이 목적은 의존성 역전 원칙과 같습니다. 참고로 포트-어댑터 패턴은 시스템 아키텍처 수준에서 외부 시스템과의 인터페이스를 추상화하는 데 사용됩니다. 디자인 패턴의 어댑터 패턴은 클래스나 객체간이 인터페이스 불일치를 해결하는 데 사용됩니다. 즉, 어댑터 패턴은 시스템에 새로운 라이브러리나 클래스가 도입될 때 기존 코드를 변경하지 않고 새로운 클래스를 통합하는 목적으로 사용됩니다.
포트 어댑터 != 어댑터 패턴 != 의존성 역전
부록B - 클린 아키텍처
클린 아키텍처를 구성하는 요소는 아래와 같습니다.
1. Entities : 시스템의 비즈니스 로직과 객체가 정의됩니다. 시스템의 핵심이므로 외부에 영향을 받지 않아야 하고, 가장 변하지 않는 영역.
2. Use cases : 비즈니스 흐름과 엔티티 간의 상호작용을 관리합니다. 사용자의 요청을 처리하고, 그에 따라 엔티티를 조작합니다.
3. Interface adapters : 외부 인터페이스와 Use cases를 연결하는 역할을 합니다. 외부의 요청을 내부 형식으로 변환하거나 내부의 데이터를 외부 형식으로 변환합니다.
4. Frameworks & Drivers : 시스템의 외부와 상호 작용합니다. 주로 사용자와 상호 작용하거나 데이터베이스나 외부 서비스 등과 통신하는 역할을 담당합니다.
클린 아키텍처는 위와 같은 기준으로 시스템의 경계를 나누고, 이 경계의 방향이 항상 단방향이 될 것을 강조합니다. 더불어 의존 방향이 항상 바깥 원에서 안쪽 원을 향하도록 강조합니다. 내부의 원이 외부의 원에 관해 알아서는 안 된다는 말입니다. 영역별 관심사 분리라는 목적을 달성합니다.
특이한 점은 클린 아키텍처에서는 실제 개발 단계에서 구채적으로 어떤 식으로 개발하는지 설명하지 않습니다. 개념적인 해결책만 제시할 뿐이죠. 실전 아키텍처로는 헥사고날과 양파 아키텍처 같은 것이 있습니다. 즉, 클린 아키텍처가 추상이라면 헥사고날 아키텍처와 양파 아키텍처는 구현체라고 볼 수 있습니다.
부록C - 소프트웨어 엔지니어
'소프트웨어 엔지니어'에 관한 주제로 프로그램의 기대 수명을 이야기하는 이유는 '프로그램의 요구사항', '개발 난이도', '요구사항의 복잡도 대비 얻을 수 있는 비즈니스 가치'가 무엇이냐에 따라 투자해야 하는 시간이 기대 수명에 따라 다릅니다. 시간이라는 변수는 시스템을 지속하게 어렵게 만드는 요인 중 하나입니다. 때문에 외부적인 변화에도 대응할 수 있어야 합니다. 구글이 생각하는 소프트웨어 엔지니어는 프로그래머이면서 동시에 '시간', '확장', '트레이드오프'라는 3가지 개념까지 생각하는 사람입니다. 장기적인 안목으로 바라보는 것이죠.
부록D - 실용주의
소프트웨어는 비즈니스 문제를 해결할 수 있어야 합니다. 사용자에게 어떤 비즈니스 가치를 전달할 수 있어야 합니다. 개발자의 목표는 소프트웨어를 만들어서 사용자에게 가치를 전달하는 것입니다. 우리의 목표가 '소프트웨어 개발'이 아니라 '비즈니스 가치 전달'이라는 사실을 기억해야 합니다.
'독서 > 자바,스프링 개발자를 위한 실용주의 프로그래밍' 카테고리의 다른 글
16장 테스트와 설계 (0) | 2024.11.19 |
---|---|
15. 테스트 가능성 (0) | 2024.11.18 |
14. 테스트 대역 (0) | 2024.11.17 |
[12-17 하] 12. 자동 테스트 & 13. 테스트 피라미드 (2) | 2024.11.15 |
10. 도메인 & 11. 알아두면 유용한 스프링 활용법 (2) | 2024.11.13 |