일일 그림: 실습 1—사진 저장 및 분석 (네이티브 Java)

1. 개요

첫 번째 Codelab에서는 사진을 버킷에 저장합니다. 이렇게 하면 Cloud Run에 배포된 서비스에서 처리할 파일 생성 이벤트가 생성됩니다. 서비스는 Vision API를 호출하여 이미지 분석을 수행하고 결과를 Datastore에 저장합니다.

c0650ee4a76db35e.png

학습할 내용

  • Cloud Storage
  • Cloud Run
  • Cloud Vision API
  • Cloud Firestore

2. 설정 및 요구사항

자습형 환경 설정

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

295004821bab6a87.png

37d264871000675d.png

96d86d3d5655cdbe.png

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

Cloud Shell 시작

Google Cloud를 노트북에서 원격으로 실행할 수 있지만, 이 Codelab에서는 Cloud에서 실행되는 명령줄 환경인 Google Cloud Shell을 사용합니다.

Google Cloud Console의 오른쪽 상단 툴바에 있는 Cloud Shell 아이콘을 클릭합니다.

84688aa223b1c3a2.png

환경을 프로비저닝하고 연결하는 데 몇 분 정도 소요됩니다. 완료되면 다음과 같이 표시됩니다.

320e18fedb7fbe0.png

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

3. API 사용 설정

이 실습에서는 Cloud Functions 및 Vision API를 사용하지만 먼저 Cloud 콘솔 또는 gcloud에서 사용 설정해야 합니다.

Cloud 콘솔에서 Vision API를 사용 설정하려면 검색창에서 Cloud Vision API를 검색합니다.

8f3522d790bb026c.png

Cloud Vision API 페이지로 이동합니다.

d785572fa14c87c2.png

ENABLE 버튼을 클릭합니다.

또는 gcloud 명령줄 도구를 사용하여 Cloud Shell에서 사용 설정할 수도 있습니다.

Cloud Shell 내에서 다음 명령어를 실행합니다.

gcloud services enable vision.googleapis.com

작업이 성공적으로 완료됩니다.

Operation "operations/acf.12dba18b-106f-4fd2-942d-fea80ecc5c1c" finished successfully.

Cloud Run 및 Cloud Build도 사용 설정합니다.

gcloud services enable cloudbuild.googleapis.com \
  run.googleapis.com

4. 버킷 만들기 (콘솔)

사진을 저장할 스토리지 버킷을 만듭니다. Google Cloud Platform 콘솔(console.cloud.google.com)이나 로컬 개발 환경(Cloud Shell)의 gsutil 명령줄 도구를 사용하여 지정할 수 있습니다.

'햄버거'에서 (❯) 메뉴에서 Storage 페이지로 이동합니다.

d08ecb0ae29330a1.png

버킷 이름 지정

CREATE BUCKET 버튼을 클릭합니다.

8951851554a430d2.png

CONTINUE 아이콘을 클릭합니다.

위치 선택

24b24625157ab467.png

원하는 리전 (여기 Europe)에 멀티 리전 버킷을 만듭니다.

CONTINUE 아이콘을 클릭합니다.

기본 스토리지 클래스 선택

9e7bd365fa94a2e0.png

데이터의 Standard 스토리지 클래스를 선택합니다.

CONTINUE 아이콘을 클릭합니다.

액세스 제어 설정

1ff4a1f6e57045f5.png

공개적으로 액세스할 수 있는 이미지로 작업하므로 이 버킷에 저장된 모든 사진에 동일하게 균일한 액세스 제어를 적용하고자 합니다.

Uniform 액세스 제어 옵션을 선택합니다.

CONTINUE 아이콘을 클릭합니다.

보호/암호화 설정

2d469b076029d365.png

자체 암호화 키를 사용하지 않으므로 기본값 (Google-managed key))을 유지합니다.

CREATE를 클릭하여 버킷 만들기를 완료합니다.

allUsers를 스토리지 뷰어로 추가하기

Permissions 탭으로 이동합니다.

19564b3ad8688ae8.png

다음과 같이 Storage > Storage Object Viewer 역할의 allUsers 구성원을 버킷에 추가합니다.

d655e760c76d62c1.png

SAVE 아이콘을 클릭합니다.

5. 버킷 만들기 (gsutil)

Cloud Shell의 gsutil 명령줄 도구를 사용하여 버킷을 만들 수도 있습니다.

Cloud Shell에서 고유한 버킷 이름의 변수를 설정합니다. Cloud Shell에 이미 고유한 프로젝트 ID로 설정된 GOOGLE_CLOUD_PROJECT가 있습니다. 버킷 이름에 추가할 수 있습니다.

예를 들면 다음과 같습니다.

export BUCKET_PICTURES=uploaded-pictures-${GOOGLE_CLOUD_PROJECT}

유럽에 표준 멀티 리전 영역을 만듭니다.

gsutil mb -l EU gs://${BUCKET_PICTURES}

균일한 버킷 수준 액세스를 확인합니다.

gsutil uniformbucketlevelaccess set on gs://${BUCKET_PICTURES}

버킷을 공개로 만듭니다.

gsutil iam ch allUsers:objectViewer gs://${BUCKET_PICTURES}

