Cloud Workstations 및 Cloud Code를 사용한 개발

1. 개요

이 실습에서는 컨테이너화된 환경에서 Java 애플리케이션을 개발하는 소프트웨어 엔지니어의 개발 워크플로를 간소화하도록 설계된 기능을 보여줍니다. 일반적인 컨테이너 개발에서는 사용자가 컨테이너와 컨테이너 빌드 프로세스에 대한 세부정보를 이해해야 합니다. 또한 개발자는 일반적으로 흐름을 중단하고 IDE에서 벗어나 원격 환경에서 애플리케이션을 테스트하고 디버그해야 합니다. 이 튜토리얼에서 언급한 도구와 기술을 사용하면 개발자는 IDE를 벗어나지 않고도 컨테이너화된 애플리케이션으로 효과적으로 작업할 수 있습니다.

학습할 내용

이 실습에서는 다음을 포함하여 GCP에서 컨테이너를 사용하여 개발하는 방법을 알아봅니다.

  • Cloud Workstations를 사용한 InnerLoop 개발
  • 새로운 Java 시작 애플리케이션 만들기
  • 개발 과정 살펴보기
  • 간단한 CRUD REST 서비스 개발
  • GKE 클러스터의 애플리케이션 디버깅
  • Cloud SQL 데이터베이스에 애플리케이션 연결

58a4cdd3ed7a123a.png

2. 설정 및 요구사항

자습형 환경 설정

  1. Google Cloud Console에 로그인하여 새 프로젝트를 만들거나 기존 프로젝트를 재사용합니다. 아직 Gmail이나 Google Workspace 계정이 없는 경우 계정을 만들어야 합니다.

b35bf95b8bf3d5d8.png

a99b7ace416376c4.png

bd84a6d3004737c5.png

  • 프로젝트 이름은 이 프로젝트 참가자의 표시 이름입니다. 이는 Google API에서 사용하지 않는 문자열이며 언제든지 업데이트할 수 있습니다.
  • 프로젝트 ID는 모든 Google Cloud 프로젝트에서 고유하며, 변경할 수 없습니다(설정된 후에는 변경할 수 없음). Cloud 콘솔이 고유한 문자열을 자동으로 생성합니다. 보통은 그게 뭔지 상관하지 않습니다. 대부분의 Codelab에서는 프로젝트 ID (일반적으로 PROJECT_ID로 식별됨)를 참조해야 합니다. 생성된 ID가 마음에 들지 않으면 무작위로 다른 ID를 생성할 수 있습니다. 또는 직접 시도해 보고 사용 가능한지 확인할 수도 있습니다. 이 단계 이후에는 변경할 수 없으며 프로젝트 기간 동안 유지됩니다.
  • 참고로 세 번째 값은 일부 API에서 사용하는 프로젝트 번호입니다. 이 세 가지 값에 대한 자세한 내용은 문서를 참고하세요.
  1. 다음으로 Cloud 리소스/API를 사용하려면 Cloud 콘솔에서 결제를 사용 설정해야 합니다. 이 Codelab 실행에는 많은 비용이 들지 않습니다. 이 튜토리얼이 끝난 후에 요금이 청구되지 않도록 리소스를 종료하려면 만든 리소스를 삭제하거나 전체 프로젝트를 삭제하면 됩니다. Google Cloud 새 사용자에게는 미화 $300 상당의 무료 체험판 프로그램에 참여할 수 있는 자격이 부여됩니다.

Cloud Shell 편집기 시작

이 실습은 Google Cloud Shell 편집기에서 사용할 수 있도록 설계 및 테스트되었습니다. 편집기에 액세스하려면

  1. https://console.cloud.google.com에서 Google 프로젝트에 액세스합니다.
  2. 오른쪽 상단에서 Cloud Shell 편집기 아이콘을 클릭합니다.

8560cc8d45e8c112.png

  1. 창 하단에 새 창이 열립니다.
  2. 편집기 열기 버튼을 클릭합니다.

9e504cb98a6a8005.png

  1. 편집기가 오른쪽에 탐색기가 표시되고 중앙에 편집기가 열립니다.
  2. 화면 하단에서 터미널 창도 사용할 수 있습니다.
  3. 터미널이 열려 있지 않으면 `ctrl+` 키 조합을 사용하여 새 터미널 창을 엽니다.

gcloud 설정

Cloud Shell에서 프로젝트 ID와 애플리케이션을 배포할 리전을 설정합니다. 이러한 변수를 PROJECT_IDREGION 변수로 저장합니다.

export REGION=us-central1
export PROJECT_ID=$(gcloud config get-value project)
export PROJECT_NUMBER=$(gcloud projects describe $PROJECT_ID --format='value(projectNumber)')

소스 코드 클론

이 실습의 소스 코드는 GitHub의 GoogleCloudPlatform에 있는 container-developer-workshop에 있습니다. 아래 명령어로 클론한 후 디렉터리로 변경합니다.

