이 글은 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-jsr310LocalDateTime 같은 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 의존관계 테스트

testBaseObjectMappertestBaseJavaTimeModule 빈에 의존한다.

@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의 한계

기존 RsDatadata 필드에는 하나의 값만 담을 수 있었다.

하지만 프론트에서 글 작성 후 다음 데이터를 함께 원할 수 있다.

  • 생성된 게시글
  • 전체 게시글 수
  • 추가 메타 정보

이런 경우 단순히 data 하나만으로는 표현이 애매해질 수 있다.


9. ResBody 클래스 도입

복잡한 응답 데이터를 담기 위해 액션 메서드 전용 응답 본문 클래스를 두는 방식으로 개선했다.

처음에는 Map을 사용할 수도 있지만,
명확한 응답 구조를 위해 전용 클래스를 만드는 것이 더 좋다.

public record PostWriteResBody(
    PostDto post,
    long totalCount
) {
}
  • 응답 구조 명확화
  • 타입 안정성 증가
  • 프론트와의 API 계약 관리 쉬움

10. HTTP 상태코드

게시글 작성 성공 시에는 201 Created가 가장 의미상 적절하다.

상태코드의미
200OK
201Created
204No Content
400Bad Request
401Unauthorized
403Forbidden
404Not Found
422Unprocessable Entity
500Internal Server Error

실제로는 200으로 응답해도 동작에는 문제가 없지만,
의미를 정확히 표현하려면 201이 더 적절하다.


11. ResponseEntity

HTTP 상태코드를 직접 지정하기 위해 ResponseEntity를 사용할 수 있다.

return ResponseEntity.status(201).body(rsData);

기존 응답 본문 구조를 유지하면서 HTTP 상태코드를 원하는 값으로 바꿀 수 있다.


12. ResponseAspect 도입

RsDataresultCode를 기준으로 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
PostModifyResBody

16. 댓글 수정 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"))

컨트롤러 테스트는 크게 두 단계로 볼 수 있다.

  1. 요청을 날린다.
  2. 결과를 검증한다.

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 계약을 지킬 수 있다.
  • 자동화 테스트가 있으면 전역적인 리팩토링과 요구사항 변경에도 더 안전하게 대응할 수 있다.

0개의 댓글