CS: REST API와 RESTful API

hyeppy·2025년 9월 14일

CS

목록 보기
4/11
post-thumbnail

1. API의 기본 개념

API(Application Programming Interface)는 “네가 이렇게 말하면 내가 이렇게 해 줄게.”라는 약속이다. 두 소프트웨어가 서로 요청과 응답을 주고받을 때, 한쪽이 어떻게 요청을 보내고 반대쪽은 어떻게 응답을 보내면 되는지 규칙을 메뉴판으로 정해 두는 것과 같다.

예를 들어, 카페에서 메뉴판(API 문서)을 보고 “아메리카노”라는 단어를 정확히 써야 원하는 음료가 나오는 것처럼, API도 정해진 규격대로 요청해야 원하는 응답을 받을 수 있다. 또한, 바리스타가 주문을 받고 마음대로 제조해 음료를 주면 안 되듯, 서버는 정해진 규격대로만 응답해야 한다.


2. REST의 핵심 개념

REST(Representational State Transfer)는 웹의 기존 기술과 HTTP 프로토콜을 그대로 활용하여 웹의 장점을 최대한 활용할 수 있는 아키텍처 설계 원칙의 집합체다.

2.1 REST의 6가지 제약 조건

  1. Client-Server: 사용자 인터페이스와 데이터 저장 관심사를 분리하여 각각이 독립적으로 진화할 수 있도록 한다.
  2. Stateless: 서버는 클라이언트의 상태 정보를 저장하지 않으며, 각 요청은 완전히 독립적이어야 한다.
  3. Cacheable: 응답은 캐시 가능 여부를 명시해야 하며, 캐시를 통해 성능을 향상시킬 수 있어야 한다.
  4. Uniform Interface: 시스템 전체에 걸쳐 일관된 인터페이스를 제공해야 한다.
  5. Layered System: 클라이언트는 직접 서버에 연결되어 있는지 중간 서버에 연결되어 있는지 알 수 없어야 한다.
  6. Code on Demand (선택): 서버가 클라이언트에 실행 가능한 코드를 전송할 수 있다. 다만, 실제 REST API에서는 거의 쓰이지 않는 제약이다.

2.2 일관된 인터페이스(Uniform Interface)의 4가지 원칙

  1. Resource Identification: 개별 리소스가 URI를 통해 식별되어야 한다.
  2. Manipulation through Representations: 클라이언트가 리소스 표현을 통해 원래 리소스를 조작할 수 있어야 한다.
  3. Self-Descriptive Messages: 각 메시지는 자신을 처리하는 방법에 대한 충분한 정보를 포함해야 한다.
  4. HATEOAS: 하이퍼미디어 링크를 통해 애플리케이션 상태를 전이할 수 있어야 한다. 다만, 실무에서는 잘 사용되지 않는다.

2.3 HATEOAS (Hypermedia As The Engine Of Application State)

각 요청의 응답에 가용한 다른 요청들의 정보를 포함하는 원칙으로, 클라이언트가 API 문서를 참조하지 않아도 다음에 수행할 수 있는 작업 혹은 이전에 작업했던 작업을 발견할 수 있게 해 준다.

{
  "id": 1,
  "title": "Spring Boot 완벽 가이드",
  "author": "김개발",
  "_links": {
    "self": {
      "href": "http://localhost:8080/api/v1/books/1"
    },
    "reviews": {
      "href": "http://localhost:8080/api/v1/books/1/reviews"
    },
    "author": {
      "href": "http://localhost:8080/api/v1/authors/5"
    }
  }
}

3. REST API vs RESTful API

REST API는 위에서 설명한 6가지 제약 조건을 모두 준수하는 이론적으로 완벽한 API를 의미한다. 즉, 자원 식별, 상태 비저장(Stateless), 캐시 처리, 일관된 인터페이스(Uniform Interface), 계층화, 그리고 선택적인 Code on Demand, HATEOAS까지 모두 구현해야 한다.

반면에, RESTful API는 REST의 핵심 원칙(Stateless, URI를 통한 자원 식별, HTTP 메서드 활용 등)을 따르지만 실무적 제약 때문에 HATEOAS나 Code on Demand 같은 요소는 생략하는 경우가 많다. 따라서 대부분의 웹 서비스는 이론적인 REST API라기보다는 RESTful API라고 부르는 것이 더 적합하다.

