REST API 개발 가이드
dictionary-api에서 실제로 적용 중인 API 경로 네이밍/컨트롤러 메소드 규칙이다. 새 컨트롤러(메소드)를 추가할 때
이 문서를 템플릿으로 삼는다.
기본 경로 구조
Section titled “기본 경로 구조”- 모든 경로는
/v1/<feature>형태다 —/v1은 API 버전,<feature>는 소문자 단수 명사 하나(board,dictionary,menu,search,tools,auth)로@RequestMapping에 고정한다. <feature>는 패키지명(api/<feature>/...)과 동일하게 맞춘다.- 경로 세그먼트는 항상 소문자 단어 + 하이픈(
-) 조합(kebab-case)으로 짓는다 — 대문자·camelCase·snake_case(_)는 쓰지 않는다(find-all,find-by-page,jasypt-encrypt).
리소스 CRUD 엔드포인트 (board가 원형 템플릿)
Section titled “리소스 CRUD 엔드포인트 (board가 원형 템플릿)”| HTTP Method | Path | 컨트롤러 메소드명 | 요청 DTO | 응답 | 비고 |
|---|---|---|---|---|---|
POST |
/find-all |
findAll |
RequestDto<XxxFindAllRequestDto> |
ListResponseDto<XxxResponseDto> |
페이지네이션 없는 전체 조회 |
POST |
/find-by-page |
findByPage |
PaginationRequestDto<XxxFindByPageRequestDto> |
PaginationResponseDto<XxxResponseDto> |
목록 조회는 전부 이 방식 |
GET |
/{id} |
find |
없음(@PathVariable long id) |
ResponseDto<XxxResponseDto> |
존재하지 않으면 404 + NOT_FOUND |
POST |
(경로 없음) | create |
RequestDto<XxxCreateRequestDto> |
ResponseDto<XxxResponseDto>, 201 |
created(body, URI)로 Location 헤더 포함 |
PUT |
/{id} |
update |
RequestDto<XxxUpdateRequestDto> |
ResponseDto<XxxResponseDto>, 200 |
id 필드를 두지 않고 항상 경로에서만 받는다 |
DELETE |
/{id} |
delete |
없음 | 본문 없음, 204 | noContent() 사용 |
DELETE |
/delete-all |
deleteAll |
RequestDto<XxxDeleteAllRequestDto> |
본문 없음, 204 | 여러 건 일괄 삭제, 아래 예외 규칙 참고 |
- 응답 DTO(
XxxResponseDto)는find/findAll/findByPage/create/update다섯 API가 전부 공유하는 클래스 하나다. 반대로 요청 DTO(XxxFindAllRequestDto/XxxCreateRequestDto/XxxUpdateRequestDto등)는 API마다 독자적인 파일/클래스를 가진다 — 응답은 공유, 요청은 API별 독립이라는 비대칭은 의도적이다. find-all/find-by-page가 조회임에도GET이 아니라POST인 이유: 필터 조건·정렬·페이지 정보를 JSON 본문으로 받기 때문이다(GET에 본문을 싣는 것은 HTTP 관례상 지양). 단순 키 하나로 찾는 조회(/{id})만GET을 쓴다.- 일괄 삭제(
deleteAll) 예외 규칙:delete(단건)는id를 경로 변수로만 받아 본문이 필요 없지만, 여러 건을 지우려면id목록을 실을 곳이 필요하다.find-all이 이 문제를 HTTP Method를POST로 바꿔 해결한 것과 달리,deleteAll은 HTTP Method를 그대로DELETE로 유지하면서 요청 바디를 싣는다 —DELETE도 HTTP 스펙상 요청 바디 자체는 허용되며, Spring(@RequestBody)·RestAssured·axios(axios.delete(url, {data})) 모두 문제없이 지원한다.- 빈 목록은 400:
ids가 비어 있으면@NotEmpty검증에 걸려INVALID_PARAMETER(400)로 컨트롤러 진입 전에 거부된다. - 부분 삭제를 허용하지 않는다(all-or-nothing): 삭제 실행 전에
repository.findAllById(ids)로 조회된 건수와 요청 개수(중복 제거 후)를 비교해, 하나라도 존재하지 않으면 아무것도 지우지 않고NOT_FOUND(404)로 실패시킨다.
- 빈 목록은 400:
- 엔티티를 API 응답으로 직접 노출하지 않고 항상
XxxResponseDto.from(entity)정적 팩토리로 변환한다. 감사 컬럼 6개(createDt/createUserId/createIp/updateDt/updateUserId/updateIp) 중 시각(createDt/updateDt)만 응답에 남기고, 사용자 ID·IP(createUserId/createIp/updateUserId/updateIp)는 제외한다.
계층형/참조 데이터 조회 엔드포인트 (menu)
Section titled “계층형/참조 데이터 조회 엔드포인트 (menu)”리소스 하나를 id로 특정하는 게 아니라 트리 구조를 순회하는 조회는 CRUD 패턴 대신 동사형 경로를 쓴다.
| HTTP Method | Path | 컨트롤러 메소드명 | 응답 |
|---|---|---|---|
GET |
/find-root |
findRoot |
ListResponseDto<MenuResponseDto> |
GET |
/find-child/{parentId} |
findChild |
ListResponseDto<MenuResponseDto> |
요청 DTO 없이 @PathVariable만 쓴다(필터 조건이 없으므로 GET으로 충분). 쓰기 API는 없다(참조 데이터 조회 전용).
순수 외부 조회 엔드포인트 (search)
Section titled “순수 외부 조회 엔드포인트 (search)”리소스가 아니라 외부 사전별로 별도 조회를 표현하므로 menu와 동일한 패턴(동사형 경로 + @PathVariable)을 쓴다.
| HTTP Method | Path | 컨트롤러 메소드명 | 응답 |
|---|---|---|---|
GET |
/find-stdict/{word} |
findStdict |
ResponseDto<SearchStdictResponseDto> |
GET |
/find-krdict/{word} |
findKrdict |
ResponseDto<SearchKrdictResponseDto> |
DB를 거치지 않아 repository/entity가 없다. 두 외부 API의 응답 스키마가 서로 달라(krdict만 영어 번역 포함)
하나로 합쳐 반환하지 않고 API를 나눈다.
커맨드형 유틸리티 엔드포인트 (tools)
Section titled “커맨드형 유틸리티 엔드포인트 (tools)”암호화/인코딩처럼 리소스 개념이 없는 순수 기능 호출은 전부 POST /v1/tools/<동사-kebab-case> 형태다:
jasypt-encrypt, encode-password, compare-password. 경로의 kebab-case 동사구는 컨트롤러 메소드의
camelCase 이름과 1:1 대응한다(jasypt-encrypt → jasyptEncrypt).
표준 동사에 없는(비-CRUD) 액션 메소드 규칙
Section titled “표준 동사에 없는(비-CRUD) 액션 메소드 규칙”find/findAll/create/update/delete 같은 표준 동사로 표현할 수 없는(리소스가 아니라 “동작”을 표현하는)
API에 적용한다(auth의 login/logout, tools의 encodePassword 등).
- 우선순위: 조회/생성/수정/삭제로 표현 가능한 API는 이 규칙이 아니라 위 CRUD/조회 규칙을 먼저 따른다.
- 형태: 영어 동사(+목적어) 구를 camelCase로 짓되 동사를 항상 앞에 둔다 —
encodePassword,login처럼. 경로는 동일한 문구를 kebab-case로 옮긴다(encode-password). - HTTP Method: 상태를 변경하는(멱등하지 않은) 액션은
POST로 고정한다. 결과가 URL만으로 결정되는 순수 조회 액션이면 예외적으로GET도 허용한다(파일 다운로드 등). - 동사 선택:
find/create/update/delete처럼 CRUD로 오해될 수 있는 동사는 피하고, 의미가 분명한 동사(encode,compare,login)를 쓴다.
공통 메소드/경로 네이밍 규칙 정리
Section titled “공통 메소드/경로 네이밍 규칙 정리”- 경로(kebab-case) ↔ 메소드명(camelCase) 1:1 대응:
find-by-page→findByPage,find-root→findRoot. 경로 세그먼트를 그대로 camelCase로 옮긴 이름을 쓰고 임의로 줄이거나 다르게 짓지 않는다. - 조회 메소드는
find로 시작한다:find(단건),findAll(전체),findByPage(페이지),findRoot/findChild(계층).get/retrieve/list등 다른 동사를 섞지 않는다. - 쓰기 메소드는
create/update/delete로 고정한다(save/modify/remove등 동의어를 쓰지 않는다). 일괄 삭제는delete에All을 붙인deleteAll을 쓴다. - 컨트롤러 클래스는
AbstractController를 상속하고,@RestController+@RequestMapping("/v1/<feature>")+@RequiredArgsConstructor를 붙인다. 서비스는 생성자 주입 하나만 필드로 둔다. - 응답은 항상
ok()/created()/created(body, location)/noContent()헬퍼로 감싼다 —ResponseEntity.status(...)를 직접 호출하지 않는다. - **요청 검증은
@Valid @RequestBody**로 컨트롤러 진입점에서 수행한다(서비스 레이어에서 재검증하지 않는다). - Swagger 문서화:
@Tag(name, description)을 클래스에 붙이고(기능 이름을 한글로), 각 메소드에@Operation(summary, description)을 붙인다. 기본 상태 코드(200)를 벗어나는 경우(201/204)는@ApiResponse(responseCode, description)으로 명시한다. - 로그인 없이 열어야 하는 API는 클래스 또는 메소드에
@PublicApi를 붙인다(붙이지 않으면 기본값은 로그인 필요).
DTO 클래스 네이밍(<Feature> 접두어)
Section titled “DTO 클래스 네이밍(<Feature> 접두어)”모든 DTO 클래스는 클래스명 맨 앞에 기능 이름을 접두어로 붙인다(BoardCreate, ToolsJasyptEncrypt). 자세한
이유는 패키지 가이드의 “DTO 클래스 네이밍” 항목 참고.