[Spring] Spring Data Rest Respository 소개 및 샘플

|

다른 작업을 하려고 Spring Initializr에서 프로젝트 생성중 “Rest Repository”라는 Dependency항목이 있어 먼가 하고 보다가 서칭한 내용을 정리해보았습니다. 아래 내용에 대한 샘플은 다음 주소에서 확인 할 수 있습니다.

https://github.com/jistol/sample/tree/master/ex-springdata-rest-sample

Spring Data Rest Respository

Spring Data Rest Respository는 Spring Data 프로젝트의 서브 프로젝트로 Repository의 설정만으로 REST API 서버를 구성해주는 신박한 기능입니다. 사용자는 Entity 클래스와 Repository 인터페이스만 작성하면 나머지 CRUD 작업은 모두 알아서 RESTful하게 생성됩니다.

SpringData REST의 주요 기능은 Data Repository로부터 Resource를 추출하는 것으로 핵심은 Repository 인터페이스입니다. 예를 들어 OrderRepository와 같은 Repository인터페이스가 있을 경우 소문자의 복수형 resource를 뽑아내어 /orders 를 만듭니다. 그리고 /orders/{id} 하위에 각 item을 관리할 수 있는 resource를 추출해 냅니다.

시작하기

Spring Initializr페이지에서 아래와 같이 Dependency를 선택하고 “Generate Project”를 눌러 zip으로 다운 받습니다.

project configuration

압축을 풀어 프로젝트의 pom.xml파일을 보면 아래와 같이 Dependency가 포함되 있는 것을 확인 할 수 있습니다.

    <dependencies>
  		<dependency>
  			<groupId>org.springframework.boot</groupId>
  			<artifactId>spring-boot-starter-data-jpa</artifactId>
  		</dependency>
  		<dependency>
  			<groupId>org.springframework.boot</groupId>
  			<artifactId>spring-boot-starter-data-rest</artifactId>
  		</dependency>
  		<dependency>
  			<groupId>org.springframework.data</groupId>
  			<artifactId>spring-data-rest-hal-browser</artifactId>
  		</dependency>

  		<dependency>
  			<groupId>com.h2database</groupId>
  			<artifactId>h2</artifactId>
  			<scope>runtime</scope>
  		</dependency>
  		<dependency>
  			<groupId>org.springframework.boot</groupId>
  			<artifactId>spring-boot-starter-test</artifactId>
  			<scope>test</scope>
  		</dependency>
  	</dependencies>
    

SpringData REST 자체가 어떤 DB를 쓸 지에 대한 설정을 포함하고 있지 않기 때문에 따로 H2 DB를 사용하도록 추가해주었으며 구축된 REST를 쉽게 테스트 해보기 위해 HAL Browser를 추가하였습니다.

먼저 application.properties를 설정합니다.

    # SpringData REST의 기본 context path
    spring.data.rest.basePath=api

    # JPA 설정
    spring.jpa.hibernate.ddl-auto=create
    spring.jpa.show-sql=true

    # H2 DB설정
    spring.datasource.url=jdbc:h2:file:./db/devdb;AUTO_SERVER=TRUE
    spring.datasource.username=test
    spring.datasource.password=test
    spring.datasource.driver-class-name=org.h2.Driver

    spring.h2.console.enabled=true
    spring.h2.console.path=/console
    

Entity는 장바구니(Cart)클래스와 물건(Item)클래스를 만들도록 하겠습니다.

    @Entity
    public class Cart
    {
        @Id
        @GeneratedValue
        private int id;

        private String name;

        private boolean paid;

        @OneToMany(mappedBy = "cart")
        private List<Item> items;

        ....
    }

    @Entity
    public class Item
    {
        @Id
        @GeneratedValue
        private int id;

        private String name;

        private int price;

        @ManyToOne
        @JoinColumn(name = "CART_ID")
        private Cart cart;

        ....
    }
    

그리고 각 Entity의 Repository 인터페이스를 생성합니다. SpringData REST Documentation 사이트에는 CrudRepository를 상속하도록 예제가 나오지만 JpaRepository를 이용해도 무방합니다.

    public interface CartRepository extends JpaRepository<Cart, Integer> {}

    public interface ItemRepository extends JpaRepository<Item, Integer> {}
    

코딩 할 작업은 모두 끝났습니다. 이제 돌려봅시다.

    mvn clean package spring-boot:run
    

project run