3.1 RESTful API의 핵심 특성

  1. Stateless, 무상태성
    : 서버는 클라이언트의 이전 요청에 대한 정보를 저장하지 않으며, 각 요청은 완전히 독립적이어야 한다.
    @GetMapping("/books")
    public List<Book> getBooks(@RequestParam int page, @RequestParam int size) {
        // 모든 필요한 정보를 요청에 포함 (O)
        return bookService.getBooks(page, size);
    }
  1. Cacheable, 캐시 가능성
    : 응답은 캐시 가능 여부를 명시해야 한다. 클라이언트와 서버는 서로의 정보를 기억해서는 안 되지만, 한 번 얻어낸 데이터를 재사용할 수 있도록 저장해 두는 것(= 캐싱)은 권장된다.
    @GetMapping("/books/{id}")
    public ResponseEntity<Book> getBook(@PathVariable Long id) {
        Book book = bookService.findById(id);
        return ResponseEntity.ok()
                .cacheControl(CacheControl.maxAge(Duration.ofMinutes(10)))
                .body(book);
    }
  1. Idempotent 멱등성
    : 실제 데이터가 변경되었을 경우를 제외하고, 같은 요청을 여러 번 보내도 결과가 동일해야 한다.
    1. 멱등함: GET, PUT, DELETE, HEAD, OPTIONS
    2. 멱등하지 않음: POST, PATCH(설계에 따라 멱등할 수도 있음)

4. API URL 설계 원칙

4.1 기본 구조

https://api.example.com/v1/books
[프로토콜]://[도메인]/[API버전]/[리소스]

4.2 REST API URL 설계 원칙

  1. URL은 리소스(명사)를 나타내야 하고, 동사는 HTTP 메서드로 표현한다.
  2. 리소스는 복수형을 사용한다. (/books, /users)
  3. 계층 관계는 슬래시(/)로 표현한다.
  4. 특정 리소스는 고유 식별자로 구분한다.
  5. 동사는 URL에 가급적 포함하지 않는다.
원칙설명좋은 예시나쁜 예시
리소스는 명사(복수형)URL은 동사가 아닌 자원(리소스)를 나타내야 하며, 복수형을 사용/api/v1/books/api/v1/getBooks, /api/v1/book/123
HTTP 메서드 활용동작(조회/생성/수정/삭제)은 URL이 아닌 HTTP 메서드로 표현GET /api/v1/books/123GET /api/v1/books/delete/123
계층적 구조URL 경로는 리소스 간의 계층 관계를 반영/api/v1/books/123/reviews/api/v1/reviews?bookId=123 (계층 구조 무시)
쿼리 파라미터 활용검색, 필터링, 정렬, 페이지네이션 등은 쿼리 파라미터 사용/api/v1/books?author=tolkien&page=2/api/v1/books/filterByAuthor/tolkien
일관된 규칙소문자 사용, 단어는 하이픈(-)으로 연결, 언더스코어(_) 지양/api/v1/user-profiles/api/v1/userProfiles, /api/v1/user_profiles
버전 관리URL 경로에 버전 명시 또는 HTTP Header 활용/api/v1/books Accept: application/vnd.example.v1+json/api/books (버전 불명확)

4.3 API 버저닝 (API Versioning)

API 버저닝은 기존 API를 사용하는 클라이언트들의 호환성을 보장하면서 새로운 기능을 추가하거나 변경사항을 적용하는 방법이다. 카카오톡 앱이 업데이트되어도 구버전을 사용하는 사용자들이 앱이 작동하지 않는 것이 아니듯, API도 마찬가지로 기존 버전을 유지하면서 새 버전을 제공해야 한다.

API 버저닝이 필요한 상황

  1. 데이터 구조 변경: 응답 필드 추가/삭제/타입 변경
  2. 비즈니스 로직 변경: 계산 방식이나 처리 로직 변경
  3. 보안 정책 변경: 인증 방식 변경
  4. 성능 개선: 기존 동작을 유지하면서 내부 로직 최적화

버저닝 방식

  • URL 경로 버저닝 (가장 일반적)
@RestController
@RequestMapping("/api/v1/books")
public class BookControllerV1 {
    @GetMapping("/{id}")
    public BookResponseV1 getBook(@PathVariable Long id) {
        return bookService.getBookV1(id);
    }
}