git clone https://github.com/GoogleCloudPlatform/container-developer-workshop.git
cd container-developer-workshop/labs/spring-boot

이 실습에서 사용하는 인프라를 프로비저닝합니다.

이 실습에서는 GKE에 코드를 배포하고 CloudSQL 데이터베이스에 저장된 데이터에 액세스합니다. 아래 설정 스크립트가 이 인프라를 준비합니다. 프로비저닝 프로세스는 25분 이상 걸립니다. 스크립트가 완료될 때까지 기다렸다가 다음 섹션으로 넘어가세요.

./setup_with_cw.sh &

Cloud Workstations 클러스터

Cloud 콘솔에서 Cloud Workstations를 엽니다. 클러스터가 READY 상태가 될 때까지 기다립니다.

305e1a3d63ac7ff6.png

워크스테이션 구성 만들기

Cloud Shell 세션이 연결 해제된 경우 '다시 연결'을 클릭합니다. gcloud cli 명령어를 실행하여 프로젝트 ID를 설정합니다. 명령어를 실행하기 전에 아래의 샘플 프로젝트 ID를 Qwiklabs 프로젝트 ID로 바꿉니다.

gcloud config set project qwiklabs-gcp-project-id

터미널에서 아래 스크립트를 실행하여 Cloud Workstations 구성을 만듭니다.

cd ~/container-developer-workshop/labs/spring-boot
./workstation_config_setup.sh

구성 섹션에서 결과를 확인합니다. READY 상태로 전환되기까지 2분이 소요됩니다.

7a6af5aa2807a5f2.png

콘솔에서 Cloud Workstations를 열고 새 인스턴스를 만듭니다.

a53adeeac81a78c8.png

이름을 my-workstation(으)로 변경하고 기존 구성(codeoss-java)을 선택합니다.

f21c216997746097.png

워크스테이션 섹션에서 결과를 확인합니다.

66a9fc8b20543e32.png

워크스테이션 실행

워크스테이션을 시작하고 실행합니다.

c91bb69b61ec8635.png

주소 표시줄의 아이콘을 클릭하여 타사 쿠키를 허용합니다. 1b8923e2943f9bc4.png

fcf9405b6957b7d7.png

'작동하지 않는 사이트인가요?'를 클릭합니다.

36a84c0e2e3b85b.png

'쿠키 허용'을 클릭합니다.

2259694328628fba.png

워크스테이션이 실행되면 Code OSS IDE가 표시됩니다. '완료로 표시'를 클릭합니다. 워크스테이션 IDE의

94874fba9b74cc22.png

3. 새로운 Java 시작 애플리케이션 만들기

이 섹션에서는 stack.io에서 제공하는 샘플 애플리케이션을 활용하여 새로운 Java Spring Boot 애플리케이션을 처음부터 만듭니다. 새 터미널을 엽니다.

c31d48f2e4938c38.png

샘플 애플리케이션 클론

  1. 시작 애플리케이션 만들기
curl  https://start.spring.io/starter.zip -d dependencies=web -d type=maven-project -d javaVersion=17 -d packageName=com.example.springboot -o sample-app.zip

이 메시지가 표시되면 허용 버튼을 클릭하여 워크스테이션에 복사하여 붙여넣을 수 있도록 합니다.

58149777e5cc350a.png

  1. 애플리케이션의 압축을 풉니다.
unzip sample-app.zip -d sample-app
  1. 'sample-app'을 엽니다. 폴더
cd sample-app && code-oss-cloud-workstations -r --folder-uri="$PWD"

Spring-boot-devtools 추가 및 지브

Spring Boot DevTools를 사용 설정하려면 편집기의 탐색기에서 pom.xml을 찾아 엽니다. 다음으로 <description>Demo project for Spring Boot</description>라는 내용 입력란 뒤에 다음 코드를 붙여넣습니다.

  1. pom.xml에Spring-boot-devtools 추가

프로젝트의 루트에서 pom.xml를 엽니다. Description 항목 뒤에 다음 구성을 추가합니다.

pom.xml

  <!--  Spring profiles-->
  <profiles>
    <profile>
      <id>sync</id>
      <dependencies>
        <dependency>
          <groupId>org.springframework.boot</groupId>
          <artifactId>spring-boot-devtools</artifactId>
        </dependency>
      </dependencies>
    </profile>
  </profiles>
  1. pom.xml에서 jib-maven-plugin 사용 설정

Jib는 Google의 오픈소스 Java 컨테이너화 도구로, Java 개발자가 자신이 알고 있는 Java 도구를 사용하여 컨테이너를 빌드할 수 있도록 지원합니다. Jib는 애플리케이션을 컨테이너 이미지로 패키징하는 모든 단계를 처리하는 빠르고 간단한 컨테이너 이미지 빌더입니다. Dockerfile을 작성하거나 Docker를 설치하지 않아도 되며 Maven 및 Gradle에 직접 통합됩니다.

