티스토리 뷰
1. 단위 테스트의 목표
단위 테스트를 배우는 것은 테스트 프레임워크나 목 라이브러리 등과 같은 기술적인 부분을 익히는 것에 그치지 않는다. 단위 테스트는 단순히 테스트를 작성하는 것보다 더 큰 범주다. 단위 테스트에 시간을 투자할 때는 항상 최대한 이득을 얻도록 노력해야 하며, 테스트에 드는 노력을 가능한 한 줄이고 그에 따르는 이득을 최대화해야 한다. 두 가지를 모두 달성하기란 쉬운 일이 아니다.
[ 1.2 단위 테스트의 목표 ]
그럼 단위 테스트의 목표는 무엇인가? 소프트웨어 프로젝트의 지속 가능한 성장을 가능하게 하는 것이다.
테스트는 안전망 역할을 하며, 대부분의 회귀에 대한 보험을 제공하는 도구라 할 수 있다.
지속성과 확장성이 핵심이며, 이를 통해 장기적으로 개발 속도를 유지할 수 있다.
1.2.1 좋은 테스트와 좋지 않은 테스트를 가르는 요인
모든 테스트를 작성할 필요는 없다. 일부 테스트는 아주 중요하고 소프트웨어 품질에 매우 많은 기여를 한다. 그 밖에 다른 테스트는 그렇지 않다. 잘못된 경고가 발생하고, 회귀 오류를 알아내는 데 도움이 되지 않으며, 유지 보수가 어렵고 느리다.
테스트의 가치와 유지 비용 모두를 고려해야 한다. 비용 요소는 다양한 활동에 필요한 시간에 따라 결정된다.
- 기반 코드를 리팩터링할 때 테스트도 리팩터링하라
- 각 코드 변경 시 테스트를 실행하라
- 테스트가 잘못된 경고를 발생시킬 경우 처리하라
- 기반 코드가 어떻게 동작하는지 이해하려고 할 때는 테스트를 읽는 데 시간을 투자하라
지속 가능한 프로젝트 성장을 위해서는 고품질 테스트에만 집중해야 한다.
[ 1.3 테스트 스위트 품질 측정을 위한 커버리지 지표 ]
1.3.1 코드 커버리지 지표에 대한 이해
코드 커버리지가 너무 적을 때는(예를 들면, 10%) 테스트가 충분치 않다는 좋은 증거다. 그러나 반대의 경우는 그렇지 못하다. 100% 커버리지라고 해서 반드시 양질의 테스트 스위트라고 보장하지는 않는다. 높은 커버리지의 테스트 스위트도 품질이 떨어질 수 있다.
[ 1.4 무엇이 성공적인 테스트 스위트를 만드는가? ]
믿을 만한 방법은 스위트 내 각 테스트를 하나씩 따로 평가하는 것뿐이다.
성공적인 테스트 스위트는 다음과 같은 특성을 갖고 있다.
- 개발 주기와 통합돼 있다
- 코드베이스에서 가장 중요한 부분만을 대상으로 한다.
- 최소한의 유지비로 최대의 가치를 끌어낸다.
1.4.2 코드베이스에서 가장 중요한 부분만을 대상으로 함
시스템의 가장 중요한 부분에 단위 테스트 노력을 기울이고, 다른 부분은 간략하게 또는 간접적으로 검증하는 것이 좋다. 대부분의 애플리케이션에서 가장 중요한 부분은 비즈니스 로직(도메인 모델)이 있는 부분이다. 비즈니스 로직 테스트가 시간 투자 대비 최고의 수익을 낼 수 있다.
1.4.3 최소한 유지비로 최대 가치를 끌어냄
가치가 유지비를 상회하는 테스트만 스위트에 유지하는 것이 중요하다.
가치가 높은 테스트를 식별하려면 기준틀(frame of reference)이 필요하다.
2. 단위 테스트란 무엇인가?
[ 2.1 ‘단위 테스트’의 정의 ]
단위 테스트는
- 작은 코드 조각(단위라고도 함)을 검증하고,
- 빠르게 수행하고,
- 격리된 방식으로 처리하는 자동화된 테스트다.
[ 2.2 단위 테스트의 런던파와 고전파 ]
런던파는 테스트 대상 시스템에서 협력자를 격리하는 것으로 보는 반면, 고전파는 단위 테스트끼리 격리하는 것으로 본다.
종합하면 세 가지 주요 주제에 대해 의견 차이가 있다.
격리 주체 단위의 크기 테스트 대역 사용 대상
런던파 | 단위 | 단일 클래스 | 불변 의존성 외 모든 의존성 |
고전파 | 단위 테스트 | 단일 클래스 또는 클래스 세트 | 공유 의존성 |
[ 2.3 고전파와 런던파의 비교 ]
2.3.1 한 번에 한 클래스만 테스트하기
런던파는 클래스를 단위로 간주한다.
이로 인해 자연스럽게 클래스를 테스트에서 검증할 원자 단위로도 취급하게 된다. 이런 경향은 이해되기는 하지만 오해의 소지가 있다.
===== 팁 =====
테스트는 코드의 단위를 검증해서는 안 된다. 오히려 동작의 단위, 즉 문제 영역에 의미가 있는 것, 이상적으로는 비즈니스 담당자가 유용하다고 인식할 수 있는 것을 검증해야 한다. 동작 단위를 구현하는 데 클래스가 얼마나 필요한지는 상관 없다. 단위는 여러 클래스에 걸쳐 있거나 한 클래스에만 있을 수 있고, 심지어 아주 작은 메서드가 될 수도 있다.
테스트가 단일 동작 단위를 검증하는 한 좋은 테스트다. 이보다 적은 것을 목표로 삼는다면 사실 단위 테스트를 훼손하는 결과를 가져온다. 이 테스트가 무엇을 검증하는지 정확히 이해하기가 더 어려워지기 때문이다. 테스트는 해결하는 데 도움이 되는 문제에 대한 이야기를 들려줘야 하며, 이 이야기는 프로그래머가 아닌 일반 사람들에게 응집도가 높고 의미가 있어야 한다.
2.3.2 상호 연결된 클래스의 큰 그래프를 단위 테스트하기
테스트 대역을 쓰면 클래스의 직접적인 의존성을 대체해 그래프를 나눌 수 있으며, 아는 단위 테스트에서 준비해야 할 작업량을 크게 줄일 수 있다. 고전파를 따라 테스트 대상 시스템을 설정하려면 전체 객체 그래프를 다시 생성해야 하는데, 작업이 많을 수 있다.
모두 사실이지만, 이 추리 과정은 잘못된 문제에 초점을 맞추고 있다. 상호 연결된 클래스의 크고 복잡한 그래프를 테스트할 방법을 찾는 대신, 먼저 이러한 클래스 그래프를 갖지 않는 데 집중해야 한다. 대개 클래스 그래프가 커진 것은 코드 설계 문제의 결과다.
2.3.3 버그 위치 정확히 찾아내기
런던 스타일 테스트가 있는 시스템에 버그가 생기면, 보통 SUT에 버그가 포함된 테스트만 실패한다. 하지만 고전적인 방식이면, 오작동하는 클래스를 참조하는 클라이언트를 대상으로 하는 테스트도 실패할 수 있다. 즉, 하나의 버그가 전체 시스템에 걸쳐 테스트 실패를 야기하는 파급 효과를 초래한다. 결국 문제의 원인을 찾기가 더 어려워진다.
우려할 만하지만, 큰 문제는 아니다. 테스트를 정기적으로 실행하면 버그의 원인을 알아낼 수 있다. 즉, 마지막으로 한 수정이 무엇인지 알기 때문에 문제를 찾는 것은 그리 어렵지 않다. 또한 실패한 테스트를 모두 볼 필요는 없다. 하나를 고치면 다른 것들도 자동으로 고쳐진다.
버그가 테스트 하나뿐만 아니라 많은 테스트에서 결함으로 이어진다면, 방금 고장 낸 코드 조각이 큰 가치가 있다는 것을 보여준다.
[ 2.4 두 분파의 통합 테스트 ]
런던파는 실제 협력자 객체를 사용하는 모든 테스트를 통합 테스트로 간주한다. 고전 스타일로 작성된 대부분의 테스트는 런던파 지지자들에게 통합 테스트로 느껴질 것이다.
2.4.1 통합 테스트의 일부인 엔드 투 엔드 테스트
가끔 경계가 흐리지만, 일반적으로 통합 테스트는 프로세스 외부 의존성을 한두 개만 갖고 작동한다. 반면에 엔드 투 엔드 테스트는 프로세스 외부 의존성을 전부 또는 대다수 갖고 작동한다. 따라서 엔드 투 엔드라는 명칭은 모든 외부 애플리케이션을 포함해 시스템을 최종 사용자의 관점에서 검증하는 것을 의미한다.
엔드 투 엔드 테스트를 하더라도 모든 프로세스 외부 의존성을 처리하지 못할 수도 있다. 일부 의존성의 테스트 버전이 없거나 해당 의존성을 필요한 상태로 자동으로 가져오는 것이 불가능할 수 있다. 따라서 여전히 테스트 대역을 사용할 필요가 있고, 통합 테스트와 엔드 투 엔드 테스트 사이에 뚜렷한 경계가 없다는 사실을 강조한다.
3. 단위 테스트 구조
[ 3.1 단위 테스트를 구성하는 방법 ]
3.1.1 AAA 패턴 사용
AAA 패턴은 스위트 내 모든 테스트가 단순하고 균일한 구조를 갖는 데 도움이 된다. 이러한 일관성이 이 패턴의 가장 큰 장점 중 하나다. 일단 익숙해지면 모든 테스트를 쉽게 읽을 수 있고 이해할 수 있다. 결국 전체 테스트 스위트의 유지 보수 비용이 줄어든다.
===== Given-When-Then 패턴 =====
테스트 구성 측면에서 두 가지 패턴 사이에 차이는 없다. 유일한 차이점은 프로그래머가 아닌 사람에게 Given-When-Then 구조가 더 읽기 쉽다는 것이다. 그러므로 Given-When-Then은 비기술자들과 공유하는 테스트에 더 적합하다.
3.1.2 여러 개의 준비, 실행, 검증 구절 피하기
검증 구절로 구분된 여러 개의 실행 구절을 보면, 여러 개의 동작 단위를 검증하는 테스트를 뜻한다.
이러한 테스트 구조는 피하는 것이 좋다. 실행이 하나면 테스트가 단위 테스트 범주에 있게끔 보장하고, 간단하고, 빠르며, 이해하기 쉽다.
3.1.3 테스트 내 if 문 피하기
if 문은 테스트가 한 번에 너무 많은 것을 검증한다는 표시다.
3.1.5 검증 구절에는 검증문이 얼마나 있어야 하는가
단위 테스트의 단위는 동작의 단위이지 코드의 단위가 아니다. 단일 동ㅇ작 단위는 여러 결과를 낼 수 있으며, 하나의 테스트로 그 모든 결과를 평가하는 것이 좋다.
3.1.7 테스트 대상 시스템 구별하기
테스트 대상을 찾는 데 시간을 너무 많이 들일 필요가 없다. 그렇게 하기 위해 테스트 내 SUT 이름을 sut로 하라.
[ 3.3 테스트 간 테스트 픽스처 재사용 ]
테스트에서 언제 어떻게 코드를 재사용하는지 아는 것이 중요하다. 준비 구절에서 코드를 재사용하는 것이 테스트를 줄이면서 단순화하기 좋은 방법이고, 이 절에서는 올바른 방법을 알아본다.
테스트 픽스처를 재사용하는 첫 번째 방법은 다음과 같이 테스트 생성자에서 픽스처를 초기화하는 것이다.
그러나 이 기법은 두 가지 중요한 단점이 있다.
- 테스트 간 결합도가 높아진다.
- 테스트 가독성이 떨어진다.
3.3.3 더 나은 테스트 픽스처 재사용법
두 번째 방법은 다음 예제와 같이 테스트 클래스에 비공개 팩토리 메서드(private factory method)를 두는 것이다.
공통 초기화 코드를 비공개 팩토리 메서드로 추출해 테스트 코드를 짧게 하면서, 동시에 테스트 진행 상황에 대한 전체 맥락을 유지할 수 있다. 게다가 비공개 메서드를 충분히 일반화하는 한 테스트가 서로 결합되지 않는다. 테스트에 픽스처를 어떻게 생성할지 지정할 수 있다.
이는 매우 읽기 쉽고 재사용이 가능하다.
테스트 픽스처 재사용 규칙에 한 가지 예외가 있다. 테스트 전부 또는 대부분에 사용되는 생성자에 픽스처를 인스턴스화할 수 있다.
[ 3.4 단위 테스트 명명법 ]
3.4.1 단위 테스트 명명 지침
표현력 있고 읽기 쉬운 테스트 이름을 지으려면 다음 지침을 따르자.
- 엄격한 명명 정책을 따르지 않는다. 복잡한 동작에 대한 높은 수준의 설명을 이러한 정책의 좁은 상자 안에 넣을 수 없다. 표현의 자유를 허용하자.
- 문제 도메인에 익숙한 비개발자들에게 시나리오를 설명하는 것처럼 테스트 이름을 짓자. 도메인 전문가나 비즈니스 분석가가 좋은 예다.
- 단어를 밑줄 표시(_, underscore)로 구분한다. 그러면 특히 긴 이름에서 가독성을 향상시키는 데 도움이 된다.
4. 좋은 단위 테스트의 4대 요소
[ 4.1 좋은 단위 테스트의 4대 요소 자세히 살펴보기 ]
좋은 단위 테스트에는 다음 네 가지 특성이 있다.
- 회귀 방지
- 리팩터링 내성
- 빠른 피드백
- 유지 보수성
4.1.1 첫 번째 요소: 회귀 방지
회귀는 소프트웨어 버그다. 코드를 수정한 후 기능이 의도한 대로 작동하지 않는 경우다.
최악인 것은 개발한 기능이 많을수록, 새로운 릴리스에서 기능이 하나라도 고장 날 가능성이 높다는 점이다. 프로그래밍을 하는 삶에 있어 불행한 사실은 코드가 자산이 아니라 책임이라는 점이다. 코드베이스가 커질수록 잠재적인 버그에 더 많이 노출된다. 그렇기 때문에 회귀에 대해 효과적인 보호를 개발하는 것이 중요하다. 이러한 보호가 없다면 프로젝트가 오랫동안 성장할 수 없으며 점점 더 많은 버그가 쌓일 것이다.
회귀 방지 지표에 대한 테스트 점수가 얼마나 잘 나오는지 평가하려면 다음 사항을 고려해야 한다.
- 테스트 중에 실행되는 코드의 양
- 코드 복잡도
- 코드의 도메인 유의성
일반적으로 실행되는 코드가 많을수록 테스트에서 회귀가 나타날 가능성이 높다.
코드의 양뿐만 아니라 복잡도와 도메인 유의성도 중요하다. 복잡한 비즈니스 로직을 나타내는 코드가 보일러플레이트 코드보다 훨씬 더 중요하다. 비즈니스에 중요한 기능에서 발생한 버그가 가장 큰 피해를 입히기 때문이다.
반면에 단순한 코드를 테스트하는 것은 가치가 거의 없다. 이러한 코드는 짧고, 비즈니스 로직을 많이 담고 있지도 않다. 단순한 코드를 다루는 테스트는 실수할 여지가 많이 줄기 때문에 회귀 오류가 많이 생기지 않는다.
게다가 여러분의 코드 외에 작동하지 않은 코드도 중요하다. 이 코드는 작성한 코드만큼이나 소프트웨어 작동에 영향을 미친다.
4.1.2 두 번째 요소: 리팩터링 내성
이는 테스트를 “빨간색”(실패)으로 바꾸지 않고 기본 애플리케이션 코드를 리팩터링할 수 있는지에 대한 척도다.
4.1.3 무엇이 거짓 양성의 원인은 무엇인가?
테스트와 테스트 대상 시스템(SUT)의 구현 세부 사항이 많이 결합할수록 허위 경보가 더 많이 생긴다. 거짓 양성이 생길 가능성을 줄이는 방법은 해당 구현 세부 사항에서 테스트를 분리하는 것뿐이다.
테스트는 최종 사용자의 관점에서 SUT를 검증해야 하고 최종 사용자에게 의미 있는 결과만 확인해야 한다. 다른 모든 것은 무시해야 한다.
4.1.4 구현 세부 사항 대신 최종 결과를 목표로 하기
앞에서 언급한 바와 같이, 테스트를 깨지지 않게 하고 리팩터링 내성을 높이는 방법은 SUT의 구현 세부 사항과 테스트 간의 결합도를 낮추는 것뿐이다. 즉, 코드의 내부 작업과 테스트 사이를 가능한 한 멀리 떨어뜨리고 최종 결과를 목표로 하는 것이다.
[ 4.2 첫 번째 특성과 두 번째 특성 간의 본질적인 관계 ]
4.2.1 테스트 정확도 극대화
테스트 정확도를 향상시키는 방법은 두 가지가 있다. 첫 번째는 분자, 즉 신호를 증가시키는 것이다. 이는 회귀를 더 잘 찾아내는 테스트로 개선하는 것이다. 두 번째는 분모, 즉 소음을 줄이는 것이다. 이는 허위 경보를 발생시키지 않는 테스트로 개선하는 것이다.
테스트 정확도 = 신호(발견된 버그 수) / 소음(허위 경보 발생 수)
4.2.2 거짓 양성과 거짓 음성의 중요성: 역학 관계
리팩터링은 바로 중요하지는 않으며, 시간이 지나면서 점차 중요해진다. 프로젝트 초기에는 코드 정리를 많이 할 필요가 없다.
그러나 시간이 흐를수록 코드베이스는 나빠진다. 점점 복잡해지고 체계적이지 않게 된다. 따라서 이러한 경향을 줄이려면 정기적으로 리팩터링을 해야 한다. 그렇지 않으면 새로운 기능에 드는 비용이 결국 엄청나게 커진다.
리팩터링이 점점 더 필요해짐에 따라 테스트에서 리팩터링 내성도 점점 더 중요해진다. 앞에서 설명했듯이 이 테스트에서 계속 “늑대”라고 울리면 리팩터링을 할 수 없고, 존재하지 않는 버그에 대해 경고를 계속 받게 된다. 이러한 테스트는 신뢰를 빠르게 잃고, 신뢰할 수 있는 피드백의 자료로 여기지 않게 된다.
4.3 세 번째 요소와 네 번째 요소: 빠른 피드백과 유지 보수성
테스트가 빠르게 실행되면 코드에 결함이 생기자마자 버그에 대해 경고하기 시작할 정도로 피드백 루프를 대폭 줄여서, 버그를 수정하는 비용을 거의 0까지 줄일 수 있다. 반면에 느린 테스트는 피드백을 느리게 하고 잠재적으로 버그를 뒤늦게 눈에 띄게 해서 버그 수정 비용이 증가한다. 오래 걸리는 테스트는 자주 실행하지 못하기 때문에 잘못된 방향으로 가면서 시간을 더 많이 낭비하게 된다.
마지막으로, 좋은 단위 테스트의 네 번째 특성인 유지 보수성 지표는 유지비를 평가한다. 이 지표는 다음 두 가지 주요 요소로 구성된다.
- 테스트가 얼마나 이해하기 어려운가: 이 구성 요소는 테스트의 크기와 관련이 있다. 테스트는 코드 라인이 적을수록 더 읽기 쉽다. 작은 테스트는 필요할 때 변경하는 것도 쉽다. 물론 단지 라인 수를 줄이려고 테스트 코드를 인위적으로 압축하지 않는다고 가정할 때다. 테스트 코드의 품질은 제품 코드만큼 중요하다. 테스트를 작성할 때 절차를 생략하지 마라. 테스트 코드를 일급 시민으로 취급하라.
- 테스트가 얼마나 실행하기 어려운가: 테스트가 프로세스 외부 종속성으로 작동하면, 데이터베이스 서버를 재부팅하고 네트워크 연결 문제를 해결하는 등 의존성을 상시 운영하는 데 시간을 들여야 한다.
[ 4.4 이상적인 테스트를 찾아서 ]
4.4.1 이상적인 테스트를 만들 수 있는가?
안타깝게도 그런 이상적인 테스트를 만드는 것은 불가능하다. 처음 세 가지 특성인 회귀 방지, 리팩터링 내성, 빠른 피드백은 상호 배타적이기 때문이다. 세 가지 특성 모두 최대로 하는 것은 불가능하다.
4.4.5 이상적인 테스트를 찾아서: 결론
회귀 방지, 리팩터링 내성, 빠른 피드백의 상호 배타성 때문에 세가지 특성 모두를 양보할 만큼 서로 조금씩 인정하는 것이 최선의 전략이라고 생각할 수 있다.
그러나 실제로는 리팩터링 내성을 포기할 수 없다. 엔드 투 엔드 테스트만 쓰거나 테스트가 상당히 빠르지 않은 한, 리팩터링 내성을 최대한 많이 갖는 것을 목표로 해야 한다. 따라서 테스트가 얼마나 버그를 잘 찾아내는지(회귀 방지)와 얼마나 빠른지(빠른 피드백) 사이의 선택으로 절충이 귀결된다. 이 선택은 회귀 방지와 빠른 피드백 사이에서 자유롭게 움직을 수 있는 슬라이더를 떠올리며 이해할 수 있다.
[ 4.5 대중적인 테스트 자동화 개념 살펴보기 ]
4.5.1 테스트 피라미드 분해
테스트 유형 간의 정확한 비율은 각 팀과 프로젝트마다 다를 것이다. 그러나 일반적으로 피라미드 형태를 유지해야 한다. 즉 엔드 투 엔드 테스트가 가장 적고, 단위 테스트가 가장 많으며, 통합 테스트는 중간 어딘가에 있어야 한다.
엔드 투 엔드 테스트는 빠른 피드백 지표에서 매우 낮은 점수를 받는다. 또 유지 보수성이 결여돼 있는데, 이는 크기가 더 큰 편이라 관련 프로세스 외부 의존성을 유지하는 데 노력을 더 들여야 한다. 따라서 엔드 투 엔드 테스트는 가장 중요한 기능 적용할 때와 단위 테스트나 통합 테스트와 동일한 수준으로 보호할 때만 적용된다.
4.5.2 블랙박스 테스트와 화이트박스 테스트 간의 선택
화이트박스 테스트가 더 철저한 편이다. 소스 코드를 분석하면 외부 명세에만 의존할 때 놓칠 수 있는 많은 오류를 발견할 수 있다. 반면에 화이트박스 테스트는 테스트 대상 코드의 특정 구현과 결합돼 있기 때문에 깨지기 쉽다. 이러한 테스트는 거짓 양성을 많이 내고 리팩터링 내성 지표가 부족하다. 또한 비즈니스 담당자에게 의미가 있는 동작으로 유추할 수 없는데, 화이트박스 테스트가 취약하고 가치를 많이 부여하지 않는다는 강력한 신호다. 블랙박스 테스트는 이와 정반대의 장단점을 제공한다.
리팩터링 내성은 타협할 수 없다. 즉, 테스트는 리팩터링 내성이 있거나 아예 없다. 따라서 화이트박스 테스트 대신 블랙박스 테스트를 기본으로 선택하라. 모든 테스트가 시스템을 블랙박스로 보게 만들고 문제 영역에 의미 있는 동작을 확인하라. 테스트를 통해 비즈니스 요구사항으로 거슬러 올라갈 수 없다면 이는 테스트가 깨지기 쉬움을 나타낸다. 이 테스트를 재구성하거나 삭제하라. 기존 테스트 스위트로 두지 말라. 유일한 예외는 알고리즘 복잡도가 높은 유틸리티 코드를 다루는 경우다.
테스트를 작성할 때는 블랙박스 테스트가 바람직하지만, 테스트를 분석할 때는 화이트 박스 방법을 사용할 수 있다. 코드 커버리지 도구를 사용해서 어떤 코드 분기를 실행하지 않았는지 확인한 다음 코드 내부 구조에 대해 전혀 모르는 것처럼 테스트하라. 이러한 화이트박스 방법과 블랙박스 방법의 조합이 가장 효과적이다.
5. 목과 테스트 취약성
런던파는 테스트 대상 코드 조각을 서로 분리하고 불변 의존성을 제외한 모든 의존성에 테스트 대역을 써서 격리하자고 한다.
고전파는 단위 테스트를 분리해서 병렬로 실행할 수 있게 하자고 한다. 테스트 간에 공유하는 의존성에 대해서만 테스트 대역을 사용한다.
[ 5.1 목과 스텁 구분 ]
5.1.1 테스트 대역 유형
실제로는 목과 스텁의 두 가지 유형으로 나눌 수 있다.
- 목은 외부로 나가는 상호 작용을 모방하고 검사하는 데 도움이 된다. 이러한 상호작용은 SUT가 상태를 변경하기 위한 의존성을 호출하는 것에 해당한다.
- 소텁은 내부로 들어오는 상호 작용을 모방하는 데 도움이 된다. 이러한 상호작용은 SUT가 입력 데이터를 얻기 위한 의존성을 호출하는 것에 해당한다.
5.1.2 도구로서의 목과 테스트 대역으로서의 목
최종 결과가 아닌 사항을 검증하는 이러한 관행을 과잉 명세(overspecificaiton)라고 부른다. 과잉 명세는 상호 작용을 검사할 때 가장 흔하게 발생한다.
5.1.5 목과 스텁은 명려오가 조회에 어떻게 관련돼 있는가?
물론 항상 CQS 원칙을 따를 수 있는 것은 아니다. 사이드 이펙트를 초래하고 값을 반환하는 것이 적절한 메서드가 있기 마련이다. 전형적인 예로 stack.pop()이 있다.
[ 5.2 식별할 수 있는 동작과 구현 세부 사항 ]
단위 테스트에 리팩터링 내성 지표가 있는지 여부는 대부분 이진 선택이므로 리팩터링 내성 지표가 가장 중요하다. 따라서 테스트가 단위 테스트 영역에 있고 엔드 투 엔드 테스트 범주로 넘어가지 않는 한 리팩터링 내성을 최대한 활용하는 것이 좋다. 엔드 투 엔드 테스트 범주로 넘어가면, 리팩터링 내성이 가장 뛰어나도 일반적으로 유지보수하기가 훨씬 까다롭다.
이러한 강결합을 피하는 방법은 코드가 생성하는 최종 결과를 검증하고 구현 세부 사항과 테스트를 가능한 한 떨어뜨리는 것뿐이다. 즉, 테스트는 “어떻게”가 아니라 “무엇”에 중점을 둬야 한다.
5.2.3 잘 설계된 API와 캡슐화
장기적으로 코드베이스 유지 보수에서는 캡슐화가 중요하다. 복잡도 때문이다. 코드 복잡도는 소프트웨어 개발에서 가장 큰 어려움 중 하나다. 코드베이스가 점점 복잡해질수록 작업하기가 더 어려워지고, 개발 속도가 느려지고, 버그 수가 증가하게 된다.
계속해서 증가하는 코드 복잡도에 대처할 수 있는 방법은 실질적으로 캡슐화 말고는 없다.
5.2.4 구현 세부 사항 유출: 상태의 예
모든 구현 세부 사항을 비공개로 하면 테스트가 식별할 수 있는 동작을 검증하는 것 외에는 다른 선택지가 없으며, 이로 인해 리팩터링 내성도 자동으로 좋아진다.
[ 5.4 단위 테스트의 고전파와 런던파 재고 ]
런던파를 따라 목을 무분별하게 사용하면 종종 구현 세부 사항에 결합돼 테스트에 리팩터링 내성이 없게 된다.
리팩터링 내성이 저하되면 테스트는 가치가 없어진다.
고전파는 테스트 간에 공유하는 의존성만 교체하자고 하므로 이 문제에 훨씬 유리하다. 그러나 고전파 역시 시스템 간 통신에 대한 처리에 이상적이지는 않다.
6. 단위 테스트 스타일
[ 6.3 함수형 아키텍처 이해 ]
6.3.1 함수형 프로그래밍이란?
메서드가 수학적 함수인지 판별하는 가장 좋은 방법은 프로그램의 동작을 변경하지 않고 해당 메서드에 대한 호출을 반환 값으로 대체할 수 있는지 확인하는 것이다. 메서드 호출을 해당 값으로 바꾸는 것을 참조 투명성(referential transparency)이라고 한다.
public int increment(int x) {
return x + 1;
}
int y = increment(4);
int y = 5;
6.3.2 함수형 아키텍처란?
물론 어떤 사이드 이펙트도 일으키지 않는 애플리케이션을 만들 수는 없다. 이러한 애플리케이션은 비현실적이다.
함수형 프로그래밍의 목표는 사이드 이펙트를 완전히 제거하는 것이 아니라 비즈니스 로직을 처리하는 코드와 사이드 이펙트를 일으키는 코드를 분리하는 것이다.
7. 가치 있는 단위 테스트를 위한 리팩터링
단위 테스트와 기반 코드는 서로 매우 얽혀 있기 때문에 코드베이스에 노력을 기울이지 않고서는 가치 있는 테스트를 만들 수 없다.
[ 7.1 리팩터링할 코드 식별하기 ]
기반 코드를 리팩터링하지 않고서는 테스트 스위트를 크게 개선할 수 없다. 테스트 코드와 제품 코드는 본질적으로 관련돼 있기 때문에 다른 방도는 없다.
7.1.1 코드의 네 가지 유형
도메인 유의성(domain significance)은 코드가 프로젝트의 문제 도메인에 대해 얼마나 의미 있는지를 나타낸다. 일반적으로 도메인 계층의 모든 코드는 최종 사용자의 목표와 직접적인 연관성이 있으므로 도메인 유의성이 높다. 반면에 유틸리티 코드는 그런 연관성이 없다.
복잡한 코드와 도메인 유의성을 갖는 코드가 단위 테스트에서 가장 이롭다. 해당 테스트가 회귀 방지에 뛰어나기 때문이. 도메인 코드는 복잡할 필요가 없으며, 복잡한 코드는 도메인 유의성이 나타나지 않아도 테스트할 만하다. 이 두 요소는 서로 독립적이다.
두 번째 차원은 클래스 또는 메서드가 가진 협력자 수다.
협력자가 많은 코드는 테스트 비용이 많이 든다.
지나치게 복잡한 코드를 피하고 도메인 모델과 알고리즘만 단위 테스트하는 것이 매우 가치 있고 유지 보수가 쉬운 테스트 스위트로 가는 길이다. 하지만 이 방법으로도 테스트 커버리지를 100% 달성할 수 없으며, 이를 목표로 해서도 안 된다.
물론 지나치게 복잡한 코드를 제거하는 것은 말처럼 쉬운 일이 아니다. 그럼에도 불구하고 도움이 되는 기법이 있다.
7.1.2 험블 객체 패턴을 사용해 지나치게 복잡한 코드 분할하기
지나치게 복잡한 코드를 쪼개려면, 허블 객체(Humble Object) 패턴을 써야 한다.
나는 코드가 프레임워크 의존성에 결합돼 있기 때문에 테스트가 어렵다는 사실을 깨달은 적이 종종 있다. 예를 들어 비동기 또는 멀티쓰레드 실행, 사용자 인터페이스, 프로세스 외부 의존성과의 통신 등이 있다.
테스트 대상 코드의 로직을 테스트하려면, 테스트가 가능한 부분을 추출해야 한다. 결과적으로 코드는 테스트 가능한 부분을 둘러싼 얇은 험블 래퍼(humble wrapper)가 된다. 이 험블 래퍼가 테스트하기 어려운 의존성과 새로 추출된 구성 요소를 붙이지만, 자체적인 로직이 거의 없거나 전혀 없으므로 테스트할 필요가 없다.
[ 7.3 최적의 단위 테스트 커버리지 분석 ]
7.3.1 도메인 계층과 유틸리티 코드 테스트하기
코드의 복잡도나 도메인 유의성이 높으면 회귀 방지가 뛰어나고 협력자가 거의 없어 유지비도 가장 낮다.
7.3.3 전제 조건을 테스트해야 하는가?
일반적으로 권장하는 지침은 도메인 유의성이 있는 모든 전제 조건을 테스트하라는 것이다.
그러나 도메인 유의성이 없는 전제 조건을 테스트하는 데 시간을 들이지 말라.
[ 7.4 컨트롤러에서 조건부 로직 처리 ]
이러한 상황에서는 다음과 같이 세 가지 방법이 있다.
- 어쨌든 외부에 대한 모든 읽기와 쓰기를 가장자리로 밀어낸다. 이 방법은 “읽고-결정하고-실행하기” 구조를 유지하지만 성능이 저하된다. 필요 없는 경우에도 컨트롤러각 프로세스 외부 의존성을 호출한다.
- 도메인 모델에 프로세스 외부 의존성을 주입하고 비즈니스 로직이 해당 의존성을 호출할 시점을 직접 결정할 수 있게 한다.
- 의사 결정 프로세스 단계를 더 세분화하고, 각 단계별로 컨트롤러를 실행하도록 한다.
대부분의 소프트웨어 프로젝트에서는 성능이 매우 중요하므로 첫 번째 방법은 고려할 필요가 없다. 두 번째 옵션은 대부분 코드를 지나치게 복잡한 사분면에 넣는다. 이러한 코드는 더 이상 비즈니스 로직과 프로세스 외부 의존성과의 통신을 분리하지 않으므로 테스트와 유지 보수가 훨씬 어려워지므로 이러한 방법은 피하는 것이 좋다.
그러면 세 번째 옵션만 남게 된다. 이 방식을 쓰면 컨트롤러를 더 복잡하게 만들기 때문에 지나치게 복잡한 사분면에 더 가까워지게 된다. 그러나 이 문제를 완화할 수 는 방법이 있다.
7.4.1 CanExcute/Execute 패턴 사용
public string canChangeEmail() {
if (isEmailConfirmed) {
return "not confirmed";
}
return null;
}
public void changeEmail(String newEmail, Company company) {
check(canChangeEmail() == null);
...
}
이 패턴을 사용하면 도메인 계층의 모든 결정을 통합할 수 있다. 이제 컨트롤러에 이메일을 확인할 일이 없기 때문에 더 이상 의사 결정 지점은 없다.
7.4.2 도메인 이벤트를 사용해 도메인 모델 변경 사항 추적
애플리케이션에서 정확히 무슨 일이 일어나는지 외부 시스템에 알려야 하기 때문에 이러한 단계들을 아는 것이 중요할지도 모른다.
도메인 모델에서 중요한 변경 사항을 추적하고 비즈니스 연산이 완료된 후 해당 변경 사항을 프로세스 외부 의존성 호출로 변환한다. 도메인 이벤트로 이러한 추적을 구현할 수 있다.
===== 정보 =====
도메인 이벤트는 애플리케이션 내에서 도메인 전문가에게 중요한 이벤트를 말한다.
도메인 이벤트는 종종 시스템에서 발생하는 중요한 변경 사항을 외부 애플리케이션에 알리는 데 사용된다.
[ 7.5 결론 ]
도메인 클래스에서 모든 협력자를 제거할 수 있는 경우는 거의 없을 것이다. 하지만 괜찮다. 협력자가 하나나 둘, 심지어 셋이 있더라도 프로세스 외부 의존성을 참조하지 않는 한, 도메인 클래스는 지나치게 복잡한 코드가 아닐 것이다.
그러나 이러한 협력자와의 상호작용을 검증하려고 목을 사용하지는 말라.
외부 계층의 관점에서 각 계층을 테스트하고, 해당 계층이 기저 계층과 어떻게 통신하는지는 무시하라.
8. 통합 테스트를 하는 이유
[ 8.1 통합 테스트는 무엇인가? ]
8.1.1 통합 테스트의 역할
2장에서 살펴봤듯이, 단위 테스트는 다음 세 가지 요구 사항을 충족하는 테스트다.
- 단일 동작 단위를 검증하고,
- 빠르게 수행하고,
- 다른 테스트와 별도로 처리한다.
이 세 가지 요구 사항 중 하나라도 충족하지 못하는 테스트는 통합 테스트 범주에 속한다. 단위 테스트가 아닌 모든 테스트가 통합 테스트에 해당한다.
단위 테스트는 도메인 모델을 다루는 반면, 통합 테스트는 프로세스 외부 의존성과 도메인 모델을 연결하는 코드를 확인한다.
8.1.2 다시 보는 테스트 피라미드
통합 테스트가 프로세스 외부 의존성에 직접 작동하면 느려지며, 이러한 테스트는 유지비가 많이 든다. 유지비 증가의 이유는 다음과 같다.
- 프로세스 외부 의존성 운영이 필요함
- 관련된 협력자가 많아서 테스트가 비대해짐
반면 통합 테스트는 코드를 더 많이 거치므로 회귀 방지가 단위 테스트보다 우수하다. 또한 제품 코드와의 결합도가 낮아섯 리팩터링 내성도 우수하다.
단위 테스트로 가능한 한 많이 비즈니스 시나리오의 예외 상황을 확인하고, 통합 테스트는 주요 흐름(happy path)과 단위 테스트가 다루지 못하는 기타 예외 상황(edge case)를 다룬다.
8.1.3 통합 테스트와 빠른 실패
버그를 빨리 나타나게 하는 것을 빠른 실패 원칙(Fast Fail principle)이라고 하며, 통합 테스트에서 할 수 있는 대안이다.
===== 빠른 실패 원칙 =====
빠른 실패 원칙은 예기치 않은 오류가 발생하자마자 현재 연산을 중단하는 것을 의미한다. 이 원칙은 다음을 통해 애플리케이션의 안정성을 높인다.
- 피드백 루프 단축: 버그를 빨리 발견할수록 더 쉽게 해결할 수 있다. 이미 운영 환경으로 넘어온 버그는 개발 중에 발견된 버그보다 수정 비용이 훨씬 더 크다.
- 지속성 상태 보호: 버그는 애플리케이션 상태를 손상시킨다. 손상된 상태가 데이터베이스로 침투하면, 고치기가 훨씬 어려워진다. 빨리 실패하면 손상이 확산되는 것을 막을 수 있다.
보통 예외를 던져서 현재 연산을 중지한다. 예외는 그 위미가 빠른 실패 원칙에 완벽히 부합되기 때문이다. 예외는 프로그램 흐름을 중단하고 실행 스택에서 가장 높은 레벨로 올라간 후 로그를 남기고 작업을 종료하거나 재시작할 수 있다.
전제 조건은 빠른 실패 원칙의 예다. 전제 조건이 실패하면 애플리케이션 상태에 대해 가정이 잘못된 것을 의미하는데, 이는 항상 버그에 해당한다. 또 다른 예는 설정 파일에서 데이터를 읽는 것이다. 설정 파일의 데이터가 불완전하거나 잘못된 경우 예외가 발생하도록 판독 로직을 구성할 수 있다. 이 로직을 애플리케이션 시작 부근에 둬서 문제가 있으면 애플리케이션이 시작하지 않도록 할 수 있다.
[ 8.3 통합 테스트: 예제 ]
8.3.1 어떤 시나리오를 테스트할까?
단위 테스트로 테스트하지 않는 한 가지 예외 상황이 있는데, 바로 이메일을 변경할 수 없는 시나리오다. 그러나 이 시나리오를 테스트할 필요는 없다. 컨트롤러에 이러한 확인이 없으면 애플리케이션이 빨리 실패하기 때문이다.
[ 8.4 의존성 추상화를 위한 인터페이스 사용 ]
8.4.1 인터페이스와 느슨한 결합
단일 구현을 위한 인터페이스는 추상화가 아니며, 해당 인터페이스를 구현하는 구체 클래스보다 결합도가 낮지 않다. 진정한 추상화는 발견하는 것이지, 발명하는 것이 아니다. 의미상 추상화가 이미 존재하지만 코드에서 아직 명확하게 정의되지 않았을 때 그 이후에 발견되는 것이다. 따라서 인터페이스가 진정으로 추상화되려면 구현이 적어도 두 가지는 있어야 한다.
[ 8.5 통합 테스트 모범 사례 ]
통합 테스트를 최대한 활용하는 데 도움이 되는 몇 가지 일반적인 지침이 있다.
- 도메인 모델 경계 명시하기
- 애플리케이션 내 계층 줄이기
- 순환 의존성 제거하기
8.5.1 도메인 모델 경계 명시하기
항상 도메인 모델을 코드베이스에서 명시적이고 잘 알려진 위치에 두도록 하라. 도메인 모델은 프로젝트가 해결하고자 하는 문제에 대한 도메인 지식의 모음이다. 도메인 모델에 명시적 경계를 지정하면 코드의 해당 부분을 더 잘 보여주고 더 잘 설명할 수 있다.
이렇게 하면 테스트에도 도임이 된다.
8.5.2 계층 수 줄이기
극단적인 경우로, 애플리케이션에 추상 계층이 너무 많으면 코드베이스를 탐ㅅ핵하기 어렵고 아주 간단한 연산이라 해도 숨은 로직을 이해하기가 너무 어려워진다.
간접 계층은 코드를 추론하는 데 부정적인 영향을 미친다. 모든 기능이 각각의 계층으로 전개되면 모든 조각을 하나의 그림으로 만드는 데 상당한 노력이 필요하다. 이는 개발 과정을 방해해서 정신적으로 더 부담된다.
추상화가 지나치게 많으면 단위 테스트와 통합 테스트에도 도움이 되지 않는다. 간접 계층이 많은 코드베이스는 컨트롤러와 도메인 모델 사이에 명확한 경계가 없는 편이다. 그리고 각 계층을 따로 검증하는 경향이 훨씬 강하다. 이러한 경향으로 인해 통합 테스트는 가치가 떨어지며, 각 테스트는 특정 계층의 코드만 실행하고 하위 계층은 목으로 처리한다. 최종 결과는 항상 똑같이 낮은 리팩터링 내성과 불충분한 회귀 방지다.
8.5.3 순환 의존성 제거하기
추상 계층이 너무 많은 것과 마찬가지로, 순환 의존성은 코드를 읽고 이해하려고 할 때 알아야 할 것이 많아서 큰 부담이 된다. 순환 의존성이 있으면 해결책을 찾기 위한 출발점이 명확하지 않기 때문이다. 하나의 클래스를 이해하려면 주변 클래스 그래프 전체를 한 번에 읽고 이해해야 하며, 심지어 소규모의 독립된 클래스조차도 파악하기가 어려워질 수 있다.
또한 순환 의존성은 테스트를 방해한다.
9. 목 처리에 대한 모범 사례
[ 9.2 목 처리에 대한 모범 사례 ]
9.2.1 목은 통합 테스트만을 위한 것
목이 통합 테스트만을 위한 것이며 단위 테스트에서 목을 사용하면 안 된다는 지침은 7장에서 설명한 기본 원칙인 비즈니스 로직과 오케스트레이션의 분리에서 비롯된다.
9.2.2 테스트당 목이 하나일 필요는 없음
“단위”라는 용어는 코드 단위가 아니라 동작 단위를 의미한다. 동작 단위를 구현하는 데 필요한 코드의 양은 관계가 없다.
목을 사용해도 같은 원칙이 적용된다. 동작 단위를 검증하는 데 필요한 목의 수는 관계가 없다.
9.2.3 호출 횟수 검증하기
비관리 의존성과의 통신에 관해서는 다음 두 가지 모두 확인하는 것이 중요하다.
- 예상하는 호출이 있는가?
- 예상치 못한 호출은 없는가?
10. 데이터베이스 테스트
[ 10.3 테스트 데이터 생명 주기 ]
공유 데이터베이스를 사용하면 통합 테스트를 서로 분리할 수 없는 문제가 생긴다. 이 문제를 해결하려면,
- 통합 테스트를 순차적으로 실행하라
- 테스트 실행 간에 남은 데이터를 제거하라
10.3.1 병렬 테스트 실행과 순차 테스트 실행
통합 테스트를 병렬로 실행하려면 상당한 노력이 필요하다. 모든 테스트 데이터가 고유한 지 확인해야 하며 데이터베이스 제약 조건을 위반하지 않고 테스트가 다른 테스트 후에 입력 데이터를 잘못 수집하는 일이 없다. 남은 데이터를 정리하는 것도 까다로워진다. 성능 향상을 위해 시간을 허비하지 말고 순차적으로 통합 테스트를 실행하는 것이 더 실용적이다.
대안으로 컨테이너를 사용해 테스트를 병렬 처리할 수도 있다.
그러나 이러한 방식은 실제로 유지 보수 부담이 너무 커지게 된다. 도커를 사용하면 데이터베이스를 추적해야 할 뿐만 아니라,
- 도커 이미지를 유지 보수해야 하고
- 각 테스트마다 컨테이너 인스턴스가 잇는지 확인해야 하며
- 통합 테스트를 일괄 처리하고
- 다 사용한 컨테이너는 폐기해야 한다.
통합 테스트의 실행 시간을 최소화해야 하는 경우가 아니라면 컨테이너를 사용하지 않는 것이 좋다.
10.3.3 인메모리 데이터베이스 피하기
인메모리 데이터베이스는 다음과 같은 장점이 있다.
- 테스트 데이터를 제거할 필요가 없음
- 작업 속도 향상
- 테스트가 실행될 때마다 인스턴스화 가능
이러한 모든 장점에도 불구하고, 인메모리 데이터베이스는 일반 데이터베이스와 기능적으로 일관성이 없기 떄문에 사용하지 않는 것이 좋다. 이는 또 다시 운영 환경과 테스트 환경이 일치하지 않는 문제이며, 일반 데이터베이스와 인메모리 데이터베이스의 차이로 인해 테스트에서 거짓 양성 또는 거짓 음성이 발생하기 쉽다. 이러한 테스트로는 높은 보호 수준을 기대하기 아렵고, 어쨌든 수동으로 회귀 테스트를 많이 수행해야 할 것이다.
11. 단위 테스트 안티 패턴
[ 11.1 비공개 단위 테스트 ]
11.1.1 비공개 메서드와 테스트 취약성
단위 테스트를 하려고 비공개 메서드를 노출하는 경우에는 5장에서 다룬 기본 원칙 중 하나인 식별할 수 있는 동작만 테스트하는 것을 위반한다. 비공개 메서드를 노출하면 테스트가 구현 세부 사항과 결합되고 결과적으로 리팩터링 내성이 떨어진다. 비공개 메서드를 직접 테스트하는 대신, 포괄적인 식별할 수 있는 동작으로서 간접적으로 테스트하는 것이 좋다.
11.1.3 비공개 메서드 테스트가 타당한 경우
비공개 메서드를 테스트하는 것 자체는 나쁘지 않다. 비공개 메서드가 구현 세부 사항의 프록시에 해당하므로 나쁜 것이다. 구현 세부 사항을 테스트하면 궁극적으로 테스트가 깨지기 쉽다.
[ 11.2 비공개 상태 노출 ]
또 다른 일반적인 안티 패턴으로 단위 테스트 목적으로만 비공개 상태를 노출하는 것이 있다.
[ 11.3 테스트로 유출된 도메인 지식 ]
도메인 지식을 테스트로 유출하는 거은 또 하나의 흔한 안티 패턴이며, 보통 복잡한 알고리즘을 다루는 테스트에서 일어난다.
이러한 테스트는 제품 코드에서 알고리즘 구현을 복사했다.
이러한 테스트는 결국 구현 세부 사항과 결합되는 또 다른 예다. 리팩터링 내성 지표에서 거의 0점을 받게 되고 결국 가치가 없다. 이러한 테스트는 타당한 실패와 거짓 양성을 구별할 가능성이 없다. 알고리즘 변경으로 인해 테스트가 실패하면 개발 팀은 원인을 파악하려고 노력하지 않으며 해당 알고리즘의 새 버전을 테스트에 복사할 가능성이 높다.
테스트를 작성할 때 특정 구현을 암시하지 말라.
class CalculatorTest {
@Test
void addTest() {
int num1 = 1;
int num2 = 3;
int expected = num1 + num2; // 유출
int actual = new Calculator().add(num1, num2);
assertThat(actual).isEqualTo(expected);
}
}
처음에는 직관적이지 않아 보일 수 있지만, 단위 테스트에서는 예상 결과를 하드코딩하는 것이 좋다. 하드코딩된 값의 중요한 부분은 SUT가 아닌 다른 것을 사용해 미리 계산하는 것이다.
[ 11.4 코드 오염 ]
===== 정의 =====
코드 오염은 테스트에만 필요한 제품 코드를 추가하는 것이다.
[ 11.6 시간 처리하기 ]
시간에 따라 달라지는 기능을 테스트하면 거짓 양성이 발생할 수 있다. 실행 단계의 시간이 검증 단계의 시간과 다를 수 있다.
11.6.2 명시적 의존성으로서의 시간
시간을 서비스로 주입하는 것보다는 값으로 주입하는 것이 더 낫다. 제품 코드에서 일반 값으로 작업하는 것이 더 쉽고, 테스트에서 해당 값을 스텁으로 처리하기도 더 쉽다.
아마 시간을 항상 일반 값으로 주입할 수는 없을 것이다. 의존성 주입 프레임워크가 값 객체와 잘 어울리지 않기 때문이다. 비즈니스 연산을 시작할 때는 서비스로 시간을 주입한 다음, 나머지 연산에서 값을 전달하는 것이 좋다.
'나의 공부방' 카테고리의 다른 글
[Tool] 유용한 MacOS 앱 및 크롬 익스텐션(Chrome Extensions) 정리 (7) | 2024.10.29 |
---|---|
[Architecture] 헥사고날 아키텍처를 통한 의미 수준과 구현 수준에 대한 이해(semantic and implementation level with hexagonal architecture) (24) | 2024.09.24 |
[개발서적] 스트리트 코더(Street Coder) 핵심 내용 정리 및 요약 (10) | 2024.08.27 |
[개발서적] 오브젝트 핵심 내용 정리 및 요약 (2) | 2024.08.20 |
[개발서적] 프로그래머, 열정을 말하다 핵심 내용 정리 및 요약 (8) | 2024.08.13 |