실행 로그를 보면 /api로 시작하는 Mapping정보들이 만들어지는것을 볼 수 있습니다. HAL Browser를 통해 실제 Request를 날려봅시다. 아래 URL로 접속합니다.

http://localhost:8080/api

HAL Browser

Explorer에서 직접 주소를 쳐서 호출할 수도 있고 아래 Links를 통해 호출 할 수도 있습니다. Links항목중 Carts의 get버튼을 클릭해보면 현재 Cart목록이 나옵니다.

Cart Empty List

현재는 값이 비어 있는데 Cart값을 하나 넣어보겠습니다. Carts의 non-get버튼을 부르면 Create/Update할 수 있는 화면이 뜹니다.

Cart Insert

다시 Cart목록을 호출해보면 아래와 같이 입력한 Cart가 조회됩니다.

Cart Non-Empty List

/api/{repository}/{id}형태로 단일 목록도 조회 가능합니다. 아래는 Cart의 1번 목록을 조회한 결과 입니다.

Cart 1

그 외의 CRUD 항목도 자동으로 생성하여 제공합니다.

설정

SpringData REST에서 설정 방식은 3가지가 있습니다. 단, Framework가 SpringBoot 1.2 이상 버전일 경우에만 1번 방식을 사용 가능합니다.

  1. application.properties(xml,yaml…)에 설정하기
    spring.data.rest.basePath=/api
    spring.data.rest.defaultPageSize=10
    
  1. @Configuration 사용하기
    @Configuration
    class CustomRestMvcConfiguration {

      @Bean
      public RepositoryRestConfigurer repositoryRestConfigurer() {

        return new RepositoryRestConfigurerAdapter() {

          @Override
          public void configureRepositoryRestConfiguration(RepositoryRestConfiguration config) {
            config.setBasePath("/api");
          }
        };
      }
    }
    
  1. RepositoryRestConfigurerAdapter를 상속받기
    @Component
    public class CustomizedRestMvcConfiguration extends RepositoryRestConfigurerAdapter {

      @Override
      public void configureRepositoryRestConfiguration(RepositoryRestConfiguration config) {
        config.setBasePath("/api");
      }
    }
    

설정 항목은 아래 표를 참고하세요.

Name Description
basePath root URI for Spring Data REST
defaultPageSize change default number of items served in a single page
maxPageSize change maximum number of items in a single page
pageParamName change name of the query parameter for selecting pages
limitParamName change name of the query parameter for number of items to show in a page
sortParamName change name of the query parameter for sorting
defaultMediaType change default media type to use when none is specified
returnBodyOnCreate change if a body should be returned on creating a new entity
returnBodyOnUpdate change if a body should be returned on updating an entity

참고

Spring Data REST - Reference Documentation 2.6.1.RELEASE

Caused by: org.hibernate.AnnotationException: No identifier specified for entity

|

Spring Data JPA + REST 소개블로그 글을 보고 10분만에 REST-API서비스를 만들수 있다는 “spring-data-rest” 샘플 코드를 만들어보던 중에 아래와 같은 오류를 만났습니다.

Caused by: org.hibernate.AnnotationException: No identifier specified for entity: io.github.jistol.Article

Article은 단순한 Entity 클래스로 소스는 아래와 같습니다.

package io.githug.jistol.entity;

import com.fasterxml.jackson.annotation.JsonIgnore;
import org.springframework.data.annotation.Id;

import javax.persistence.Entity;
import javax.persistence.GeneratedValue;
import javax.persistence.OneToMany;
import java.util.Date;
import java.util.List;

@Entity(name = "Article")
public class Article {
    @Id
    @GeneratedValue
    private int id;

    private String nickname;

    @JsonIgnore
    private String password;

    private String content;

    private Date addDate = new Date();

    @OneToMany(mappedBy = "article")
    private List<Comment> comments;

    ....
}

왜 이게 오류가 나지? 라고 생각하며 이것저것 빼먹은게 있나 넣어보고 수정하던 찰라 아주 사소한 실수를 발견했습니다.

import org.springframework.data.annotation.Id;    (X)
import javax.persistence.Id;                      (O)

위와 같이 @Id의 import문이 틀렸던 것입니다. 사소한 부분이지만 생각없이 막 카피하다가 실수하기 딱 좋은 부분이라 메모해둡니다.

Java 이전 JDK 다운로드 경로

|

예전엔 Oracle에서 이전 JDK 다운로드 경로를 “Previus Release”링크를 통해 제공했는데 오늘 들어가보니 없어졌더군요.
대신 JDK다운로드 경로의 맨 하단에 “Java Archive”라는 항목으로 다운로드 링크를 제공합니다.(WARNING 경고와 함께..)
img

