7단계. Accessing JPA Data with REST

Back·2026년 7월 31일

지난 글에서 Spring Data JPA로 Customer 엔티티를 저장하고 조회하는 방법을 정리했었는데요, 이번엔 한 걸음 더 나아가서 JPA 리포지토리를 아예 REST API로 자동 노출시켜주는 Spring Data REST를 정리해봤습니다. 컨트롤러를 따로 만들지 않아도, 리포지토리 인터페이스만 정의하면 CRUD용 REST 엔드포인트가 자동으로 생긴다는 게 핵심입니다.

Spring Data REST가 하는 일

Spring Data REST는 Spring HATEOASSpring Data JPA의 기능을 자동으로 결합해줍니다. 쉽게 말하면, JPA 리포지토리를 만들기만 하면 그걸 기반으로 하이퍼미디어(HAL) 형식의 RESTful API를 자동으로 만들어주는 도구입니다.

Spring Data REST는 JPA뿐 아니라 Neo4j, Gemfire, MongoDB 같은 다른 Spring Data 백엔드도 지원하지만, 이 글에서는 JPA만 다룹니다.

사전 준비물

  • Java 17 이상
  • 소요 시간 약 15분

프로젝트 시작하기

Spring Initializr에서 다음 세 가지 의존성을 추가해서 프로젝트를 생성합니다.

  • Rest Repositories
  • Spring Data JPA
  • H2 Database

1. 도메인 객체 만들기 - Person

먼저 REST로 노출할 대상이 될 엔티티를 만듭니다.

package com.example.accessingdatarest;

import jakarta.persistence.Entity;
import jakarta.persistence.GeneratedValue;
import jakarta.persistence.GenerationType;
import jakarta.persistence.Id;

@Entity
public class Person {

  @Id
  @GeneratedValue(strategy = GenerationType.AUTO)
  private Long id;

  private String firstName;
  private String lastName;

  public String getFirstName() {
    return firstName;
  }

  public void setFirstName(String firstName) {
    this.firstName = firstName;
  }

  public String getLastName() {
    return lastName;
  }

  public void setLastName(String lastName) {
    this.lastName = lastName;
  }
}

이전 글에서 다뤘던 Customer 엔티티와 구조가 거의 같습니다. @Id@GeneratedValue로 자동 생성되는 ID를 가지고, firstName, lastName 두 속성만 가진 단순한 객체입니다.

2. 리포지토리 만들기 - 여기가 핵심

package com.example.accessingdatarest;

import java.util.List;

import org.springframework.data.repository.PagingAndSortingRepository;
import org.springframework.data.repository.CrudRepository;
import org.springframework.data.repository.query.Param;
import org.springframework.data.rest.core.annotation.RepositoryRestResource;

@RepositoryRestResource(collectionResourceRel = "people", path = "people")
public interface PersonRepository extends PagingAndSortingRepository<Person, Long>, CrudRepository<Person,Long> {

  List<Person> findByLastName(@Param("name") String name);

}

이전 JPA 글에서 봤던 CrudRepository와 비슷하지만, 몇 가지 새로운 부분이 있습니다.

요소의미
PagingAndSortingRepository<Person, Long>CrudRepository의 기본 CRUD 기능에 더해 페이징·정렬 기능까지 함께 제공하는 인터페이스
@RepositoryRestResource(collectionResourceRel = "people", path = "people")REST 엔드포인트 경로를 커스터마이징하는 어노테이션. 기본값은 /persons인데, 이 어노테이션으로 /people을 쓰도록 지정
findByLastName(@Param("name") String name)lastName으로 검색하는 커스텀 쿼리 메서드. @Param으로 지정한 이름(name)이 나중에 REST 요청의 쿼리 파라미터 이름이 됨

@RepositoryRestResource는 필수는 아니고, 경로 이름 같은 노출 방식을 바꾸고 싶을 때만 붙이면 됩니다. 어노테이션이 없어도 리포지토리는 자동으로 REST 엔드포인트로 노출됩니다.

애플리케이션을 실행하면 Spring Boot가 알아서 이 인터페이스의 구현체를 만들고, 인메모리 데이터베이스(H2)와 연결해줍니다. 그리고 Spring Data REST가 그 위에 컨트롤러, JSON 컨버터 등을 자동으로 얹어서 진짜 REST API로 만들어줍니다. 컨트롤러 코드를 단 한 줄도 작성하지 않았다는 점이 이 예제의 핵심입니다.

3. 애플리케이션 실행

./gradlew bootRun

또는 Maven이라면

./mvnw spring-boot:run

4. curl로 API 테스트해보기

최상위 엔드포인트 확인

$ curl http://localhost:8080
{
  "_links" : {
    "people" : {
      "href" : "http://localhost:8080/people{?page,size,sort*}",
      "templated" : true
    },
    "profile" : {
      "href" : "http://localhost:8080/profile"
    }
  }
}

people이라는 링크가 자동으로 생성된 걸 볼 수 있습니다. ?page, ?size, ?sort 같은 페이징/정렬 옵션도 함께 노출됩니다. 응답 형식은 HAL(Hypertext Application Language) 이라는 JSON 포맷을 사용하는데, 데이터 옆에 관련 링크를 함께 실어 보내는 방식입니다.

