Skip to content

GET/POST/PUT/DELETE 사용 패턴

같은 HTTP Method라도 dictionary-api의 기능마다 실제로 쓰는 패턴이 다르다. 기능별 실제 컨트롤러 코드로 “이럴 땐 이 Method, 이런 모양”을 정리한다. 경로/메소드명 네이밍 규칙 자체는 REST API 개발 가이드 참고.

  1. 단건 조회(/{id}) — board
    @GetMapping(value = "/{id}", produces = MediaType.APPLICATION_JSON_VALUE)
    public ResponseEntity<ResponseDto<BoardResponseDto>> find(@PathVariable final long id) { ... }
    가장 기본적인 형태 — 요청 DTO 없이 @PathVariable 하나만 받는다. 존재하지 않으면 서비스가 BizException(NOT_FOUND)를 던져 404로 응답한다. 전체 코드는 MVC 코드 작성 가이드의 Controller 항목 참고.
  2. 계층형 조회(동사 경로 + @PathVariable) — menu
    api/menu/controller/MenuController.java
    @GetMapping(value = "/find-root", produces = MediaType.APPLICATION_JSON_VALUE)
    public ResponseEntity<ListResponseDto<MenuResponseDto>> findRoot() {
    final List<MenuResponseDto> payload =
    menuService.findRoot().stream().map(MenuResponseDto::from).toList();
    return ok(new ListResponseDto<>(payload));
    }
    @GetMapping(value = "/find-child/{parentId}", produces = MediaType.APPLICATION_JSON_VALUE)
    public ResponseEntity<ListResponseDto<MenuResponseDto>> findChild(
    @PathVariable final long parentId) {
    final List<MenuResponseDto> payload =
    menuService.findChild(parentId).stream().map(MenuResponseDto::from).toList();
    return ok(new ListResponseDto<>(payload));
    }
    리소스 하나를 id로 특정하는 게 아니라 트리를 순회하는 조회 — 필터 조건이 없어 요청 DTO 없이 GET으로 충분하다.
  3. 순수 외부 API 조회 — search
    api/search/controller/SearchController.java
    @GetMapping(value = "/find-stdict/{word}", produces = MediaType.APPLICATION_JSON_VALUE)
    public ResponseEntity<ResponseDto<SearchStdictResponseDto>> findStdict(
    @PathVariable final String word) {
    return ok(new ResponseDto<>(searchService.findStdict(word)));
    }
    DB를 거치지 않아 repository/entity가 없다 — 서비스가 외부 API 클라이언트(StdictClient)를 바로 호출해 변환한 응답을 돌려준다.
  4. 파일 다운로드(JSON이 아닌 바이너리 응답) — file
    api/file/controller/FileController.java
    @GetMapping("/download/{fileKey}")
    public ResponseEntity<byte[]> download(@PathVariable final String fileKey) {
    final FileDownloadContext context = fileService.download(FileKey.from(fileKey));
    return ok(context.getContent(), context.getFileName(), context.getContentType());
    }
    상태를 바꾸지 않는 순수 조회이고, 프런트엔드가 <a href>로 직접 이동시켜야 브라우저가 파일 저장 UI를 띄우므로 GET을 쓴다. 응답을 ResponseDto(JSON)로 감싸지 않고 ResponseEntity<byte[]>에 Content-Disposition: attachment를 직접 실어 반환한다.
  1. 필터 조건이 있는 목록 조회 — board
    api/board/controller/BoardController.java
    @PostMapping(
    value = "/find-by-page",
    consumes = MediaType.APPLICATION_JSON_VALUE,
    produces = MediaType.APPLICATION_JSON_VALUE)
    public ResponseEntity<PaginationResponseDto<BoardResponseDto>> findByPage(
    @Valid @RequestBody final PaginationRequestDto<BoardFindByPageRequestDto> request) {
    return ok(new PaginationResponseDto<>(boardService.findByPage(request).map(BoardResponseDto::from)));
    }
    조회인데도 GET이 아니라 POST인 이유: 필터·정렬·페이지 정보를 JSON 본문으로 받아야 하기 때문이다(GET에 본문을 싣는 것은 HTTP 관례상 지양한다).
  2. 리소스 생성(201 + Location 헤더) — board
    @PostMapping(consumes = MediaType.APPLICATION_JSON_VALUE, produces = MediaType.APPLICATION_JSON_VALUE)
    public ResponseEntity<ResponseDto<BoardResponseDto>> create(
    @Valid @RequestBody final RequestDto<BoardCreateRequestDto> request) { ... }
    created(body, URI)로 생성된 리소스의 위치를 Location 헤더에 실어 응답한다. 전체 코드는 MVC 코드 작성 가이드의 Controller 항목 참고.
  3. 상태를 바꾸는 인증 액션(리소스가 아니라 “동작”) — auth
    api/auth/controller/AuthController.java
    @PublicApi
    @PostMapping("/login")
    public ResponseEntity<ResponseDto<SessionUser>> login(
    @Valid @RequestBody RequestDto<AuthLoginRequestDto> request) {
    final SessionUser sessionUser = authService.login(request);
    return ok(new ResponseDto<>(sessionUser));
    }
    로그인/로그아웃처럼 리소스가 아니라 “동작”을 표현하는 API는 명사(/{id})가 아니라 동사 경로를 쓰고, 세션 생성처럼 멱등하지 않은 상태 변경이라 POST를 쓴다.
  4. 커맨드형 유틸리티(리소스 개념이 없는 순수 기능 호출) — tools
    api/tools/controller/ToolsController.java
    @PostMapping("/jasypt-encrypt")
    public ResponseEntity<ResponseDto<ToolsJasyptEncryptResponseDto>> jasyptEncrypt(
    @Valid @RequestBody RequestDto<ToolsJasyptEncryptRequestDto> request) {
    final ToolsJasyptEncryptContext context = request.getPayload().toContext();
    final ToolsJasyptEncryptResponseDto payload =
    ToolsJasyptEncryptResponseDto.from(toolsService.jasyptEncrypt(context));
    return ok(new ResponseDto<>(payload));
    }
    Entity가 없는 기능이라 RequestDto/ResponseDto 사이 중간 표현으로 Context 클래스를 쓴다 (toContext()로 변환).
  1. 리소스 수정 — board
    api/board/controller/BoardController.java
    @GuestForbidden
    @PutMapping(
    value = "/{id}",
    consumes = MediaType.APPLICATION_JSON_VALUE,
    produces = MediaType.APPLICATION_JSON_VALUE)
    public ResponseEntity<ResponseDto<BoardResponseDto>> update(
    @PathVariable final long id,
    @Valid @RequestBody final RequestDto<BoardUpdateRequestDto> request) {
    final BoardEntity result = boardService.update(id, request);
    return ok(new ResponseDto<>(BoardResponseDto.from(result)));
    }
    id는 요청 DTO 필드가 아니라 항상 경로에서만 받는다 — 수정 대상은 URL이 정하고, 본문은 바뀔 값만 담는다.
  1. 단건 삭제(본문 없음) — board
    @DeleteMapping(value = "/{id}")
    public ResponseEntity<Void> delete(@PathVariable final long id) { ... }
    id를 경로 변수로만 받아 본문이 필요 없다 — 존재하지 않으면 404, 성공하면 204(No Content). 전체 코드는 MVC 코드 작성 가이드의 Controller 항목 참고.
  2. 일괄 삭제(예외적으로 요청 바디를 싣는 DELETE) — board
    api/board/controller/BoardController.java
    @GuestForbidden
    @DeleteMapping(value = "/delete-all", consumes = MediaType.APPLICATION_JSON_VALUE)
    public ResponseEntity<Void> deleteAll(
    @Valid @RequestBody final RequestDto<BoardDeleteAllRequestDto> request) {
    boardService.deleteAll(request);
    return noContent();
    }
    여러 건을 한 번에 지우려면 삭제할 id 목록을 실을 곳이 필요하다. find-all이 같은 문제(GET은 본문을 못 싣는다)를 HTTP Method를 POST로 바꿔 해결한 것과 달리, deleteAll은 DELETE를 그대로 유지하면서 요청 바디를 싣는다 — delete와 같은 “삭제” 동사 계열이라는 의미를 유지하기 위해서다. DELETE도 HTTP 스펙상 요청 바디 자체는 허용되며, Spring(@RequestBody)·RestAssured·axios(axios.delete(url, {data})) 모두 문제없이 지원한다. 하나라도 존재하지 않는 id가 섞여 있으면 아무것도 지우지 않고 404(NOT_FOUND)로 응답한다(all-or-nothing).