찾아 찾아 갈 수 있긴 하지만 찾기 힘드니깐 링크를 적어 둡니다.

이전 JDK 다운받기 링크

img

추가로 최신 버전 JDK는 아래 링크에서 받을 수 있습니다.
Java SE - Download

(SpringBoot) Spring Initializr - 프로젝트 쉽게 생성하기

|

SpringBoot를 이용하여 간단한 POC를 자주 진행하곤 했었는데 매번 Maven/Gradle 설정하고 프로젝트 구조 맞추고 하기가 번거로워 샘플 프로젝트를 하나 만들어두고 사용하고 있었습니다.
그러던 와중에 Spring Initializr를 알게 되어 사용해봤는데 완전 신세계였습니다.

초기화면은 아래와 같습니다.
spring-initializr-main

빌드 툴은 Maven과 Gradle중에 선택할 수 있습니다.
spring-initializr-build-tool

사용할 SpringBoot Version을 선택하고, spring-initializr-version

Group/Artifact를 지정하고,
spring-initializr-group-artifact

사용할 Dependencies를 추가로 선택할 수 있습니다. 키워드 자동완성식 검색을 제공하여 검색하기 편하네요 :) spring-initializr-dependencies

선택한 Dependencies는 아래와 같이 표시됩니다.
spring-initializr-dependencies-list

“Generate Project”버튼을 클릭하여 zip파일로 다운을 받고 압축을 풀어 pom.xml파일을 확인해보면 우아하게 Maven 설정이 되어있습니다.
spring-initializr-pom

프로젝트 구조도 자동으로 잡아주고, 기본 설정파일도 자동으로 포함시켜줍니다. spring-initializr-structure

gitignore파일까지 자동세팅 해주네요. IntelliJ사용자에겐 저거 세팅도 귀찮은데 세심함에 감동 :) spring-initializr-gitignore

아래 “Switch to the full version”을 클릭하면 좀 더 세밀한 설정이 가능합니다.
spring-initializr-full-link

spring-initializr-full-screen

(SpringBoot) Remoting 예제 (RMI, HTTP)

|

Spring에서 RMI사용 예제는 많은데 SpringBoot에서 XML없이 사용하는 예제는 찾기 힘들더군요. Annotation을 Customizing해서 사용하는 예제를 찾았는데 조금 쓰기 편하게 고쳐봤습니다.

구조

Spring에서 지원하는 Remoting 중 HTTP/RMI 통신 예제만 작성하였습니다 Bean등록방식은 @Bean어노테이션을 사용하는 방법과 @Service로 등록한 Bean 객체를 커스텀 어노테이션을 적용하여 등록하는 방식으로 구현하였습니다.

실제 서비스할 객체는 DefaultService인터페이스와 , DefaultServiceImpl구현 객체로 아래와 같습니다.

    public interface DefaultService
    {
         String say(String prefix);
    }

    @Service("defaultService")
    public class DefaultServiceImpl implements DefaultService
    {
        @Override
        public String say(String prefix) {
            return "Hello " + prefix;
        }
    }
    

@Bean 어노테이션 사용방식

RMI의 경우 ServiceName과 Port정보를 직접등록하나 HTTP는 Bean이름과 컨테이너의 포트정보를 그대로 사용합니다.

아래 예제의 경우 다음과 같은 주소로 lookup됩니다.

  • RMI : rmi://127.0.0.1:1099/DefaultServiceRmiRemoteBean
  • HTTP : http://127.0.0.1:{server.port}/DefaultServiceHttpRemoteBean
    @Configuration
    public class RemoteConfiguration implements BeanPostProcessor
    {
        ......

        @Bean
        public RmiServiceExporter regRmiService()
        {
            RmiServiceExporter rmiServiceExporter = new RmiServiceExporter();
            rmiServiceExporter.setServiceName("DefaultServiceRmiRemoteBean");
            rmiServiceExporter.setService(new DefaultServiceImpl());
            rmiServiceExporter.setServiceInterface(DefaultService.class);
            rmiServiceExporter.setRegistryPort(1099);

            return rmiServiceExporter;
        }

        @Bean("/DefaultServiceHttpRemoteBean")
        public HttpInvokerServiceExporter regHttpService()
        {
            HttpInvokerServiceExporter httpInvokerServiceExporter = new HttpInvokerServiceExporter();
            httpInvokerServiceExporter.setServiceInterface(DefaultService.class);
            httpInvokerServiceExporter.setService(new DefaultServiceImpl());
            httpInvokerServiceExporter.afterPropertiesSet();
            return httpInvokerServiceExporter;
        }
    }
    

