Ryan's Log

SpringBoot + JPA(HANA Cloud DB) + BTP 배포 (gradle project) 본문

SAP BTP

SpringBoot + JPA(HANA Cloud DB) + BTP 배포 (gradle project)

Ryan c 2022. 11. 19. 19:10
728x90

SpringBoot과 JPA에 HANA Cloud DB를  사용하는 데모에 이를 BTP에 배포

 

이 글은 아래 사항들을 설명합니다.
1. SpringBoot로 JPA와 HANA DB를 사용
2. gradle에 HANA DB Driver dependency 추가
3. BTP trial HANA DB에 연결 설정
4. 간단한 JPA Entity 및 서비스 코딩
5. BTP에 배포

 

https://start.spring.io 새로운 spring-boot demo 를 생성한다.
Gradle Project에 Java 8로 진행하며, Spring Web, Spring Data JPA를 선택한다.

GENERATE를 통해 다운로드 된  starj-frame-jpa.demo.zip 파일을 압축 해제한 후 Visual Studio Code에서 연다.

HANA DB에 연결 할 수 있도록 HANA JDBC Driver를 검색하여 추가 해야 한다.
google에서 "mvn ngdbc" 또는 "maven hana jdbc"등 SAP HANA DB Driverfmf 검색한다.

 

현재 최신의 ngdbc 버전은 2.14.10 

 

gradle을 사용하므로 build.gradle파일의 dependencies에 다음을 추가한다.

// https://mvnrepository.com/artifact/com.sap.cloud.db.jdbc/ngdbc
implementation 'com.sap.cloud.db.jdbc:ngdbc:2.14.10'

 

사용 할 HANA Cloud DB를 구동한다. (생성 및 구동은 아래 링크 참조)
https://ryans-log.tistory.com/entry/sap-btp-and-hanadb-trial

 

gradle build를 진행 하기 전 SpringBoot 이 HANA DB에 연결 할 수 있도록 연결정보를 설정해야 한다.

application.properies를 application.yaml로 파일의 확장자를 변경한 후 다음을 작성한다.
익히 알려진 jdbc 설정을 추가하고  JPA관련 설정 및 dialect등이 추가된다.
spring.datasource.url, username, password 등을 본인의 HANA Cloud DB에 맞게 변경한다.

spring:
  datasource:
    driverClassName: com.sap.db.jdbc.Driver
    url: jdbc:sap://7d81a93f-b9f1-4ddd-8a3e-1e69d412ca35.hana.trial-us10.hanacloud.ondemand.com:443
    username: DBADMIN
    password: Password1
  jpa:
    show-sql: true
    generate-ddl: true
    hibernate:
      dialect: org.hibernate.dialect.HANAColumnStoreDialect
      show_sql: true
      format_sql: true
      use_sql_comments: true
      ddl-auto: create

** 아래 그림의 SQL Endpoint를 사용하여 위 URL을 완성한다. **

 

터미널에서 "gradle build"로 구동하고 "gradle bootRun"으로 spring을 구동한다.

gradle이 설치되지 않았다면,  starj-boot-jpa-demo spring-boot 프로젝트 루트에 설치되어 있는 gradlew를 사용해도 된다.
> ./gradlew build
> ./gradlew bootRun

 

gradle bootRun으로 Spring은 구동되었으나 아직 실행할 수 있는 것은 아무것도 작성하지 않았다.
아래 JPA 소스코드들 생성하여 실행 가능한 REST API들을 만든다.

 

** 아래 소스들은 Project Lombok을 사용 하며 getter, setter, constructor등은 lombok의 annotation으로 대체된다. **

1. Entity 작성
model 폴더를 만들고 하위에 User.java 파일 생성 후 Entity를 아래와 같이 작성한다.

package org.starj.boot.jpa.demo.model;

import java.lang.reflect.Field;

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

import org.hibernate.annotations.GenericGenerator;

import lombok.AccessLevel;
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Data;
import lombok.EqualsAndHashCode;
import lombok.NoArgsConstructor;

