개발 가이드 백엔드 API 설계 규칙
최종 수정:

API 설계 규칙

REST API 설계 규칙 종합 참고 (레이어 아키텍처, URL, 응답 형식)

API 설계 규칙

레이어 아키텍처

Controller → Service → Repository
레이어역할금지 사항
Controller요청/응답 처리, 입력값 검증비즈니스 로직
Service비즈니스 로직, 트랜잭션 관리DB 직접 접근
RepositoryDB 접근비즈니스 판단

패키지 구조

domain/
└── board/
    ├── controller/
    │   └── BoardController.java
    ├── service/
    │   └── BoardService.java
    ├── repository/
    │   └── BoardRepository.java
    ├── vo/
    │   ├── BoardCreateReq.java     # 요청 VO (Req 접미사)
    │   └── BoardDetailRes.java     # 응답 VO (Res 접미사)
    └── Board.java                  # Entity (도메인 루트)

REST API 설계

URL 규칙

GET    /api/v1/posts           # 목록 조회
GET    /api/v1/posts/{id}      # 단건 조회
POST   /api/v1/posts           # 생성
PUT    /api/v1/posts/{id}      # 전체 수정
PATCH  /api/v1/posts/{id}      # 부분 수정
DELETE /api/v1/posts/{id}      # 삭제
  • URL: 소문자 케밥케이스 (/api/v1/user-posts)
  • 복수형 명사 사용 (/posts, /users)
  • 버전 포함 (/api/v1/)

HTTP 상태코드

상황코드
조회 성공200 OK
생성 성공201 Created
잘못된 요청400 Bad Request
인증 실패401 Unauthorized
권한 없음403 Forbidden
리소스 없음404 Not Found
서버 오류500 Internal Server Error

컨트롤러 작성 규칙

@Controller
@RequestMapping("/board")
@RequiredArgsConstructor
public class BoardController {

    private final BoardService boardService;

    // 화면 반환 (Thymeleaf)
    @GetMapping("/{id}")
    public String detail(@PathVariable Long id, Model model) {
        model.addAttribute("board", boardService.getBoard(id));
        return "board/detail";
    }
}

// REST API 컨트롤러
@RestController
@RequestMapping("/api/v1/boards")
@RequiredArgsConstructor
public class BoardApiController {

    private final BoardService boardService;

    @PostMapping
    public ResponseEntity<ApiResponse<BoardDetailRes>> regBoard(
            @RequestBody @Valid BoardCreateReq req) {
        return ResponseEntity.status(201)
                .body(ApiResponse.ok(boardService.regBoard(req)));
    }

    @GetMapping("/{id}")
    public ResponseEntity<ApiResponse<BoardDetailRes>> getBoard(@PathVariable Long id) {
        return ResponseEntity.ok(ApiResponse.ok(boardService.getBoard(id)));
    }
}

응답 형식

모든 REST API 응답은 ApiResponse<T> 래퍼 사용:

// 성공
{
    "success": true,
    "data": { ... },
    "error": null
}

// 실패
{
    "success": false,
    "data": null,
    "error": "에러 메시지"
}
// 성공
return ResponseEntity.ok(ApiResponse.ok(data));

// 실패 (GlobalExceptionHandler에서 처리)
return ResponseEntity.badRequest().body(ApiResponse.fail("잘못된 요청입니다."));

입력값 검증

// VO에 Bean Validation 어노테이션
@Getter
@NoArgsConstructor
@AllArgsConstructor
public class BoardCreateReq {
    @NotBlank(message = "제목은 필수입니다.")
    @Size(max = 200, message = "제목은 200자 이내여야 합니다.")
    private String title;

    @NotBlank(message = "내용은 필수입니다.")
    @Size(max = 10000)
    private String content;
}

// 컨트롤러에서 @Valid 적용
@PostMapping
public ResponseEntity<ApiResponse<BoardDetailRes>> regBoard(
        @RequestBody @Valid BoardCreateReq req) { ... }

메서드 네이밍 규칙

동작접두사예시
등록regregBoard, regUser
수정uptuptBoard, uptUser
삭제deldelBoard, delUser
조회getgetBoard, getUser

트랜잭션 관리

@Service
@Transactional(readOnly = true)   // 클래스 기본값: 읽기 전용
@RequiredArgsConstructor
public class BoardService {

    private final BoardRepository boardRepository;

    @Transactional                 // 쓰기 작업에만 별도 선언
    public BoardDetailRes regBoard(BoardCreateReq req) {
        Board board = Board.builder()
                .title(req.getTitle())
                .content(req.getContent())
                .build();
        Board saved = boardRepository.save(board);
        return BoardDetailRes.from(saved);
    }

    public BoardDetailRes getBoard(Long id) {
        return boardRepository.findById(id)
                .map(BoardDetailRes::from)
                .orElseThrow(() -> new IllegalArgumentException("게시글을 찾을 수 없습니다: " + id));
    }
}

Entity → DTO 변환

// Entity를 Controller에서 직접 반환 금지
// Service에서 DTO로 변환 후 반환

@Getter
@Builder
@NoArgsConstructor
@AllArgsConstructor
public class BoardDetailRes {
    private Long id;
    private String title;
    private String content;
    private LocalDateTime createdAt;

    public static BoardDetailRes from(Board board) {
        return BoardDetailRes.builder()
                .id(board.getId())
                .title(board.getTitle())
                .content(board.getContent())
                .createdAt(board.getCreatedAt())
                .build();
    }
}
AI 문서 검색

현재 페이지 내용을 기반으로 질문하세요.