
이 글은 2026년 05월 22일 작성된 글입니다.
오늘은 IoC 컨테이너의 @Bean 처리 구조와
REST API 응답 구조 개선, AOP, DTO 활용 기준,
그리고 컨트롤러 TDD 흐름까지 정리했다.
Java 객체를 JSON으로 변환하기 위해 Jackson 라이브러리를 추가했다.
implementation("com.fasterxml.jackson.core:jackson-databind:2.18.2")
implementation("com.fasterxml.jackson.datatype:jackson-datatype-jsr310:2.18.2")
jackson-datatype-jsr310은 LocalDateTime 같은 Java Time API 타입을 처리하기 위해 필요하다.
기존에는 @Component 계열 클래스만 빈으로 등록했다.
이제는 설정 클래스 안의 메서드에 @Bean을 붙여서
메서드의 반환 객체도 빈으로 등록할 수 있도록 구조를 확장했다.
@Configuration
public class TestJacksonConfig {
@Bean
public JavaTimeModule testBaseJavaTimeModule() {
return new JavaTimeModule();
}
}
ObjectMapper를 직접 생성하고,
JavaTimeModule을 등록한 뒤 빈으로 관리하도록 했다.
@Bean
public ObjectMapper testBaseObjectMapper(JavaTimeModule testBaseJavaTimeModule) {
ObjectMapper objectMapper = new ObjectMapper();
objectMapper.registerModule(testBaseJavaTimeModule);
return objectMapper;
}
이제 ObjectMapper도 IoC 컨테이너가 관리하는 객체가 되었다.
testBaseObjectMapper는 testBaseJavaTimeModule 빈에 의존한다.
@Test
@DisplayName("@Bean, testBaseJavaTimeModule 빈에 의존하는 testBaseObjectMapper 빈을 생성")
public void t8() {
ObjectMapper testBaseObjectMapper = applicationContext.genBean("testBaseObjectMapper");
assertThat(testBaseObjectMapper).isNotNull();
}
@Bean 메서드도 생성자 주입처럼 필요한 의존 빈을 매개변수로 받을 수 있다.
빈 생성 방식이 늘어나면서 단순히 Class 정보만으로는 빈을 만들기 어려워졌다.
기존 방식:
@Component 계열 클래스의 생성자 기반 빈 생성새 방식:
@Bean 메서드 기반 빈 생성그래서 빈 생성에 필요한 정보를 담기 위한 BeanDefinition 클래스를 도입했다.
BeanDefinition<TestPostService> beanDefinition =
new BeanDefinition<>(TestPostService.class);
BeanDefinition에서 빈 생성에 필요한 의존성 이름을 가져올 수 있게 했다.
String[] parameterNames = beanDefinition.getParameterNames();
테스트 예시:
assertThat(parameterNames).containsExactly("testPostRepository");
이를 통해 해당 빈을 만들기 위해 어떤 빈이 먼저 필요한지 알 수 있다.
BeanDefinition에 다음 기능을 추가했다.
beanDefinition.getBeanName();
beanDefinition.isCreateTypeMethod();
예시:
assertThat(beanDefinition.getBeanName()).isEqualTo("testPostService");
assertThat(beanDefinition.isCreateTypeMethod()).isFalse();
isCreateTypeMethod()는 빈 생성 방식이 메서드 기반인지 확인하기 위한 기능이다.
기존 RsData의 data 필드에는 하나의 값만 담을 수 있었다.
하지만 프론트에서 글 작성 후 다음 데이터를 함께 원할 수 있다.
이런 경우 단순히 data 하나만으로는 표현이 애매해질 수 있다.
복잡한 응답 데이터를 담기 위해 액션 메서드 전용 응답 본문 클래스를 두는 방식으로 개선했다.
처음에는 Map을 사용할 수도 있지만,
명확한 응답 구조를 위해 전용 클래스를 만드는 것이 더 좋다.
public record PostWriteResBody(
PostDto post,
long totalCount
) {
}
게시글 작성 성공 시에는 201 Created가 가장 의미상 적절하다.
| 상태코드 | 의미 |
|---|---|
| 200 | OK |
| 201 | Created |
| 204 | No Content |
| 400 | Bad Request |
| 401 | Unauthorized |
| 403 | Forbidden |
| 404 | Not Found |
| 422 | Unprocessable Entity |
| 500 | Internal Server Error |
실제로는 200으로 응답해도 동작에는 문제가 없지만,
의미를 정확히 표현하려면 201이 더 적절하다.
HTTP 상태코드를 직접 지정하기 위해 ResponseEntity를 사용할 수 있다.
return ResponseEntity.status(201).body(rsData);
기존 응답 본문 구조를 유지하면서 HTTP 상태코드를 원하는 값으로 바꿀 수 있다.
RsData의 resultCode를 기준으로 HTTP 상태코드를 자동 반영하기 위해 AOP를 도입했다.
@Aspect
@Component
public class ResponseAspect {
}
컨트롤러 메서드가 RsData를 반환하면
그 안의 statusCode를 HttpServletResponse에 반영한다.
if (proceed instanceof RsData<?> rsData) {
response.setStatus(rsData.statusCode());
}
스프링에서 AOP는 간접적으로 자주 사용한다.
예시:
@Transactional@Cacheable이번에는 @Aspect를 직접 만들어서 응답 후처리 로직을 구현했다.
| 구분 | 간접 사용 | 직접 사용 |
|---|---|---|
| 예시 | @Transactional | @Aspect |
| 난이도 | 낮음 | 높음 |
| 유연성 | 제한적 | 높음 |
RsData 내부의 statusCode는 HTTP 응답 상태코드 설정에만 사용된다.
응답 JSON에는 굳이 노출할 필요가 없으므로 @JsonIgnore를 적용했다.
@JsonIgnore
public int statusCode() {
return statusCode;
}
REST API에서 DTO는 역할에 따라 나눌 수 있다.
조회 API를 제외한 대부분의 요청 응답에 사용한다.
RsData<Void>
RsData<PostDto>
RsData<List<PostDto>>
RsData<PostWriteResBody>
엔티티를 외부에 직접 노출하지 않기 위해 사용한다.
PostDto
MemberDto
JSON 요청을 받을 때 사용한다.
PostWriteReqBody
PostModifyReqBody
응답 데이터가 복잡할 때만 만든다.
PostWriteResBody
PostModifyResBody
댓글 수정 API를 구현했다.
댓글 작성, 삭제에 이어 수정까지 구현하면서 댓글 CRUD 흐름이 더 완성되었다.
REST API 컨트롤러 테스트를 시작했다.
@SpringBootTest
@AutoConfigureMockMvc
@ActiveProfiles("test")
@Transactional
class ApiV1PostControllerTest {
}
주요 테스트 환경:
@SpringBootTest@AutoConfigureMockMvc@ActiveProfiles("test")@TransactionalMockMvc를 사용하면 실제 서버를 띄우지 않고도 HTTP 요청과 응답을 테스트할 수 있다.
mockMvc.perform(post("/api/v1/posts"))
컨트롤러 테스트는 크게 두 단계로 볼 수 있다.
POST 요청으로 글 작성 API를 테스트했다.
resultActions
.andExpect(status().isCreated());
또한 andDo(print())를 추가해서 요청과 응답 내용을 확인했다.
REST API 테스트에서는 응답 JSON의 모든 필드를 검사하는 것이 좋다.
이유:
테스트 코드는 API 문서 역할도 함께 한다.
ResponseAspect처럼 프로젝트 전반에 영향을 주는 코드를 수정할 때
자동화된 테스트가 안전망 역할을 한다.
테스트가 있으면 리팩토링 시 기존 기능이 깨졌는지 빠르게 확인할 수 있다.
기존 기능의 요구사항이 바뀌면 구현보다 테스트를 먼저 수정하는 흐름이 좋다.
예시:
totalCount가 필요 없음PostWriteResBody 제거이 흐름이 TDD의 안정적인 변경 방식이다.
@Bean 방식이 추가되면서 빈 생성 정보를 표현하기 위해 BeanDefinition이 필요해졌다.RsData와 응답 DTO를 분리하면 REST API 응답 구조를 더 명확하게 관리할 수 있다.RsData의 resultCode를 기반으로 HTTP 상태코드를 자동 반영할 수 있다.