이 글은 2026년 05월 20일 작성된 글입니다.

오늘은 ApplicationContext의 동적 빈 탐색 개선과
REST API 삭제/작성 요청 처리, RsData 응답 구조까지 정리했다.


1. ApplicationContext 하드코딩 제거

기존에는 ApplicationContext 내부에서
특정 클래스 이름이나 생성 방식을 하드코딩하는 부분이 있었다.

이를 ClsUtil을 활용하는 방식으로 변경하여
코드를 더 유연하게 만들었다.

ClsUtil.construct(cls, args);
  • 하드코딩 감소
  • 동적 객체 생성 가능
  • IoC 컨테이너 구조 개선

2. 테스트용 패키지 이동

com.ll.domain.testPost.testPost 패키지를
테스트 폴더로 옮길 수 있게 되어 구조를 정리했다.

테스트 전용 클래스는 실제 애플리케이션 코드와 분리하는 것이 좋다.

  • 테스트 코드와 운영 코드 분리
  • 프로젝트 구조 명확화

3. Reflections 라이브러리 추가

패키지 스캔을 위해 org.reflections:reflections 라이브러리를 추가했다.

implementation("org.reflections:reflections:0.10.2")
implementation("ch.qos.logback:logback-classic:1.5.12")

이 라이브러리를 사용하면
특정 패키지 하위에서 어노테이션이 붙은 클래스를 자동으로 찾을 수 있다.


4. @Component 계열 클래스 스캔

@Component가 붙은 클래스를 스캔하여
빈 후보로 수집했다.

Reflections reflections = new Reflections("com.ll", TypesAnnotated);

Map<String, Class<?>> clsMap = reflections
    .getTypesAnnotatedWith(Component.class)
    .stream()
    .filter(cls -> !cls.isAnnotation())
    .collect(
        LinkedHashMap::new,
        (map, cls) -> map.put(Ut.str.lcfirst(cls.getSimpleName()), cls),
        Map::putAll
    );

@Service, @Repository처럼
@Component를 기반으로 한 어노테이션도 함께 탐색할 수 있다.


5. 빈 이름 규칙

스프링 빈 이름 규칙처럼
클래스명의 첫 글자를 소문자로 바꾸어 빈 이름을 만들었다.

MemberService -> memberService

이를 위해 lcfirst 유틸을 구현했다.

public static String lcfirst(String str) {
    if (str == null || str.isEmpty()) return str;

    char firstChar = str.charAt(0);

    if (Character.isLowerCase(firstChar)) return str;

    return Character.toLowerCase(firstChar) + str.substring(1);
}

6. annotatedClasses 함수 구현

특정 패키지 하위에서
특정 어노테이션을 가진 클래스를 수집하는 함수를 구현했다.

Map<String, Class<?>> annotatedClasses =
    ClsUtil.annotatedClasses("com.ll", Component.class);

테스트에서는 다음 빈들이 잘 수집되는지 확인했다.

  • testFacadePostService
  • testPostService
  • testPostRepository

7. ApplicationContext 동적 빈 검색

이제 ApplicationContext는 더 이상 하드코딩 방식이 아니라
패키지를 스캔해서 동적으로 빈을 찾도록 변경했다.

applicationContext.genBean("testPostService");

내부적으로는 스캔된 클래스 목록에서
빈 이름에 맞는 클래스를 찾아 객체를 생성한다.


8. 댓글 삭제 API 구현

REST API에서 댓글 삭제 기능을 구현했다.

/api/v1/posts/1/comments/2/delete

처음에는 개발 편의를 위해 GET 방식으로 호출할 수 있게 만들었다.

하지만 삭제는 서버 상태를 변경하는 작업이므로
원칙적으로는 GET이 아니라 DELETE 메서드를 사용하는 것이 맞다.


9. orphanRemoval과 더티체킹

댓글 삭제 과정에서 orphanRemoval = true와 더티체킹을 활용했다.

@OneToMany(mappedBy = "post", orphanRemoval = true)
private List<PostComment> comments;

부모 엔티티에서 자식 엔티티를 제거하면
JPA가 변경을 감지해서 삭제 쿼리를 실행한다.

단, 더티체킹은 트랜잭션 안에서 동작하므로
@Transactional이 필요하다.


10. 조회 메서드 readOnly 트랜잭션

