6회차 멘토링
6회차는 개발 진척 상황과 코드 리뷰 등을 위주로 진행하였다.
코드 리뷰에서는 다음과 같은 주요 피드백을 받을 수 있었다.
• swagger I/F로 빼는 건 아주 좋아 보인다.
• toParam, from 같은 건 AOP(Argument Resolver) 제안
이번 멘토링 일지에서는 이 피드백을 바탕으로
왜 이 방식이 좋은 설계인지,
Argument Resolver가 어떤 문제를 해결하는지,
그리고 앞으로 어떻게 개선할 수 있을지를 정리해보려 한다.
⸻
1. Swagger I/F로 분리한 구조에 대한 피드백
1-1. 내가 했던 설계
컨트롤러에서 Swagger 관련 어노테이션을
실제 비즈니스 DTO와 분리해서 Swagger 전용 인터페이스(I/F) 로 관리했다.
@Tag(name="매칭", description = "매칭 관련 API")
public interface AgreementSwagger {
@Operation(
summary = "매칭 확인서 생성",
description = "매칭 확인서를 생성합니다."
)
@ApiResponses({
@ApiResponse(
responseCode = "201",
description = "매칭 확인서 생성 성공",
content = @Content(
mediaType = "application/json",
schema = @Schema(implementation = AgreementCreateResDTO.class)
)
),
// ...
})
ResponseEntity<AgreementCreateResDTO> createAgreement(
@RequestBody(
required = true,
content = @Content(
mediaType = "application/json",
schema = @Schema(implementation = AgreementCreateReqDTO.class),
examples = {
@ExampleObject(
name = "하루도움 매칭 확인서 생성 요청 예시",
value = """
{
"memberId": 101,
"type": "DAY",
"isVolunteer": false,
"unitHoney": 100,
"totalHoney": 100,
"region": "서울특별시 강남구 논현동",
"helpCategoryIds": [1, 2],
}
"""
),
// ...
}
)
)
AgreementCreateReqDTO request
);
}
@RestController
@RequestMapping("/agreements")
public class AgreementController implements AgreementSwagger {
@PostMapping
public ResponseEntity<AgreementCreateResDTO> createAgreement(@RequestBody AgreementCreateReqDTO request) {
// ...
}
}
1-2. 이게 왜 좋은 구조일까?
보기에 깔끔하다는 장점도 있지만, 꼭 그런 단순한 이유만은 아니다.
✅ 책임 분리
• Controller: 요청 처리 + 유즈케이스 연결
• Swagger I/F: API 문서화 책임
Swagger 어노테이션은 비즈니스 로직과 직접적인 관련이 없다.
따라서 이를 인터페이스로 분리함으로써 관심사의 분리가 명확해진다.
✅ 가독성과 유지보수성
• 컨트롤러 코드가 Swagger 어노테이션으로 오염되지 않는다.
• API 문서 변경 시, 인터페이스만 수정하면 된다.
✅ 실무 친화적 설계
• 실제 대규모 프로젝트에서도 Controller + Swagger Interface 패턴을 종종 사용한다고 한다.
• 이는 특히 API 명세가 자주 바뀌는 환경에서 효과적이다.
2. toParam, from 메서드에 대한 AOP(Argument Resolver) 제안
2-1. 기존 방식의 문제점
컨트롤러 단에서 자주 보이던 코드 패턴이 있었다.
PostCreateResDTO resDTO = PostCreateResDTO.from(result);
return ResponseEntity.ok().body(resDTO);
GetPostsUseCase.Param param = reqDTO.toParam(
Long.parseLong(currentMemberId),
type,
isMatched,
lastPostId,
count
);
이 패턴은 처음엔 괜찮아 보이지만, 점점 이런 문제가 생긴다.
• 컨트롤러가 변환 로직을 알고 있음
• 모든 컨트롤러에서 비슷한 변환 코드가 반복됨
• 요청 → 도메인 파라미터 변환이 흩어짐
2-2. Argument Resolver란?
멘토님이 제안해주신 Argument Resolver는 Spring MVC의 확장 포인트 중 하나였다.
✅ 컨트롤러 메서드의 파라미터를 프레임워크 레벨에서 자동으로 만들어주는 기능
@GetMapping("/matches")
public ResponseEntity<?> getMatches(MatchSearchParam param) {
// ...
}
여기서 MatchSearchParam을 RequestParam들을 조합해서 자동으로 만들어줄 수 있다.
2-3. 이걸 쓰면 뭐가 좋아질까?
👎 기존 방식
@GetMapping("/matches")
public ResponseEntity<?> getMatches(
@RequestParam Long memberId,
@RequestParam LocalDate date
) {
MatchSearchParam param = MatchSearchParam.from(memberId, date);
}
👍 Argument Resolver 적용 후
@GetMapping("/matches")
public ResponseEntity<?> getMatches(MatchSearchParam param) {
}
• 컨트롤러는 유즈케이스 호출에만 집중
• 변환 로직은 한 곳에만 존재
• 테스트 용이성 증가
즉, 해당 피드백은 “toParam / from 같은 변환 책임은 컨트롤러가 아니라 프레임워크 레벨에서 처리하는 게 더 좋다”
라는 의미였음을 생각해볼 수 있다.
3. 앞으로의 개선 방향
• Swagger I/F 분리 → 문서와 로직의 분리
• Argument Resolver → 변환 책임의 상향 이동
두 가지 피드백이 공통적으로 가지는 방향성은 결국
컨트롤러를 최대한 얇게 만들자
는 것이다.
고로 컨트롤러가 가지는 역할을 오로지 요청 진입점, 유즈케이스 호출, 응답 반환으로 좁히고
변환 책임을 더 이상 Controller도 Service도 아닌 Argument Resolver가 갖게 하는 것으로 수정할 계획이다.
구조를 그려보면 이런 식이다.
Request
→ Argument Resolver
→ Param Object
→ UseCase / Service
이번 피드백을 통해 레이어 설계와 책임 분리에 대한 관점에서 현재 코드 방식을 한 번 더 고민해보는 계기가 되었다.
이런 시도 하나하나가 쌓여 “지금 당장 동작하는 코드”에서 “확장 가능한 구조”로 생각하는 힘을 기르는 데에 도움이 되었으면 한다.