기본 콘텐츠로 건너뛰기

Spring Data Projection 기능 활용

Projection

Spring Data REST에서는 Projection 기능을 제공하고 있습니다.


Spring Data란?
  Data Access Layer에 대한 추상화 기능을 제공하는 프레임워크입니다.
간단히 설명하자면 기존에는 Database에 데이터를 저장하고 읽어오기 위해서는 Database의 종류, 스키마를 고려하여 SQL Query를 사용해서 데이터를 저장하고 읽어왔습니다. 그러나 Spring Data를 이용하게 되면 이 과정을 ORM을 사용하여 함수를 사용하여 쉽게 처리할 수 있습니다.
ORM(Object Relational Mapping)란?
  데이터베이스가 테이블, 필드간 상관 관계를 통해 도메인 데이터를 표현하였다고 한다면,  ORM은 Object, 클래스 간의 상관 관계를 통해 도메인 데이터를 표현하는 것을 말합니다.

다음의 링크는 Spring Data REST Documentation 중에서 Projection 부분입니다.

             8. Projections and Excerpts


그러나 실제 "Projection" 기능을 사용하는데는 한가지 문제점이 있습니다. 레거시 웹서비스를 쓰고 있다거나, Spring Data  REST를 이용하지 않는다면 "Projection" 기능을 활용하는 것이 힘들다는 것입니다. Spring Data REST의 경우 데이터  Response Type이 json-hal 타입만을 지원하기 때문입니다.

JSON-HAL 타입으로 경우 웹서비스 제공 입장에서 최선의 선택일 수 있습니다. 그러나 실제 프로젝트에서는 이를 쓰려는 클라이언트가 없을 경우 사용할 수 없게 될 것입니다. 그렇다면 Projection 기능만을 사용할 수 있는 방법이 없을까요?

있습니다.

ProjectionFactory를 직접 사용하는 것입니다. 다음과 같이 Spring + Gradle을 사용하여 테스트 프로젝트를 만들어보겠습니다.

Gradle Build Script


먼저 다음과 같이 Gradle build 스크립트를 작성하여 기본적인 프로젝트를 생성합니다.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
buildscript {
 ext {
  springBootVersion = '1.4.0.M1'
 }
 repositories {
  mavenCentral()
  maven { url "https://repo.spring.io/snapshot" }
  maven { url "https://repo.spring.io/milestone" }
 }
 dependencies {
  classpath("org.springframework.boot:spring-boot-gradle-plugin:${springBootVersion}") 
 }
}

apply plugin: 'java'
apply plugin: 'eclipse'
apply plugin: 'spring-boot'
apply plugin: 'idea'

jar {
 baseName = 'sprisngdata'
 version = '0.0.1-SNAPSHOT'
}
sourceCompatibility = 1.8
targetCompatibility = 1.8

repositories {
 mavenCentral()
 maven { url "https://repo.spring.io/snapshot" }
 maven { url "https://repo.spring.io/milestone" }
}


dependencies {
    compile("org.springframework.boot:spring-boot-starter")
    compile("org.springframework.boot:spring-boot-starter-data-rest")
    compile("org.springframework.boot:spring-boot-starter-data-jpa")
    compile("com.h2database:h2")
    compile("org.projectlombok:lombok:1.16.8")

    testCompile('com.jayway.restassured:rest-assured:2.9.0')
    testCompile('org.springframework.boot:spring-boot-starter-test')
}


eclipse {
 classpath {
   containers.remove('org.eclipse.jdt.launching.JRE_CONTAINER')
   containers 'org.eclipse.jdt.launching.JRE_CONTAINER/org.eclipse.jdt.internal.debug.ui.launcher.StandardVMType/JavaSE-1.8'
 }
}

task wrapper(type: Wrapper) {
 gradleVersion = '2.9'
}

ProjectionFactory


여기서 중요한 것은 "Projection" 기능을 사용할 수 있도록 Factory Bean을 생성하는 것입니다. Bean 생성은 다음과 같이 간단히 합니다.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
package com.ryanjin.example;

import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.data.projection.SpelAwareProxyProjectionFactory;

/**
 * Created by Donghyun Ryan Jin [https://github.com/atinjin]
 * spring_data_projection on 2016. 4. 8..
 */
@Configuration
public class FactoryConfig {
    @Bean
    public SpelAwareProxyProjectionFactory projectionFactory() {
        return new SpelAwareProxyProjectionFactory();
    }
}

실제 ProjectionFactory를 사용하기 위해서 Domain 모델 클래스와 해당 모델을 projection할 인터페이스 클래스를 작성합니다.

Domain Model Class

ShoppingItem.class
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
package com.ryanjin.example;

import lombok.Data;

import javax.persistence.Entity;
import javax.persistence.GeneratedValue;
import javax.persistence.Id;

/**
 * Created by Donghyun Ryan Jin [https://github.com/atinjin]
 * spring_data_projection on 2016. 4. 4..
 */
@Entity
@Data
public class ShoppingItem {
    @Id
    @GeneratedValue
    private int id;
    private String name;
    private String description;
    private String category;
    private int price;
    private String seller;
}


Projection Interface

OnlyItemName.class
1
2
3
4
5
6
7
8
9
package com.ryanjin.example;

/**
 * Created by Donghyun Ryan Jin [https://github.com/atinjin]
 * spring_data_projection on 2016. 4. 8..
 */
public interface OnlyItemName {
    String getName();
}

이제 웹서비스 인터페이스에서 Projection을 통해 ShoppingItem 정보 중에서 사용자가 원하는 "name" 만을 웹서비스 Response로 내보내는 것입니다.