@RestController 
@RequestMapping("/api/v2/books")
public class BookControllerV2 {
    @GetMapping("/{id}")
    public BookResponseV2 getBook(@PathVariable Long id) {
        return bookService.getBookV2(id);
    }
}
GET /api/v1/books/123  // 구버전
GET /api/v2/books/123  // 신버전
  • 헤더 버저닝
@GetMapping(value = "/{id}", headers = "X-API-Version=1")
public BookResponseV1 getBookV1(@PathVariable Long id) {
    return bookService.getBookV1(id);
}

@GetMapping(value = "/{id}", headers = "X-API-Version=2")
public BookResponseV2 getBookV2(@PathVariable Long id) {
    return bookService.getBookV2(id);
}

API 버저닝 시, 권장사항

  1. URL 경로 버저닝 우선 고려
  2. 버전 번호는 단순하게: v1, v2, v3 형태로 유지
  3. 하위 호환성 유지: 가능한 한 기존 API 동작 보장
  4. 버전 수명 주기 관리: 구버전 지원 기간과 EOL 계획 수립

API 버저닝은 사용자 호환성을 위해 구버전을 일정 기간 유지하되, 코드 복잡성을 줄이기 위해 실무에서는 가능한 한 빨리 Deprecated 처리 후 최신 버전으로 통합·업데이트하는 방식으로 관리한다. 이 과정에서 개발자에게 마이그레이션 가이드와 유예 기간을 제공하며, 최근에는 GraphQL이나 BFF 패턴, 혹은 하위 호환 가능한 변경을 통해 버저닝 자체를 최소화하려는 접근도 널리 쓰이고 있다.


5. HTTP 메서드와 CRUD 매핑

5.1 GET (Read) - 조회

GET 메서드는 서버로부터 데이터를 조회할 때 사용하는 메서드로, Request Body를 포함하지 않고 URL과 쿼리 파라미터만으로 요청을 전달한다.

이 메서드는 멱등성을 보장하기 때문에 같은 요청을 여러 번 보내더라도 항상 동일한 결과를 얻을 수 있으며, 서버의 상태를 변경하지 않는 안전한 메서드이기도 하다. 또한 응답 결과를 캐시할 수 있어 성능 최적화에 유리하다. 실무에서는 전체 목록 조회와 특정 항목 조회로 나누어 사용하며, 전체 조회 시에는 페이징, 필터링, 정렬 등의 쿼리 파라미터를 함께 사용하기도 한다.

@GetMapping("/books")
public ResponseEntity<List<Book>> getAllBooks(
    @RequestParam(required = false) String category,
    @RequestParam(defaultValue = "0") int page,
    @RequestParam(defaultValue = "10") int size) {
    
    Page<Book> bookPage = bookService.findBooks(category, PageRequest.of(page, size));
    return ResponseEntity.ok()
            .header("X-Total-Count", String.valueOf(bookPage.getTotalElements()))
            .body(bookPage.getContent());
}

@GetMapping("/books/{id}")
public ResponseEntity<Book> getBook(@PathVariable Long id) {
    return bookService.findById(id)
            .map(book -> ResponseEntity.ok().body(book))
            .orElse(ResponseEntity.notFound().build());
}

5.2 POST (Create) - 생성

POST 메서드는 새로운 리소스를 생성할 때 사용하며, Request Body에 큰 용량의 데이터를 담아 전송할 수 있다.

이 메서드는 GET 요청과 달리 멱등하지 않기 때문에, 같은 POST 요청을 여러 번 보내면 매번 새로운 리소스가 생성된다. 예를 들어 같은 책 정보로 POST를 3번 보내면, 동일한 내용의 데이터가 3개 생성되는 것이다. 성공적으로 리소스가 생성되면 201 Created 상태 코드와 함께 생성된 리소스 정보를 응답으로 돌려주는 것이 일반적이며, 이를 통해 클라이언트는 방금 생성한 데이터의 ID나 기타 서버에서 생성한 정보를 확인할 수 있다.