목록 조회 (아직 데이터 없음)

$ curl http://localhost:8080/people
{
  "_embedded" : {
    "people" : [ ]
  },
  "_links" : {
    "self" : { "href" : "http://localhost:8080/people?page=0&size=20" },
    "profile" : { "href" : "http://localhost:8080/profile/people" },
    "search" : { "href" : "http://localhost:8080/people/search" }
  },
  "page" : {
    "number" : 0,
    "size" : 20,
    "totalElements" : 0,
    "totalPages" : 0
  }
}

새 데이터 생성 (POST)

$ curl -i -H "Content-Type:application/json" \
  -d '{"firstName": "Frodo", "lastName": "Baggins"}' \
  http://localhost:8080/people

응답 헤더에 Location: http://localhost:8080/people/1이 포함되어, 새로 생성된 리소스의 URI를 알려줍니다.

Windows 환경에서 작은따옴표(') 사용이 안 된다면, WSL을 쓰거나 -d "{\"firstName\": \"Frodo\", \"lastName\": \"Baggins\"}"처럼 큰따옴표를 이스케이프해서 사용하면 됩니다.

기본적으로 POST 응답은 생성된 리소스 본문을 바로 돌려주지 않는데, RepositoryRestConfigurationsetReturnBodyOnCreate() / setReturnBodyOnUpdate() 메서드로 이 동작을 바꿀 수 있습니다.

개별/전체 조회 (GET)

$ curl http://localhost:8080/people/1
$ curl http://localhost:8080/people

목록 조회 시 people이라는 이름으로 묶여서 나오는데, 이는 Spring Data REST가 엔티티 이름을 자동으로 복수형으로 바꿔주기 때문입니다.

커스텀 쿼리 사용하기

리포지토리에 정의했던 findByLastName도 자동으로 검색 엔드포인트가 됩니다.

$ curl http://localhost:8080/people/search
{
  "_links" : {
    "findByLastName" : {
      "href" : "http://localhost:8080/people/search/findByLastName{?name}",
      "templated" : true
    },
    "self" : { "href" : "http://localhost:8080/people/search" }
  }
}

실제 검색:

$ curl http://localhost:8080/people/search/findByLastName?name=Baggins

쿼리 파라미터 이름(name)이 리포지토리 메서드에 붙였던 @Param("name")과 정확히 일치하는 걸 확인할 수 있습니다.

참고: 메서드 반환 타입을 List<Person>으로 선언했기 때문에 여러 건이 반환됩니다. 만약 반환 타입을 Person 하나로만 선언했다면, 결과가 여러 건이어도 그중 하나만 임의로 반환되므로 여러 건이 나올 수 있는 쿼리에는 단일 타입 반환을 쓰지 않는 게 좋습니다.

수정과 삭제 (PUT / PATCH / DELETE)

# 전체 교체
$ curl -X PUT -H "Content-Type:application/json" \
  -d '{"firstName": "Bilbo", "lastName": "Baggins"}' \
  http://localhost:8080/people/1

# 일부 필드만 수정
$ curl -X PATCH -H "Content-Type:application/json" \
  -d '{"firstName": "Bilbo Jr."}' \
  http://localhost:8080/people/1

# 삭제
$ curl -X DELETE http://localhost:8080/people/1

PUTPATCH의 차이가 명확한데요.

  • PUT: 레코드 전체를 교체합니다. 요청 본문에 없는 필드는 null로 바뀝니다.
  • PATCH: 요청 본문에 포함된 필드만 부분적으로 수정합니다.

정리

이번 예제에서 확인한 핵심 내용을 정리하면 다음과 같습니다.

  • PagingAndSortingRepository(+CrudRepository)를 상속하는 인터페이스 하나만 만들면, Spring Data REST가 그 위에 REST API 전체(목록/단건 조회, 생성, 수정, 삭제, 커스텀 검색)를 자동으로 얹어준다.
  • @RepositoryRestResource로 엔드포인트 경로 등 노출 방식을 커스터마이징할 수 있다.
  • 응답은 HAL 형식을 사용해서, 데이터와 함께 관련 링크(self, search 등)를 같이 내려준다.
  • 컨트롤러 코드 없이도 curl 하나로 API 전체를 탐색할 수 있다는 게 이 방식의 가장 큰 장점이다. 클라이언트와 별도의 API 문서를 주고받지 않아도, 응답에 포함된 링크만 따라가면 어떤 엔드포인트가 있는지 스스로 발견할 수 있다.

지난 글의 CustomerRepository와 비교해보면, JPA 리포지토리 자체는 거의 똑같은데 @RepositoryRestResource 어노테이션 하나만 추가했을 뿐인데도 완전한 REST API가 만들어진다는 점이 인상 깊었습니다. 다만 이런 자동 생성 방식은 프로토타이핑이나 내부 관리용 API에는 좋지만, 외부에 공개하는 API라면 응답 구조나 보안 설정을 더 세밀하게 통제할 수 있는 방식도 함께 고려해볼 필요가 있을 것 같습니다.

0개의 댓글