Hibernate ORM을 사용한 Cloud Spanner

1. 개요

Hibernate는 Java 프로젝트의 사실상 표준 ORM 솔루션이 되었습니다. 모든 주요 관계형 데이터베이스를 지원하며 Spring Data JPA와 같은 훨씬 더 강력한 ORM 도구를 지원합니다. 또한, Spring Boot, Microprofile, Quarkus 등 많은 Hibernate 호환 프레임워크가 있습니다.

Hibernate ORM을 위한 Cloud Spanner Dialect를 사용하면 Cloud Spanner에서 Hibernate를 사용할 수 있습니다. Hibernate의 관용적 지속성을 통해 Cloud Spanner의 확장성 및 관계형 시맨틱스의 이점을 누릴 수 있습니다. 이를 통해 Hibernate 기반 기술에서 제공되는 향상된 개발자 생산성을 활용하여 기존 애플리케이션을 클라우드로 마이그레이션하거나 새 애플리케이션을 작성할 수 있습니다.

학습할 내용

  • Cloud Spanner에 연결되는 간단한 Hibernate 애플리케이션을 작성하는 방법
  • Cloud Spanner 데이터베이스를 만드는 방법
  • Hibernate ORM에 Cloud Spanner Dialect를 사용하는 방법
  • Hibernate를 사용하여 CRUD (create-read-update-delete) 작업을 구현하는 방법

필요한 항목

  • Google Cloud Platform 프로젝트
  • 브라우저(Chrome, Firefox 등)

2. 설정 및 요구사항

자습형 환경 설정

  1. Cloud Console에 로그인하고 새 프로젝트를 만들거나 기존 프로젝트를 다시 사용합니다. (Gmail 또는 G Suite 계정이 없으면 만들어야 합니다.)

k6ai2NRmxIjV5iMFlVgA_ZyAWE4fhRrkrZZ5mZuCas81YLgk0iwIyvgoDet4s2lMYGC5K3xLSOjIbmC9kjiezvQuxuhdYRolbv1rft1lOmA_P2U3OYcaAzN9JgP-Ncm18i5qgf9LzA

UtcCMcSYtCOrEWuILx3XBwb3GILPqXDd6cJiQQxmylg8GNftqlnE7u8aJLhlr1ZLRkpncKdj8ERnqcH71wab2HlfUnO9CgXKd0-CAQC2t3CH0kuws4QRxd5P

KoK3nfWQ73s_x4QI69xqzqdDR4tUuNmrv4FC9Yq8vtK5IVm49h_8h6x9X281hAcJcOFDtX7g2BXPvP5O7SOR2V4UI6W8gN6cTJCVAdtWHRrS89zH-qWE0IQdjFpOs_8T-s4vQCXA6w

모든 Google Cloud 프로젝트에서 고유한 이름인 프로젝트 ID를 기억하세요(위의 이름은 이미 사용되었으므로 사용할 수 없습니다). 이 ID는 나중에 이 Codelab에서 PROJECT_ID라고 부릅니다.

  1. 그런 후 Google Cloud 리소스를 사용할 수 있도록 Cloud Console에서 결제를 사용 설정해야 합니다.

이 Codelab 실행에는 많은 비용이 들지 않습니다. 이 가이드를 마친 후 비용이 결제되지 않도록 리소스 종료 방법을 알려주는 '삭제' 섹션의 안내를 따르세요. Google Cloud 새 사용자에게는 미화 $300 상당의 무료 체험판 프로그램에 참여할 수 있는 자격이 부여됩니다.

Cloud Shell 활성화

  1. Cloud Console에서 Cloud Shell 활성화R47NpBm6yyzso5vnxnRBikeDAXxl3LsM6tip3rJxnKuS2EZdCI0h-eIOGm9aECq8JXbMFlJkd68uTutXU8gGmQUVa5iI1OdZczXP2tzqZ_mj0pR4sZ8eSwOwUlWADR7ARCqrMTQPQA를 클릭합니다.

STsYbcAtkIQyN6nL9BJhld3Fv5KxedYynpUVcRWwvIR-sYMMc4kfK-unIYgtsD4P6T0P8z-A12388tPmAh-Trsx80qobaW4KQXHJ7qJI6rwm762LrxurYbxwiDG-v_HiUYsWnXMciw