조회 전용 API에는 readOnly = true 옵션을 적용했다.

@Transactional(readOnly = true)

조회 작업임을 명확히 표현할 수 있고,
불필요한 변경 감지를 줄일 수 있다.


11. resultCode와 msg

조회가 아닌 요청에는
처리 결과를 명확하게 알려주는 응답이 필요하다.

{
  "resultCode": "200-1",
  "msg": "댓글이 삭제되었습니다."
}

resultCode는 HTTP 상태코드와 세부 구분코드를 함께 표현한다.

200-1
  • 200: 성공
  • -1: 세부 구분 코드

12. RsData 클래스 도입

조회 API를 제외한 요청 응답에 사용할
공통 응답 객체를 만들었다.

public class RsData<T> {
    private String resultCode;
    private String msg;
    private T data;
}

사용 예시:

  • 글 작성 응답
  • 글 수정 응답
  • 글 삭제 응답
  • 댓글 작성 응답
  • 댓글 삭제 응답

13. data 필드 추가

resultCodemsg만으로 부족한 경우를 위해
data 필드를 추가했다.

RsData<PostDto>

응답 결과와 함께
추가 데이터를 담을 수 있게 되었다.


14. RsData 제네릭 적용

처음에는 data 타입을 Object로 처리했지만,
이후 제네릭을 적용했다.

RsData<PostDto>
RsData<PostCommentDto>
RsData<Void>

제네릭을 사용하면
응답 데이터 타입을 더 명확하게 표현할 수 있다.


15. RsData

응답에 추가 데이터가 필요 없는 경우에는
RsData<Void>를 사용한다.

RsData<Void>

데이터 없이 resultCode와 msg만 전달할 때 적합하다.


16. 글 삭제 API 구현

글 삭제 기능도 구현했다.

/api/v1/posts/1/delete

글 삭제 시 관련 댓글은
CascadeType.REMOVE 설정 덕분에 함께 삭제된다.


17. Postman 사용

Postman을 사용해 REST API를 직접 호출하고 테스트했다.

예시 요청:

GET /api/v1/posts
GET /api/v1/posts/2
GET /api/v1/posts/2/comments
GET /api/v1/posts/2/comments/4

Postman을 사용하면
브라우저로 호출하기 어려운 DELETE, POST 요청도 쉽게 테스트할 수 있다.


18. DELETE 메서드 적용

삭제 요청을 GET에서 DELETE 방식으로 변경했다.

기존:

GET /api/v1/posts/1/delete

변경:

DELETE /api/v1/posts/1

REST API에서는 삭제 작업에 DELETE 메서드를 사용하는 것이 더 자연스럽다.


19. POST 요청과 @RequestBody

글 작성 API는 POST 요청으로 처리하고,
요청 본문은 JSON 형태로 받도록 변경했다.

{
  "title": "제목",
  "content": "내용"
}
@PostMapping
public RsData<PostDto> write(@RequestBody PostWriteReqBody reqBody) {
}

@RequestBody는 JSON 요청 본문을 자바 객체로 변환해준다.


20. REST API와 BindingResult

REST API에서는 @Valid를 사용하더라도
BindingResult를 직접 사용하는 경우가 많지 않다.

폼 기반 서버 렌더링에서는 검증 실패 후
다시 폼 화면을 보여줘야 하지만,
REST API에서는 보통 에러 응답을 JSON으로 내려준다.


21. ReqBody 네이밍

기존 PostWriteForm이라는 이름을
PostWriteReqBody로 변경했다.

public record PostWriteReqBody(
    String title,
    String content
) {
}

REST API에서는 폼보다는 요청 본문을 의미하는
ReqBody라는 이름이 더 정확하다.


✅ 정리

  • Reflections 라이브러리를 사용해 @Component 계열 클래스를 동적으로 수집할 수 있었다.
  • ApplicationContext에서 하드코딩을 줄이고 패키지 스캔 기반 빈 생성을 구현할 수 있었다.
  • 댓글 삭제에서는 orphanRemoval과 더티체킹, 트랜잭션의 관계를 이해할 수 있었다.
  • RsData를 도입하면서 REST API 응답 구조를 공통화할 수 있었다.
  • DELETE, POST, @RequestBody를 사용하면서 REST API다운 요청 구조에 가까워졌다.

0개의 댓글