pom.xml 파일에서 아래로 스크롤하여 Jib 플러그인을 포함하도록 Build 섹션을 업데이트합니다. 빌드 섹션이 완료되면 다음과 일치해야 합니다.

pom.xml

  <build>
    <plugins>
      <plugin>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-maven-plugin</artifactId>
      </plugin>
      <!--  Jib Plugin-->
      <plugin>
        <groupId>com.google.cloud.tools</groupId>
        <artifactId>jib-maven-plugin</artifactId>
        <version>3.2.0</version>
      </plugin>
       <!--  Maven Resources Plugin-->
      <plugin>
        <groupId>org.apache.maven.plugins</groupId>
        <artifactId>maven-resources-plugin</artifactId>
        <version>3.1.0</version>
      </plugin>
    </plugins>
  </build>

매니페스트 생성

Skaffold는 컨테이너 개발을 간소화하는 통합 도구를 제공합니다. 이 단계에서는 기본 Kubernetes YAML 파일을 자동으로 생성하는 Skaffold를 초기화합니다. 이 프로세스는 Dockerfile과 같은 컨테이너 이미지 정의가 있는 디렉터리를 식별한 다음, 각각에 대해 배포 및 서비스 매니페스트를 생성합니다.

터미널에서 아래 명령어를 실행하여 프로세스를 시작합니다.

d869e0cd38e983d7.png

  1. 터미널에서 다음 명령어를 실행합니다.
skaffold init --generate-manifests
  1. 메시지가 표시되면 다음을 수행합니다.
  • 화살표를 사용하여 커서를 Jib Maven Plugin 위치로 이동합니다.
  • 스페이스바를 눌러 옵션을 선택합니다.
  • 계속하려면 Enter 키를 누르세요.
  1. 포트에 8080을 입력합니다.
  2. y를 입력하여 구성을 저장합니다.

작업공간 skaffold.yamldeployment.yaml에 파일 2개가 추가되었습니다.

Skaffold 출력:

b33cc1e0c2077ab8.png

앱 이름 업데이트

구성에 포함된 기본값이 현재 애플리케이션의 이름과 일치하지 않습니다. 기본값이 아닌 애플리케이션 이름을 참조하도록 파일을 업데이트합니다.

  1. Skaffold 구성의 항목 변경
  • skaffold.yaml 열기
  • 현재 pom-xml-image(으)로 설정된 이미지 이름을 선택합니다.
  • 마우스 오른쪽 버튼을 클릭하고 '모든 어커런스 변경'을 선택합니다.
  • 새 이름을 demo-app(으)로 입력합니다.
  1. Kubernetes 구성의 항목 변경
  • deployment.yaml 파일 열기
  • 현재 pom-xml-image(으)로 설정된 이미지 이름을 선택합니다.
  • 마우스 오른쪽 버튼을 클릭하고 '모든 어커런스 변경'을 선택합니다.
  • 새 이름을 demo-app(으)로 입력합니다.

자동 동기화 모드 사용 설정

최적화된 핫 리로드 환경을 위해 Jib에서 제공하는 동기화 기능을 활용합니다. 이 단계에서는 빌드 프로세스에서 이 기능을 활용하도록 Skaffold를 구성합니다.

참고로 'sync' Skaffold 구성에서 구성 중인 프로필은 Spring '동기화'를 활용합니다. 이전 단계에서 구성한 프로필(Spring-dev-tools 지원을 사용 설정함)입니다.

  1. Skaffold 구성 업데이트

skaffold.yaml 파일에서 파일의 전체 빌드 섹션을 다음 사양으로 바꿉니다. 파일의 다른 섹션은 변경하지 마세요.

skaffold.yaml

build:
  artifacts:
  - image: demo-app
    jib:
      project: com.example:demo
      type: maven
      args: 
      - --no-transfer-progress
      - -Psync
      fromImage: gcr.io/distroless/java17-debian11:debug
    sync:
      auto: true

기본 경로 추가

/src/main/java/com/example/springboot/ 폴더에 HelloController.java라는 파일을 만듭니다.

a624f5dd0c477c09.png

다음 내용을 파일에 붙여넣어 기본 http 경로를 만듭니다.

HelloController.java

package com.example.springboot;

import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;
import org.springframework.beans.factory.annotation.Value;

@RestController
public class HelloController {

    @Value("${target:local}")
    String target;

    @GetMapping("/") 
    public String hello()
    {
        return String.format("Hello from your %s environment!", target);
    }
}

4. 개발 과정 살펴보기

이 섹션에서는 Cloud Code 플러그인을 사용하여 기본 프로세스를 학습하고 시작 애플리케이션의 구성 및 설정을 검증하는 몇 가지 단계를 안내합니다.

