
오늘 목표는 “회원가입 API + 통합테스트(Testcontainers)”였는데, 구현보다 환경/설정/Gradle/IDE 에러 처리가 더 큰 비중을 차지했다. 결과적으로 다음을 정리했다.
- DTO/엔티티 설계 관점(왜 엔티티는 생성 제한하고 DTO는 덜 엄격한지)
- 정적 팩터리 네이밍(of/from)
- 테스트 전략(단위/슬라이스/WebMvcTest/통합테스트)
- Testcontainers + MySQL 세팅과 application-test.yml 정석
- 실제로 뜬 에러들(Import 안 뜸, ApplicationContext 실패, Enum 변환 실패 등)
- 실무형 이슈/PR/커밋 운영(스쿼시/머지 차이 포함)
DTO는 ‘도메인 무결성’을 지키는 목적이 아니고 “전송”이 목적이라 외부 생성 가능해도 보통 문제로 보지 않는다.
클라이언트가 서버 DTO를 “직접 new 해서” 서버에 넣는 구조가 아님(그건 서버 내부 코드임)
중요한 건 서버가 어떤 DTO를 만들어서 반환하느냐이고, DTO 자체를 잠그는 건 보통 우선순위가 낮다.
반대로 엔티티는 다르다.
엔티티는 단순 데이터 묶음이 아니라 도메인 규칙/무결성을 가진 객체라서, “아무나 new”를 허용하면 규칙이 깨진 상태로 퍼질 수 있다.
그래서 실무에서 많이 하는 패턴:
생성자 protected/private + 정적 팩터리로만 생성
생성 시점에 필수값 검증 / 초기 상태 강제 / 불변 조건 강제
즉, DTO는 느슨, 엔티티는 엄격이 기본 성향.
오늘 질문했던 “of/from 언제 쓰냐” 정리:
of(...): 값들을 받아 그대로 조립해서 생성 (변환 뉘앙스 약함)
from(...): 다른 타입(객체/DTO/엔티티)에서 꺼내 변환해 생성 (변환 뉘앙스 강함)
ex)
UserRole.of("USER")
SignupResponse.from(user)
회원가입은 DB 저장이 핵심이라 통합테스트 가치가 크다.
단위테스트: 빠르지만 DB 제약조건/실제 저장/트랜잭션은 검증이 약함
통합테스트(@SpringBootTest + DB): 실제 흐름(서비스→레포→DB) 검증 가능
그리고 @WebMvcTest / 슬라이스 테스트도 얘기했는데:
슬라이스 테스트 = 애플리케이션을 “일부 레이어만” 띄워 테스트
@WebMvcTest = MVC(컨트롤러) 슬라이스만 띄움 (서비스는 보통 Mock)
오늘은 “회원가입 저장까지”가 목적이라 통합테스트 중심으로 갔다.
로컬 DB 연결은 내 PC에서는 잘 되는데 아래가 흔히 문제다:
팀원/다른 PC/CI 환경에서 DB가 없거나 설정이 달라서 깨짐
환경변수/포트/계정 등 편차가 커서 “재현”이 어려움
Testcontainers는:
테스트 실행 시점에 도커로 MySQL을 띄우고
스프링이 그 DB에 붙게 만들어서
어디서 돌려도 동일한 DB 환경으로 테스트가 돌아간다(특히 CI에서 강력)
즉, 실무에서 “내 컴퓨터에서만 되는 테스트”를 줄이려는 목적.
@Testcontainers를 붙이려는데 import 자동완성이 안 뜸
빨간줄 + 심볼 못 찾음
Gradle 의존성 동기화가 안 됨
IntelliJ 인덱스/캐시 꼬임
Gradle refresh(우측 Gradle 패널에서 Reload)
IntelliJ 재시작 / Invalidate Caches / Restart
그래도 안 되면 ./gradlew clean test로 “Gradle 기준” 정상 여부부터 확인
ClassNotFoundException: ...ApplicationTests./gradlew test --tests ... 했는데 테스트 클래스를 못 찾음기본 @SpringBootTest조차 컨텍스트 로딩 실패
로그에 ${DB_URL} 그대로 찍힘
src/test/resources/application-test.yml에 ${DB_URL} 같은 개발 환경처럼 환경변수 설정해주기@DynamicPropertySource로 container의 JDBC URL/계정을 스프링에 주입
이 방식이 “환경변수 없이도” 돌아가서 CI에 특히 강하다.
Testcontainers는 “컨테이너를 띄우는 테스트”라서 도커 엔진이 필요하다.
Docker Desktop이 꺼져 있으면 컨테이너를 못 띄워서 테스트가 실패한다.
“그냥 켜기만 하면 되나?”
→ 보통은 켜고, WSL2/엔진 정상 동작 상태면 충분. (이미지 pull이 필요할 수는 있음)
요청 role을 "user"로 넣었더니 enum 변환 실패
핵심 버그:
return UserRole.valueOf(role); // role이 소문자면 바로 터짐
해결
return UserRole.valueOf(role.toUpperCase());
문제
Cannot resolve method 'extracting(<method reference>)'
thenThrownBy(() -> authService.signup(req2))
.isInstanceOfSatisfying(BaseException.class, ex ->
then(ex.getErrorCode()).isEqualTo(ErrorCode.USER_EMAIL_DUPLICATED)
);
isInstanceOfSatisfying 방식 사용간단히:
Open Session In View(OSIV)는 “웹 요청이 끝날 때까지 영속성 컨텍스트를 열어두는 옵션”
켜져 있으면 컨트롤러/뷰까지 lazy loading이 가능해서 편하지만,
트랜잭션 경계가 애매해지고 성능/쿼리 예측이 어려워져서 실무에선 false로 두는 팀이 많다.
정답은 “팀/서비스 성격 따라 둘 다 씀”.
조회 메서드가 많으면:
클래스에 @Transactional(readOnly = true)
쓰기 메서드만 @Transactional 따로
쓰기가 많거나 단순하면:
클래스에 @Transactional
조회 메서드만 readOnly = true
헷갈렸던 포인트:
“이슈 연결”은 보통 커밋보다 PR 본문에서 Closes #12로 연결하는 게 정석
커밋에 (#12)를 다는 건 팀 컨벤션이면 OK지만 필수는 아님
Squash merge를 쓰면 PR 커밋 여러 개가 메인에 1개로 합쳐져 들어가서 메인이 깔끔해짐