모집마감
AI시대, 왜 지금 TDD인가요?
₩3,300
초급 / TDD, 인공지능(AI), 리팩터링
신청
25. 06. 23 ~ 25. 06. 30
일정
25. 07. 02 ~ 25. 07. 02
초급
TDD, 인공지능(AI), 리팩터링
여러 스타트업에서 CTO로 활동하며 기술팀의 역량 성장을 이끌어왔습니다.
코딩만큼이나 기술 지식과 경험을 나누는 일에서도 큰 즐거움을 느낍니다.
모집마감
AI시대, 왜 지금 TDD인가요?
₩3,300
초급 / TDD, 인공지능(AI), 리팩터링
신청
25. 06. 23 ~ 25. 06. 30
일정
25. 07. 02 ~ 25. 07. 02
초급
TDD, 인공지능(AI), 리팩터링
Spring Boot TDD - 입문부터 실전까지 정확하게
₩154,000
2일만
30%
₩107,800
초급 / TDD, Spring Boot, JUnit, 아키텍처
5.0
(15)
더 빠르고 더 견고하게 Spring Boot 응용프로그램을 개발하세요. 정확한 이론 설명과 실무 수준의 연결된 실습을 통해 HTTP API 응용프로그램 개발에 TDD를 사용하는 효과적인 방법을 자연스럽게 익히게 됩니다.
초급
TDD, Spring Boot, JUnit
질문&답변
테스트 격리에서 테스트 랜덤 실패 이유
안녕하세요. 강의 수강해주셔서 고맙습니다.69번 수업이면 많이 진행하셨네요. 꼭 끝까지 완료하세요. :) 해당 수업에서 실패하는 테스트가 달라지는 정확한 이유는 저도 모르겠습니다. 그래서 프로그래밍이 어렵다고 말씀드린 것인데요.분명한 것은 README.md 파일 내용에 따라서 JUnit이 테스트를 실행하는 순서가 달라진다는 점입니다. 그래서 서로 맥락을 공유하는 테스트 중 먼저 실행되는 테스트는 성공하고 나중에 실행되는 테스트는 실패하는데, 이 순서가 테스트와 무관한 파일의 내용에 영향을 받다 보니 우리 입장에서는 말씀하신 것처럼 '랜덤으로' 실패하는 것처럼 느껴지는 것입니다. 혹시 설명이 부족하다면 말씀해주세요!
질문&답변
테스트 코드 작성 범위 고민
폭식님 안녕하세요. 좋은 평가에 질문까지 남겨주셔서 정말 고맙습니다. 우리가 시스템을 개발할 때 테스트 대상이 될 수 있는 코드는 많습니다. 그리고 그 중에서도 실제로 어떤 영역을 대상으로 테스트를 작성해야 하는지는 여러가지 상황에 따라 달라집니다. 여기서는 말씀하신 구조를 가정하고 제 의견을 말씀 드리겠습니다. +--------+ +-----+ +---------+ +------------+ | Client | ---> | API | ---> | Service | ---> | Repository | +--------+ +-----+ +---------+ +------------+ 한가지 규칙만 있다면 참 쉬울 것 같은데요, 안타깝게도 그렇지는 않고 여러가지 상황 변수에 따라서 결정해야 합니다. 그래서 몇 가지 상황에 대해서 제가 선택할 수 있는 방법을 소개해드리겠습니다. 1. API 계층의 동작이 가벼워서 테스트 실행 성능에 큰 문제가 되지 않는 경우우리가 응용프로그램을 개발하는 목적은 클라이언트의 요구사항을 만족하는 것입니다. 이 목적이 달성되는지 확신할 수 있다면 다른 작업들은 선택적입니다. 그래서 모든 요구사항을 클라이언트와 직접 만나는 API 대상으로만 테스트합니다.+--------+ +-----+ +---------+ +------------+ | Client | ---> | API | ---> | Service | ---> | Repository | +--------+ +-----+ +---------+ +------------+ ^ | Tests 2. API 계층 동작이 무거워서 테스트 실행 성능에 많은 영향을 미치는 경우API를 대상으로만 테스트를 작성해서 클라이언트의 모든(또는 대부분의) 요구사항을 확인할 수 있더라도 테스트 실행에 너무 긴 시간이 소비되면 프로그래머는 심리적으로 테스트를 덜 자주 실행하게 되고 작업 리듬에 부정적인 영향을 받게 됩니다.이 때는 Service 대상으로 테스트를 많이 작성해서 테스트 실행 성능을 관리할 수 있습니다.Service 대상으로 최대한 많은 요구사항을 검증합니다.API가 Service를 사용하는지 확인할 수 있도록 가장 간단한 성공 경우에 대한 테스트 하나를 작성합니다.사용자 인증 등 API 계층의 책임인 기능을 검증하기 위해서 필요한 API 대상 테스트를 작성합니다.+--------+ +-----+ +---------+ +------------+ | Client | ---> | API | ---> | Service | ---> | Repository | +--------+ +-----+ +---------+ +------------+ ^ ^ | | Tests Tests 3. Service 계층 테스트에 Repository에 대한 테스트 대역(test double)을 사용하는 경우어떤 이유로 인해서 Service 개체를 테스트할 때 운영 환경과 동일한 Repository가 아니라 mock 등 테스트 대역을 사용하는 경우도 있습니다. 이런 경우 Service 개체를 대상으로 테스트를 충분히 작성해도 테스트에 의해서 Repository 코드가 실행되지는 않기 때문에 Repository가 운영 환경에서 정상적으로 동작하는지 확신할 수 없습니다. 그래서 이런 상황이라면 Repository에 대한 테스트를 별도로 작성합니다.+--------+ +-----+ +---------+ +------------+ | Client | ---> | API | ---> | Service | ---+------> | Repository | +--------+ +-----+ +---------+ | +------------+ ^ ^ | ^ | | | | Tests Tests | Tests | | +------------------------+ +--> | Repository Test Double | +------------------------+ 이 강의 실습에서는 1번 방식을 사용하고 저는 실무에서도 자주 사용합니다. 그리고 2번의 방법도 실무에서 많이 사용하고요, 테스트 대역을 잘 사용하지 않기 때문에 3번 방법은 가끔씩만 사용합니다. 클라이언트의 요구사항을 검증하는 것이 테스트를 작성하는 가장 큰 목적이고, 엔지니어링 측면에서 문제가 있을 때 이를 해결하기 위한 기법을 추가 사용한다고 제 기준을 정리하겠습니다. 더 설명이 필요하다면 말씀해주세요!
질문&답변
질문드립니다.
오개발님 안녕하세요. 강의 수강해주셔서 감사드립니다. :) 첫번째 질문부터 답변 드리겠습니다.우선 테스트 코드의 @Transactional 사용 여부에 대해서는 강의에서 언급한 기억이 없고 자료를 찾아봐도 발견되지 않는데요, 제 기억이 잘 못 되었을 수도 있으니 제가 어떤 수업에서 말씀드렸는지 알려주시면 더 설명드리겠습니다.디스코드 대화에서 이런 말씀을 드린 적이 있기는 합니다."또 하나는 예전에 데이터베이스가 완전히 초기화된 상태에서 테스트가 거짓 음성을 일으킨 일이 있었습니다. 다른 데이터들이 섞여 있었다면 실패할 논리가 운영코드에 들어갔는데 딱 테스트에 필요한 데이터만 있어서 테스트가 성공한 적이 있었습니다."다만 테스트 코드에서 데이터베이스 롤백을 사용한 적이 없는 것은 맞고 이 내용은 강의에서 한두번 언급됩니다. :)CRUD는 유일한 방식은 아니며 설계 패턴 중 한가지고 저는 CRUD 패턴을 잘 사용하지 않기는 합니다만, 데이터베이스가 사용되는 테스트를 지칭하신다면 테스트에 운영 환경과 격리된 데이터베이스를 사용하는 것은 맞습니다. 강의 실습에서는 이런 내용에 대해서 다루지 않았지만 실무에서는 운영 환경과 테스트 환경은 염격하게 격리해야 합니다. 이것은 데이터베이스 롤백 사용 여부와 무관하게 안정적인 서비스 운영을 위해 반드시 지켜야 합니다. 그럼 두번째 질문에 대해서 말씀드릴게요.TDD는 테스트 기법이 아니라 테스트를 사용해서 소프트웨어를 개발하는 기법입니다. 그러니까 이미 만들어진 소프트웨어를 테스트 한다면 TDD가 필요하지는 않습니다. 그리고 이런 사례는 드물지 않게 발생하고 어색하거나 문제가 있는 상황은 아닙니다. :) 강의의 남은 수업들도 오개발님에게 도움이 되시길 바라겠습니다!
질문&답변
거짓 양성, 거짓 음성 질문
태영님 안녕하세요. 강의 수강해주셔서 고맙습니다. :) 강의 내용 중 테스트 양성과 음성이 어떤 의미인지 혼동되시는군요. 종종 발생하는 일이라 오래전에 이 주제로 글을 쓰기도 했으니까 한 번 참고해 보시고요,소프트웨어 테스팅의 False Positive | 프로그래머 이규원의 웹사이트 도움이 되시도록 좀 더 설명을 드려 보겠습니다. 우선 코로나 바이러스 테스트 얘기부터 해보면요, 자가 코로가 바이러스 테스트를 아마 해보셨을 것 같은데, 양성 반응이 나오면 코로나 바이러스에 감염된 것(또는 감염되었을 가능성이 매우 높은 것)입니다. 그렇죠?식품의약품안전처 자료 참고6. 검사결과가 시험선(T)과 대조선(C)이두 줄로 나타나면 양성으로 선별진료소 등을 방문, PCR 검사를 받아야 합니다. 검사 후엔 자택으로 이동, 결과확인 전까지 자가격리를 해야합니다.여기서 코로나 바이러스 감염을 양성으로 보는 것은 테스트의 귀무가설(null hypothesis)을 '우리 몸이 코로나 바이러스에 감염되지 않았다'로 설정한 것입니다. 그래서 테스트를 통해서 뭔가 특별한 근거를 발견하지 못했다면 귀무가설을 채택하게 되고 이것을 음성이라고 합니다.그리고 실제로는 귀무가설을 채택해야 하는데 기각하게 되는 상황(즉, 코로나 바이러스에 감염되지 않았다는 가설을 채택하지 않음)을 일종 오류(type I error), 또는 거짓 양성(false positive)라고 합니다. 그리고 다른 표현이 또 있는데 거짓 경보(false alarm)입니다.그러니까귀무가설 기각 = 양성 = 경보가 됩니다. 이제 우리 몸을 시스템으로, 코로나 바이러스를 버그로 치환해보면 귀무가설은 '시스템에 버그가 없다'가 됩니다. 그래서 양성(경보)은 시스템에 버그가 있다는 뜻이고 테스트가 실패해서 프로그래머에게 알려주기를 기대합니다. 그런데 양성이 발생했는데 실제로는 버그가 없다면 테스트가 우리에게 잘못된 경보를 준 것이고 이 양성은 거짓 양성이 됩니다. 반대로 실제로는 버그가 있지만 이 버그를 감지해야 할 테스트가 실패하지 않고 음성 반응을 보이면 이것을 거짓 음성이라고 합니다. 질문을 보면 테스트 통과를 양성이라고 혼동하고 계신 것 같습니다. 테스트 시나리오 문구는 귀무가설입니다. 코로나 바이러스 검사 테스트 시나리오를 작성한다면 '우리 몸이 코로나 바이러스에 감염되지 않았다'가 되겠죠. 그리고 테스트가 성공하면 귀무가설을 채택하고 테스트가 실패하면 귀무가설을 기각합니다. 학문적으로 전자를 음성(negative)이라고 하고 후자를 양성(positive)이라고 합니다. 혹시 아직도 이해가 잘 안되시면 더 말씀해 주시거나 "what are false positive and false negative in software testing"으로 구글링을 해보시면 도움될 자료들을 많이 보실 수 있을거에요.
질문&답변
cqrs 명령 아키텍처 개선 질문
규영님 안녕하세요. 강의 수강해주셔서 고맙습니다. :) 실습 코드를 정확히 이해하기 위해서 열심히 노력하고 계신 것 같습니다. 👍 도움이 되도록 이해하기 어려우신 내용에 대해 설명을 드려 보겠습니다. Product 엔터티는 JPA를 사용하는데요, JPA를 사용할 때 JpaRepository를 제공하는 Spring Data JPA를 반드시 사용해야 하는 것은 아닙니다. JPA를 지원하는 다른 도구들도 많습니다. 그래서 RegisterProductCommandExecutor를 만들 때 EntityManager 등 다른 도구를 사용하고 싶을 수 있습니다.EntityManager em = ... var executor = new RegisterProductCommandExecutor(p -> { em.getTransaction().begin(); em.persist(p); em.getTransaction().commit(); });이 때 어떤 도구가 사용되는지는 RegisterProductCommandExecutor는 알지 못하고 알 필요도 없습니다. 그래서 RegisterProductCommandExecutor는 JpaRepository 또는 Spring Data JPA라는 특정 기반구조 도구로부터 독립적인 설계가 됩니다. 재사용성이 조금 더 올라가는 거죠. 만약 RegisterProductCommandExecutor 클래스가 ProductRepository를 주입받으면 Spring Data JPA가 있어야 동작하는 특정 기반구조에 의존적인 코드가 되죠. 명령 모델과 API가 별도의 모듈로 분리된다면 의미가 더 명확해 집니다.+--------------------------------+ | Command Model | +---------+ | RegisterProductCommandExecutor |------->| JPA | +--------------------------------+ +---------+ ^ ^ | | | | +--------------------------+ +----------------------+ | API | | Spring Data JPA | | SellerProductsController |---->| JpaRepository | +--------------------------+ +----------------------+ 다만 이런 독립적인 설계가 유용한지 아닌지는 각 프로젝트마다 상황이 다릅니다. 따라서 규영님이 말씀하신 2번 설계를 사용하는 것이 더 좋을 수도 있습니다. 엔지니어는 한 가지 설계 방식만 아는 것 보다는 다양한 설계 방식을 이해하고 실무에서 주어진 환경에 따라 가장 적절한 방식을 사용하는 것이 더 바람직 하다는 관점에서, 의존성이 극히 낮은 설계 방식도 경험해 본다고 이해하시면 좋을 것 같습니다. :) 혹시 이해하시기에 설명이 부족하다면 말씀해주세요!
질문&답변
거짓 양성 감지 노하우 질문입니다
예찬님 안녕하세요. 강의 수강해주셔서 감사드립니다. :)좋은 질문이라고 생각합니다! 실수에서 비롯된 것 같기는 하지만 좋은 경험을 하신 것 같아요. 최대한 이해하실 수 있도록 답변해 보겠습니다.먼저 가볍게 용어 사용을 바로 잡아보면, 테스트가 통과했으니 '거짓 양성'은 아니고 음성의 일종입니다.양성: 테스트 실패음성: 테스트 통과그러면 이제 예찬님이 @RequestBody를 누락해서 테스트를 통과한 것이 '참 음성'인지 '거짓 음성'인지 생각을 해보죠.거짓 음성이라면 테스트는 통과했지만 운영 환경에서는 테스트 통과 기준과 다른 결과가 있을 거에요. 여기서 다른 결과라면,assertThat(response.getStatusCode().value()).isEqualTo(400);코드를 통과하지 못할 결과여야 하니까 상태 코드가 400이 아니어야 합니다.하지만 제 예상으로는 운영 환경에서도 잘못된 비밀번호를 입력했을 때 400 상태 코드가 반환될 것 같습니다. 즉, 테스트 환경과 운영 환경의 결과가 다르지 않은 것이죠.그렇다면 이 테스트 통과는 '거짓 음성'이 아니라 '참 음성'입니다.그런데 예찬님은(그리고 아마도 대부분의 다른 프로그래머들도) 구현에 문제가 있다고 느끼실 거에요. 그래서 단언(assertion)문이 부족한 것이 아닌가 하고 고민하시는 거고요. 정말 무엇인가 부족한 건지 살펴보겠습니다. 구매자 접근 토큰 발행 기능에는 다음 시나리오가 있습니다.올바르게 요청하면 200 OK 상태코드와 접근 토큰을 반환한다접근 토큰은 JWT 형식을 따른다존재하지 않는 이메일 주소가 사용되면 400 Bad Request 상태코드를 반환한다잘못된 비밀번호가 사용되면 400 Bad Request 상태코드를 반환한다IssueShopperToken query 매개변수에서 @RequestBody 애노테이션을 제거하고 판매자 접근 토큰 발행 테스트를 실행하면 결과는 이렇습니다.(사진)위 테스트 시나리오 목록에서 1번과 2번을 만족하지 못합니다.그러니까 @RequestBody 애노테이션은 4번 시나리오 만족을 위해 필요한 코드라기 보다는, 4개 테스트 시나리오를 모두 만족시키기 위해 필요한 코드입니다. 그렇다면 아직은 누락된 테스트 시나리오가 있다고 판단할 근거는 발견하지 못한 것이고요, 테스트가 잘 동작하고 있으니 아직 우리는 치명적인 상황에 놓인 것 같지는 않습니다.정리하면, 잘못된_비밀번호가_사용되면_400_Bad_Request_상태코드를_반환한다 테스트는 통과해야 할 테스트가 통과한다고 할 수 있겠고요. @RequestBody 애노테이션 누락에 대해서는 올바르게_요청하면_200_OK_상태코드와_접근_토큰을_반환한다 테스트와 접근_토큰은_JWT_형식을_따른다 테스트가 실패해서 우리에게 알려줍니다. 구현 코드에서 @RequestBody 애노테이션 빠졌는지 보다는 준비한 테스트 시나리오가 충분한지, 그리고 시스템이 이 시나리오들을 모두 만족시키는지에 더 관심을 가져보면 어떨까요? 구현 코드인 @RequestBody 애노테이션에 집중해서 테스트 코드를 작성하다가는 테스트가 내부 구현에 의존해서 설계가 경직되고, 또 오히려 거짓 음성이 생겨날 수도 있습니다. 만약에, @RequestBody 애노테이션을 제거했는데도 모든 테스트를 통과하고, 누락된 시나리오가 있다는 근거도 없고, 사용자도 아무 불편함 없이 시스템을 사용하고 있다고 가정을 한다면, 그건 테스트에 문제가 있는 것이 아니라 @RequestBody 애노테이션이 불필요한 코드입니다.
질문&답변
질문드립니다.
최강개발자님 안녕하세요. 수강해주셔서 감사드립니다. 🙂질문 답변드리겠습니다.만약에 모든 테스트가 통과하는 상태를 green이라고 한다면, 그 시점까지 작성된 테스트가 API 상태값을 검사하는 테스트까지만 작성되었을 수도 있고 테스트가 더 추가돼서 다른 요구사항을 검사하는 테스트들이 작성되었을 수도 있습니다. 테스트 작성이 어디까지 진행되었는지와 상관없이 커밋을 만들 때는 그 시점까지 작성된 모든 테스트가 통과하는 상태인 것이 바람직합니다.TDD를 사용하지 않아도 API 개발이 끝났다고 판단하는 기준은 항상 있습니다. TDD는 그 기준을 테스트라는 형태로 사전에 작성하는 프로세스죠. 그러니까 작업 완료 기준 자체가 변하는 것이 아니라 그 기준을 표현하는 방식이 다른 것입니다.일반적으로 작업 완료 기준은 모든 요구사항이 만족되었는지입니다. 데이터베이스 스키마를 아무리 잘 설계했어도 요구사항에 포함된 기능이 동작하지 않으면 의미가 없겠죠? 🙂그러니까 말씀하신 '해당 테스트 시나리오를 전부 만족'한다는 것은 모든 요구사항이 만족되었다는 의미이고 그러면 작업이 완료됐다고 판단할 수 있겠습니다. 만약 둘 사이에 차이가 있다면 테스트 시나리오를 충분히 작성하지 못한 것이겠죠.리팩터링은 인터페이스 설계와 기능 동작을 유지하면서 내부 설계를 개선하는 모든 활동을 의미합니다. 입력값을 필터링하는 요구사항과 테스트가 있고 @Valid 애노테이션을 사용하지 않고 테스트를 통과했다면 요구사항을 만족시킨 것입니다. 그런데 @Valid 애노테이션을 사용해서 내부 설계가 더 좋게 개선된다면 리팩터링이라고 할 수 있겠죠.하지만 @Valid 애노테이션을 사용하지 않아도 테스트가 모두 통과했다면 이미 요구사항이 만족된 것이니 반드시 @Valid 애노테이션을 사용하도록 코드를 수정해야 하는 것은 아닙니다. 그렇기 때문에 리팩터링은 필수가 아닌 선택입니다.각 팀의 상황에 최적인 도구를 사용하시면 됩니다. 저는 이슈 트래커를 이용하기도 하고 노션을 이용하기도 합니다. 그리고 오픈소스 프로젝트에서는 강의 실습처럼 마크다운 파일을 사용합니다. 환경에 맞게 작성, 관리, 확인이 수월한 방식을 선택하시면 됩니다.
질문&답변
프로젝트 규모가 큰 경우 @SpringBootTest 실행 속도 문제
답변이 간단하지 않은 시기인 것 같습니다. 우선 강의 범위에서 얘기해 보면요, 이 강의에서는 Spring Boot 앱을 TDD로 개발하는 기본적인 방법을 단단히 다지는 것이 목적이라서 모든 테스트에 @SpringBootTest를 사용합니다. 실습 마지막에 테스트 실행 성능을 개선하는데 Spring 컨텍스트 준비에는 긴 시간이 소비되지 않는다는 것을 알 수 있습니다. 대량 400~700 밀리초 정도 소요되는 것 같습니다. 또 그것이 큰 문제가 되지 않는 이유는 Spring 컨텍스트 구성이 CommerceApiApp_specs에서 사용되는 구성과 @CommerceApiTest에서 사용되는 구성 2개 밖에 없다는 점입니다. Spring 테스트 프레임워크는 서로 다른 컨텍스트 구성마다 새로운 Spring 컨텍스트를 준비합니다. 그러니까 강의에서 100개 이상의 테스트가 실행되는 과정에서 Spring 컨텍스트는 2개만 준비되어 재사용됩니다. Spring 컨텍스트 사용이 문제가 되는 경우는 테스트 대역을 적극적으로 사용해서 테스트 클래스 마다 테스트 대역 설정 때문에 컨텍스트 구성이 계속 달라질 때입니다. 이런 경우 Spring 테스트 프레임워크는 새로운 Spring 컨텍스트를 계속 준비하기 때문에 실행시간이 길어지게 됩니다. 과도한 테스트 대역 사용이 일으킬 수 있는 문제 중 하나죠.범위 상 이번 강의에서 테스트 대역을 다루지는 않았지만 테스트 대역을 사용하면서도 Spring 컨텍스트를 재사용할 수 있는 방법도 있기는 합니다. 저는 얼마전까지 현업에서는 @SpringBootTest 등 Spring 컨텍스트를 사용한 테스트는 10~20% 정도를 하고 나머지는 Spring 독립적인 순수한 모델 영역의 범위를 크게 만들어서 이 부분을 대상으로 80~90% 정도 테스트를 했습니다. 그러면 Spring 컨텍스트 준비 시간이 방해가 되는 일이 많지는 않았습니다. 물론 그렇다고 하더라도 Spring 컨텍스트 준비 시간이 길어지는 것은 여러모로 좋지 않기 때문에 Spring 컨텍스트 준비 시간을 관리하긴 합니다. 그렇게 준비 시간이 너무 길어지지 않는다고 하면 이제는 Spring 컨텍스트를 사용하는 테스트가 가진 단점의 영향이 얼마나 될지 고민이 됩니다. 항상 코딩 AI 에이전트와 협업하기 때문인데요. 직접 코딩하는 시간 대비 AI에게 지시할 업무를 계획하는 시간이 점점 늘어나다 보니까 테스트 실행 성능에 대해서 과거보다 점점 관대해 지는 것 같습니다. 실행 시간을 견딜 수 있다면 외부 인터페이스 대상 테스트가 가진 장점의 효과가 더 커져서, 요즘 바이브 코딩을 연습하면서 테스트 실행 시간에 대한 생각이 다시 정립되는 느낌입니다. 실용적인 방법을 찾으시는 데 도움이 됐으면 좋겠습니다.
이규원님의 소개 - 인프런