Day 56 - Contract
이 글은 2026년 05월 22일 작성된 글입니다.
오늘은 IoC 컨테이너의 @Bean 처리 구조와
REST API 응답 구조 개선, AOP, DTO 활용 기준,
그리고 컨트롤러 TDD 흐름까지 정리했다.
1. Jackson 라이브러리 추가
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 타입을 처리하기 위해 필요하다.
2. @Bean 어노테이션 추가
기존에는 @Component 계열 클래스만 빈으로 등록했다.
이제는 설정 클래스 안의 메서드에 @Bean을 붙여서
메서드의 반환 객체도 빈으로 등록할 수 있도록 구조를 확장했다.
@Configuration
public class TestJacksonConfig {
@Bean
public JavaTimeModule testBaseJavaTimeModule() {
return new JavaTimeModule();
}
}3. ObjectMapper 빈 등록
ObjectMapper를 직접 생성하고,
JavaTimeModule을 등록한 뒤 빈으로 관리하도록 했다.
@Bean
public ObjectMapper testBaseObjectMapper(JavaTimeModule testBaseJavaTimeModule) {
ObjectMapper objectMapper = new ObjectMapper();
objectMapper.registerModule(testBaseJavaTimeModule);
return objectMapper;
}이제 ObjectMapper도 IoC 컨테이너가 관리하는 객체가 되었다.
4. @Bean 의존관계 테스트
testBaseObjectMapper는 testBaseJavaTimeModule 빈에 의존한다.
@Test
@DisplayName("@Bean, testBaseJavaTimeModule 빈에 의존하는 testBaseObjectMapper 빈을 생성")
public void t8() {
ObjectMapper testBaseObjectMapper = applicationContext.genBean("testBaseObjectMapper");
assertThat(testBaseObjectMapper).isNotNull();
}@Bean 메서드도 생성자 주입처럼 필요한 의존 빈을 매개변수로 받을 수 있다.
5. BeanDefinition 도입
빈 생성 방식이 늘어나면서 단순히 Class 정보만으로는 빈을 만들기 어려워졌다.
기존 방식:
@Component계열 클래스의 생성자 기반 빈 생성
새 방식:
@Bean메서드 기반 빈 생성
그래서 빈 생성에 필요한 정보를 담기 위한 BeanDefinition 클래스를 도입했다.
BeanDefinition<TestPostService> beanDefinition =
new BeanDefinition<>(TestPostService.class);6. BeanDefinition.getParameterNames()
BeanDefinition에서 빈 생성에 필요한 의존성 이름을 가져올 수 있게 했다.
String[] parameterNames = beanDefinition.getParameterNames();테스트 예시:
assertThat(parameterNames).containsExactly("testPostRepository");이를 통해 해당 빈을 만들기 위해 어떤 빈이 먼저 필요한지 알 수 있다.
7. BeanDefinition의 생성 방식 구분
BeanDefinition에 다음 기능을 추가했다.
beanDefinition.getBeanName();
beanDefinition.isCreateTypeMethod();예시:
assertThat(beanDefinition.getBeanName()).isEqualTo("testPostService");
assertThat(beanDefinition.isCreateTypeMethod()).isFalse();isCreateTypeMethod()는 빈 생성 방식이 메서드 기반인지 확인하기 위한 기능이다.
8. RsData의 한계
기존 RsData의 data 필드에는 하나의 값만 담을 수 있었다.
하지만 프론트에서 글 작성 후 다음 데이터를 함께 원할 수 있다.
- 생성된 게시글
- 전체 게시글 수
- 추가 메타 정보
이런 경우 단순히 data 하나만으로는 표현이 애매해질 수 있다.
9. ResBody 클래스 도입
복잡한 응답 데이터를 담기 위해 액션 메서드 전용 응답 본문 클래스를 두는 방식으로 개선했다.
처음에는 Map을 사용할 수도 있지만,
명확한 응답 구조를 위해 전용 클래스를 만드는 것이 더 좋다.
public record PostWriteResBody(
PostDto post,
long totalCount
) {
}- 응답 구조 명확화
- 타입 안정성 증가
- 프론트와의 API 계약 관리 쉬움
10. HTTP 상태코드
게시글 작성 성공 시에는 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이 더 적절하다.
11. ResponseEntity
HTTP 상태코드를 직접 지정하기 위해 ResponseEntity를 사용할 수 있다.
return ResponseEntity.status(201).body(rsData);기존 응답 본문 구조를 유지하면서 HTTP 상태코드를 원하는 값으로 바꿀 수 있다.
12. ResponseAspect 도입
RsData의 resultCode를 기준으로 HTTP 상태코드를 자동 반영하기 위해 AOP를 도입했다.
@Aspect
@Component
public class ResponseAspect {
}컨트롤러 메서드가 RsData를 반환하면
그 안의 statusCode를 HttpServletResponse에 반영한다.
if (proceed instanceof RsData<?> rsData) {
response.setStatus(rsData.statusCode());
}13. AOP 직접 사용
스프링에서 AOP는 간접적으로 자주 사용한다.
예시:
@Transactional@Cacheable
이번에는 @Aspect를 직접 만들어서 응답 후처리 로직을 구현했다.
| 구분 | 간접 사용 | 직접 사용 |
|---|---|---|
| 예시 | @Transactional | @Aspect |
| 난이도 | 낮음 | 높음 |
| 유연성 | 제한적 | 높음 |
14. statusCode는 JSON에서 제외
RsData 내부의 statusCode는 HTTP 응답 상태코드 설정에만 사용된다.
응답 JSON에는 굳이 노출할 필요가 없으므로 @JsonIgnore를 적용했다.
@JsonIgnore
public int statusCode() {
return statusCode;
}15. DTO 활용 기준
REST API에서 DTO는 역할에 따라 나눌 수 있다.
RsData
조회 API를 제외한 대부분의 요청 응답에 사용한다.
RsData<Void>
RsData<PostDto>
RsData<List<PostDto>>
RsData<PostWriteResBody>엔티티 DTO
엔티티를 외부에 직접 노출하지 않기 위해 사용한다.
PostDto
MemberDto요청 본문 DTO
JSON 요청을 받을 때 사용한다.
PostWriteReqBody
PostModifyReqBody응답 본문 DTO
응답 데이터가 복잡할 때만 만든다.
PostWriteResBody
PostModifyResBody16. 댓글 수정 API 구현
댓글 수정 API를 구현했다.
댓글 작성, 삭제에 이어 수정까지 구현하면서 댓글 CRUD 흐름이 더 완성되었다.
17. 컨트롤러 TDD 시작
REST API 컨트롤러 테스트를 시작했다.
@SpringBootTest
@AutoConfigureMockMvc
@ActiveProfiles("test")
@Transactional
class ApiV1PostControllerTest {
}주요 테스트 환경:
@SpringBootTest@AutoConfigureMockMvc@ActiveProfiles("test")@Transactional
18. MockMvc
MockMvc를 사용하면 실제 서버를 띄우지 않고도 HTTP 요청과 응답을 테스트할 수 있다.
mockMvc.perform(post("/api/v1/posts"))컨트롤러 테스트는 크게 두 단계로 볼 수 있다.
- 요청을 날린다.
- 결과를 검증한다.
19. 글 작성 테스트
POST 요청으로 글 작성 API를 테스트했다.
resultActions
.andExpect(status().isCreated());또한 andDo(print())를 추가해서 요청과 응답 내용을 확인했다.
20. 응답 JSON 전체 검증
REST API 테스트에서는 응답 JSON의 모든 필드를 검사하는 것이 좋다.
이유:
- 프론트와의 API 계약 보호
- 필드 누락 방지
- 응답 구조 변경 감지
- 회귀 버그 조기 발견
테스트 코드는 API 문서 역할도 함께 한다.
21. 자동화 테스트의 장점
ResponseAspect처럼 프로젝트 전반에 영향을 주는 코드를 수정할 때
자동화된 테스트가 안전망 역할을 한다.
테스트가 있으면 리팩토링 시 기존 기능이 깨졌는지 빠르게 확인할 수 있다.
22. 요구사항 변경과 TDD
기존 기능의 요구사항이 바뀌면 구현보다 테스트를 먼저 수정하는 흐름이 좋다.
예시:
- 더 이상
totalCount가 필요 없음 PostWriteResBody제거- 테스트 실패 확인
- 구현 수정
- 테스트 통과
이 흐름이 TDD의 안정적인 변경 방식이다.
✅ 정리
@Bean방식이 추가되면서 빈 생성 정보를 표현하기 위해BeanDefinition이 필요해졌다.- Jackson과 ObjectMapper를 빈으로 등록하면서 설정 객체도 IoC 컨테이너가 관리할 수 있게 되었다.
RsData와 응답 DTO를 분리하면 REST API 응답 구조를 더 명확하게 관리할 수 있다.- AOP를 활용하면
RsData의 resultCode를 기반으로 HTTP 상태코드를 자동 반영할 수 있다. - REST API 테스트에서는 응답 JSON의 모든 필드를 검증하여 프론트와의 API 계약을 지킬 수 있다.
- 자동화 테스트가 있으면 전역적인 리팩토링과 요구사항 변경에도 더 안전하게 대응할 수 있다.