Cloud Code는 Skaffold와 통합되어 개발 프로세스를 간소화합니다. 다음 단계에서 GKE에 배포하면 Cloud Code 및 Skaffold에서 자동으로 컨테이너 이미지를 빌드하여 Container Registry로 푸시한 다음 애플리케이션을 GKE에 배포합니다. 이 작업은 개발자 흐름에서 세부정보를 추상화하는 이면에서 발생합니다. 또한 Cloud Code는 컨테이너 기반 개발에 기존 디버그 및 핫동기화 기능을 제공하여 개발 프로세스를 개선합니다.

Google Cloud에 로그인

Cloud Code 아이콘을 클릭하고 'Google Cloud에 로그인'을 선택합니다.

1769afd39be372ff.png

'로그인 진행'을 클릭합니다.

923bb1c8f63160f9.png

터미널에서 출력을 확인하고 링크를 엽니다.

517fdd579c34aa21.png

Qwiklabs 학생 인증 정보로 로그인합니다.

db99b345f7a8e72c.png

'허용'을 선택합니다.

a5376553c430ac84.png

인증 코드를 복사하고 워크스테이션 탭으로 돌아갑니다.

6719421277b92eac.png

인증 코드를 붙여넣고 Enter 키를 누릅니다.

e9847cfe3fa8a2ce.png

Kubernetes 클러스터 추가

  1. 클러스터 추가

62a3b97bdbb427e5.png

  1. Google Kubernetes Engine을 선택합니다.

9577de423568bbaa.png

  1. 프로젝트를 선택합니다.

c5202fcbeebcd41c.png

  1. 'quote-cluster'를 선택합니다. 기본 설정입니다.

366cfd8bc27cd3ed.png

9d68532c9bc4a89b.png

gcloud CLI를 사용하여 현재 프로젝트 ID 설정

Qwiklabs 페이지에서 이 실습의 프로젝트 ID를 복사합니다.

fcff2d10007ec5bc.png

gcloud cli 명령어를 실행하여 프로젝트 ID를 설정합니다. 명령어를 실행하기 전에 샘플 프로젝트 ID를 교체하세요.

gcloud config set project qwiklabs-gcp-project-id

샘플 출력:

f1c03d01b7ac112c.png

Kubernetes에서 디버깅

  1. 하단 왼쪽 창에서 Cloud Code를 선택합니다.

60b8e4e95868b561.png

  1. 개발 세션 아래에 표시되는 패널에서 'Kubernetes에서 디버그'를 선택합니다.

옵션이 표시되지 않으면 아래로 스크롤합니다.

7d30833d96632ca0.png

  1. '예'를 선택합니다. 현재 컨텍스트를 사용합니다

a024a69b64de7e9e.png

  1. 'quote-cluster'를 선택합니다. 기본 설정입니다.

faebabf372e3caf0.png

  1. 컨테이너 저장소를 선택합니다.

fabc6dce48bae1b4.png

  1. 하단 창에서 출력 탭을 선택하여 진행 상황 및 알림을 확인합니다.
  2. 'Kubernetes: Run/Debug - DETAILS(Kubernetes: 실행/디버그 - 세부정보)'를 선택합니다. 오른쪽에 있는 채널 드롭다운에서 클릭하여 컨테이너에서 라이브 스트리밍되는 추가 세부정보 및 로그를 볼 수 있습니다.

86b44c59db58f8f3.png

애플리케이션이 배포될 때까지 기다립니다.

9f37706a752829fe.png

  1. Cloud 콘솔에서 GKE에 배포된 애플리케이션을 검토합니다.

