AI 백엔드 팀으로 이직 후 작성하는 첫 회고록이다.
새로운 환경에서 업무를 시작하면서 가장 먼저 중요하게 여긴 것은 도메인 파악이었다.
이를 빠르게 진행하기 위해 설계도와 프로젝트 셋업을 우선적으로 시작했다. 이 프로젝트는 MSA 환경에서 동작하는 멀티 모듈 기반이었으며, 브랜치 전략 또한 이전 회사와 달라서 별도로 분석이 필요했다.
업무 중 자사 서비스를 이용하며 API를 살펴보던 중, 하나의 Request 객체가 여러 Controller에서 전역적으로 사용되고 있다는 점을 발견했다. 다양한 곳에서 재사용되는 구조였지만, 필드에 @Nullable 등의 명시적 제약이 없어 어떤 필드가 필수인지 파악이 어려웠다.
API 명세서는 postman, request 파일, openapi 파일 등으로 따로 관리되고 있었고, 필수값이 누락된 경우에도 런타임 단계에서 서비스 레이어에서 예외로 처리되는 방식이었다. 이로 인해 불필요한 connection, context switching이 발생할 수 있다는 우려가 들었다.
명세서는 한눈에 파악하기 어려웠고, 코드와 함께 계속 확인해야 하는 불편함이 있었다.
이러한 설계가 처음에는 개발 속도를 높이기 위한 판단일 수 있다고 생각했지만, 시간이 지날수록 유지보수성과 가독성 측면에서 개선이 필요하다고 판단했다. 특히 전역 Request 객체를 상황에 맞게 나누는 리팩터링이 필연적일 것으로 보였다.
(혹시 제 생각이 틀렸다면, 다른 생각을 알려주세요!!)
이에 따라 API 명세서를 보다 명확하고 체계적으로 관리할 필요성을 느꼈고, 테스트 기반 문서화 도구인 Spring REST Docs + OpenAPI + Swagger UI 조합을 검토하게 되었다.
다만, 현재 프로젝트는 TDD 기반이 아니었기에 이를 도입하는 데 부담이 있을 수 있었다. 대안으로 코드 침투적인 Swagger 방식도 함께 고려하였다.
두 방식 중 어떤 것이 더 적합한지 고민했지만, 협업과 신뢰도를 고려할 때 테스트 기반 문서화가 우위에 있다고 판단했다. 물론 작업량은 더 들어가지만, 명세서의 정확성과 일관성을 보장할 수 있다는 장점이 있다.
또한 일반 Swagger 방식을 사용할 경우, API가 구현된 후에야 명세서가 생성된다. 반면, Spring REST Docs 방식은 컨트롤러 없이 테스트 코드 내 필드 정의만으로 명세서가 생성되기 때문에, 개발 이전에도 프론트엔드와의 협업이 가능하다.
하지만 현재 구조는 Request 객체가 전역적으로 쓰이고 있고, Swagger에서는 모든 필드가 무조건 출력되는 문제가 있다.
결국 두 가지 방식 모두 프로젝트에 적용해보고, 장단점을 비교한 보고서를 작성하여 팀과 논의하기로 했다.
Spring Rest Docs + OpeanApi + Swagger-UI
Spring REST Docs + OpenAPI + Swagger UI 방식은 필자에게도 처음이었고, Java 8 환경에서 호환 가능한 라이브러리를 찾지 못해 직접 라이브러리를 커스텀해서 사용하는 데 많은 시간을 소요했다.

