[Spring] Author API 실습: Controller 계층의 역할

이지연·2026년 1월 20일

아래 코드는 Spring 기반 게시판 예제에서 Author(회원) 관련 API를 제공하는 AuthorController를, “컨트롤러가 맡아야 할 책임”과 “예외 처리 개선 흐름” 중심으로 정리한 글이다. (요청/응답 DTO 설계 의도까지 포함)


AuthorController의 역할

AuthorController는 Author 관련 HTTP 요청을 받아서 Service에 위임하고, Service가 반환한 DTO를 그대로 응답으로 내려주는 레이어다.

  • 컨트롤러의 핵심 책임:
    • 요청 데이터 바인딩 (@RequestBody, @PathVariable)
    • 요청 유효성 검증 트리거 (@Valid)
    • 적절한 HTTP 상태코드/응답 형식 결정 (ResponseEntity)
    • 비즈니스 로직은 Service로 위임

@RestController와 @RequestMapping

@RestController

@RestController@Controller + @ResponseBody 조합으로, 메서드 반환값을 View 이름이 아니라 HTTP 응답 바디(JSON/Text)로 바로 내려준다.

즉 이 컨트롤러는 “서버 사이드 렌더링(MVC View)”이 아니라 REST API 응답을 만드는 형태다.

@RequestMapping("/author")

클래스 레벨의 @RequestMapping("/author")는 이 컨트롤러가 처리하는 URL에 공통 prefix를 부여한다.

  • 예: /author/create, /author/list, /author/{id}

DI(의존성 주입): AuthorService를 왜 생성자 주입으로 받나

컨트롤러는 AuthorService 없이는 동작하지 않는다. 코드에서는 이를 명확히 하기 위해 다음 구조를 쓴다.

private final AuthorService authorService;

@Autowired
public AuthorController(AuthorService authorService) {
    this.authorService = authorService;
}

여기서 포인트:

  • final로 선언: 필수 의존성임을 명확히 하고, 생성 시점에 주입이 강제되어 “불완전한 객체”를 방지
  • 생성자 주입: 테스트/리팩터링에 유리하고, 의존관계가 명확함
  • 과거에 컨트롤러에서 new AuthorService()를 매번 만들면 in-memory 레포지토리 사용 시 데이터가 매번 초기화되는 문제가 생길 수 있는데, 스프링 DI는 이 문제를 구조적으로 막아준다

API 1) 회원가입(Create) — POST /author/create

요청

  • URL: POST /author/create
  • Body(JSON):
{"name":"lee", "email":"lee@naver.com", "password":"1234"}

구현

@PostMapping("/create")
public ResponseEntity<?> create(@RequestBody @Valid AuthorCreateDto dto) {
    authorService.save(dto);
    return ResponseEntity.status(HttpStatus.CREATED).body("OK");
}

핵심 포인트

  • @RequestBody: JSON 바디를 AuthorCreateDto로 바인딩
  • @Valid: DTO 필드에 선언된 validation 어노테이션을 동작시키는 트리거
  • 성공 시 201 Created 반환: “생성” API에 더 적절한 상태코드
  • 이메일 중복 같은 비즈니스 예외(IllegalArgumentException)는 Service에서 발생하고, 컨트롤러는 정상 흐름만 유지하는 방향으로 개선됨

API 2) 회원 목록(Read-List) — GET /author/list

응답

  • JSON 배열 형태
  • 비밀번호 같은 민감정보를 제외하기 위해 AuthorListDto를 사용

구현

@GetMapping("/list")
public List<AuthorListDto> findAll() {
    return authorService.findAll();
}

포인트

  • 단순 조회는 ResponseEntity를 쓰지 않아도 기본적으로 200 OK로 내려가며 JSON으로 직렬화된다
  • 목록 DTO는 “노출 가능한 필드만” 담는 것이 중요하다(민감정보 분리)

API 3) 회원 상세(Read-Detail) — GET /author/{id}

구현(개선 버전)

@GetMapping("/{id}")
public ResponseEntity<?> findById(@PathVariable Long id) {
    AuthorDetailDto dto = authorService.findById(id);
    return ResponseEntity.status(HttpStatus.OK).body(dto);
}

기존 방식의 문제(주석으로 남긴 학습 포인트)

예전 코드에서는 컨트롤러에서 try-catch로 예외를 잡고 CommonErrorDto를 반환하는 방식이 있었는데, 이 방식은 다음 문제가 있었다.

  • 상태코드가 200으로 고정되거나(혹은 분기 처리가 번거로움)
  • 컨트롤러마다 에러 DTO 생성 로직이 중복됨
  • 에러 응답 포맷이 컨트롤러마다 달라질 위험이 커짐

그래서 개선 방향은:

  • 컨트롤러는 성공 응답만 담당
  • 실패(예외)는 @RestControllerAdvice 기반 전역 예외 핸들러에서 공통 처리

이렇게 하면 응답 포맷(CommonErrorDto)과 상태코드(404, 400 등)를 한 곳에서 통일할 수 있다.


API 4) 회원 탈퇴(Delete) — DELETE /author/{id}

구현

@DeleteMapping("/{id}")
public String delete(@PathVariable Long id) {
    authorService.delete(id);
    return "OK";
}

포인트

  • 실제 삭제 로직은 Service/Repository 계층에서 처리
  • 현재는 단순히 "OK" 문자열 반환이지만, 실무적으로는 보통 ResponseEntity<Void>로 204 No Content를 주거나, 표준 응답 포맷을 맞추는 경우도 많다(프로젝트 정책에 따라 선택)

이 컨트롤러의 핵심 “개선 메시지”

이 코드가 의도하는 가장 중요한 학습 포인트는 아래 2가지로 요약된다.

  • 컨트롤러는 요청 바인딩/검증/응답에 집중하고, 비즈니스 로직은 Service로 위임한다.
  • 예외 처리는 컨트롤러에서 분기하지 말고, @RestControllerAdvice전역 공통 처리하면 코드 중복이 줄고 응답 정책이 통일된다.
profile
Eazy하게

0개의 댓글