이전에 Cloud Shell을 시작하지 않았으면 설명이 포함된 중간 화면(스크롤해야 볼 수 있는 부분)이 제공됩니다. 이 경우 계속을 클릭합니다(이후 다시 표시되지 않음). 이 일회성 화면은 다음과 같습니다.

LnGMTn1ObgwWFtWpjSdzlA9TDvSbcY76GiLQLc_f7RP1QBK1Tl4H6kLCHzsi89Lkc-serOpqNH-F2XKmV5AnBqTbPon4HvCwSSrY_ERFHzeYmK1lnTfr-6x5eVoaHpRSrCUrolXUPQ

Cloud Shell을 프로비저닝하고 연결하는 데 몇 분 정도만 걸립니다.

hfu9bVHmrWw01Hnrlf4MBNja6yvssDnZzN9oupcG12PU88Vvo30tTluX9IySwnu5_TG3U2UXAasX9eCwqwZtc6Yhwxri95zG82DLUcKxrFYaXnVd7OqVoU6zanoZa0PtvubjLLHxnA

가상 머신에는 필요한 개발 도구가 모두 들어있습니다. 영구적인 5GB 홈 디렉터리를 제공하고 Google Cloud에서 실행되므로 네트워크 성능과 인증이 크게 개선됩니다. 이 Codelab에서 대부분의 작업은 브라우저나 Chromebook만 사용하여 수행할 수 있습니다.

Cloud Shell에 연결되면 인증이 완료되었고 프로젝트가 해당 프로젝트 ID로 이미 설정된 것을 볼 수 있습니다.

  1. Cloud Shell에서 다음 명령어를 실행하여 인증되었는지 확인합니다.
gcloud auth list

명령어 결과

 Credentialed Accounts
ACTIVE  ACCOUNT
*       <my_account>@<my_domain.com>

To set the active account, run:
    $ gcloud config set account `ACCOUNT`
gcloud config list project

명령어 결과

[core]
project = <PROJECT_ID>

또는 다음 명령어로 설정할 수 있습니다.

gcloud config set project <PROJECT_ID>

명령어 결과

Updated property [core/project].

3. 데이터베이스 만들기

Cloud Shell이 실행되면 gcloud를 사용하여 GCP 프로젝트와 상호작용할 수 있습니다.

먼저 Cloud Spanner API를 사용 설정합니다.

gcloud services enable spanner.googleapis.com

이제 codelab-instance라는 Cloud Spanner 인스턴스를 만들어 보겠습니다.

gcloud spanner instances create codelab-instance \
 --config=regional-us-central1 \
 --description="Codelab Instance" --nodes=1

이제 이 인스턴스에 데이터베이스를 추가해야 합니다. 이름을 codelab-db로 지정하겠습니다.

gcloud spanner databases create codelab-db --instance=codelab-instance

4. 빈 앱 만들기

Maven 빠른 시작 Archetype을 사용하여 간단한 Java 콘솔 애플리케이션을 만들어 보겠습니다.

mvn archetype:generate \
 -DgroupId=codelab \
 -DartifactId=spanner-hibernate-codelab \
 -DarchetypeArtifactId=maven-archetype-quickstart \
 -DarchetypeVersion=1.4 \
 -DinteractiveMode=false

앱 디렉터리로 변경합니다.

cd spanner-hibernate-codelab

Maven을 사용하여 앱을 컴파일하고 실행합니다.

mvn compile exec:java -Dexec.mainClass=codelab.App

콘솔에 Hello World!가 출력됩니다.

5. 종속 항목 추가

Cloud Shell 편집기를 열고 spanner-hibernate-codelab 디렉터리 내부를 탐색하여 소스 코드를 살펴보겠습니다.

b5cb37d043d4d2b0.png

지금까지는 "Hello World!"를 출력하는 기본 Java 콘솔 앱만 있습니다. 하지만 Hibernate를 사용하여 Cloud Spanner와 통신하는 Java 애플리케이션을 작성하려고 합니다. 이를 위해서는 Hibernate용 Cloud Spanner Dialect, Cloud Spanner JDBC 드라이버, Hibernate 코어가 필요합니다. 따라서 pom.xml 파일 내 <dependencies> 블록에 다음 종속 항목을 추가해 보겠습니다.