커스텀 라이브러리를 사용할 경우 유지보수에 대한 부담과, 넥서스와 같은 사설 저장소에 물리적으로 배포해야 하는 이슈가 생긴다.
사용한 Maven 라이브러리는 https://github.com/BerkleyTechnologyServices/restdocs-spec 기반이었으나, 명확한 버전 정보가 없어 초반에 시행착오가 있었다.
다행히 이후에는 Java 8에 호환되는 적절한 버전을 찾아 적용할 수 있었다.
테스트 환경 구축하기
@SpringBootTest 문제점
- 모든 빈을 로딩하므로 테스트 시간이 매우 느림
- 단순한 API 명세 테스트에도 모든 레이어가 참여함
- 실제 배포 환경과 가장 유사하지만, 문서화 목적 테스트에는 과도한 비용
@WebMvcTest 기반 단위 테스트
- 특정 컨트롤러와 관련된 빈만 로딩
- 속도가 빠르고, 가볍다
- 테스트 목적에 최적화된 설정 가능
그러나, @WebMvcTest도 느렸던 이유
- 공통 AppConfig 등에서 등록한 불필요한 빈들이 함께 로딩됨
- TestConfig내부에 @ComponentScan, @Bean 정의가 테스트에도 적용되기 때문
TestConfig를 제거해도 AppConfig의 모든 @Bean이 로딩 되었던 이유
- @WebMvcTest(SomeController.class)는 @ComponentScan을 하지 않지만,
- 내부적으로 @SpringBootConfiguration이 있는 루트 패키지를 기준으로 기본 컴포넌트 스캔 경로가 설정됨.
- 따라서, 테스트 대상 클래스와 같은 패키지나 하위 패키지에 있는 @Configuration 클래스는 자동으로 스캔되어 빈으로 등록될 수 있다.
SpringBoot 공식 문서
@WebMvcTest will auto-configure the Spring MVC infrastructure and limit scanned beans to @Controller, @ControllerAdvice, @JsonComponent, Converter, GenericConverter, Filter, HandlerInterceptor, WebMvcConfigurer, and HandlerMethodArgumentResolver.
Regular @Component and @Configuration classes in the same package or sub-packages can be picked up by default component scanning, unless explicitly filtered out.

