드디어 시작된 백엔드 과정에서는 지난번 프론트엔드의 3챕터였던 테스트과 TDD를 학습했습니다.
지난번 작성했던 Node.js의 테스트 과정과 크게 다른 점은 없었습니다.
오랜만에 써보는 Java 문법과 처음 제대로 사용해보는 IntelliJ 환경이 살짝 어색했지만,
다행히도 하루 정도 치다보니 익숙해졌습니다.
다만 아쉬웠던 점은 아직 TDD를 준수하며 테스트를 우선 작성하는 방법을 잘 진행했지만,
테스트 툴에 대한 이해도, 실제 Service, Domain을 설계하는 역량, 책임을 분리하는 원칙 등에서 부족함을 보였습니다.
Unit Test: UserPoint의 비즈니스 로직 메서드 PointService에 대한 테스트를 대상으로 선정했습니다.Integration Test: API를 호출하는 것으로 Controller -> Service -> Record 전체 흐름을 블랙박스 관점에서 검증하는 것으로 테스트를 작성했습니다."Red → Green → Refactor"
호출할 빈 함수를 선언하는 것을 제외하고, TDD 개발 기법을 준수하기 위해 노력하였습니다.
요구사항 별 PointService 테스트 RED -> GREEN -> REFACTOR (커밋순)
| 순번 | 요구사항 | RED | GREEN | REFACTOR |
|---|---|---|---|---|
| 1-1 | 포인트_충전_성공() | 4b92656 | d674e9b | |
| 2-1 | 포인트_사용_성공() | 4b92656 | 2394a78 | |
| 2-2 | 포인트_사용_실패() | 4b92656 | 2394a78 | |
| 3 | 포인트_조회() | 11e1402 | acf5b36 | |
| 4 | 포인트_히스토리_조회() | c1d6827 | 16a5e90 | |
| 1-2 | 포인트_충전_실패_0보다_작은_금액() | 71d3641 | 7ece964 | |
| 1-3 | 포인트_충전_실패_최대_제한_금액() | fa9c042 | 3ce3d9d |
요구사항 별 PointController 테스트 (커밋순)
PointController에 return 이 new UserPoint(0, 0, 0)이 고정되어있어, 충전 테스트를 먼저 작성하였으나 isOk가 통과가 되는 현상을 확인했습니다.Controller 미구현으로 point는 0인 것을 확인 후 GREEN 진행하였습니다.| 순번 | 요구사항 | RED | GREEN | REFACTOR |
|---|---|---|---|---|
| 1-1 | 포인트_충전_정상_동작() | f9fb924 | 658fd79 | |
| 2 | 포인트_조회_동작() | 50488a9 | 0d3f66b | |
| 1-2 | 포인트_충전_실패_0보다_작은_금액() | d084247 | (즉시 통과) | |
| 1-3 | 포인트_충전_실패_최대_제한_금액() | d084247 | (즉시 통과) | |
| 3-1 | 포인트_사용_성공() | f0140eb | b1ecac4 | |
| 3-2 | 포인트_사용_실패_포인트_부족() | e6cce35 | (즉시 통과) | |
| 4 | 포인트_히스토리_조회() | 4973f8d | d112070 |
초반에는 REFACTOR를 단순 코드 정리 수준으로만 생각하고 커밋을 남겼습니다.
이후에는 테스트 코드 작성의 관점에서 반복되는 세팅 함수들의 재사용까지를 포함하기 위해 별도로 전체 REFACTOR를 수행하였습니다.
| 순번 | 파일명 | 대상 | REFACTOR |
|---|---|---|---|
| 1 | PointServiceTest.java | 충전(), 사용() | 438ae4a |
| 2 | PointServiceTestWithMockito.java | 조회_응답_설정(), 저장_응답_설정() | 5d81377 |
| 3 | PointControllerTest.java | 충전(), 사용() | 46765eb |
주요 로직 코드와 검증 코드는 가독성을 위해 그대로 두어 크게 코드량이 줄지는 않았습니다.
다만, 추후 테스트 코드를 작성할 때, 더 유용하게 사용할 수 있을 것 같다고 느꼈습니다.
동시성 문제는 여러 스레드, 혹은 여러 요청이 공유자원(해당 프로젝트에선
UserPoint)에 동시에 접근할 때,
과정중에 읽고 > 연산하고 > 쓰는 과정이 분리된 상태에서 Lock 없이 진행될 때 발생합니다.
userId로 요청을 보내는 경우 읽는 과정에서 동일한 데이터를 확인하고, 연산 후 쓰는 과정에서 하나가 다른 하나를 덮어쓰는 갱신 손실을 발생시키는 문제가 있습니다.JUnit 테스트 환경에서는 단일 함수들이 단일 스레드에서 직렬적으로 실행되기 때문에 동시성 문제가 감춰질 수도 있습니다.동시성 문제가 발생하는 근본적인 원인이 공유 자원에 대한 비원자적인 접근에 의한 것이기 때문에,
Lock 또는 원자적 연산으로 임계 구역을 보호해야 합니다.
Critical Section) 보호: 여러 프로세스에 의해 공유되는 데이터 또는 자원에 대하여 한 프로세스만 사용하도록 제한하는 방법을 말합니다.Mutual Exclusion): 공유된 자원에 대하여 하나의 프로세스만 진입할 수 있도록 하는 것을 의미합니다.Mutual Exclusion): 하나의 프로세스만 임계 구역에 진입할 수 있어야 한다.Progress): 임계 구역이 비어있는 경우, 대기중인 프로세스 중 하나가 언젠가 진입해야 한다.Bounded Waiting): 한 프로세스가 무한히 대기하지 않도록 보장해야 한다.(기아상태 방지)synchronized, ReentrantLock (Java 동기화 키워드/클래스) 👈🏻 현재 적용한 방법(@Synchronized)UPDATE user_point SET point = point + ? WHERE id = ?CQRS + 이벤트 소싱검증 도구 사용 미숙
테스트 설계의 구조 부족
역할과 책임의 불분명한 분배
예외 처리 및 입력 유효성 검증의 부족
도메인 중심 사고 부족
테스트 중복 및 과도한 상세 검증
과제를 제출하기 전부터 TDD보다 나에게 부족한 점들이 느껴졌었습니다.
그런 내용들을 과제 피드백에서 문장으로 받으니 내가 공부해야 할 것들에 대하여 정리를 할 수 있게 되었습니다.
완성한 코드가 아니라 개발 과정의 커밋을 보신 것 같긴했지만,
실제로 생각만하고 적용을 안한 부분들도 있고, 스스로 생각하기에 완전히 미숙한 개념들도 있어
다음 과제부터 완성된 커밋을 바탕으로 PR을 작성해야겠다는 생각을 하였습니다.