콘솔의 Cloud Storage 섹션으로 이동하면 공개 uploaded-pictures 버킷이 있습니다.

65c63ef4a6eb30ad.png

이전 단계에서 설명한 대로 버킷에 사진을 업로드할 수 있고 업로드된 사진이 공개적으로 사용 가능한지 테스트합니다.

6. 버킷에 대한 공개 액세스 테스트

스토리지 브라우저로 돌아가면 목록에 버킷이 '공개'로 되어 있는 것을 확인할 수 있습니다. (누구나 해당 버킷의 콘텐츠에 액세스할 수 있음을 알려주는 경고 표시 포함)

e639a9ba625b71a6.png

이제 버킷에서 사진을 수신할 준비가 되었습니다.

버킷 이름을 클릭하면 버킷 세부정보가 표시됩니다.

1f88a2290290aba8.png

여기서 Upload files 버튼을 사용하여 버킷에 사진을 추가할 수 있는지 테스트할 수 있습니다. 파일을 선택하라는 파일 선택기 팝업이 표시됩니다. 선택하면 버킷에 업로드되고 이 새 파일에 자동으로 부여된 public 액세스 권한이 다시 표시됩니다.

1209e7ebe1f63b10.png

Public 액세스 라벨 옆에 작은 링크 아이콘도 표시됩니다. 이미지를 클릭하면 브라우저가 이미지의 공개 URL로 이동합니다. URL 형식은 다음과 같습니다.

https://storage.googleapis.com/BUCKET_NAME/PICTURE_FILE.png

BUCKET_NAME은 버킷에 대해 선택한 전역적으로 고유한 이름이고 사진의 파일 이름입니다.

사진 이름의 체크박스를 클릭하면 DELETE 버튼이 사용 설정되고 이 첫 번째 이미지를 삭제할 수 있습니다.

7. 데이터베이스 준비

Vision API에서 제공한 사진에 대한 정보를 클라우드 기반의 빠르고 완전 관리형 서버리스 NoSQL 문서 데이터베이스인 Cloud Firestore 데이터베이스에 저장합니다. Cloud 콘솔의 Firestore 섹션으로 이동하여 데이터베이스를 준비합니다.

e57a673537b5deca.png

Native mode 또는 Datastore mode, 두 가지 옵션이 제공됩니다. 오프라인 지원 및 실시간 동기화와 같은 추가 기능을 제공하는 네이티브 모드를 사용하세요.

SELECT NATIVE MODE 아이콘을 클릭합니다.

1a2e363fae5c7e96.png

멀티 리전을 선택합니다 (여기서는 유럽이지만 최소한 함수 및 스토리지 버킷과 동일한 리전인 것이 좋습니다).

CREATE DATABASE 버튼을 클릭합니다.

데이터베이스가 생성되면 다음과 같이 표시됩니다.

7dcc82751ed483fb.png

+ START COLLECTION 버튼을 클릭하여 새 컬렉션을 만듭니다.

컬렉션 이름을 pictures으로 지정합니다.

dce3d73884ac8c83.png

문서를 만들지 않아도 됩니다. 새 사진이 Cloud Storage에 저장되고 Vision API에서 분석되므로 이를 프로그래매틱 방식으로 추가합니다.

Save 아이콘을 클릭합니다.

Firestore는 새로 생성된 컬렉션에 첫 번째 기본 문서를 만듭니다. 이 문서는 유용한 정보를 포함하지 않으므로 안전하게 삭제할 수 있습니다.

63e95c844b3f79d3.png