@Data
@EqualsAndHashCode(callSuper = false)
@AllArgsConstructor
@NoArgsConstructor(access = AccessLevel.PROTECTED)
@Entity(name = "User")
public class User {

    @Id
    //@GeneratedValue(generator = "uuid2")
    //@GenericGenerator(name = "uuid2", strategy = "uuid2")
    @Column(columnDefinition = "varchar(36)")
    private String uid;

    @Column(name = "username", nullable = false, columnDefinition = "varchar(100)")
    private String username;

    @Column(columnDefinition = "varchar(100)")
    private String password;

    @Column(name = "first_name", nullable = false, columnDefinition = "varchar(100)")
    private String firstName;

    @Column(name = "last_name", nullable = false, columnDefinition = "varchar(50)")
    private String lastName;

    @Column(name = "day_of_birth", columnDefinition = "varchar(8)")
    private String dayOfBirth;

    @Column(columnDefinition = "boolean default true")
    private Boolean use;

    @Builder
    public User(String username, String firstName, String lastName) {
        this.username = username;
        this.firstName = firstName;
        this.lastName = lastName;
    }

    public void apply(User source) throws IllegalArgumentException, IllegalAccessException {
        Field[] fields = User.class.getDeclaredFields();
        for (Field field : fields) {
            field.setAccessible(true);
            Object value = field.get(source);
            if (value != null) {
                field.set(this, value);
            }
        }
    }

    public void update(User source) throws IllegalArgumentException, IllegalAccessException {
        Field[] fields = User.class.getDeclaredFields();
        for (Field field : fields) {
            field.setAccessible(true);
            Object value = field.get(source);
            field.set(this, value);
        }
    }

}

apply(), update() 메소드는 각각 entity의 값 중 일부 또는 전체를 변경하도록 동작한다.
이 두 메소드는 향후 상위 class로 올려 재사용 되도록 구성한다.

** 아래 원활한 테스트를 위해 @Id인 uid 값은 자동 생성이 아닌 수동 생성으로 설정한다. 다만 이 경우 Unit Test를 진행하는 build 과정에서 오류가 발생 할 수있다.
이후 gradle build는 생략하고, gradle bootRun으로 구동 후 테스트만 진행한다.
최종 빌드 및 배포 시에는 @Id를 자동 생성하도록 주석 처리된 @GeneratedValue, @GenericGenerator 주석을 해제하고 두 annotation을 사용 하도록 한다.  **

 

 

2. Repository 작성
repository 폴더를 만들 고 그 하위에 UserRepository.java 파일 생성한 후 Repositry를 작성한다.

package org.starj.boot.jpa.demo.repository;

import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.stereotype.Repository;
import org.starj.boot.jpa.demo.model.User;

@Repository
public interface UserRepository extends JpaRepository<User, String> {

}

 

3. Service 작성
service 폴더를 만들고 그 하위에 UserService.java 파일을 생성 한 후 아래와 같이 작성한다.

package org.starj.boot.jpa.demo.service;

import java.util.List;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import org.starj.boot.jpa.demo.model.User;
import org.starj.boot.jpa.demo.repository.UserRepository;

@Service
public class UserService {

    @Autowired
    private UserRepository repository;

    public List<User> findAll() {
        return repository.findAll();
    }

    public User findById(String uid) {
        return repository.findById(uid).get();
    }

    public User save(User user) {
        repository.save(user);
        return user;
    }

    public void deleteById(String uid) {
        repository.deleteById(uid);
    }

    public void applyById(String uid, User user) throws IllegalArgumentException, IllegalAccessException {
        User storedUser = this.findById(uid);
        storedUser.apply(user);
        repository.save(storedUser);
    }

    public void updateById(String uid, User user) throws IllegalArgumentException, IllegalAccessException {
        User storedUser = this.findById(uid);
        storedUser.update(user);
        repository.save(storedUser);
    }
}

UserService는 UserRepository를 통해 finaAll(), findById()를 제공하여 User Entity를 검색한다.
save(), deleteById(), applyById(), updateById()를 통해 entity를 삭제 및 수정(일부 또는 전체)한다.

 

4. Controller 작성
controller 폴더를 만들 고 그 하위에 UserController.java 파일을 만들어 아래와 같이 작성한다.

