한줄요약 : 테스트를 통과시키기 위해 JPA의 영속성 관리와 트랜잭션 범위, 올바른 예외 처리의 중요성을 깨달은 개발기.
새로운 아키텍처, 그리고 탑다운 테스트
최근 새로운 부트캠프 프로젝트를 시작하며 interfaces-application-domain-infrastructure라는계층형 아키텍처를
처음 접하게 되었다. 익숙했던 Controller-Service-Repository 구조와는 사뭇 다른 이 구조에서, '포인트 충전 및 조회'
기능을 구현하는 과제를 받았다.
월요일에 진행한 Alen님의 TDD 라이브 코딩을 보면서 탑다운 방식의 TDD 접근 방식을 흉내 내 보기로 했다.
가장 바깥 계층인 API의 성공 시나리오인 E2E 테스트를 먼저 작성하는 것 부터 시작하였으며 그 중
"존재하는 유저가 1000 포인트를 충전하면, 충전된 최종 금액이 반환된다"라는 포인트 충전 테스트, 이 하나의 테스트를
통과시키기 위한 여정은 생각보다 많은 것을 알려주었다.
문제 : TransientPropertyValueException과 영속성 컨텍스트
🤔 내 생각의 흐름
가장 먼저 테스트 환경을 준비(arrange)해야 했다.
테스트를 위해서는 특정 유저와 그 유저의 초기 포인트 정보가 DB에 존재해야 한다. 나는 당연하게 아래와 같이 코드를 작성했다.
/ 처음 작성했던 코드
// 1. 유저 객체를 만들어 회원가입
UserModel userModel = new UserModel(
"testUser", "MALE", "2025-07-15", "test@test.com"
);
userJpaRepository.save(userModel);
// 2. 그 유저 객체로 포인트 객체를 만들고 포인트 기본 세팅
PointModel pointModel = new PointModel(user, 10L);
pointJpaRepository.save(pointModel); // 💥 여기서 에러!
코드는 단순했다. 유저를 만들고 저장한 뒤, 그 유저 정보로 포인트를 만들고 저장하면 될 것이라 생각했다.
하지만 테스트는 TransientPropertyValueException이라는 낯선 예외를 뱉어냈다.
에러 메시지는 "아직 저장되지 않은(transient) UserModel을 참조하는 PointModel은 저장할 수 없다"는 뜻이었다.
💭 판단과 깨달음
분명 userJpaRepository.save()를 호출했는데 왜 아직도 'transient' 상태일까?
여기서 JPA의 영속성 컨텍스트와 save() 메소드의 동작 방식을 다시 들여다보게 되었다.
repository.save() 메소드는 엔티티를 저장한 후, 데이터베이스와 동기화가 완료된, 즉 id가 부여된 영속(Persistent) 상태의 객체를 '반환'해준다는 점을 간과했다.
내가 PointModel을 만들 때 사용했던 userModel 변수는 save()를 거쳤음에도 불구하고, 여전히 메모리에 처음 생성된, id가 없는 객체를 가리키고 있을 수 있었던 것이다.
📐 리팩토링: save()의 반환값을 사용하다
userJpaRepository.save()의 반환값을 새로운 변수에 받아, 그 변수로 PointModel을 생성하니 문제는 간단히 해결되었다.
// 수정 후 코드
UserModel userModel = new UserModel(
"testUser", "MALE", "2025-07-15", "test@test.com"
);
// ✅ save()가 반환한, DB에 완전히 저장된 객체를 사용!!
UserModel user = userJpaRepository.save(userModel);
// ✅ 반드시 이 'user'로 자식 엔티티를 생성한다.
PointModel pointModel = new PointModel(user, 0L);
pointJpaRepository.save(pointModel);
JPA에서 연관된 엔티티를 저장할 땐, 부모 엔티티를 저장한 후 반드시 그 반환값을 사용해야 한다.
라는 첫 번째 원칙을 배우게 되었다.
> 지금 생각하면 당연한건데 당시에는 몇시간동안 허비하고 화딱지가 나서 전부 초기화하고 다시 진행했다..
마치며: 테스트는 나의 길잡이
처음 작성했던 단 하나의 E2E 테스트는 수많은 예외를 발생시키며 나를 괴롭혔다.
하지만 돌이켜보면, 그 실패하는 테스트들은 단순한 '에러'가 아니었다.
TransientPropertyValueException은 나에게 JPA 영속성 컨텍스트의 동작 방식을
헤더 누락 테스트가 500 에러를 뱉어냈을 땐, 특정 예외를 처리하는 핸들러가 필요하다는 사실을 알려주었다.
결국 하나의 테스트를 통과시키는 과정은 단순히 코드를 추가하는 행위가 아니라, 내가 사용하는 프레임워크의
동작 원리를 더 깊게 이해하고 더 견고한 설계를 고민하는 값진 여정이었다.
앞으로도 나는 실패하는 테스트를 두려워하지 않고, 그 안에 담긴 힌트를 길잡이 삼아 나아가려 한다.
'Loopers > 테크니컬 라이팅' 카테고리의 다른 글
| Kafka 찍먹해보기! : Kafka통한 서비스 경계를 넘는 이벤트 파이프라인을 구축기 (0) | 2025.09.05 |
|---|---|
| Spring 이벤트를 처음 써보며 깨달은 것들 (1) | 2025.08.28 |
| DB 조회 및 정렬 성능 개선하기(비정규화, 인덱스, 캐시(Redis)) (1) | 2025.08.15 |
| 동시성 테스트(Flaky Test 삽질기) (4) | 2025.08.09 |
| 다들 이해하지?! 설계의 청사진 시퀀스 다이어그램 (0) | 2025.07.25 |