pom.xml

    <!-- Spanner Dialect -->
    <dependency>
      <groupId>com.google.cloud</groupId>
      <artifactId>google-cloud-spanner-hibernate-dialect</artifactId>
      <version>1.5.0</version>
    </dependency>

    <!-- JDBC Driver -->
    <dependency>
      <groupId>com.google.cloud</groupId>
      <artifactId>google-cloud-spanner-jdbc</artifactId>
      <version>2.0.0</version>
    </dependency>

    <!-- Hibernate -->
    <dependency>
      <groupId>org.hibernate</groupId>
      <artifactId>hibernate-core</artifactId>
      <version>5.4.29.Final</version>
    </dependency>

6. Hibernate ORM 구성

다음으로 Hibernate 구성 파일 hibernate.cfg.xmlhibernate.properties를 만듭니다. 다음 명령어를 실행하여 빈 파일을 만든 후 Cloud Shell 편집기를 사용하여 수정합니다.

mkdir src/main/resources \
 && touch src/main/resources/hibernate.cfg.xml \
 && touch src/main/resources/hibernate.properties

따라서 hibernate.cfg.xml를 채워 데이터베이스에 매핑할 주석이 지정된 항목 클래스에 관해 Hibernate에 알려 보겠습니다. (항목 클래스는 나중에 만듭니다.)

src/main/resources/hibernate.cfg.xml

<hibernate-configuration>
  <session-factory>
    <!-- Annotated entity classes -->
    <mapping class="codelab.Album"/>
    <mapping class="codelab.Singer"/>
  </session-factory>
</hibernate-configuration>

또한 Hibernate는 Cloud Spanner 인스턴스에 연결하는 방법과 사용할 언어를 알아야 합니다. 따라서 SQL 문법에 SpannerDialect를 사용하고, Spanner JDBC 드라이버, 데이터베이스 좌표와 함께 JDBC 연결 문자열을 사용하도록 지시합니다. 그러면 hibernate.properties 파일로 이동합니다.

src/main/resources/hibernate.properties

hibernate.dialect=com.google.cloud.spanner.hibernate.SpannerDialect
hibernate.connection.driver_class=com.google.cloud.spanner.jdbc.JdbcDriver
hibernate.connection.url=jdbc:cloudspanner:/projects/{PROJECT_ID}/instances/codelab-instance/databases/codelab-db
# auto-create or update DB schema
hibernate.hbm2ddl.auto=update
hibernate.show_sql=true

{PROJECT_ID}를 프로젝트 ID로 바꿔야 합니다. 이 ID는 다음 명령어를 실행하여 가져올 수 있습니다.

gcloud config get-value project

기존 데이터베이스 스키마가 없으므로 앱을 처음 실행할 때 Hibernate가 Cloud Spanner에 두 개의 테이블을 만들 수 있도록 hibernate.hbm2ddl.auto=update 속성을 추가했습니다.

일반적으로 GOOGLE_APPLICATION_CREDENTIALS 환경 변수의 서비스 계정 JSON 파일 또는 gcloud auth application-default login 명령어를 사용하여 구성된 애플리케이션 기본 사용자 인증 정보를 사용하여 사용자 인증 정보가 설정되어 있는지도 확인합니다. 하지만 Cloud Shell에서 실행 중이므로 기본 프로젝트 사용자 인증 정보가 이미 설정되어 있습니다.

7. 주석 지정된 항목 클래스 만들기

이제 코드를 작성할 준비가 되었습니다.

Cloud Spanner의 테이블에 매핑될 두 가지 POJO (Plain Old Java Object)를 SingerAlbum로 정의합니다. AlbumSinger@ManyToOne 관계를 갖습니다. @OneToMany 주석을 사용하여 SingerAlbum 목록에 매핑할 수도 있지만 이 예에서는 데이터베이스에서 가수를 가져와야 할 때마다 모든 앨범을 로드하고 싶지는 않습니다.

SingerAlbum 항목 클래스를 추가합니다.

클래스 파일을 만듭니다.

touch src/main/java/codelab/Singer.java \
&& touch src/main/java/codelab/Album.java

파일의 콘텐츠를 붙여넣습니다.

src/main/java/codelab/Singer.java

package codelab;