package org.starj.boot.jpa.demo.controller;

import java.util.List;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.http.HttpStatus;
import org.springframework.http.MediaType;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.DeleteMapping;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PatchMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.PutMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import org.starj.boot.jpa.demo.model.User;
import org.starj.boot.jpa.demo.service.UserService;

@RestController
@RequestMapping("/user")
public class UserController {

    private final Logger log = LoggerFactory.getLogger(this.getClass());

    @Autowired
    private UserService userService;

    // GET ALL
    @GetMapping(produces = { MediaType.APPLICATION_JSON_VALUE })
    public ResponseEntity<List<User>> getAllUsers() {
        List<User> user = userService.findAll();
        return new ResponseEntity<List<User>>(user, HttpStatus.OK);
    }

    // POST
    @PostMapping
    public ResponseEntity<User> save(@RequestBody User user) {
        return new ResponseEntity<User>(userService.save(user), HttpStatus.OK);
    }

    // PUT
    @PutMapping(produces = { MediaType.APPLICATION_JSON_VALUE })
    public ResponseEntity<User> putUser(@RequestBody User user)
            throws IllegalArgumentException, IllegalAccessException {
        userService.updateById(user.getUid(), user);
        return new ResponseEntity<User>(user, HttpStatus.OK);
    }

    // GET ONE
    @GetMapping(value = "/{uid}", produces = { MediaType.APPLICATION_JSON_VALUE })
    public ResponseEntity<User> getUser(@PathVariable("uid") String uid) {
        User user = userService.findById(uid);
        return new ResponseEntity<User>(user, HttpStatus.OK);
    }

    // PUT
    @PutMapping(value = "/{uid}", produces = { MediaType.APPLICATION_JSON_VALUE })
    public ResponseEntity<User> putUser(@PathVariable("uid") String uid, @RequestBody User user) {
        try {
            user.setUid(uid);
            userService.updateById(uid, user);
            return ResponseEntity.ok(user);
        } catch (IllegalArgumentException | IllegalAccessException e) {
            return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR).build();
        }
    }

    // PATCH (apply if exists)
    @PatchMapping(value = "/{uid}", consumes = "application/json", produces = { MediaType.APPLICATION_JSON_VALUE })
    public ResponseEntity<User> patchUser(@PathVariable("uid") String uid, @RequestBody User user) {
        try {
            userService.applyById(uid, user);
            return ResponseEntity.ok(user);
        } catch (IllegalAccessException | IllegalArgumentException | SecurityException e) {
            log.error(e.getLocalizedMessage(), e);
            return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR).build();
        }
    }

    // DELETE
    @DeleteMapping(value = "/{uid}", produces = { MediaType.APPLICATION_JSON_VALUE })
    public ResponseEntity<Void> deleteUser(@PathVariable("uid") String uid) {
        userService.deleteById(uid);
        return new ResponseEntity<Void>(HttpStatus.NO_CONTENT);
    }

}

UserController는 RequestMapping으로 "/user" Entity를 통해 GET, POST, DELETE, PUT, PATCH등 RESTful API를 제공하도록 구성하며, /user 뒤에 이어 붙는 /{uuid}로  entity가 선택되어 동작한다.

 

테스트를 위해 VSCode 확장 REST Client를 이용한다. (Rest Client 대신 Postman등을 이용하여 동일하게 테스트 가능)
아래 그림의 확장 프로그램을 설치한다.

REST Client는 .http 파일을 생성하여 사용 할 수 있다.
임의의 위치에 user.http 파일을 생성한다. (이름은 무관하며 확장자만 .http로)

user.http 파일은 아래와 같이 작성 한 후 지금까지 작성 한 User entity RESTful API를 테스트한다.

@hostport=127.0.0.1:8080
@entity=user
@uuid=3a2ff586-f101-497f-955c-6e1090bcda48


####
POST http://{{hostport}}/{{entity}}/ HTTP/1.1
Content-Type: application/json