커스터마이징 어노테이션 사용방식

아래와 같이 Remoting 객체를 표시할 어노테이션을 생성합니다.

    @Retention(RetentionPolicy.RUNTIME)
    @Target({ ElementType.TYPE })
    public @interface RemoteType
    {
        Protocol protocol() default Protocol.HTTP;

        int port() default -1;

        @Required
        Class<?> serviceInterface();
    }
    

통신 프로토콜및 ServiceExporter를 구현하는 enum객체를 만듭니다.

    public enum Protocol
    {
    HTTP {
        @Override
        public Object getServiceExporter(Object bean, String beanName, RemoteType remoteType) {
            HttpInvokerServiceExporter httpInvokerServiceExporter = new HttpInvokerServiceExporter();
            httpInvokerServiceExporter.setServiceInterface(remoteType.serviceInterface());
            httpInvokerServiceExporter.setService(bean);
            httpInvokerServiceExporter.afterPropertiesSet();
            return httpInvokerServiceExporter;
        }
    },

    RMI {
        @Override
        public Object getServiceExporter(Object bean, String beanName, RemoteType remoteType) {
            RmiServiceExporter rmiServiceExporter = new RmiServiceExporter();
            rmiServiceExporter.setServiceInterface(remoteType.serviceInterface());
            rmiServiceExporter.setService(bean);
            rmiServiceExporter.setServiceName(beanName);
            if (remoteType.port() != -1)
            {
                rmiServiceExporter.setServicePort(remoteType.port());
            }
            try
            {
                rmiServiceExporter.afterPropertiesSet();
            }
            catch (RemoteException e)
            {
                throw new FatalBeanException("Exception initializing RmiServiceExporter", e);
            }
            return rmiServiceExporter;
        }
    };

    abstract public Object getServiceExporter(Object bean, String beanName, RemoteType remoteType);
    }
    

그 다음 @RemoteType어노테이션으로 다음과 같이 Service객체를 정의합니다.

    @Service("/DefaultServiceHttpRemote")
    @RemoteType(protocol = Protocol.HTTP, serviceInterface = DefaultService.class)
    public class DefaultServiceHttpRemoteImpl extends DefaultServiceImpl {}

    @Service("DefaultServiceRmiRemote")
    @RemoteType(protocol = Protocol.RMI, serviceInterface = DefaultService.class)
    public class DefaultServiceRmiRemoteImpl extends DefaultServiceImpl
    

서비스하는 객체인 DefaultServiceImpl이나 DefaultService인터페이스에 정의하지 않고 상속받은 객체를 만드는 이유는 SpringBoot에서 해당 서비스를 직접 사용할 수 있도록 하기 위함입니다.

Bean생성시 BeanPostProcessor를 이용하여 위 두 Remoting객체를 ServiceExporter객체로 변경해줍니다.

    @Configuration
    public class RemoteConfiguration implements BeanPostProcessor
    {
        ......

        @Override
        public Object postProcessAfterInitialization(Object bean, String beanName) throws BeansException
        {
            RemoteType remoteType = AnnotationUtils.findAnnotation(bean.getClass(), RemoteType.class);
            return (remoteType == null)? bean : remoteType.protocol().getServiceExporter(bean, beanName, remoteType);
        }

        ......
    }
    

그 외 구현사항

동작 확인을 위해 호출 가능한 Controller를 아래와 같이 구현해 두었습니다.

io.jistol.sample.remote.controller.HttpController io.jistol.sample.remote.controller.RmiController

  • http://127.0.0.1:{server.port}/{protocol}/service : DefaultServiceImpl을 직접 호출
  • http://127.0.0.1:{server.port}/{protocol}/bean : @Bean 어노테이션으로 구현한 객체를 이용하여 통신
  • http://127.0.0.1:{server.port}/{protocol}/extend : 커스터마이징 어노테이션으로 구현한 객체를 이용하여 통신

위 Controller를 호출하여 Test하는 단위테스트는 아래에 구현되어 있습니다.

io.jistol.sample.remote.test.SampleSpringbootRemoteApplicationTests

소스 링크

ex-springboot-remote

참고

Custom annotation configuration for Spring Remoting