@PostMapping("/books")
public ResponseEntity<Book> createBook(@Valid @RequestBody CreateBookRequest request) {
    try {
        Book savedBook = bookService.save(request);
        URI location = ServletUriComponentsBuilder
                .fromCurrentRequest()
                .path("/{id}")
                .buildAndExpand(savedBook.getId())
                .toUri();
        
        return ResponseEntity.created(location).body(savedBook);
    } catch (DuplicateIsbnException e) {
        return ResponseEntity.status(HttpStatus.CONFLICT).build();
    }
}

5.3 PUT (Update) - 전체 교체

PUT 메서드는 기존 리소스를 완전히 새로운 데이터로 교체할 때 사용하는 메서드로, Request Body에 해당 리소스의 모든 필드 정보를 포함해야 한다. 만약 일부 필드를 생략하면 해당 필드는 null이나 기본값으로 설정될 수 있으므로 주의해야 한다.

PUT 메서드는 멱등성을 보장하므로, 같은 데이터로 여러 번 요청해도 결과는 동일하다. 또한 지정된 리소스가 존재하지 않는 경우 새로 생성할 수도 있는데, 이러한 경우엔 POST와 유사한 동작을 수행한다. 응답은 POST와 동일하게 수정된 결과 전체 또는 성공 여부를 반환하며, 실무에서는 사용자 프로필 전체 업데이트나 설정 정보 전체 변경과 같은 상황에 주로 사용한다.

@PutMapping("/books/{id}")
public ResponseEntity<Book> updateBook(
    @PathVariable Long id, 
    @Valid @RequestBody UpdateBookRequest request) {
    
    return bookService.update(id, request)
            .map(book -> ResponseEntity.ok().body(book))
            .orElse(ResponseEntity.notFound().build());
}

5.4 PATCH (Update) - 부분 수정

PATCH 메서드는 기존 리소스의 일부분만 수정할 때 사용하는 메서드로, PUT과 달리 Request Body에 변경하고자 하는 부분만 포함하면 된다.

예를 들어 게시글의 제목만 변경하고 싶다면 제목 필드만 Request Body에 포함하면 되고, 다른 필드들은 기존 값을 유지한다. PATCH 메서드도 멱등성을 보장하지만, 구현 방식에 따라 멱등하지 않을 수도 있으므로 설계 시 주의가 필요하다. 응답으로는 수정된 결과 전체를 반환하거나, 단순히 성공 여부만 반환할 수 있다. 실무에서는 사용자가 프로필의 특정 정보만 변경하거나, 게시물의 일부 내용만 수정하는 상황에서 주로 사용된다.

@PatchMapping("/books/{id}")
public ResponseEntity<Book> patchBook(
    @PathVariable Long id, 
    @RequestBody Map<String, Object> updates) {
    
    return bookService.partialUpdate(id, updates)
            .map(book -> ResponseEntity.ok().body(book))
            .orElse(ResponseEntity.notFound().build());
}

// DTO를 사용한 더 안전한 방식
@PatchMapping("/books/{id}")
public ResponseEntity<Book> patchBook(
    @PathVariable Long id,
    @Valid @RequestBody PatchBookRequest request) {
    
    return bookService.partialUpdate(id, request)
            .map(book -> ResponseEntity.ok().body(book))
            .orElse(ResponseEntity.notFound().build());
}

5.5 DELETE (Delete) - 삭제

DELETE 메서드는 지정된 리소스를 삭제할 때 사용하며, 일반적으로 Request Body를 포함하지 않고 URL의 경로 변수를 통해 삭제할 리소스를 식별한다.

이 메서드는 멱등성을 보장하므로 이미 삭제된 소스에 대해 DELETE 요청을 다시 보내더라도 오류가 발생하지 않아야 한다. 성공적으로 삭제가 완료되면 204 No Content 상태 코드를 반환하여 삭제되었지만 응답 본문이 없음을 나타내거나, 200 OK와 함께 삭제된 리소스의 정보를 반환할 수도 있다. 실무에서는 물리적 삭제보다는 논리적 삭제를 구현하는 경우가 많으며, 이때는 삭제 플래그를 설정하거나 삭제 시간을 기록하는 방식을 사용한다.

@DeleteMapping("/books/{id}")
public ResponseEntity<Void> deleteBook(@PathVariable Long id) {
    if (bookService.existsById(id)) {
        bookService.delete(id);
        return ResponseEntity.noContent().build();
    }
    return ResponseEntity.notFound().build();
}

