AI Agent를 이용한 테스트 코드 경험
TN;DR: 테스트 코드와 Facade 패턴에 대한 초심자의 이해
1. 처음 접하는 테스트 코드(부끄럽게도!)
나는 테스트 코드를 짜본 적이 없다. 테스트 코드의 필요성은 알고 있었지만 회사에서는 권장하지 않았기 때문이다. 그래서 과제를 받았을 때 이론부터 감을 잡아야 했다.
멘토님이 설명해 주신 다섯 가지의 테스트 객체(Dummy, Stub, Spy, Mock, Fake)는 나에게 너무 추상적이었다. 다 비슷비슷하게 보였다. 실제 사용해야 할 때 가장 적당한 것을 선택하기 힘들어 보였다. 그래서 일단 이들을 검색해서 닥치는 대로 예시를 읽었다. 아래는 내가 이해한 테스트 객체이다.
Dummy는 가장 정적인 객체로 보았다. 빈 껍데기만 필요할 때 사용하기 좋은 것 같았다.
Fake는 그보다는 조금 더 구체적 객체로 보았다. 하드코딩된 데이터베이스 객체와 비슷해 보였다. "완전히 독립적인"
Stub은 Dummy의 동작형으로 설명되던데, 하드코딩된 껍데기로 보였다. 의도한 결과만을 위한 껍데기다.
Spy는 호출된 횟수가 필요할 때 적합하다고 들었는데, 약간 이력을 확인하는 데에 특화된 객체 같다. Mokito의 verify와 같다고 한다. "일부만 조작"이라는 설명이 인상적이었다.
Mock은 가장 정교하고 자주 쓰이는 것 같았다. Mokito도 Mock의 일종이라고 한다. 가장 복잡하기 때문에 심플해야 하는 테스트 코드에서 에너지가 필요 이상으로 쓰일 수 있다는 그런 느낌이었다.
멘토님이 상태검증과 행위검증의 개념에 대해서도 설명해 주셨는데, 검색을 통해 리마인드하며 구체화할 수 있었다.
상태 검증은 인풋과 아웃풋을 바라본다. 블랙박스 테스트와 유사해 보였다. Stub이 주로 사용된다고 한다.
행위 검증은 디버깅처럼 라인마다 pass/fail을 확인하는 것 같았다. Mock이 주로 사용된다고 한다.
그리고... 다양한 글을 읽고 다시 멘토님의 정의를 보니 정말 간결하고 핵심만을 설명하는 문장들이었다. 이론이 쏙쏙 정리되었다.
2. Facade 패턴
내가 그동안 프로젝트에서 경험했던 구조는 Controller, Service, Repository 그리고 myBatis였다. 이번 프로젝트에서 만나게 된 구조는 한눈에 플로우도 알아보기 힘들 정도로 낯선 패턴이었다. 그래서 AI의 도움과 여러 문서의 도움을 받아 패턴을 이해하기 위해 노력했다.
application
ㄴuser
ㄴUserFacade.java
ㄴUserInfo.java
domain
ㄴuser
ㄴUser.java
ㄴUserRepository.java
ㄴUserService.java
infrastructure
ㄴuser
ㄴUserJpaRepository.java
ㄴUserRepositoryImpl.java
interface
ㄴapi
ㄴuser
ㄴUserV1ApiSpec.java
ㄴUserV1ontroller.java
ㄴUserV1DTO.java
요청 흐름
1. HTTP Request
2. UserV1Controller (interface)
- UserV1DTO로 요청 데이터 수신 - 유효성 검증
3. UserFacade (application)
- DTO → 도메인 객체 변환 - 트랜잭션 시작
4. UserService (domain)
- 비즈니스 로직 수행 - UserRepository 인터페이스 호출
5. UserRepositoryImpl (infrastructure)
- UserJpaRepository를 통해 DB 접근 - User 엔티티 조회/저장
6. UserService (domain) - 비즈니스 규칙 적용
- User 엔티티 반환
7. UserFacade (application)
- User → UserInfo 변환 - 트랜잭션 커밋
8. UserV1Controller (interface)
- UserInfo → UserV1DTO 변환 - HTTP Response 반환
레이어별 역할
1. Interface 레이어 (외부 진입점)
UserV1Controller → UserV1ApiSpec (API 명세)
→ UserV1DTO (데이터 전송 객체)
- 역할: HTTP 요청을 받아 처리하는 프레젠테이션 계층
- 책임: 요청/응답 변환, 유효성 검증, API 스펙 준수
2. Application 레이어 (유스케이스 조율)
UserFacade → UserInfo (응답용 DTO)
- 역할: 비즈니스 유스케이스 조율 및 트랜잭션 경계
- 책임: 여러 도메인 서비스 조합, DTO 변환, 트랜잭션 관리
3. Domain 레이어 (핵심 비즈니스 로직)
UserService → User (엔티티)
→ UserRepository (인터페이스)
- 역할: 핵심 비즈니스 규칙과 로직
- 책임: 도메인 로직 수행, 외부 의존성을 인터페이스로 추상화
4. Infrastructure 레이어 (기술 구현)
UserRepositoryImpl → UserJpaRepository (JPA 구현체)
- 역할: 외부 시스템과의 실제 연동 구현
- 책임: DB 접근, 외부 API 호출 등 기술적 세부사항
3. AuthenticatedUserArgumentResolver는 사용하지 않는 이유가 있을까?
이것은 나 혼자 부끄러운 이야기다. AI Agent가 interface 레이어 내부에 auth 패키지를 만들어 줬다. 내부에는 AuthenticatedUserArgumentResolver.java가 있었고, 구현 내용은 이러했다.
package com.loopers.interfaces.api.auth;
import com.loopers.support.error.CoreException;
import com.loopers.support.error.ErrorType;
import org.springframework.core.MethodParameter;
import org.springframework.stereotype.Component;
import org.springframework.web.bind.support.WebDataBinderFactory;
import org.springframework.web.context.request.NativeWebRequest;
import org.springframework.web.method.support.HandlerMethodArgumentResolver;
import org.springframework.web.method.support.ModelAndViewContainer;
@Component
public class AuthenticatedUserArgumentResolver implements HandlerMethodArgumentResolver {
private static final String HEADER_LOGIN_ID = "X-Loopers-LoginId";
private static final String HEADER_LOGIN_PW = "X-Loopers-LoginPw";
@Override
public boolean supportsParameter(MethodParameter parameter) {
return parameter.getParameterType().equals(AuthenticatedUser.class);
}
@Override
public Object resolveArgument(
MethodParameter parameter,
ModelAndViewContainer mavContainer,
NativeWebRequest webRequest,
WebDataBinderFactory binderFactory
) {
String loginId = webRequest.getHeader(HEADER_LOGIN_ID);
String password = webRequest.getHeader(HEADER_LOGIN_PW);
if (loginId == null || loginId.isBlank()) {
throw new CoreException(ErrorType.BAD_REQUEST, "X-Loopers-LoginId 헤더가 필요합니다.");
}
if (password == null || password.isBlank()) {
throw new CoreException(ErrorType.BAD_REQUEST, "X-Loopers-LoginPw 헤더가 필요합니다.");
}
return new AuthenticatedUser(loginId, password);
}
}
나는 이 java 파일의 사용처를 찾아보았고, 찾을 수 없었다. 그리고 AI Agent에게 질문을 했다.
AuthenticatedUserArgumentResolver는 사용하지 않는 이유가 있을까?
⏺ E2E 테스트에서 AuthenticatedUserArgumentResolver는 사용되고 있습니다. 직접 주입하지 않을 뿐입니다.
@SpringBootTest(webEnvironment = RANDOM_PORT)로 실행하면 실제 Spring 컨텍스트가 전체 로딩되므로:
1. WebMvcConfig가 AuthenticatedUserArgumentResolver를 등록
2. 2. GET /api/v1/users/me 요청 시 X-Loopers-LoginId, X-Loopers-LoginPw 헤더를 AuthenticatedUser 객체로 변환
3. 헤더 누락 시 BAD_REQUEST, 인증 실패 시 UNAUTHORIZED 반환
테스트에서 헤더를 설정하는 부분이 이를 검증하고 있습니다:
HttpHeaders headers = new HttpHeaders();
headers.set("X-Loopers-LoginId", "testuser");
headers.set("X-Loopers-LoginPw", "Test1234!");
즉, AuthenticatedUserArgumentResolver를 테스트 코드에서 직접 참조하지 않지만, HTTP 요청 흐름 안에서 자동으로 동작하고 있습니다.
나는 그동안 이런 것조차 모르고 시니어 개발자들에게 이런 걸 마음 편하게 맡긴 채, AI가 수행하는 코더 역할만을 하고 있음을 깨달았다. 이 사실은 공부를 정말 열심히 해야겠다는 충격을 주었다. 그동안 게을렀던 업보를 받아들이며 남들보다 공부 시간과 범위를 넓혀야겠다. 이것은 선택이 아니라 필수다.