RestController

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
package com.ryanjin.example;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.domain.Page;
import org.springframework.data.domain.Pageable;
import org.springframework.data.projection.ProjectionFactory;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

/**
 * Created by Donghyun Ryan Jin [https://github.com/atinjin]
 * spring_data_projection on 2016. 4. 8..
 */
@RestController
public class ItemController {
    @Autowired
    private ItemRepository itemRepository;
    @Autowired
    private ProjectionFactory projectionFactory;

    @RequestMapping("/item-names")
    public Page<OnlyItemName> getItems(Pageable pageable) {
        Page<ShoppingItem> items = itemRepository.findAll(pageable);

        return items.map(item -> projectionFactory.createProjection(OnlyItemName.class, item));
    }

}

모두 끝났습니다.  "/item" 웹서비스에서 ShoppingItem의 모든 정보를 내보내주었다면, "/item-names"라는 웹서비스는 "name" 정보만을 내보내줄 수 있습니다. 우리가 원하는 기능을 충분히 할 수 있게 되었습니다.

같은 도메인 정보를 이용하여 다양한 종류의 웹인터페이스를 생성할 수 있게 된 것입니다. 용도, 사용자의 요구에 맞게 Response JSON의 데이터를 만들어낼 수 있게 되었습니다.

실제 예제 소스 코드는 Github에서 확인해볼 수 있습니다.

https://github.com/atinjin/spring_data_projection

댓글

이 블로그의 인기 게시물

AWS ELB 504 Error

AWS EC2  운영 중 가끔씩 볼 수 있는 에러가 있습니다. 대표적으로 다음의 세가지 502, 503, 504 입니다. 이 중에서 이번에 알아볼 문제는 HTTP 504 에러입니다 .  타임 아웃이 되어   Request 를 처리하지 못하는 상황이 됩니다 .   해결 방법부터 이야기 하자면 다음과 같이 웹서버의 Time-out 시간을 60 초 이상으로 늘리는 것입니다 . Web Server & Application Time-out >= 60 sec 그 이유는 다음과 같은 ELB의 특성 때문입니다. ELB는 클라이언트와 EC2 서버 양쪽으로 커넥션을 유지하고 있습니다. ELB는 클라이언트와  EC2 서버간의 커넥션을 관리하는 역할을 맡고 있습니다. 그래서 유효한 커넥션만을 남겨놓습니다. 이를 위해서 Time-out 시간을 가지고 이 시간동안 데이터가 송수신되지 않으면 연결을 끊습니다.  기본적으로 Elastic Load Balancing는 두 연결 모두에 대해 Time-out(유휴 시간) 시간을 60초로 되어 있습니다. 그렇기 때문에 HTTP 또는 HTTPS를 사용할 경우 "KeppAlive" 옵션을 사용하여 커넥션을 재활용해야 합니다. 이 때  ELB 커넥션도 재사용되기 때문에 CPU 사용률을 줄일 수 있습니다. Browser Time-out Opera 11.11 120 sec IE 9 60 sec Chrome 13 300 sec FireFox 4 115 sec 서버 로직 중에서 60초 이상 실행될 수 있는 부분이 있는 경우 504 에러를 자주 볼 수 있을 것입니다. 문제 해결을 위해서는 Web Server는 물론  Tomcat 설정 또한 60초 이상으로 변경해주어야 합니다. ...

Drools Fusion "Steam mode" Issue and Solution

ESP(Event Stream Processing) 센서, CCTV등 다양한 소스(Source)에서 들어오는 실시간 이벤트를 이벤트 스트림 이라고 한다. 그리고 이벤트 스트림을 받아서 이를 실시간으로 처리하는 것을 Event Stream Processing이라고 한다. 처리라는 것은 대부분 특정 이벤트에 대해서 사용자가 원하는 동작 또는 알람을 보내는 작업을 말한다. 이슈 발생 대용량 이벤트 처리 기술은 항상 퍼포먼스와 안정성이 중요하다. 지금은 서버에서 Drools Fusion 을 사용하고 있다. 이전 버전에서 Esper 를 사용하였으나 라이센스 문제로 인해서 Drools Fusion으로 옮겨왔다. 문제는 성능 테스트 중에 발생하였는데 메모리 leak 문제였다. “Always” 문제였기 때문에 문제점 파악은 쉽게 되었다. 테스트 방법 * 5대의 서버를 준비 * JMeter  _를 이용하여 throughput을 조절해가며 이벤트 발생 * 1대의 서버에 서버 App 실행 * JConsole 을 이용하여 JVM Heap 사이즈를 모니터링 결론 최종적으로 이벤트가 고속이든, 저속이든 Heap 사이즈가 늘어난다는 판단 분석 방법 JMap 을 이용하여 Memory Dump하였다. 그리고 메모리 덤프 분석툴인 MAT 를 이용하여 Dump 분석 결과를 확인 분석 결과 모든 이벤트 오브젝트가 Drools hashmap에 reference되어 있었다. 문제 해결 과정(How to solve) 우선은 내 코드들을 의심해봤다. 이벤트 수신, Pooling, 처리, 저장, DB Access 부분을 모두 훓어 보았다. 그러나 문제가 될 만한 부분은 찾지 못했다. 이건 뭐지? 내 상식 이상의 뭔가가 있나 한참을 고민했다. 결국은 소스 코드를 링크해서 디버깅을 해야하나, 소스 코드 분석을 해야하나 고민하면서 Drools 메뉴얼을 훓어 보다가 머리 속에서 스파크가 팍 튀었다. 다음은 Dro...