선택한 방법 : AppConfig 최적화 : 테스트 프로파일 분리
- @Profile("!test") 전략
@Slf4j
@Import({WebSocketConfig.class, AllSecurityConfigContainer.class})
@Configuration
@EnableJpaRepositories(basePackages = "kr.aift.davinci.common.rdb.repository")
@EnableTransactionManagement
@EnableAsync
@Profile("!test")
public class AppConfig { // 테스트 환경에서 우회 }
- 테스트 시 해당 빈들이 무시되어, @WebMvcTest의 장점을 온전히 활용 가능
- 테스트 전용 빈은 별도로 TestConfig에 분리하여 등록
/**
* Test 환경에 필요한 설정
*
*/
@Configuration
public class TestConfig {
/**
* GsonBuilder를 설정하여 LocalDateTime 타입을 처리할 수 있도록 커스터마이징.
* Java 8에서는 LocalDateTime을 기본적으로 JSON으로 직렬화/역직렬화할 수 없기 때문에,
* 이를 해결하기 위해 GsonDateConverter를 사용하여 LocalDateTime을 지원하도록 설정.
*
*/
@Bean
public GsonBuilder gsonBuilderForApi() {
return JsonUtil.getBaseBuilder()
.registerTypeAdapter(LocalDateTime.class, new GsonDateConverter.LocalDateTimeConverter());
}
}
테스트 전용 보안 설정 구성
인증 우회용 보안 설정: TestSecurityConfig
테스트에서도 실제 사용자 세션 흐름을 반영하고 싶다면 @WithMockUser가 아닌 사용자 인증 객체 자체를 SecurityContext에 수동 주입하는 방식이 필요하다.
/**
* Test 시, 인증된 사용자 정보를 설정하기 위한 SecurityConfig
*
* @author JaeWeonLee
*/
@Configuration
public class TestSecurityConfig extends WebSecurityConfigurerAdapter {
public final String LOGIN_ID = "testId";
public final Long USER_ID = 1L;
public final Long DOMAIN_ID = 1L;
final List<GrantedAuthority> authorities = GrantType.getAuthorities("SYSTEM_ADMIN");
@Override
protected void configure(HttpSecurity http) throws Exception {
http
.csrf().disable()
.authorizeRequests().anyRequest().permitAll()
.and()
.addFilterBefore((request, response, chain) -> {
SecurityContextHolder.getContext().setAuthentication(
new UserTokenAuthenticated(
LOGIN_ID,
authorities,
USER_ID,
DOMAIN_ID
)
);
chain.doFilter(request, response);
}, UsernamePasswordAuthenticationFilter.class);
}
}
이 설정을 통해, 실제 서비스 로직에서 사용하는 UserRequestInfo 등 인증 기반 서비스 흐름을 테스트 환경에서도 시뮬레이션할 수 있다.
테스트 코드 작성 방식
@WebMvcTest(controllers = SampleController.class)
@Import({TestSecurityConfig.class, TestConfig.class})
@AutoConfigureRestDocs(outputDir = "target/generated-snippets")
@ExtendWith(RestDocumentationExtension.class)
@ActiveProfiles("test")
public class SampleControllerTest {
@Autowired
private MockMvc mockMvc;
@MockBean
private SampleService sampleService;
@MockBean
private SessionService sessionService;
@MockBean
private RdbService rdbService;
@Test
void 샘플_테스트() throws Exception {
// WHEN
String sampleName = "SAMPLE_NAME";
String sampleId = "SAMPLE_ID";
String orderAsc = "ASC";
String orderDesc = "DESC";
String request = String.format(
"{\n" +
" \"master\": {},\n" +
" \"content\": {\n" +
" \"filter\": {\n" +
" \"searchTarget\": [\n" +
" \"%s\"\n" +
" ]\n" +
" },\n" +
" \"sorters\": [\n" +
" {\n" +
" \"target\": \"%s\",\n" +
" \"order\": \"%s\"\n" +
" },\n" +
" {\n" +
" \"target\": \"%s\",\n" +
" \"order\": \"%s\"\n" +
" }\n" +
" ]\n" +
" }\n" +
"}",
sampleName, sampleName, orderAsc, sampleId, orderDesc
);
// GIVEN
given(sessionService.getUserRequestInfo()).willReturn(new UserRequestInfo(1L, 1L, SYSTEM_ADMIN));
given(sampleManagerService.findSampleDtoList(any(SampleSearch.class), any(UserRequestInfo.class)))
.willReturn(InstancioDtoFactory.createSampleDtoList());
mockMvc.perform(RestDocumentationRequestBuilders.post("/admin/sample/list")
.contentType(MediaType.APPLICATION_JSON)
.accept(MediaType.APPLICATION_JSON)
.content(request) // WHEN
)
.andExpect(status().isOk())
// THEN
.andDo(MockMvcRestDocumentationWrapper.document("조회 테스트",
resource(ResourceSnippetParameters.builder()
GIVEN - WEHN - THEN 의 규칙을 권장
DTO 생성은 번거로운 부분 중 하나이다. 이를 자동화하여 테스트코드 작성의 진입 장벽을 낮추고, 생산성을 향상시키고자 한다.
고려한 옵션
Java 진영의 테스트용 객체 자동 생성 라이브러리 중 다음 두 가지를 비교 검토했다
Fixture Monkey & Instancio
설명 NAVER에서 개발한 객체 생성 도구. 복잡한 계층 구조의 객체 생성에 특화됨 Java Bean 객체 자동 생성기. 간결한 문법과 유연한 커스터마이징, JPA 연관 객체 처리 지원 장점 Bean Validation 기반으로 제약 조건을 활용한 생성 가능
Kotlin 테스트와의 호환성 우수러닝 커브가 낮고 사용법이 직관적- 가볍고 빠름
공식 문서가 풍부하고 정리되어 있음단점 상대적으로 무거움- DSL과 Introspector 설정 학습 필요
기본 생성자 등의 요구 조건이 있음복잡한 제약 조건은 수동 설정 필요
Bean Validation 기반 조건 자동 추론은 어려움
두 라이브러리의 공통점
- 모두 Java 테스트용 객체 자동 생성 도구
- 복잡한 객체 구조 생성 가능 (중첩, 컬렉션 등)
- 랜덤 데이터 생성 및 커스터마이징 기능 제공
- Fluent 스타일 API 제공
- 테스트 실패 시 재현 가능한 고정 시드(seed) 설정 지원
예시 코드 비교
Instancio 예시 코드
Person person = Instancio.create(Person.class);
Person personWithoutAgeAndAddress = Instancio.of(Person.class)
.ignore(field(Person::getAddress))
.create();
Fixture monkey 예시 코드
FixtureMonkey fixtureMonkey = FixtrueMonkey.create();
Person person = fixtureMonkey.giveMeOne(Person.class);
Person personWithoutAgeAndAddress = fixtureMonkey.giveMeBuilder(Person.class)
.setNull(javaGetter(Person::getAddress))
.sample();
Fixture Monkey를 잘 활용하려면 Introspector라는 개념에 대해 알아야한다. 어떤 Introspector를 적용하냐에 따라 Fixture Monkey의 객체 생성 방식이 달라지게 된다.
사실 이 코드의 경우 클래스에는 인수가 없는 생성자와 setter가 있어야 한다. 기본 Introspector 설정인BeanArbitraryIntrospector 로 되어있기 때문이다.
이 외에도 4가지가 더 있으며, 테스트코드에 사용할 객체에 따라 다르게 Introspector 를 설정해주어야한다.
사용 경험 및 체감
Fixture Monkey의 경우는 Introspector 설정 외에도, 기존 코드에 기본 생성자 추가 등 번거로움이 많고 러닝 커브가 높은것으로 느껴졌다.
Instancio.create() 를 호출하게되면 리플렉션을 기반으로 알아서 생성자, getter/ setter 등을 활용하여 더 폭넓게 객체를 생성해주고 러닝커브가 상대적으로 낮아보인다.
결론
Instancio는 기존 코드 변경 요구가 적고 러닝 커브가 낮아 테스트 코드 작성에 보다 적합하다고 판단하였다.
따라서 본 프로젝트의 테스트 객체 자동 생성 도구로 Instancio를 도입하고, 관련 예제 코드 및 가이드 문서를 함께 정리하여 공유할 예정이다.
REST Docs → OpenAPI → Swagger UI 연동 구조
문서 생성 프로세스
- 테스트 실행 시 @AutoConfigureRestDocs(outputDir = "target/generated-snippets") 설정으로 snippet 파일 생성
- restdocs-api-spec가 이를 기반으로 OpenAPI 3.0 JSON 파일을 생성
- 해당 JSON 파일은 Jenkins를 통해 Swagger UI 서버에 자동 배포됨

자동화를 위한 CI/CD 구성
REST Docs + Swagger UI 조합은 정적 문서(openapi-3.0.json)를 기반으로 하기 때문에, 테스트 기반 문서 갱신 과정이 CI/CD에 명시적으로 포함되어야 한다.
일반 Swagger와 달리 서버 실행만으로 문서가 반영되지 않으므로 별도 트리거 필요하다.
mvn clean package -pl <모듈 명> -Dtest="kr.sample.sample.api.swagger.*Test" -P prod
- -P DEV는 pom.xml의 restdocs-api-spec 설정에 따라 host, scheme 등을 결정
- 테스트 결과로 openapi-3.0.json 생성됨

Swagger UI 문서 자동 배포
Jenkins Job 실행 시, 파라미터 IS_SWAGGER_UPDATE 값을 기준으로 문서 최신화 여부 판단

- 생성된 openapi-3.0.json은 Swagger UI 전용 Nginx 서버로 복사되어 덮어쓰기됨
- Swagger 웹 서버는 이 파일을 기준으로 UI를 최신화함


최종 결과 요약
REST Docs + OpenAPI 3.0 + SwaggerUI 통합 문서 구성
- WebMvcTest 기반 테스트 환경 정비
- 전체 테스트 환경을 @SpringBootTest → @WebMvcTest로 경량화
- 테스트 속도 대폭 향상 (불필요한 Bean 미로딩)
- AppConfig 등 공통 설정 클래스는 @Profile("!test") 조건으로 분리
- 실제 서비스 흐름과 동일한 인증 흐름 구현을 위해 TestSecurityConfig 도입
- SecurityContextHolder에 UserTokenAuthenticated 직접 주입
- 실제 서비스 내 세션 기반 인증 API와 동일한 구조로 테스트 가능
- 테스트 성공 시 각 테스트 클래스별로 target/generated-snippets 하위에 문서 조각(snippets) 자동 생성
- restdocs-api-spec 라이브러리를 통해 스니펫 기반으로 OpenAPI 3.0 스펙 파일(openapi-3.0.json) 생성
- openapi-3.0.json은 Swagger UI 전용 웹서버(Nginx)에서 사용
- 문서화의 전 과정이 테스트 → 변환 → 배포로 자동화됨