{
  "uid": "3a2ff586-f101-497f-955c-6e1090bcda48",
  "username": "ryan@starj.org",
  "password": "Password1",
  "firstName": "Ryan",
  "lastName": "World",
  "dayOfBirth": "20020531",
  "use": true
}

####
GET http://{{hostport}}/{{entity}} HTTP/1.1
Content-Type: application/json

####
GET http://{{hostport}}/{{entity}}/{{uuid}} HTTP/1.1
Content-Type: application/json

####
PUT http://{{hostport}}/{{entity}} HTTP/1.1
Content-Type: application/json

{
  "uid": "{{uuid}}",
  "username": "ryan@starj.org",
  "password": "Password1",
  "firstName": "Ryan",
  "lastName": "World",
  "dayOfBirth": "20020531",
  "use": true
}

####
PUT http://{{hostport}}/{{entity}}/{{uuid}} HTTP/1.1
Content-Type: application/json

{
  "username": "ryan@starj.org",
  "password": "******",
  "firstName": "Ryan",
  "lastName": "World",
  "dayOfBirth": "20020531",
  "use": false
}

####
PATCH http://{{hostport}}/{{entity}}/{{uuid}} HTTP/1.1
Content-Type: application/json

{
  "lastName": "Happy Together",
  "use": false
}

####
DELETE http://{{hostport}}/{{entity}}/{{uuid}} HTTP/1.1
Content-Type: application/json

사용법은 간단하다.
작성 후 REST Client 확장 프롤그램에 의해 URL 위에 "Send Request" 버튼이 활성화된다. 

 

우선 POST를 호출하여 DB에 데이터를 생성하고, 다음에 이어진 GET으로 결과를 조회한다.

POST 실행 후
POST한 결과를 GET으로 확인

User.java의 uid의 @GeneratedValue, @GenericGenerator 두 annotation을 사용하면 POST시 uid 값 없이 자동 생성이 가능하다.

 

BTP인 Cloud Foundry에 배포하기 위해 SpringBoot을 .jar로 빌드한다.
> gradle bootJar

 

빌드된 .jar 파일 결과는 build/libs/~~.jar로 생성된다. 이 파일을 BTP로 배포한다.

 

CF(Cloud Foundry) CLI를 이용하여  BTP에 배포한다.

CF CLI 설치 (윈도우 사용자는 알아서~~)
https://docs.cloudfoundry.org/cf-cli/install-go-cli.html

 

CF에 로그인 해야한다. 로그인 할 대상 BTP의 API Endpoint를 확인한다.
아래 그림의 Cloud Foundry Environment의  API Endpoint 취득

Terminal에서 cf api, cf login을 각각 실행하여 cf에 로그인한다.

> cf api https://api.cf.us10.hana.ondemand.com/
> cf login

login시 Email과 password는 SAP 계정으로, BTP 혹은 BTP trial을 생성한 계정이다.

 

CF에 로그인 되었으니 빌드된 .jar 파일을  cf push한다.

cf push starj-boot-jpa-demo -p ./build/libs/starj-boot-jpa-demo-0.0.1-SNAPSHOT.jar -m 1024M -k 512M

 

배포를 성공하면 아래 그림과 같이 BTP Cockpit에서 배포된 application이 확인된다.

starj-boot-demo는 앞 글의 것이고, starj-boot-jpa-demo를 클릭하여 실행 가능한 endpint 주소를 확인한다.

https://starj-boot-jpa-demo.cfapps.us10.hana.ondemand.com

VSCode에 작성했던 user.http 파일을 이용하여 테스트 한다.

user-btp.http 파일을 별도 생성하여 테스트 하였으며, BTP는 https 프로토콜을 사용해야한다. 
상단 @hostport의 설정을 BTP에 배포된 appliation의 host로 변경하고, 각 Request의 앞 http를 https로 변경한다.

 

 

 

 

본 글의 소스는 아래에서 받을 수 있습니다.
https://github.com/devidiot/demo-springboot-jpa-hana_cloud_db_trial

 

GitHub - devidiot/demo-springboot-jpa-hana_cloud_db_trial

Contribute to devidiot/demo-springboot-jpa-hana_cloud_db_trial development by creating an account on GitHub.

github.com

 

728x90