티스토리 뷰
서비스의 규모가 커짐에 따라 수정 사항도 늘어나고, 서로 간의 의존성도 커지면서
다른 한쪽의 문제를 예상하지 못하고 수정해버려서 발생하는 이슈가 늘어났다.
그래서 개발을 할 때나 수정할 때 점점 부담이 많이 생기게 되서, 제대로된 테스트의 도입 방식을 검토했다.
이전에 강의를 하나보고 여기에 감명을 받았다.
2024.04.15 - [일상] - [인프런 강의] Java/Spring 테스트를 추가하고 싶은 개발자들의 오답노트를 듣고
위 그래프처럼 개발 양은 많아지는데, 이게 잘 동작할까? 에 대한 부담감이 점점 커지는 상황이라 조치가 필요해졌다.
그러나 테스트 도입에는 정말 많은 애로 사항들이 있었는데... 다음과 같은 의사결정이 필요했다.
1. 테스트의 종류 선정: 몇 종류의 테스트를 실행할 것인가?
2. 어떤 테스트 라이브러리를 선택인가?
3. 어떤 테스트 방식을 선택할 것인가?
이 내용들을 정리하면서, 어떤 방식을 선택했는지를 정리해봤다.
이 글은 헥사고날 아키텍처로의 리팩토링의 이어지는 글이라 헥사고날 아키텍처 기반의 테스트를 다룬다.
테스트의 종류
테스트는 테스트 코드를 구현할 수 있는 수량과 테스트의 크기에 따라 아래 그림과 같이 나눠진다.
그림에서도 표현되어 있지만, 아래로 갈수록 독립성이 커지면서 빠르고, 위로 갈수록 느려지고 의존성들이 늘어난다.
단위 테스트(Unit Tests)
단위 테스트는 소프트웨어 개발 과정에서 가장 작은 단위의 테스트이다. 예를 들어 함수나 메서드 등을 대상으로 하며 각 테스트가 개별적으로 정상적으로 동작하는지 확인하는게 목적이다.
단위 테스트는 빠르게 실행되며, 개발 초기 단계에서 버그를 발견하고 수정하는 데 효과적이다. 단위 테스트는 소프트웨어의 작은 부분만을 대상으로 하기 때문에, 버그의 원인을 빠르게 파악할 수 있다는 장점이 있다.
통합 테스트(Integration Tests)
통합 테스트는 두 개 이상의 단위가 함께 잘 작동하는지 확인하는 테스트이다. 단위 테스트에서 검증된 개별 단위들이 통합 후에도 올바르게 동작하는지 확인하기 위해 수행한다.
통합 테스트는 단위 테스트보다 실행 시간이 길고, 복잡하다. 통합 테스트는 소프트웨어의 여러 부분을 함께 테스트하기 때문에 실제 스프링 빈들을 사용하는 경우가 많다. 따라서 규모가 큰 통합 테스트는 개발의 후반 단계에서 수행하게 된다.
UI 테스트(UI Tests)
유닛 테스트, 통합 테스트 등 보다 대형 테스트 기법으로 분류되며, 실제 앱을 사용자가 이용하는 흐름에 따라 결과를 보는 테스트이다. 자동화 도구로는 셀레니움(Selenium)이나 혹은 UI Test 툴들을 사용한다. 테스트의 이름 그대로 유저가 바라보는 화면에서 테스트해보는 방식이다.
E2E 테스트라고도 하며, 수동으로 진행하는 Manual Test도 여기에 포함된다.
어떻게 쓰면 좋을까?
헥사고날 아키텍처에서의 테스트해야 되는 부분들은 다음과 같다.
1. 인바운드 포트와 어댑터(컨트롤러)
2. 애플리케이션 계층(서비스 로직)
3. 도메인 계층(도메인 모델과 도메인 로직)
4. 아웃바운드 포트와 어댑터(영속성)
일단 모든 단위 테스트를 구현하지는 않을 계획이다.
이 방식은 현재 조직에서의 개발방식과 어울리지 않는 방식이고, 무엇보다 실장님이 선호하지 않는다.
그래서 컨트롤러에서의 통합 테스트는 필수로 작성하고,
복잡한 서비스 로직과 도메인을 다루는 부분과 한눈에 들어오지 않는 쿼리를 테스트하기로 결론을 내렸다.
몇 가지 세부적인 기준도 정했다.
(단순 CRUD는 테스트 x, 몇 개의 테이블이 join 될 때, 조건에 따라 응답 값이 바뀔 때 등등)
컨트롤러 계층의 단위 테스트는 애플리케이션 계층의 응답을 그대로 뱉어내는 역할이라 컨트롤러 단의 단위 테스트까지 필요해 보이진 않았다.
어떤 라이브러리를 사용할 것인가?
Java에서는 테스트 라이브러리의 스택을 정하기 어렵지 않다.
가장 보편적인 스택인 Junit5와 Mockito를 주로 사용하고 필요에 따라 라이브러리를 추가하면 되는데
현재 선 도입되는 테스트 환경은 Kotlin 환경이다.
Kotlin에서만 제공되는 테스트 라이브러리들이 있어서 Java에서 사용하는 것들과 비교해봤다.
먼저 Kotest와 Junit5 그리고 Mockito와 MockK를 각각 정리해봤다.
(다른 라이브러리들도 많지만, 레퍼런스가 많은걸 선택하고 비교)
Junit5와 Kotest
너무 보편적인 스택들이라 자세한 설명은 생략하고, 아무래도 Kotest 쪽이 조금 더 가독성이 좋다 판단했다.
그리고 다양한 테스트 스펙들을 템플릿 처럼 제공한다는 것도 장점이라 생각했다.
Mockito와 MockK
양쪽다 비슷해보이는데, 조금 더 직관적이고 간결한 MockK를 좋아보였다.
그리고, Mockito는 Kotlin의 확장함수를 지원하지 않는다. 그리고 몇몇 kotlin에서 지원하는 기능을 커버치지 못해서,
모킹 라이브러리는 MockK를 선택했다.
정리하면, Kotest와 MockK를 사용하기로 했다.
어떤 테스트 방식을 사용할 것인가?
테스트 방식도 여러가지 있다.
1. TDD (Test-Driven Development)
정의: 테스트를 먼저 작성하고 그 후에 코드를 구현하는 개발 방법론
과정: Red(실패하는 테스트 작성) → Green(테스트를 통과하는 최소한의 코드 작성) → Refactor(코드 개선)
장점: 코드 품질 향상, 회귀 테스트 용이, 설계 개선
2. BDD (Behavior-Driven Development)
정의: 비즈니스 요구사항에 초점을 맞춘 개발 방법론
특징: "Given-When-Then" 구조를 사용하여 시나리오 기반 테스트 작성
장점: 비개발자와의 커뮤니케이션 향상, 명확한 요구사항 정의
3. ATDD (Acceptance Test-Driven Development)
정의: 인수 테스트를 기반으로 하는 개발 방법론
특징: 고객의 요구사항을 인수 테스트로 변환하여 개발 진행
장점: 고객 중심의 개발, 요구사항 충족 여부 명확히 확인 가능
기타 등등
TDD 처럼 모든 테스트를 구현할게 아니라면, BDD가 가장 적합하다고 생각했다.
특히 Kotest에서 BDD는 의도를 명확하게 파악할 수 있게끔 given, when, then 구문을 깔끔하게 작성할 수 있다.
자바에서도 Mockito에서 MocktoBDD라는 기능을 제공해 BDD 스타일로 테스트를 구성할 수 있다.
확실히 코틀린 쪽이 가독성이 높고, 코드가 간결하다.
Kotest에서 제공하는 테스트 방식은 BDD 외에도 몇 가지 방법이 더 있는데, 이 부분은 필요할 때마다 선택해서 사용하면 좋다. (참고 URL)
그래도 가장 적합한건 BDD라 생각되어 선정했다.
쿼리 테스트는...
현재는 Spring Data JPA + QueryDSL을 사용하고 있다.
쿼리가 복잡한 경우가 있다보니 JPA의 동작 테스트도 필요해보였다.
@DataJPATest 어노테이션을 이용하고 H2 Memory DB를 이용해 간단히 테스트하는 정도로 마무리했다.
레퍼런스가 많아서 코드는 따로 첨부하지 않았다.
정리
TDD와 같은 모든 단위 테스트 구현 방식은 현재 우리 조직에 적합하지 않다.
인원이 적고 개개인이 다양한 피쳐를 맡아 스프린트 방식으로 진행하고 있기 때문이다.
그래도 필요할 때마다 코드 리뷰나 PR 체크는 하기 때문에 다음과 같이 정리하기로 했다.
1. Controller 단에서 진행하는 통합테스트는 반드시 넣기
2. 서비스 로직이 단순한 CRUD가 아니라고 판단될 때 추가
3. QueryDSL/Spring Data JPA의 구현이 복잡해지는 것 같으면 추가
사실상 내가 리드라 코드의 의미를 잘 모르겠으면... 추가하라고 해야할 것 같다.
Github
https://github.com/imsosleepy/spring-test-repository
마치며
회사에서 테스트 컨벤션을 맞추고 도입하기 위해 진행했던 세미나를 각색했다.
테스트를 진행하기 앞서 헥사고날 아키텍처의 도입을 논의했었는데, 뭐... 다른 분들은 늘 의견이 없다.
그래서 일단은 프로젝트 사이즈가 클 거라 예상 되는 것부터 아키텍처를 도입하고 테스트를 진행하기로 결정했다.
왜 항상 에러를 내는 사람은 같은 실수를 반복하는걸까?
이젠 좀 사이드 이펙트가 줄기를 기대한다...
'개발 > SPRING' 카테고리의 다른 글
테스트를 도입하면서 느낀점과 고민들 (0) | 2024.08.14 |
---|---|
SpringBoot에서 MultipartFile 전송하기 feat. FeignClient (1) | 2024.07.23 |
Layered 아키텍처에서 Hexagonal 아키텍처로 리팩토링하면서 느낀 점 (0) | 2024.07.04 |
OpenFeign 간단 사용법과 FeignException 핸들링하기 (0) | 2024.03.22 |
Google Document Translation API 사용하기 (with. Spring Boot) (1) | 2024.02.20 |
- Total
- Today
- Yesterday
- OpenAI
- S3
- chat GPT
- 인프런
- lambda
- MySQL
- AOP
- Elastic cloud
- springboot
- JWT
- Spring
- java
- cache
- terraform
- Kotlin
- CloudFront
- EKS
- serverless
- elasticsearch
- awskrug
- openAI API
- 스프링부트
- OpenFeign
- GIT
- Log
- AWS
- AWS EC2
- 람다
- ChatGPT
- docker
일 | 월 | 화 | 수 | 목 | 금 | 토 |
---|---|---|---|---|---|---|
1 | 2 | |||||
3 | 4 | 5 | 6 | 7 | 8 | 9 |
10 | 11 | 12 | 13 | 14 | 15 | 16 |
17 | 18 | 19 | 20 | 21 | 22 | 23 |
24 | 25 | 26 | 27 | 28 | 29 | 30 |