
이 글은 2026년 05월 20일 작성된 글입니다.
오늘은 ApplicationContext의 동적 빈 탐색 개선과
REST API 삭제/작성 요청 처리, RsData 응답 구조까지 정리했다.
기존에는 ApplicationContext 내부에서
특정 클래스 이름이나 생성 방식을 하드코딩하는 부분이 있었다.
이를 ClsUtil을 활용하는 방식으로 변경하여
코드를 더 유연하게 만들었다.
ClsUtil.construct(cls, args);
com.ll.domain.testPost.testPost 패키지를
테스트 폴더로 옮길 수 있게 되어 구조를 정리했다.
테스트 전용 클래스는 실제 애플리케이션 코드와 분리하는 것이 좋다.
패키지 스캔을 위해 org.reflections:reflections 라이브러리를 추가했다.
implementation("org.reflections:reflections:0.10.2")
implementation("ch.qos.logback:logback-classic:1.5.12")
이 라이브러리를 사용하면
특정 패키지 하위에서 어노테이션이 붙은 클래스를 자동으로 찾을 수 있다.
@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를 기반으로 한 어노테이션도 함께 탐색할 수 있다.
스프링 빈 이름 규칙처럼
클래스명의 첫 글자를 소문자로 바꾸어 빈 이름을 만들었다.
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);
}
특정 패키지 하위에서
특정 어노테이션을 가진 클래스를 수집하는 함수를 구현했다.
Map<String, Class<?>> annotatedClasses =
ClsUtil.annotatedClasses("com.ll", Component.class);
테스트에서는 다음 빈들이 잘 수집되는지 확인했다.
이제 ApplicationContext는 더 이상 하드코딩 방식이 아니라
패키지를 스캔해서 동적으로 빈을 찾도록 변경했다.
applicationContext.genBean("testPostService");
내부적으로는 스캔된 클래스 목록에서
빈 이름에 맞는 클래스를 찾아 객체를 생성한다.
REST API에서 댓글 삭제 기능을 구현했다.
/api/v1/posts/1/comments/2/delete
처음에는 개발 편의를 위해 GET 방식으로 호출할 수 있게 만들었다.
하지만 삭제는 서버 상태를 변경하는 작업이므로
원칙적으로는 GET이 아니라 DELETE 메서드를 사용하는 것이 맞다.
댓글 삭제 과정에서 orphanRemoval = true와 더티체킹을 활용했다.
@OneToMany(mappedBy = "post", orphanRemoval = true)
private List<PostComment> comments;
부모 엔티티에서 자식 엔티티를 제거하면
JPA가 변경을 감지해서 삭제 쿼리를 실행한다.
단, 더티체킹은 트랜잭션 안에서 동작하므로
@Transactional이 필요하다.
조회 전용 API에는 readOnly = true 옵션을 적용했다.
@Transactional(readOnly = true)
조회 작업임을 명확히 표현할 수 있고,
불필요한 변경 감지를 줄일 수 있다.
조회가 아닌 요청에는
처리 결과를 명확하게 알려주는 응답이 필요하다.
{
"resultCode": "200-1",
"msg": "댓글이 삭제되었습니다."
}
resultCode는 HTTP 상태코드와 세부 구분코드를 함께 표현한다.
200-1
조회 API를 제외한 요청 응답에 사용할
공통 응답 객체를 만들었다.
public class RsData<T> {
private String resultCode;
private String msg;
private T data;
}
사용 예시:
resultCode와 msg만으로 부족한 경우를 위해
data 필드를 추가했다.
RsData<PostDto>
응답 결과와 함께
추가 데이터를 담을 수 있게 되었다.
처음에는 data 타입을 Object로 처리했지만,
이후 제네릭을 적용했다.
RsData<PostDto>
RsData<PostCommentDto>
RsData<Void>
제네릭을 사용하면
응답 데이터 타입을 더 명확하게 표현할 수 있다.
응답에 추가 데이터가 필요 없는 경우에는
RsData<Void>를 사용한다.
RsData<Void>
데이터 없이 resultCode와 msg만 전달할 때 적합하다.
글 삭제 기능도 구현했다.
/api/v1/posts/1/delete
글 삭제 시 관련 댓글은
CascadeType.REMOVE 설정 덕분에 함께 삭제된다.
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 요청도 쉽게 테스트할 수 있다.
삭제 요청을 GET에서 DELETE 방식으로 변경했다.
기존:
GET /api/v1/posts/1/delete
변경:
DELETE /api/v1/posts/1
REST API에서는 삭제 작업에 DELETE 메서드를 사용하는 것이 더 자연스럽다.
글 작성 API는 POST 요청으로 처리하고,
요청 본문은 JSON 형태로 받도록 변경했다.
{
"title": "제목",
"content": "내용"
}
@PostMapping
public RsData<PostDto> write(@RequestBody PostWriteReqBody reqBody) {
}
@RequestBody는 JSON 요청 본문을 자바 객체로 변환해준다.
REST API에서는 @Valid를 사용하더라도
BindingResult를 직접 사용하는 경우가 많지 않다.
폼 기반 서버 렌더링에서는 검증 실패 후
다시 폼 화면을 보여줘야 하지만,
REST API에서는 보통 에러 응답을 JSON으로 내려준다.
기존 PostWriteForm이라는 이름을
PostWriteReqBody로 변경했다.
public record PostWriteReqBody(
String title,
String content
) {
}
REST API에서는 폼보다는 요청 본문을 의미하는
ReqBody라는 이름이 더 정확하다.