컬렉션에서 프로그래매틱 방식으로 만들어지는 문서에는 4개의 필드가 포함됩니다.

  • name (문자열): 업로드된 사진의 파일 이름으로, 문서의 키이기도 합니다.
  • labels (문자열 배열): Vision API에서 인식된 항목의 라벨
  • color (문자열): 주요 색상의 16진수 색상 코드입니다 (예: #ab12ef)는
  • created (날짜): 이 이미지의 메타데이터가 저장된 시점의 타임스탬프
  • thumbnail (불리언): 표시되고 이 사진의 미리보기 이미지가 생성된 경우 true로 표시되는 선택 필드입니다.

사용 가능한 썸네일이 있는 사진을 찾기 위해 Firestore에서 검색하고 생성 날짜를 기준으로 정렬하므로 검색 색인을 만들어야 합니다.

Cloud Shell에서 다음 명령어를 사용하여 색인을 만들 수 있습니다.

gcloud firestore indexes composite create \
  --collection-group=pictures \
  --field-config field-path=thumbnail,order=descending \
  --field-config field-path=created,order=descending

또는 Cloud 콘솔에서 왼쪽 탐색 열의 Indexes를 클릭한 다음 아래와 같이 복합 색인을 만들어 생성할 수도 있습니다.

2236d3a024a59232.png

Create 아이콘을 클릭합니다. 색인을 만드는 데 몇 분 정도 걸릴 수 있습니다.

8. 코드 클론

이전 Codelab에서 아직 클론하지 않았다면 코드를 클론합니다.

git clone https://github.com/GoogleCloudPlatform/serverless-photosharing-workshop

그런 다음 서비스가 포함된 디렉터리로 이동하여 실습 빌드를 시작할 수 있습니다.

cd serverless-photosharing-workshop/services/image-analysis/java

서비스의 파일 레이아웃은 다음과 같습니다.

4c2a18a2c8b69dc5.png

9. 서비스 코드 살펴보기

먼저 BOM을 사용하여 pom.xml에서 Java 클라이언트 라이브러리가 사용 설정되는 방법을 살펴보겠습니다.

먼저 Java 앱의 종속 항목이 나열된 pom.xml 파일을 엽니다. Vision, Cloud Storage, Firestore API의 사용에 중점을 둡니다.

<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
        xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
        <modelVersion>4.0.0</modelVersion>
    <parent>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-parent</artifactId>
        <version>3.2.0-M3</version>
        <relativePath/> <!-- lookup parent from repository -->
    </parent>
    <groupId>services</groupId>
        <artifactId>image-analysis</artifactId>
        <version>0.0.1</version>
        <name>image-analysis</name>
        <description>Spring App for Image Analysis</description>
    <properties>
        <java.version>17</java.version>
        <maven.compiler.target>17</maven.compiler.target>
        <maven.compiler.source>17</maven.compiler.source>        
        <spring-cloud.version>2023.0.0-M2</spring-cloud.version>
        <testcontainers.version>1.19.1</testcontainers.version>
    </properties>
...
  <dependencyManagement>
    <dependencies>
        <dependency>
            <groupId>com.google.cloud</groupId>
            <artifactId>libraries-bom</artifactId>
            <version>26.24.0</version>
            <type>pom</type>
            <scope>import</scope>
        </dependency>
    </dependencies>
  </dependencyManagement>
— 
    <dependencies>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-web</artifactId>
        </dependency>
                <dependency>
                        <groupId>org.springframework.cloud</groupId>
                        <artifactId>spring-cloud-function-web</artifactId>
                </dependency>
        <dependency>
            <groupId>com.google.cloud.functions</groupId>
            <artifactId>functions-framework-api</artifactId>
            <version>1.1.0</version>
            <type>jar</type>
        </dependency>
        <dependency>
            <groupId>com.google.cloud</groupId>
            <artifactId>google-cloud-firestore</artifactId>
        </dependency>
        <dependency>
            <groupId>com.google.cloud</groupId>
            <artifactId>google-cloud-vision</artifactId>
        </dependency>
        <dependency>
            <groupId>com.google.cloud</groupId>
            <artifactId>google-cloud-storage</artifactId>
        </dependency>        

이 기능은 EventController 클래스에서 구현됩니다. 새 이미지가 버킷에 업로드될 때마다 서비스에서 다음과 같이 처리하라는 알림을 받습니다.

@RestController
public class EventController {
  private static final Logger logger = Logger.getLogger(EventController.class.getName());
    
  private static final List<String> requiredFields = Arrays.asList("ce-id", "ce-source", "ce-type", "ce-specversion");

  @RequestMapping(value = "/", method = RequestMethod.POST)
  public ResponseEntity<String> receiveMessage(
    @RequestBody Map<String, Object> body, @RequestHeader Map<String, String> headers) throws IOException, InterruptedException, ExecutionException {
...
}

코드는 Cloud Events 헤더의 유효성을 검사합니다.

System.out.println("Header elements");
for (String field : requiredFields) {
    if (headers.get(field) == null) {
    String msg = String.format("Missing expected header: %s.", field);
    System.out.println(msg);
    return new ResponseEntity<String>(msg, HttpStatus.BAD_REQUEST);
    } else {
    System.out.println(field + " : " + headers.get(field));
    }
}

System.out.println("Body elements");
for (String bodyField : body.keySet()) {
    System.out.println(bodyField + " : " + body.get(bodyField));
}

if (headers.get("ce-subject") == null) {
    String msg = "Missing expected header: ce-subject.";
    System.out.println(msg);
    return new ResponseEntity<String>(msg, HttpStatus.BAD_REQUEST);
} 

이제 요청을 빌드할 수 있으며 코드에서 이러한 요청 하나를 Vision API로 전송할 준비를 합니다.

try (ImageAnnotatorClient vision = ImageAnnotatorClient.create()) {
    List<AnnotateImageRequest> requests = new ArrayList<>();
    
    ImageSource imageSource = ImageSource.newBuilder()
        .setGcsImageUri("gs://" + bucketName + "/" + fileName)
        .build();

    Image image = Image.newBuilder()
        .setSource(imageSource)
        .build();

    Feature featureLabel = Feature.newBuilder()
        .setType(Type.LABEL_DETECTION)
        .build();
    Feature featureImageProps = Feature.newBuilder()
        .setType(Type.IMAGE_PROPERTIES)
        .build();
    Feature featureSafeSearch = Feature.newBuilder()
        .setType(Type.SAFE_SEARCH_DETECTION)
        .build();
        
    AnnotateImageRequest request = AnnotateImageRequest.newBuilder()
        .addFeatures(featureLabel)
        .addFeatures(featureImageProps)
        .addFeatures(featureSafeSearch)
        .setImage(image)
        .build();
    
    requests.add(request);

Vision API에는 3가지 주요 기능이 필요합니다.

  • 라벨 인식: 사진의 내용 파악
  • 이미지 속성: 사진에 흥미로운 속성을 제공하기 위해 (Google은 사진의 주요 색상에 관심이 있음)
  • 세이프서치: 이미지가 표시해도 안전한지 확인 (성인 / 의료적 / 선정적인 / 폭력적인 콘텐츠가 포함되지 않아야 함)

이 시점에서 Vision API를 호출할 수 있습니다.

...
logger.info("Calling the Vision API...");
BatchAnnotateImagesResponse result = vision.batchAnnotateImages(requests);
List<AnnotateImageResponse> responses = result.getResponsesList();
...

참고로 Vision API의 응답은 다음과 같습니다.

{
  "faceAnnotations": [],
  "landmarkAnnotations": [],
  "logoAnnotations": [],
  "labelAnnotations": [
    {
      "locations": [],
      "properties": [],
      "mid": "/m/01yrx",
      "locale": "",
      "description": "Cat",
      "score": 0.9959855675697327,
      "confidence": 0,
      "topicality": 0.9959855675697327,
      "boundingPoly": null
    },
    ✄ - - - ✄
  ],
  "textAnnotations": [],
  "localizedObjectAnnotations": [],
  "safeSearchAnnotation": {
    "adult": "VERY_UNLIKELY",
    "spoof": "UNLIKELY",
    "medical": "VERY_UNLIKELY",
    "violence": "VERY_UNLIKELY",
    "racy": "VERY_UNLIKELY",
    "adultConfidence": 0,
    "spoofConfidence": 0,
    "medicalConfidence": 0,
    "violenceConfidence": 0,
    "racyConfidence": 0,
    "nsfwConfidence": 0
  },
  "imagePropertiesAnnotation": {
    "dominantColors": {
      "colors": [
        {
          "color": {
            "red": 203,
            "green": 201,
            "blue": 201,
            "alpha": null
          },
          "score": 0.4175916016101837,
          "pixelFraction": 0.44456374645233154
        },
        ✄ - - - ✄
      ]
    }
  },
  "error": null,
  "cropHintsAnnotation": {
    "cropHints": [
      {
        "boundingPoly": {
          "vertices": [
            { "x": 0, "y": 118 },
            { "x": 1177, "y": 118 },
            { "x": 1177, "y": 783 },
            { "x": 0, "y": 783 }
          ],
          "normalizedVertices": []
        },
        "confidence": 0.41695669293403625,
        "importanceFraction": 1
      }
    ]
  },
  "fullTextAnnotation": null,
  "webDetection": null,
  "productSearchResults": null,
  "context": null
}

오류가 반환되지 않으면 계속 진행할 수 있습니다. 따라서 if block이 있는 이유는 다음과 같습니다.

if (responses.size() == 0) {
    logger.info("No response received from Vision API.");
    return new ResponseEntity<String>(msg, HttpStatus.BAD_REQUEST);
}

AnnotateImageResponse response = responses.get(0);
if (response.hasError()) {
    logger.info("Error: " + response.getError().getMessage());
    return new ResponseEntity<String>(msg, HttpStatus.BAD_REQUEST);
}

그림에서 인식되는 사물, 카테고리 또는 테마의 라벨을 가져옵니다.

List<String> labels = response.getLabelAnnotationsList().stream()
    .map(annotation -> annotation.getDescription())
    .collect(Collectors.toList());
logger.info("Annotations found:");
for (String label: labels) {
    logger.info("- " + label);
}

사진의 주요 색상을 파악하는 데 관심이 있습니다.

String mainColor = "#FFFFFF";
ImageProperties imgProps = response.getImagePropertiesAnnotation();
if (imgProps.hasDominantColors()) {
    DominantColorsAnnotation colorsAnn = imgProps.getDominantColors();
    ColorInfo colorInfo = colorsAnn.getColors(0);

    mainColor = rgbHex(
        colorInfo.getColor().getRed(), 
        colorInfo.getColor().getGreen(), 
        colorInfo.getColor().getBlue());

    logger.info("Color: " + mainColor);
}

사진을 표시해도 안전한지 확인해 보겠습니다.

boolean isSafe = false;
if (response.hasSafeSearchAnnotation()) {
    SafeSearchAnnotation safeSearch = response.getSafeSearchAnnotation();

    isSafe = Stream.of(
        safeSearch.getAdult(), safeSearch.getMedical(), safeSearch.getRacy(),
        safeSearch.getSpoof(), safeSearch.getViolence())
    .allMatch( likelihood -> 
        likelihood != Likelihood.LIKELY && likelihood != Likelihood.VERY_LIKELY
    );

    logger.info("Safe? " + isSafe);
}

성인용 / 위장 / 의학 / 폭력 / 선정적인 특징을 검토하여 가능성이 높지 않거나 가능성이 매우 높음이 아닌지 확인합니다.

세이프서치의 결과가 양호하면 Firestore에 메타데이터를 저장할 수 있습니다.

// Saving result to Firestore
if (isSafe) {
          ApiFuture<WriteResult> writeResult = 
               eventService.storeImage(fileName, labels,
                                       mainColor);
          logger.info("Picture metadata saved in Firestore at " + 
               writeResult.get().getUpdateTime());
}
...
  public ApiFuture<WriteResult> storeImage(String fileName, 
                                           List<String> labels, 
                                           String mainColor) {
    FirestoreOptions firestoreOptions = FirestoreOptions.getDefaultInstance();
    Firestore pictureStore = firestoreOptions.getService();

    DocumentReference doc = pictureStore.collection("pictures").document(fileName);

    Map<String, Object> data = new HashMap<>();
    data.put("labels", labels);
    data.put("color", mainColor);
    data.put("created", new Date());

    return doc.set(data, SetOptions.merge());
  }

10. GraalVM으로 앱 이미지 빌드

이 선택적 단계에서는 GraalVM을 사용하여 JIT based app image를 빌드한 후 Native Java app image를 빌드합니다.

빌드를 실행하려면 적절한 JDK와 네이티브 이미지 빌더가 설치 및 구성되었는지 확인해야 합니다. 몇 가지 옵션이 있습니다.

To start를 실행하려면 GraalVM 22.3.x Community Edition을 다운로드하고 GraalVM 설치 페이지의 안내를 따릅니다.

SDKMAN!을 사용하면 이 프로세스를 크게 단순화할 수 있습니다.

SDKman로 적절한 JDK 배포를 설치하려면 먼저 설치 명령어를 사용합니다.

sdk install java 17.0.8-graal

JIT 및 AOT 빌드에 모두 이 버전을 사용하도록 SDKman에 지시합니다.

sdk use java 17.0.8-graal

Cloudshell에서 편의를 위해 다음과 같은 간단한 명령어를 사용하여 GraalVM과 네이티브 이미지 유틸리티를 설치할 수 있습니다.

# download GraalVM
wget https://download.oracle.com/graalvm/17/latest/graalvm-jdk-17_linux-x64_bin.tar.gz 
tar -xzf graalvm-jdk-17_linux-x64_bin.tar.gz

ls -lart

# configure Java 17 and GraalVM for Java 17
# note the name of the latest GraalVM version, as unpacked by the tar command
echo Existing JVM: $JAVA_HOME
cd graalvm-jdk-17.0.8+9.1

export JAVA_HOME=$PWD
cd bin
export PATH=$PWD:$PATH

echo JAVA HOME: $JAVA_HOME
echo PATH: $PATH

cd ../..

# validate the version with
java -version 

# observe
Java(TM) SE Runtime Environment Oracle GraalVM 17.0.8+9.1 (build 17.0.8+9-LTS-jvmci-23.0-b14)
Java HotSpot(TM) 64-Bit Server VM Oracle GraalVM 17.0.8+9.1 (build 17.0.8+9-LTS-jvmci-23.0-b14, mixed mode, sharing)

먼저 GCP 프로젝트 환경 변수를 설정합니다.

export GOOGLE_CLOUD_PROJECT=$(gcloud config get-value project)

그런 다음 서비스가 포함된 디렉터리로 이동하여 실습 빌드를 시작할 수 있습니다.

cd serverless-photosharing-workshop/services/image-analysis/java

JIT 애플리케이션 이미지를 빌드합니다.

./mvnw package

터미널에서 빌드 로그를 확인합니다.

...
[INFO] Results:
[INFO] 
[INFO] Tests run: 6, Failures: 0, Errors: 0, Skipped: 0
[INFO] 
[INFO] 
[INFO] --- maven-jar-plugin:3.3.0:jar (default-jar) @ image-analysis ---
[INFO] Building jar: /home/user/serverless-photosharing-workshop/services/image-analysis/java/target/image-analysis-0.0.1.jar
[INFO] 
[INFO] --- spring-boot-maven-plugin:3.2.0-M3:repackage (repackage) @ image-analysis ---
[INFO] Replacing main artifact /home/user/serverless-photosharing-workshop/services/image-analysis/java/target/image-analysis-0.0.1.jar with repackaged archive, adding nested dependencies in BOOT-INF/.
[INFO] The original artifact has been renamed to /home/user/serverless-photosharing-workshop/services/image-analysis/java/target/image-analysis-0.0.1.jar.original
[INFO] ------------------------------------------------------------------------
[INFO] BUILD SUCCESS
[INFO] ------------------------------------------------------------------------
[INFO] Total time:  15.335 s
[INFO] Finished at: 2023-10-10T19:33:25Z
[INFO] ------------------------------------------------------------------------

네이티브(AOT 사용) 이미지를 빌드합니다.

./mvnw native:compile -Pnative

터미널에서 네이티브 이미지 빌드 로그를 비롯한 빌드 로그를 확인합니다.

테스트 중인 머신에 따라 빌드에 더 오랜 시간이 걸립니다.

...
[2/7] Performing analysis...  [*********]                                                              (124.5s @ 4.53GB)
  29,732 (93.19%) of 31,905 classes reachable
  60,161 (70.30%) of 85,577 fields reachable
 261,973 (67.29%) of 389,319 methods reachable
   2,940 classes, 2,297 fields, and 97,421 methods registered for reflection
      81 classes,    90 fields, and    62 methods registered for JNI access
       4 native libraries: dl, pthread, rt, z
[3/7] Building universe...                                                                              (11.7s @ 4.67GB)
[4/7] Parsing methods...      [***]                                                                      (6.1s @ 5.91GB)
[5/7] Inlining methods...     [****]                                                                     (4.5s @ 4.39GB)
[6/7] Compiling methods...    [******]                                                                  (35.3s @ 4.60GB)
[7/7] Creating image...                                                                                 (12.9s @ 4.61GB)
  80.08MB (47.43%) for code area:   190,483 compilation units
  73.81MB (43.72%) for image heap:  660,125 objects and 189 resources
  14.95MB ( 8.86%) for other data
 168.84MB in total
------------------------------------------------------------------------------------------------------------------------
Top 10 packages in code area:                               Top 10 object types in image heap:
   2.66MB com.google.cloud.vision.v1p4beta1                   18.51MB byte[] for code metadata
   2.60MB com.google.cloud.vision.v1                           9.27MB java.lang.Class
   2.49MB com.google.protobuf                                  7.34MB byte[] for reflection metadata
   2.40MB com.google.cloud.vision.v1p3beta1                    6.35MB byte[] for java.lang.String
   2.17MB com.google.storage.v2                                5.72MB java.lang.String
   2.12MB com.google.firestore.v1                              4.46MB byte[] for embedded resources
   1.64MB sun.security.ssl                                     4.30MB c.oracle.svm.core.reflect.SubstrateMethodAccessor
   1.51MB i.g.xds.shaded.io.envoyproxy.envoy.config.core.v3    4.27MB byte[] for general heap data
   1.47MB com.google.cloud.vision.v1p2beta1                    2.50MB com.oracle.svm.core.hub.DynamicHubCompanion
   1.34MB i.g.x.shaded.io.envoyproxy.envoy.config.route.v3     1.17MB java.lang.Object[]
  58.34MB for 977 more packages                                9.19MB for 4667 more object types
------------------------------------------------------------------------------------------------------------------------
                        13.5s (5.7% of total time) in 75 GCs | Peak RSS: 9.44GB | CPU load: 6.13
------------------------------------------------------------------------------------------------------------------------
Produced artifacts:
 /home/user/serverless-photosharing-workshop/services/image-analysis/java/target/image-analysis (executable)
 /home/user/serverless-photosharing-workshop/services/image-analysis/java/target/image-analysis.build_artifacts.txt (txt)
========================================================================================================================
Finished generating '/home/user/serverless-photosharing-workshop/services/image-analysis/java/target/image-analysis' in 3m 57s.
[INFO] ------------------------------------------------------------------------
[INFO] BUILD SUCCESS
[INFO] ------------------------------------------------------------------------
[INFO] Total time:  04:28 min
[INFO] Finished at: 2023-10-10T19:53:30Z
[INFO] ------------------------------------------------------------------------

11. 컨테이너 이미지 빌드 및 게시

JIT imageNative Java image이라는 두 가지 버전으로 컨테이너 이미지를 빌드해 보겠습니다.

먼저 GCP 프로젝트 환경 변수를 설정합니다.

export GOOGLE_CLOUD_PROJECT=$(gcloud config get-value project)

JIT 이미지를 빌드합니다.

./mvnw spring-boot:build-image -Pji

터미널에서 빌드 로그를 확인합니다.

[INFO]     [creator]     Timer: Saving docker.io/library/image-analysis-maven-jit:latest... started at 2023-10-10T20:00:31Z
[INFO]     [creator]     *** Images (4c84122a1826):
[INFO]     [creator]           docker.io/library/image-analysis-maven-jit:latest
[INFO]     [creator]     Timer: Saving docker.io/library/image-analysis-maven-jit:latest... ran for 6.975913605s and ended at 2023-10-10T20:00:38Z
[INFO]     [creator]     Timer: Exporter ran for 8.068588001s and ended at 2023-10-10T20:00:38Z
[INFO]     [creator]     Timer: Cache started at 2023-10-10T20:00:38Z
[INFO]     [creator]     Reusing cache layer 'paketo-buildpacks/syft:syft'
[INFO]     [creator]     Adding cache layer 'buildpacksio/lifecycle:cache.sbom'
[INFO]     [creator]     Timer: Cache ran for 200.449002ms and ended at 2023-10-10T20:00:38Z
[INFO] 
[INFO] Successfully built image 'docker.io/library/image-analysis-maven-jit:latest'
[INFO] 
[INFO] ------------------------------------------------------------------------
[INFO] BUILD SUCCESS
[INFO] ------------------------------------------------------------------------
[INFO] Total time:  43.887 s
[INFO] Finished at: 2023-10-10T20:00:39Z
[INFO] ------------------------------------------------------------------------

AOT(네이티브) 이미지를 빌드합니다.

./mvnw spring-boot:build-image -Pnative

터미널에서 네이티브 이미지 빌드 로그를 비롯한 빌드 로그를 확인합니다.

참고:

  • 사용하는 기기에 따라 빌드가 상당히 오래 걸릴 수 있습니다
  • 이미지는 UPX를 사용하여 추가로 압축할 수 있지만 시작 성능에 약간의 부정적인 영향을 미칩니다. 따라서 이 빌드는 UPX를 사용하지 않습니다.
...
[INFO]     [creator]     Saving docker.io/library/image-analysis-maven-native:latest...
[INFO]     [creator]     *** Images (13167702674e):
[INFO]     [creator]           docker.io/library/image-analysis-maven-native:latest
[INFO]     [creator]     Adding cache layer 'paketo-buildpacks/bellsoft-liberica:native-image-svm'
[INFO]     [creator]     Adding cache layer 'paketo-buildpacks/syft:syft'
[INFO]     [creator]     Adding cache layer 'paketo-buildpacks/native-image:native-image'
[INFO]     [creator]     Adding cache layer 'buildpacksio/lifecycle:cache.sbom'
[INFO] 
[INFO] Successfully built image 'docker.io/library/image-analysis-maven-native:latest'
[INFO] 
[INFO] ------------------------------------------------------------------------
[INFO] BUILD SUCCESS
[INFO] ------------------------------------------------------------------------
[INFO] Total time:  03:37 min
[INFO] Finished at: 2023-10-10T20:05:16Z
[INFO] ------------------------------------------------------------------------

이미지가 빌드되었는지 확인합니다.

docker images | grep image-analysis

두 이미지에 태그를 지정하고 GCR로 푸시합니다.

# JIT image
docker tag image-analysis-maven-jit gcr.io/${GOOGLE_CLOUD_PROJECT}/image-analysis-maven-jit
docker push gcr.io/${GOOGLE_CLOUD_PROJECT}/image-analysis-maven-jit

# Native(AOT) image
docker tag image-analysis-maven-native gcr.io/${GOOGLE_CLOUD_PROJECT}/image-analysis-maven-native
docker push  gcr.io/${GOOGLE_CLOUD_PROJECT}/image-analysis-maven-native

12. Cloud Run에 배포

서비스를 배포할 시간입니다.

서비스를 두 번 배포합니다. 한 번은 JIT 이미지를 사용하고, 두 번째에는 AOT(네이티브) 이미지를 사용합니다. 비교를 위해 두 서비스 배포 모두 버킷의 동일한 이미지를 동시에 처리합니다.

먼저 GCP 프로젝트 환경 변수를 설정합니다.

export GOOGLE_CLOUD_PROJECT=$(gcloud config get-value project)
gcloud config set project ${GOOGLE_CLOUD_PROJECT}
gcloud config set run/region 
gcloud config set run/platform managed
gcloud config set eventarc/location europe-west1

JIT 이미지를 배포하고 콘솔에서 배포 로그를 관찰합니다.

gcloud run deploy image-analysis-jit \
     --image gcr.io/${GOOGLE_CLOUD_PROJECT}/image-analysis-maven-jit \
     --region europe-west1 \
     --memory 2Gi --allow-unauthenticated

...
Deploying container to Cloud Run service [image-analysis-jit] in project [...] region [europe-west1]
✓ Deploying... Done.                                                                                                                                                               
  ✓ Creating Revision...                                                                                                                                                           
  ✓ Routing traffic...                                                                                                                                                             
  ✓ Setting IAM Policy...                                                                                                                                                          
Done.                                                                                                                                                                              
Service [image-analysis-jit] revision [image-analysis-jvm-00009-huc] has been deployed and is serving 100 percent of traffic.
Service URL: https://image-analysis-jit-...-ew.a.run.app

네이티브 이미지를 배포하고 콘솔에서 배포 로그를 관찰합니다.

gcloud run deploy image-analysis-native \
     --image gcr.io/${GOOGLE_CLOUD_PROJECT}/image-analysis-maven-native \
     --region europe-west1 \
     --memory 2Gi --allow-unauthenticated 
...
Deploying container to Cloud Run service [image-analysis-native] in project [...] region [europe-west1]
✓ Deploying... Done.                                                                                                                                                               
  ✓ Creating Revision...                                                                                                                                                           
  ✓ Routing traffic...                                                                                                                                                             
  ✓ Setting IAM Policy...                                                                                                                                                          
Done.                                                                                                                                                                              
Service [image-analysis-native] revision [image-analysis-native-00005-ben] has been deployed and is serving 100 percent of traffic.
Service URL: https://image-analysis-native-...-ew.a.run.app

13. Eventarc 트리거 설정

Eventarc는 분리된 마이크로서비스 간의 상태 변경 흐름(이벤트)을 관리하는 표준화된 솔루션을 제공합니다. Eventarc가 트리거되면 전송, 보안, 승인, 관측 가능성, 오류 처리를 자동으로 관리하면서 Pub/Sub 구독을 통해 이러한 이벤트를 다양한 대상 (이 문서에서는 이벤트 대상 참조)으로 라우팅합니다.

Cloud Run 서비스가 지정된 이벤트 또는 이벤트 집합에 대한 알림을 수신하도록 Eventarc 트리거를 만들 수 있습니다. 트리거에 대한 필터를 지정하면 이벤트 소스 및 대상 Cloud Run 서비스를 포함하여 이벤트의 라우팅을 구성할 수 있습니다.

먼저 GCP 프로젝트 환경 변수를 설정합니다.

export GOOGLE_CLOUD_PROJECT=$(gcloud config get-value project)
gcloud config set project ${GOOGLE_CLOUD_PROJECT}
gcloud config set run/region 
gcloud config set run/platform managed
gcloud config set eventarc/location europe-west1

Cloud Storage 서비스 계정에 pubsub.publisher를 부여합니다.

SERVICE_ACCOUNT="$(gsutil kms serviceaccount -p ${GOOGLE_CLOUD_PROJECT})"

gcloud projects add-iam-policy-binding ${GOOGLE_CLOUD_PROJECT} \
    --member="serviceAccount:${SERVICE_ACCOUNT}" \
    --role='roles/pubsub.publisher'

이미지를 처리하도록 JIT 및 네이티브 서비스 이미지 모두에 Eventarc 트리거를 설정합니다.

gcloud eventarc triggers list --location=eu

gcloud eventarc triggers create image-analysis-jit-trigger \
     --destination-run-service=image-analysis-jit \
     --destination-run-region=europe-west1 \
     --location=eu \
     --event-filters="type=google.cloud.storage.object.v1.finalized" \
     --event-filters="bucket=uploaded-pictures-${GOOGLE_CLOUD_PROJECT}" \
     --service-account=${PROJECT_NUMBER}-compute@developer.gserviceaccount.com

gcloud eventarc triggers create image-analysis-native-trigger \
     --destination-run-service=image-analysis-native \
     --destination-run-region=europe-west1 \
     --location=eu \
     --event-filters="type=google.cloud.storage.object.v1.finalized" \
     --event-filters="bucket=uploaded-pictures-${GOOGLE_CLOUD_PROJECT}" \
     --service-account=${PROJECT_NUMBER}-compute@developer.gserviceaccount.com    

두 개의 트리거가 만들어졌는지 확인합니다.

gcloud eventarc triggers list --location=eu

14. 테스트 서비스 버전

서비스 배포가 완료되면 Cloud Storage에 사진을 게시하고, 서비스가 호출되었는지, Vision API가 반환하는지, 메타데이터가 Firestore에 저장되었는지 확인합니다.

Cloud Storage로 돌아가서 실습 시작 부분에서 생성한 버킷을 클릭합니다.

33442485a1d76921.png

버킷 세부정보 페이지에서 Upload files 버튼을 클릭하여 사진을 업로드합니다.

예를 들어 GeekHour.jpeg 이미지는 /services/image-analysis/java 아래에 코드베이스와 함께 제공됩니다. 이미지를 선택하고 Open button를 누릅니다.

d57529452f62bd32.png

이제 image-analysis-jit로 시작하고 image-analysis-native가 뒤따르는 서비스 실행을 확인할 수 있습니다.

'햄버거'에서 (❯) 메뉴에서 Cloud Run > image-analysis-jit 서비스로 이동합니다.

로그를 클릭하고 출력을 확인합니다.

ae1a4a94c7c7a166.png

실제로 로그 목록에서 JIT 서비스 image-analysis-jit가 호출된 것을 확인할 수 있습니다.

로그는 서비스 실행의 시작과 끝을 나타냅니다. 그 중간에 INFO 레벨의 로그 구문에서 함수에 넣은 로그를 볼 수 있습니다. 다음과 같은 내용을 확인했습니다.

  • 함수를 트리거하는 이벤트의 세부정보,
  • Vision API 호출의 원시 결과
  • 우리가 업로드한 사진에 있는 라벨은
  • 주요 색상 정보는
  • 사진이 표시해도 안전한지
  • 그리고 사진에 대한 메타데이터는 결국 Firestore에 저장되었습니다.

image-analysis-native 서비스에 대해 이 과정을 반복합니다.

'햄버거'에서 (❯) 메뉴에서 Cloud Run > image-analysis-native 서비스로 이동합니다.

로그를 클릭하고 출력을 확인합니다.

4afe22833c1fd14c.png

이제 이미지 메타데이터가 Fiorestore에 저장되었는지 관찰할 수 있습니다.

다시 '햄버거'에서 (❯) 메뉴에서 Firestore 섹션으로 이동합니다. Data 하위 섹션 (기본적으로 표시됨)에 방금 업로드한 사진에 해당하는 새 문서가 추가된 pictures 컬렉션이 표시됩니다.

82d6c468956e7cfc.png

15. 정리(선택 사항)

이 시리즈의 다른 실습을 계속하지 않으려는 경우 리소스를 삭제하여 비용을 절감하고 전반적으로 클라우드를 잘 활용할 수 있습니다. 다음과 같이 리소스를 개별적으로 삭제할 수 있습니다.

버킷을 삭제합니다.

gsutil rb gs://${BUCKET_PICTURES}

함수를 삭제합니다.

gcloud functions delete picture-uploaded --region europe-west1 -q

컬렉션에서 컬렉션 삭제를 선택하여 Firestore 컬렉션을 삭제합니다.

6cc86a7b88fdb4d3.png

또는 전체 프로젝트를 삭제할 수 있습니다.

gcloud projects delete ${GOOGLE_CLOUD_PROJECT} 

16. 축하합니다.

축하합니다. 프로젝트의 첫 번째 키 관리 서비스를 구현했습니다.

학습한 내용

  • Cloud Storage
  • Cloud Run
  • Cloud Vision API
  • Cloud Firestore
  • 네이티브 Java 이미지

다음 단계