6ad220e5d1980756.png

  1. 'Kubernetes: Run/Debug(Kubernetes: 실행/디버그)'를 선택하여 간소화된 뷰로 돌아갑니다. 출력 탭의 드롭다운에서 선택합니다.
  2. 빌드 및 테스트가 완료되면 출력 탭에 Resource deployment/demo-app status completed successfully이 표시되고 URL이 나열됩니다. "Forwarded URL from service demo-app: http://localhost:8080"
  3. Cloud Code 터미널에서 출력의 URL (http://localhost:8080)에 마우스를 가져간 후 표시되는 도움말에서 팔로우 링크를 선택합니다.

28c5539880194a8e.png

새 탭이 열리고 아래에 출력이 표시됩니다.

d67253ca16238f49.png

중단점 활용

  1. /src/main/java/com/example/springboot/HelloController.java에 있는 HelloController.java 애플리케이션을 엽니다.
  2. return String.format("Hello from your %s environment!", target);인 루트 경로의 return 문을 찾습니다.
  3. 줄 번호 왼쪽의 빈 공간을 클릭하여 해당 줄에 중단점을 추가합니다. 중단점이 설정되었음을 알리는 빨간색 표시기가 표시됨

5027dc6da2618a39.png

  1. 브라우저를 새로고침하고 디버거가 중단점에서 프로세스를 중지하며 GKE에서 원격으로 실행 중인 애플리케이션의 변수와 상태를 조사할 수 있습니다.

71acfb426623cec2.png

  1. '타겟'을 찾을 때까지 변수 섹션을 클릭합니다. 변수의 값을 반환합니다.
  2. 현재 값을 'local'로 확인

a1160d2ed2bb5c82.png

  1. 변수 이름 'target'을 더블클릭합니다. 팝업에서

값을 'Cloud Workstations'로 변경합니다.

e597a556a5c53f32.png

  1. 디버그 제어판에서 계속 버튼을 클릭합니다.

ec17086191770d0d.png

  1. 브라우저에서 응답을 검토합니다. 이제 방금 입력한 업데이트된 값이 표시됩니다.

6698a9db9e729925.png

  1. 줄 번호 왼쪽의 빨간색 표시기를 클릭하여 중단점을 삭제합니다. 이렇게 하면 실습을 진행하면서 코드가 이 줄에서 실행을 중지하지 않게 됩니다.

핫 리로드

  1. 'Hello from %s Code'와 같은 다른 값을 반환하도록 문을 변경합니다.
  2. 파일이 자동으로 저장되고 GKE의 원격 컨테이너에 동기화됩니다.
  3. 업데이트된 결과를 보려면 브라우저를 새로고침하세요.
  4. 디버그 툴바의 빨간색 정사각형을 클릭하여 디버깅 세션을 중지합니다.

a541f928ec8f430e.png c2752bb28d82af86.png

'예, 각 실행 후 정리'를 선택합니다.

984eb2fa34867d70.png

5. 간단한 CRUD REST 서비스 개발

이제 애플리케이션이 컨테이너화된 개발을 위해 완전히 구성되었으며 Cloud Code를 사용하여 기본 개발 워크플로를 살펴봤습니다. 다음 섹션에서는 Google Cloud의 관리형 데이터베이스에 연결하는 REST 서비스 엔드포인트를 추가하여 학습한 내용을 연습합니다.

종속 항목 구성

애플리케이션 코드는 데이터베이스를 사용하여 나머지 서비스 데이터를 유지합니다. pom.xl에 다음을 추가하여 종속 항목을 사용할 수 있는지 확인하세요.

  1. pom.xml 파일을 열고 구성의 종속 항목 섹션에 다음을 추가합니다.

pom.xml

    <!--  Database dependencies-->
    <dependency>
      <groupId>org.springframework.boot</groupId>
      <artifactId>spring-boot-starter-data-jpa</artifactId>
    </dependency>
    <dependency>
      <groupId>com.h2database</groupId>
      <artifactId>h2</artifactId>
      <scope>runtime</scope>
    </dependency>
    <dependency>
      <groupId>org.postgresql</groupId>
      <artifactId>postgresql</artifactId>
      <scope>runtime</scope>
    </dependency>
    <dependency>
      <groupId>org.flywaydb</groupId>
      <artifactId>flyway-core</artifactId>
    </dependency>
    <dependency>
      <groupId>javax.persistence</groupId>
      <artifactId>javax.persistence-api</artifactId>
      <version>2.2</version>
    </dependency>

코드 REST 서비스

Quote.java

/src/main/java/com/example/springboot/Quote.java라는 파일을 만들고 아래 코드를 복사합니다. 이는 애플리케이션에서 사용되는 Quote 객체의 항목 모델을 정의합니다.

package com.example.springboot;

import jakarta.persistence.Column;
import jakarta.persistence.Entity;
import jakarta.persistence.Id;
import jakarta.persistence.Table;

import java.util.Objects;

@Entity
@Table(name = "quotes")
public class Quote
{
    @Id
    @Column(name = "id")
    private Integer id;

    @Column(name="quote")
    private String quote;

    @Column(name="author")
    private String author;

    public Integer getId() {
        return id;
    }

    public void setId(Integer id) {
        this.id = id;
    }

    public String getQuote() {
        return quote;
    }

    public void setQuote(String quote) {
        this.quote = quote;
    }

    public String getAuthor() {
        return author;
    }

    public void setAuthor(String author) {
        this.author = author;
    }

    @Override
    public boolean equals(Object o) {
      if (this == o) {
        return true;
      }
      if (o == null || getClass() != o.getClass()) {
        return false;
      }
        Quote quote1 = (Quote) o;
        return Objects.equals(id, quote1.id) &&
                Objects.equals(quote, quote1.quote) &&
                Objects.equals(author, quote1.author);
    }

    @Override
    public int hashCode() {
        return Objects.hash(id, quote, author);
    }
}

QuoteRepository.java

src/main/java/com/example/springbootQuoteRepository.java라는 파일을 만들고 다음 코드를 복사합니다.

package com.example.springboot;

import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.data.jpa.repository.Query;

public interface QuoteRepository extends JpaRepository<Quote,Integer> {

    @Query( nativeQuery = true, value =
            "SELECT id,quote,author FROM quotes ORDER BY RANDOM() LIMIT 1")
    Quote findRandomQuote();
}

이 코드는 데이터를 유지하기 위해 JPA를 사용합니다. 이 클래스는 Spring JPARepository 인터페이스를 확장하고 맞춤 코드를 만들 수 있게 해줍니다. 코드에 findRandomQuote 커스텀 메서드를 추가했습니다.

QuoteController.java

서비스의 엔드포인트를 노출하기 위해 QuoteController 클래스가 이 기능을 제공합니다.

src/main/java/com/example/springbootQuoteController.java라는 파일을 만들고 다음 콘텐츠를 복사합니다.

package com.example.springboot;

import java.util.ArrayList;
import java.util.List;
import java.util.Optional;

import org.springframework.dao.EmptyResultDataAccessException;
import org.springframework.http.HttpStatus;
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.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.RestController;

@RestController
public class QuoteController {

    private final QuoteRepository quoteRepository;

    public QuoteController(QuoteRepository quoteRepository) {
        this.quoteRepository = quoteRepository;
    }

    @GetMapping("/random-quote") 
    public Quote randomQuote()
    {
        return quoteRepository.findRandomQuote();  
    }

    @GetMapping("/quotes") 
    public ResponseEntity<List<Quote>> allQuotes()
    {
        try {
            List<Quote> quotes = new ArrayList<Quote>();
            
            quoteRepository.findAll().forEach(quotes::add);

            if (quotes.size()==0 || quotes.isEmpty()) 
                return new ResponseEntity<List<Quote>>(HttpStatus.NO_CONTENT);
                
            return new ResponseEntity<List<Quote>>(quotes, HttpStatus.OK);
        } catch (Exception e) {
            System.out.println(e.getMessage());
            return new ResponseEntity<List<Quote>>(HttpStatus.INTERNAL_SERVER_ERROR);
        }        
    }

    @PostMapping("/quotes")
    public ResponseEntity<Quote> createQuote(@RequestBody Quote quote) {
        try {
            Quote saved = quoteRepository.save(quote);
            return new ResponseEntity<Quote>(saved, HttpStatus.CREATED);
        } catch (Exception e) {
            System.out.println(e.getMessage());
            return new ResponseEntity<Quote>(HttpStatus.INTERNAL_SERVER_ERROR);
        }
    }     

    @PutMapping("/quotes/{id}")
    public ResponseEntity<Quote> updateQuote(@PathVariable("id") Integer id, @RequestBody Quote quote) {
        try {
            Optional<Quote> existingQuote = quoteRepository.findById(id);
            
            if(existingQuote.isPresent()){
                Quote updatedQuote = existingQuote.get();
                updatedQuote.setAuthor(quote.getAuthor());
                updatedQuote.setQuote(quote.getQuote());

                return new ResponseEntity<Quote>(updatedQuote, HttpStatus.OK);
            } else {
                return new ResponseEntity<Quote>(HttpStatus.NOT_FOUND);
            }
        } catch (Exception e) {
            System.out.println(e.getMessage());
            return new ResponseEntity<Quote>(HttpStatus.INTERNAL_SERVER_ERROR);
        }
    }     

    @DeleteMapping("/quotes/{id}")
    public ResponseEntity<HttpStatus> deleteQuote(@PathVariable("id") Integer id) {
        Optional<Quote> quote = quoteRepository.findById(id);
        if (quote.isPresent()) {
            quoteRepository.deleteById(id);
            return new ResponseEntity<>(HttpStatus.NO_CONTENT);
        } else {
            return new ResponseEntity<>(HttpStatus.INTERNAL_SERVER_ERROR);
        }
    }

}

데이터베이스 구성 추가

application.yaml

서비스에서 액세스하는 백엔드 데이터베이스의 구성을 추가합니다. src/main/resources에서 application.yaml이라는 파일을 편집 (또는 없는 경우 생성)하고 백엔드의 매개변수화된 Spring 구성을 추가합니다.

target: local

spring:
  config:
    activate:
      on-profile: cloud-dev
  datasource:
    url: 'jdbc:postgresql://${DB_HOST:127.0.0.1}/${DB_NAME:quote_db}'
    username: '${DB_USER:user}'
    password: '${DB_PASS:password}'
  jpa:
    properties:
      hibernate:
        jdbc:
          lob:
            non_contextual_creation: true
        dialect: org.hibernate.dialect.PostgreSQLDialect
    hibernate:
      ddl-auto: update

데이터베이스 마이그레이션 추가

src/main/resources 아래에 db/migration 폴더 만들기

SQL 파일 만들기: V1__create_quotes_table.sql

다음 내용을 파일에 붙여넣습니다.

V1__create_quotes_table.sql

CREATE TABLE quotes(
   id INTEGER PRIMARY KEY,
   quote VARCHAR(1024),
   author VARCHAR(256)
);

INSERT INTO quotes (id,quote,author) VALUES (1,'Never, never, never give up','Winston Churchill');
INSERT INTO quotes (id,quote,author) VALUES (2,'While there''s life, there''s hope','Marcus Tullius Cicero');
INSERT INTO quotes (id,quote,author) VALUES (3,'Failure is success in progress','Anonymous');
INSERT INTO quotes (id,quote,author) VALUES (4,'Success demands singleness of purpose','Vincent Lombardi');
INSERT INTO quotes (id,quote,author) VALUES (5,'The shortest answer is doing','Lord Herbert');

Kubernetes 구성

deployment.yaml 파일에 다음을 추가하면 애플리케이션이 CloudSQL 인스턴스에 연결할 수 있습니다.

  • TARGET - 앱이 실행되는 환경을 나타내도록 변수를 구성
  • SPRING_PROFILES_ACTIVE - cloud-dev으로 구성될 활성 Spring 프로필을 표시합니다.
  • DB_HOST - 데이터베이스 인스턴스를 만들 때 또는 Google Cloud 콘솔의 탐색 메뉴에서 SQL를 클릭하여 기록되는 데이터베이스의 비공개 IP입니다. 값을 변경하세요.
  • DB_USER 및 DB_PASS - CloudSQL 인스턴스 구성에 설정됨, GCP에 보안 비밀로 저장됨

아래 내용으로 deployment.yaml을 업데이트합니다.

deployment.yaml

apiVersion: v1
kind: Service
metadata:
  name: demo-app
  labels:
    app: demo-app
spec:
  ports:
  - port: 8080
    protocol: TCP
  clusterIP: None
  selector:
    app: demo-app
---
apiVersion: apps/v1
kind: Deployment
metadata:
  name: demo-app
  labels:
    app: demo-app
spec:
  replicas: 1
  selector:
    matchLabels:
      app: demo-app
  template:
    metadata:
      labels:
        app: demo-app
    spec:
      containers:
      - name: demo-app
        image: demo-app
        env:
          - name: PORT
            value: "8080"
          - name: TARGET
            value: "Local Dev - CloudSQL Database - K8s Cluster"
          - name: SPRING_PROFILES_ACTIVE
            value: cloud-dev
          - name: DB_HOST
            value: ${DB_INSTANCE_IP}   
          - name: DB_PORT
            value: "5432"  
          - name: DB_USER
            valueFrom:
              secretKeyRef:
                name: gke-cloud-sql-secrets
                key: username
          - name: DB_PASS
            valueFrom:
              secretKeyRef:
                name: gke-cloud-sql-secrets
                key: password
          - name: DB_NAME
            valueFrom:
              secretKeyRef:
                name: gke-cloud-sql-secrets
                key: database

터미널에서 아래 명령어를 실행하여 DB_HOST 값을 데이터베이스 주소로 바꿉니다.

export DB_INSTANCE_IP=$(gcloud sql instances describe quote-db-instance \
    --format=json | jq \
    --raw-output ".ipAddresses[].ipAddress")

envsubst < deployment.yaml > deployment.new && mv deployment.new deployment.yaml

Deployment.yaml을 열고 DB_HOST 값이 인스턴스 IP로 업데이트되었는지 확인합니다.

fd63c0aede14beba.png

애플리케이션 배포 및 검증

  1. Cloud Shell 편집기 하단의 창에서 Cloud Code를 선택한 후 화면 상단에서 Debug on Kubernetes를 선택합니다.

33a5cf41aae91adb.png

  1. 빌드와 테스트가 완료되면 출력 탭에 Resource deployment/demo-app status completed successfully이 표시되고 'Forwarded URL from service demo-app: http://localhost:8080'이 표시됩니다. 포트가 8081과 같이 다를 수도 있습니다. 이 경우 적절한 값을 설정합니다. 터미널에서 URL 값 설정
export URL=localhost:8080
  1. 임의 인용문 보기

터미널에서 임의 따옴표 엔드포인트에 대해 아래 명령어를 여러 번 실행합니다. 다른 따옴표를 반환하는 반복 호출 관찰

curl $URL/random-quote | jq
  1. 견적 추가

아래 명령어를 사용하여 id=6인 새 견적을 만들고 요청이 다시 에코되는 것을 관찰합니다.

curl -H 'Content-Type: application/json' -d '{"id":"6","author":"Henry David Thoreau","quote":"Go confidently in the direction of your dreams! Live the life you have imagined"}' -X POST $URL/quotes
  1. 견적 삭제

이제 delete 메서드로 추가한 따옴표를 삭제하고 HTTP/1.1 204 응답 코드를 확인합니다.

curl -v -X DELETE $URL/quotes/6
  1. 서버 오류

항목이 이미 삭제된 후 마지막 요청을 다시 실행하면 오류 상태가 발생합니다.

curl -v -X DELETE $URL/quotes/6

응답이 HTTP:500 Internal Server Error를 반환합니다.

애플리케이션 디버그

이전 섹션에서는 데이터베이스에 없는 항목을 삭제하려고 할 때 애플리케이션에서 오류 상태를 발견했습니다. 이 섹션에서는 문제를 찾기 위해 중단점을 설정합니다. DELETE 작업에서 오류가 발생했으므로 QuoteController 클래스를 사용합니다.

  1. src/main/java/com/example/springboot/QuoteController.java 열기
  2. deleteQuote() 메서드 찾기
  3. 다음 행을 찾습니다. Optional<Quote> quote = quoteRepository.findById(id);
  4. 줄 번호 왼쪽의 빈 공간을 클릭하여 해당 줄에 중단점을 설정합니다.
  5. 중단점이 설정되었음을 나타내는 빨간색 표시기가 나타납니다.
  6. delete 명령어를 다시 실행합니다.
curl -v -X DELETE $URL/quotes/6
  1. 왼쪽 열에 있는 아이콘을 클릭하여 디버그 보기로 다시 전환합니다.
  2. QuoteController 클래스에서 중지된 디버그 줄을 관찰합니다.
  3. 디버거에서 step over 아이콘 b814d39b2e5f3d9e.png을 클릭합니다.
  4. 코드가 클라이언트에 내부 서버 오류 HTTP 500을 반환하는지 확인합니다. 이는 바람직하지 않습니다.
   Trying 127.0.0.1:8080...
* Connected to 127.0.0.1 (127.0.0.1) port 8080 (#0)
> DELETE /quotes/6 HTTP/1.1
> Host: 127.0.0.1:8080
> User-Agent: curl/7.74.0
> Accept: */*
>
* Mark bundle as not supporting multiuse
< HTTP/1.1 500
< Content-Length: 0
< Date: 
<
* Connection #0 to host 127.0.0.1 left intact

코드 업데이트

코드가 잘못되었으므로 else 블록을 리팩터링하여 HTTP 404 찾을 수 없음 상태 코드를 다시 전송해야 합니다.

오류를 수정합니다.

  1. 디버그 세션이 아직 실행 중인 상태에서 'continue'(계속)를 눌러 요청을 완료합니다. 버튼 을 클릭합니다.
  2. 그런 다음 else 블록을 코드로 변경합니다.
       else {
                return new ResponseEntity<HttpStatus>(HttpStatus.NOT_FOUND);
            }

메서드는 다음과 같아야 합니다.

@DeleteMapping("/quotes/{id}")
public ResponseEntity<HttpStatus> deleteQuote(@PathVariable("id") Integer id) {
        Optional<Quote> quote = quoteRepository.findById(id);
        if (quote.isPresent()) {
            quoteRepository.deleteById(id);
            return new ResponseEntity<>(HttpStatus.NO_CONTENT);
        } else {
            return new ResponseEntity<HttpStatus>(HttpStatus.NOT_FOUND);
        }
    }
  1. delete 명령어 재실행
curl -v -X DELETE $URL/quotes/6
  1. 디버거를 단계별로 진행하면서 호출자에게 반환되는 HTTP 404 Not Found를 관찰합니다.
   Trying 127.0.0.1:8080...
* Connected to 127.0.0.1 (127.0.0.1) port 8080 (#0)
> DELETE /quotes/6 HTTP/1.1
> Host: 127.0.0.1:8080
> User-Agent: curl/7.74.0
> Accept: */*
>
* Mark bundle as not supporting multiuse
< HTTP/1.1 404
< Content-Length: 0
< Date: 
<
* Connection #0 to host 127.0.0.1 left intact
  1. 디버그 툴바의 빨간색 정사각형을 클릭하여 디버깅 세션을 중지합니다.

12bc3c82f63dcd8a.png

6f19c0f855832407.png

6. 축하합니다

축하합니다. 이 실습에서는 새로운 Java 애플리케이션을 처음부터 만들고 컨테이너와 함께 효과적으로 작동하도록 구성했습니다. 그런 다음 기존 애플리케이션 스택에 있는 것과 동일한 개발자 흐름에 따라 애플리케이션을 원격 GKE 클러스터에 배포하고 디버깅했습니다.

학습한 내용

  • Cloud Workstations를 사용한 InnerLoop 개발
  • 새로운 Java 시작 애플리케이션 만들기
  • 개발 과정 살펴보기
  • 간단한 CRUD REST 서비스 개발
  • GKE 클러스터의 애플리케이션 디버깅
  • Cloud SQL 데이터베이스에 애플리케이션 연결