REST API 개발 가이드
dictionary-api에서 실제로 적용 중인 API 경로 네이밍/컨트롤러 메소드 규칙이다. 새 컨트롤러(메소드)를 추가할 때
이 문서를 템플릿으로 삼는다.
기본 경로 구조
Section titled “기본 경로 구조”- 모든 경로는
/v1/<feature>로 시작하고, 이 부분을@RequestMapping에 고정한다./v1은 API 버전이고,<feature>는 소문자 단수 명사 하나다(board,dictionary,menu,search,tools,auth). <feature>는 패키지명(api/<feature>/...)과 동일하게 맞춘다.- 경로 세그먼트는 kebab-case로 짓는다(
find-all,find-by-page,jasypt-encrypt). 대문자, camelCase, snake_case(_)는 쓰지 않는다.
리소스 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등)는 API마다 따로 파일과 클래스를 둔다. find-all/find-by-page는 조회지만POST를 쓴다. 필터·정렬·페이지 정보를 JSON 본문으로 받는데,GET에 본문을 싣는 것은 HTTP 관례상 피하기 때문이다. 키 하나로 찾는 조회(/{id})만GET을 쓴다.- 일괄 삭제(
deleteAll) 예외 규칙: 단건delete는id를 경로로 받아 본문이 필요 없다. 하지만 여러 건을 지우려면id목록을 실을 곳이 필요하다.find-all은 이 문제를POST로 바꿔 풀었지만,deleteAll은DELETE를 유지한 채 요청 바디를 싣는다. HTTP 스펙상DELETE에도 바디를 실을 수 있고, Spring(@RequestBody)·RestAssured·axios(axios.delete(url, {data})) 모두 지원한다.- 빈 목록은 400:
ids가 비어 있으면@NotEmpty검증에 걸려INVALID_PARAMETER(400)로 컨트롤러 진입 전에 거부된다. - 부분 삭제는 허용하지 않는다(all-or-nothing). 삭제 전에
repository.findAllById(ids)로 조회한 건수를 요청 개수(중복 제거 후)와 비교한다. 하나라도 없으면 아무것도 지우지 않고NOT_FOUND(404)로 응답한다.
- 빈 목록은 400:
- 엔티티를 응답으로 직접 노출하지 않고, 항상
XxxResponseDto.from(entity)로 변환한다. 감사 컬럼 6개 중 시각(createDt/updateDt)만 응답에 남기고, 사용자 ID·IP는 뺀다.
계층형/참조 데이터 조회 엔드포인트 (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). 단, 결과가 URL만으로 결정되는 순수 조회이자
브라우저가 파일로 저장해야 하는 export-data(테이블 데이터 SQL 내보내기)는 file의 다운로드 엔드포인트와 같은 이유로
예외적으로 GET /v1/tools/export-data/{tableKey}를 쓴다.
표준 동사에 없는(비-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같은 동의어는 쓰지 않고, 일괄 삭제는deleteAll로 짓는다. - 컨트롤러 클래스의 상속·애너테이션 구성은 패키지 가이드 참고. 필드는 생성자로 주입받는 서비스 하나만 둔다.
- 응답은 항상
ok()/created()/noContent()헬퍼로 감싼다.ResponseEntity.status(...)를 직접 호출하지 않는다. - 요청 검증은
@Valid @RequestBody로 컨트롤러 진입점에서 수행한다(서비스 레이어에서 재검증하지 않는다). - Swagger 문서화: 클래스에는 기능 이름을 한글로 적은
@Tag(name, description)을, 메소드에는@Operation(summary, description)을 붙인다. 200이 아닌 응답(201/204)은@ApiResponse로 명시한다. - 로그인 없이 열어야 하는 API는 클래스 또는 메소드에
@PublicApi를 붙인다(붙이지 않으면 기본값은 로그인 필요).
DTO 클래스 네이밍(<Feature> 접두어)
Section titled “DTO 클래스 네이밍(<Feature> 접두어)”모든 DTO 클래스는 클래스명 맨 앞에 기능 이름을 접두어로 붙인다(BoardCreate, ToolsJasyptEncrypt). 자세한
이유는 패키지 가이드의 “DTO 클래스 네이밍” 항목 참고.