// 논리적 삭제를 사용하는 경우
@DeleteMapping("/books/{id}")
public ResponseEntity<BookDeleteResponse> softDeleteBook(@PathVariable Long id) {
    return bookService.softDelete(id)
            .map(response -> ResponseEntity.ok().body(response))
            .orElse(ResponseEntity.notFound().build());
}

6. HTTP 상태 코드

6.1 2XX Success (성공)

  • 200 OK: 성공적인 GET, PUT, PATCH 요청
  • 201 Created: 성공적인 POST 요청으로 새 리소스 생성
  • 204 No Content: 성공적인 DELETE 요청, 응답 본문 없음

6.2 4XX Client Error (클라이언트 오류)

  • 400 Bad Request : 잘못된 요청
  • 401 Unauthorized : 인증 실패
  • 403 Forbidden : 권한 없음
  • 404 Not Found : 리소스 없음
  • 409 Conflict : 충돌
  • 422 Unprocessable Entity : 유효하지만 처리 불가
  • 429 Too Many Requests : 요청 과다 (Rate Limit)

6.3 5XX Server Error (서버 오류)

  • 500 Internal Server Error: 서버 내부 오류
  • 503 Service Unavailable: 서비스 이용 불가

7. 쿼리 파라미터

쿼리 파라미터는 URL 뒤에 ?를 붙이고 key=value 형태로 추가 정보를 전달하는 방식이다. 웹에서 클라이언트가 서버에게 추가적인 정보나 옵션을 전달할 때 사용하는 가장 일반적인 방법 중 하나이다.

7.1 기본 구조

https://example.com/api/books?category=fiction&page=1&size=10
                             ↑
                         쿼리 시작
  • ?: 쿼리 파라미터의 시작을 알리는 구분자
  • &: 여러 파라미터를 구분하는 구분자
  • key=value: 파라미터 이름과 값의 쌍

7.2 쿼리 파라미터의 주요 용도

필터링 (Filtering)

@GetMapping("/books")
public List<Book> getBooks(
    @RequestParam(required = false) String category,    // 카테고리 필터
    @RequestParam(required = false) String author,      // 작가 필터
    @RequestParam(required = false) String keyword      // 검색 키워드
) {
    return bookService.findBooks(category, author, keyword);
}
GET /books?category=fiction&author=얄코
GET /books?keyword=스프링부트
GET /books?category=programming&keyword=java

페이징 (Pagination)
:
실무에서는 전체 데이터를 한 번에 반환하면 성능 문제가 발생할 수 있으므로 페이징을 사용한다.

@GetMapping("/books")
public Page<Book> getBooks(
    @RequestParam(defaultValue = "0") int page,      // 페이지 번호
    @RequestParam(defaultValue = "10") int size      // 페이지 크기
) {
    Pageable pageable = PageRequest.of(page, size);
    return bookService.findAll(pageable);
}
GET /books?page=0&size=20    // 첫 페이지, 20개씩
GET /books?page=2&size=5     // 3번째 페이지, 5개씩

정렬 (Sorting)

@GetMapping("/books")
public List<Book> getBooks(
    @RequestParam(defaultValue = "title") String sortBy,    // 정렬 기준
    @RequestParam(defaultValue = "asc") String sortDir      // 정렬 방향
) {
    return bookService.findAllSorted(sortBy, sortDir);
}
GET /books?sortBy=publishedDate&sortDir=desc    // 출간일 내림차순
GET /books?sortBy=title&sortDir=asc             // 제목 오름차순

7.3 Query Parameter(쿼리 파라미터) VS Path Variable(경로 변수)

Path Variable (경로 변수): 특정 리소스를 식별할 때

@GetMapping("/books/{id}")          // 특정 리소스 식별
public ResponseEntity<Book> getBook(@PathVariable Long id) {
    return bookService.findById(id)
            .map(ResponseEntity::ok)
            .orElse(ResponseEntity.notFound().build());
}

Query Parameter (쿼리 파라미터): 리소스를 필터링, 정렬, 페이징할 때

@GetMapping("/books")               // 리소스 필터링/옵션
public ResponseEntity<List<Book>> getBooks(@RequestParam String category) {
    List<Book> books = bookService.findByCategory(category);
    return ResponseEntity.ok(books);
}

REST API - 이거 하나로 끝남


profile
Backend

0개의 댓글