import java.util.Date;
import java.util.UUID;
import javax.persistence.Entity;
import javax.persistence.GeneratedValue;
import javax.persistence.Id;
import javax.persistence.Temporal;
import javax.persistence.TemporalType;
import org.hibernate.annotations.Type;

@Entity
public class Singer {

  @Id
  @GeneratedValue
  @Type(type = "uuid-char")
  private UUID singerId;

  private String firstName;

  private String lastName;

  @Temporal(TemporalType.DATE)
  private Date birthDate;

  public Singer() {
  }

  public Singer(String firstName, String lastName, Date birthDate) {
    this.firstName = firstName;
    this.lastName = lastName;
    this.birthDate = birthDate;
  }

  public UUID getSingerId() {
    return singerId;
  }

  public void setSingerId(UUID singerId) {
    this.singerId = singerId;
  }

  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;
  }

  public Date getBirthDate() {
    return birthDate;
  }

  public void setBirthDate(Date birthDate) {
    this.birthDate = birthDate;
  }

  @Override
  public boolean equals(Object o) {
    if (this == o) {
      return true;
    }
    if (!(o instanceof Singer)) {
      return false;
    }

    Singer singer = (Singer) o;

    if (!firstName.equals(singer.firstName)) {
      return false;
    }
    if (!lastName.equals(singer.lastName)) {
      return false;
    }
    return birthDate.equals(singer.birthDate);
  }

  @Override
  public int hashCode() {
    int result = firstName.hashCode();
    result = 31 * result + lastName.hashCode();
    result = 31 * result + birthDate.hashCode();
    return result;
  }
}

src/main/java/codelab/Album.java

package codelab;

import java.util.UUID;
import javax.persistence.Entity;
import javax.persistence.GeneratedValue;
import javax.persistence.Id;
import javax.persistence.ManyToOne;
import org.hibernate.annotations.Type;

@Entity
public class Album {

  @Id
  @GeneratedValue
  @Type(type = "uuid-char")
  UUID albumId;

  @ManyToOne
  Singer singer;

  String albumTitle;

  public Album() {
  }

  public Album(Singer singer, String albumTitle) {
    this.singer = singer;
    this.albumTitle = albumTitle;
  }

  public UUID getAlbumId() {
    return albumId;
  }

  public void setAlbumId(UUID albumId) {
    this.albumId = albumId;
  }

  public Singer getSinger() {
    return singer;
  }

  public void setSinger(Singer singer) {
    this.singer = singer;
  }

  public String getAlbumTitle() {
    return albumTitle;
  }

  public void setAlbumTitle(String albumTitle) {
    this.albumTitle = albumTitle;
  }

  @Override
  public boolean equals(Object o) {
    if (this == o) {
      return true;
    }
    if (!(o instanceof Album)) {
      return false;
    }

    Album album = (Album) o;

    if (!singer.equals(album.singer)) {
      return false;
    }
    return albumTitle.equals(album.albumTitle);
  }

  @Override
  public int hashCode() {
    int result = singer.hashCode();
    result = 31 * result + albumTitle.hashCode();
    return result;
  }
}

이 예에서는 기본 키로 자동 생성된 UUID를 사용합니다. 이는 시스템이 키 범위에 따라 서버 간에 데이터를 나누므로 핫스팟을 방지하기 때문에 Cloud Spanner에서 선호되는 ID 유형입니다. 단조롭게 증가하는 정수 키도 작동하지만 성능이 떨어질 수 있습니다.

8. 항목 저장 및 쿼리

모든 구성 객체와 항목 객체가 정의되면 데이터베이스에 쓰고 쿼리를 시작할 수 있습니다. Hibernate Session를 열고 이를 사용하여 먼저 clearData() 메서드에서 모든 테이블 행을 삭제하고 writeData() 메서드에 일부 항목을 저장하며 readData() 메서드에서 Hibernate 쿼리 언어 (HQL)를 사용하여 일부 쿼리를 실행합니다.

App.java의 내용을 다음으로 바꿉니다.

src/main/java/codelab/App.java

package codelab;

import java.text.DateFormat;
import java.text.ParseException;
import java.text.SimpleDateFormat;
import java.util.Date;
import java.util.List;
import org.hibernate.Session;
import org.hibernate.SessionFactory;
import org.hibernate.boot.MetadataSources;
import org.hibernate.boot.registry.StandardServiceRegistry;
import org.hibernate.boot.registry.StandardServiceRegistryBuilder;

public class App {

  public final static DateFormat DATE_FORMAT = new SimpleDateFormat("yyyy-MM-dd");

  public static void main(String[] args) {
    // create a Hibernate sessionFactory and session
    StandardServiceRegistry registry = new StandardServiceRegistryBuilder().configure().build();
    SessionFactory sessionFactory = new MetadataSources(registry).buildMetadata()
        .buildSessionFactory();
    Session session = sessionFactory.openSession();

    clearData(session);

    writeData(session);

    readData(session);

    // close Hibernate session and sessionFactory
    session.close();
    sessionFactory.close();
  }

  private static void clearData(Session session) {
    session.beginTransaction();

    session.createQuery("delete from Album where 1=1").executeUpdate();
    session.createQuery("delete from Singer where 1=1").executeUpdate();

    session.getTransaction().commit();
  }

  private static void writeData(Session session) {
    session.beginTransaction();

    Singer singerMelissa = new Singer("Melissa", "Garcia", makeDate("1981-03-19"));
    Album albumGoGoGo = new Album(singerMelissa, "Go, Go, Go");
    session.save(singerMelissa);
    session.save(albumGoGoGo);

    session.save(new Singer("Russell", "Morales", makeDate("1978-12-02")));
    session.save(new Singer("Jacqueline", "Long", makeDate("1990-07-29")));
    session.save(new Singer("Dylan", "Shaw", makeDate("1998-05-02")));

    session.getTransaction().commit();
  }

  private static void readData(Session session) {
    List<Singer> singers = session.createQuery("from Singer where birthDate >= '1990-01-01' order by lastName")
        .list();
    List<Album> albums = session.createQuery("from Album").list();

    System.out.println("Singers who were born in 1990 or later:");
    for (Singer singer : singers) {
      System.out.println(singer.getFirstName() + " " + singer.getLastName() + " born on "
          + DATE_FORMAT.format(singer.getBirthDate()));
    }

    System.out.println("Albums: ");
    for (Album album : albums) {
      System.out
          .println("\"" + album.getAlbumTitle() + "\" by " + album.getSinger().getFirstName() + " "
              + album.getSinger().getLastName());
    }
  }

  private static Date makeDate(String dateString) {
    try {
      return DATE_FORMAT.parse(dateString);
    } catch (ParseException e) {
      e.printStackTrace();
      return null;
    }
  }
}

이제 코드를 컴파일하고 실행해 보겠습니다. -Dexec.cleanupDaemonThreads=false 옵션을 추가하여 Maven이 시도하는 데몬 스레드 정리에 관한 경고를 표시하지 않습니다.

mvn compile exec:java -Dexec.mainClass=codelab.App -Dexec.cleanupDaemonThreads=false

출력에 다음과 같이 표시됩니다.

Singers who were born in 1990 or later:
Jacqueline Long born on 1990-07-29
Dylan Shaw born on 1998-05-02
Albums: 
"Go, Go, Go" by Melissa Garcia

이 시점에서 Cloud Spanner 콘솔로 이동하여 데이터베이스의 Singer 및 앨범 테이블의 데이터를 보면 다음과 같이 표시됩니다.

f18276ea54cc266f.png

952d9450dd659e75.png

9. 삭제

리소스를 불필요하게 사용하지 않도록 처음에 만든 Cloud Spanner 인스턴스를 삭제해 보겠습니다.

gcloud spanner instances delete codelab-instance

10. 축하합니다

축하합니다. Hibernate를 사용하여 Cloud Spanner에 데이터를 유지하는 Java 애플리케이션을 빌드했습니다.

  • Cloud Spanner 인스턴스와 데이터베이스를 만들었습니다.
  • 최대 절전 모드를 사용하도록 애플리케이션을 구성함
  • 아티스트와 앨범이라는 두 개의 항목을 만들었습니다.
  • 애플리케이션의 데이터베이스 스키마가 자동으로 생성되었습니다.
  • 항목을 Cloud Spanner에 저장하고 쿼리했습니다.

지금까지 Cloud Spanner로 Hibernate 애플리케이션을 작성하는 데 필요한 주요 단계를 알아봤습니다.

다음 단계