Pub/Sub로 방화벽 뒤에 있는 Google Chat 앱 빌드하기

이 페이지에서는 Pub/Sub를 사용하여 채팅 앱을 만드는 방법을 설명합니다. 이 채팅 앱 아키텍처 유형은 Chat이 채팅 앱에 메시지를 보내지 못하도록 하는 방화벽이 조직에 있거나 채팅 앱에서 Google Workspace Events API를 사용하는 경우에 유용합니다. 하지만 이 아키텍처에는 이러한 채팅 앱은 비동기 메시지만 주고받을 수 있으므로 다음과 같은 제한사항이 있습니다.

  • 메시지에서는 대화상자를 사용할 수 없습니다. 대신 카드 메시지를 사용하세요.
  • 동기 응답으로 개별 카드를 업데이트할 수 없습니다. 대신 patch 메서드를 호출하여 전체 메시지를 업데이트하세요.

다음 다이어그램은 Pub/Sub로 빌드된 채팅 앱의 아키텍처를 보여줍니다.

Pub/Sub로 구현된 채팅 앱의 아키텍처

앞의 다이어그램에서 Pub/Sub 채팅 앱과 상호작용하는 사용자는 다음과 같은 정보 흐름을 보입니다.

  1. 사용자가 채팅 메시지 또는 Chat 스페이스에서 Chat 앱에 메시지를 보내거나 Chat 앱에 활성 구독이 있는 Chat 스페이스에서 이벤트가 발생합니다.

  2. Chat이 Pub/Sub 주제로 메시지를 보냅니다.

  3. 채팅 앱 로직이 포함된 클라우드 또는 온프레미스 시스템인 애플리케이션 서버는 방화벽을 통해 메시지를 수신하기 위해 Pub/Sub 주제를 구독합니다.

  4. 원하는 경우 채팅 앱에서 Chat API를 호출하여 비동기식으로 메시지를 게시하거나 다른 작업을 실행할 수 있습니다.

기본 요건

Java

환경 설정

Google API를 사용하려면 먼저 Google Cloud 프로젝트에서 사용 설정해야 합니다. 단일 Google Cloud 프로젝트에서 하나 이상의 API를 사용 설정할 수 있습니다.
  • Google Cloud 콘솔에서 Google Chat API 및 Pub/Sub API를 사용 설정합니다.

    API 사용 설정

Pub/Sub 설정

  1. Chat API가 메시지를 보낼 수 있는 Pub/Sub 주제를 만듭니다. 채팅 앱당 하나의 주제를 사용하는 것이 좋습니다.

  2. 다음 서비스 계정에 Pub/Sub 게시자 역할을 할당하여 주제에 게시할 권한을 Chat에 부여합니다.

    chat-api-push@system.gserviceaccount.com
    
  3. 채팅 앱이 Pub/Sub 및 Chat을 승인할 수 있도록 서비스 계정을 만들고 비공개 키 파일을 작업 디렉터리에 저장합니다.

  4. 주제에 대한 가져오기 구독을 만듭니다.

  5. 이전에 만든 서비스 계정에 대해 구독에서 Pub/Sub 구독자 역할을 할당합니다.

스크립트 작성

Java

  1. CLI에서 서비스 계정 사용자 인증 정보를 제공합니다.

    export GOOGLE_APPLICATION_CREDENTIALS=SERVICE_ACCOUNT_FILE_PATH
    
  2. 작업 디렉터리에서 pom.xml라는 파일을 만듭니다.

  3. pom.xml 파일에 다음 코드를 붙여넣습니다.

    <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 http://maven.apache.org/xsd/maven-4.0.0.xsd">
    <modelVersion>4.0.0</modelVersion>
    
    <groupId>com.google.chat.pubsub</groupId>
    <artifactId>java-pubsub-app</artifactId>
    <version>0.1.0</version>
    
    <name>java-pubsub-app</name>
    
    <properties>
      <maven.compiler.target>11</maven.compiler.target>
      <maven.compiler.source>11</maven.compiler.source>
    </properties>
    
    <dependencyManagement>
      <dependencies>
        <dependency>
          <groupId>com.google.cloud</groupId>
          <artifactId>libraries-bom</artifactId>
          <version>26.26.0</version>
          <type>pom</type>
          <scope>import</scope>
        </dependency>
      </dependencies>
    </dependencyManagement>
    
    <dependencies>
      <dependency>
        <groupId>com.google.code.gson</groupId>
        <artifactId>gson</artifactId>
        <version>2.9.1</version>
      </dependency>
      <dependency>
        <groupId>com.google.api-client</groupId>
        <artifactId>google-api-client</artifactId>
        <version>1.32.1</version>
      </dependency>
      <dependency>
        <groupId>com.google.cloud</groupId>
        <artifactId>google-cloud-pubsub</artifactId>
      </dependency>
      <dependency>
        <groupId>com.fasterxml.jackson.core</groupId>
        <artifactId>jackson-databind</artifactId>
        <version>2.14.2</version>
      </dependency>
    </dependencies>
    
    <build>
      <pluginManagement>
        <plugins>
          <plugin>
            <artifactId>maven-compiler-plugin</artifactId>
            <version>3.8.0</version>
          </plugin>
        </plugins>
      </pluginManagement>
    </build>
    </project>
    
  4. 작업 디렉터리에 src/main/java 디렉터리 구조를 만듭니다.

  5. src/main/java 디렉터리에서 Main.java라는 파일을 만듭니다.

  6. Main.java에 다음 코드를 붙여넣습니다.

    import com.fasterxml.jackson.databind.JsonNode;
    import com.fasterxml.jackson.databind.ObjectMapper;
    import com.fasterxml.jackson.databind.node.JsonNodeFactory;
    import com.fasterxml.jackson.databind.node.ObjectNode;
    import com.google.api.client.googleapis.auth.oauth2.GoogleCredential;
    import com.google.api.client.googleapis.javanet.GoogleNetHttpTransport;
    import com.google.api.client.http.ByteArrayContent;
    import com.google.api.client.http.GenericUrl;
    import com.google.api.client.http.HttpContent;
    import com.google.api.client.http.HttpRequest;
    import com.google.api.client.http.HttpRequestFactory;
    import com.google.api.client.http.HttpTransport;
    import com.google.cloud.pubsub.v1.AckReplyConsumer;
    import com.google.cloud.pubsub.v1.MessageReceiver;
    import com.google.cloud.pubsub.v1.Subscriber;
    import com.google.pubsub.v1.PubsubMessage;
    import com.google.pubsub.v1.ProjectSubscriptionName;
    import java.io.FileInputStream;
    import java.util.Collections;
    
    public class Main {
    
      public static final String CREDENTIALS_PATH_ENV_PROPERTY = "GOOGLE_APPLICATION_CREDENTIALS";
    
      // Google Cloud Project ID
      public static final String PROJECT_ID = PROJECT_ID;
    
      // Cloud Pub/Sub Subscription ID
      public static final String SUBSCRIPTION_ID = SUBSCRIPTION_ID
    
      public static void main(String[] args) throws Exception {
        ProjectSubscriptionName subscriptionName =
            ProjectSubscriptionName.of(PROJECT_ID, SUBSCRIPTION_ID);
    
        // Instantiate app, which implements an asynchronous message receiver.
        EchoApp echoApp = new EchoApp();
    
        // Create a subscriber for <var>SUBSCRIPTION_ID</var> bound to the message receiver
        final Subscriber subscriber =
            Subscriber.newBuilder(subscriptionName, echoApp).build();
        System.out.println("Starting subscriber...");
        subscriber.startAsync();
    
        // Wait for termination
        subscriber.awaitTerminated();
      }
    }
    
    / **
    * A demo app which implements {@link MessageReceiver} to receive messages. It simply echoes the
    * incoming messages.
    */
    class EchoApp implements MessageReceiver {
    
      // Path to the private key JSON file of the service account to be used for posting response
      // messages to Google Chat.
      // In this demo, we are using the same service account for authorizing with Cloud Pub/Sub to
      // receive messages and authorizing with Google Chat to post messages. If you are using
      // different service accounts, please set the path to the private key JSON file of the service
      // account used to post messages to Google Chat here.
      private static final String SERVICE_ACCOUNT_KEY_PATH =
          System.getenv(Main.CREDENTIALS_PATH_ENV_PROPERTY);
    
      // Developer code for Google Chat API scope.
      private static final String GOOGLE_CHAT_API_SCOPE = "https://www.googleapis.com/auth/chat.bot";
    
      // Response URL Template with placeholders for space id.
      private static final String RESPONSE_URL_TEMPLATE =
          "https://chat.googleapis.com/v1/__SPACE_ID__/messages";
    
      // Response echo message template.
      private static final String RESPONSE_TEMPLATE = "You said: `__MESSAGE__`";
    
      private static final String ADDED_RESPONSE = "Thank you for adding me!";
    
      GoogleCredential credential;
      HttpTransport httpTransport;
      HttpRequestFactory requestFactory;
    
      EchoApp() throws Exception {
        credential =
            GoogleCredential.fromStream(new FileInputStream(SERVICE_ACCOUNT_KEY_PATH))
                .createScoped(Collections.singleton(GOOGLE_CHAT_API_SCOPE));
        httpTransport = GoogleNetHttpTransport.newTrustedTransport();
        requestFactory = httpTransport.createRequestFactory(credential);
      }
    
      // Called when a message is received by the subscriber.
      @Override
      public void receiveMessage(PubsubMessage pubsubMessage, AckReplyConsumer consumer) {
        System.out.println("Id : " + pubsubMessage.getMessageId());
        // handle incoming message, then ack/nack the received message
        try {
          ObjectMapper mapper = new ObjectMapper();
          JsonNode dataJson = mapper.readTree(pubsubMessage.getData().toStringUtf8());
          System.out.println("Data : " + dataJson.toString());
          handle(dataJson);
          consumer.ack();
        } catch (Exception e) {
          System.out.println(e);
          consumer.nack();
        }
      }
    
      public void handle(JsonNode eventJson) throws Exception {
        JsonNodeFactory jsonNodeFactory = new JsonNodeFactory(false);
        ObjectNode responseNode = jsonNodeFactory.objectNode();
    
        // Construct the response depending on the event received.
    
        String eventType = eventJson.get("type").asText();
        switch (eventType) {
          case "ADDED_TO_SPACE":
            responseNode.put("text", ADDED_RESPONSE);
            // An app can also be added to a space by @mentioning it in a message. In that case, we fall
            // through to the MESSAGE case and let the app respond. If the app was added using the
            // invite flow, we just post a thank you message in the space.
            if(!eventJson.has("message")) {
              break;
            }
          case "MESSAGE":
            responseNode.put("text",
                RESPONSE_TEMPLATE.replaceFirst(
                    "__MESSAGE__", eventJson.get("message").get("text").asText()));
            // In case of message, post the response in the same thread.
            ObjectNode threadNode = jsonNodeFactory.objectNode();
            threadNode.put("name", eventJson.get("message").get("thread").get("name").asText());
            responseNode.put("thread", threadNode);
            break;
          case "REMOVED_FROM_SPACE":
          default:
            // Do nothing
            return;
        }
    
        // Post the response to Google Chat.
    
        String URI =
            RESPONSE_URL_TEMPLATE.replaceFirst(
                "__SPACE_ID__", eventJson.get("space").get("name").asText());
        GenericUrl url = new GenericUrl(URI);
    
        HttpContent content =
            new ByteArrayContent("application/json", responseNode.toString().getBytes("UTF-8"));
        HttpRequest request = requestFactory.buildPostRequest(url, content);
        com.google.api.client.http.HttpResponse response = request.execute();
      }
    }
    

    다음을 바꿉니다.

    • PROJECT_ID: Google Cloud 프로젝트 ID입니다.
    • SUBSCRIPTION_ID: 이전에 만든 Pub/Sub 구독의 구독 ID입니다.

Chat에 앱 게시

  1. Google Cloud 콘솔에서 메뉴 > API 및 서비스 > 사용 설정된 API 및 서비스 > Google Chat API > 구성으로 이동합니다.

    구성으로 이동

  2. Pub/Sub용 채팅 앱을 구성합니다.

    1. 앱 이름Quickstart App를 입력합니다.
    2. 아바타 URLhttps://developers.google.com/chat/images/quickstart-app-avatar.png를 입력합니다.
    3. 설명Quickstart app을 입력합니다.
    4. 기능에서 1:1 메시지 수신스페이스 및 그룹 대화 참여를 선택합니다.
    5. 연결 설정에서 Cloud Pub/Sub를 선택하고 앞에서 만든 Pub/Sub 주제의 이름을 붙여넣습니다.
    6. 공개 상태에서 내 도메인의 특정 사용자 및 그룹에서 이 Google Chat 앱을 사용할 수 있도록 설정을 선택하고 이메일 주소를 입력합니다.
    7. 로그에서 Logging에 오류 로깅을 선택합니다.
  3. 저장을 클릭합니다.

이제 앱이 Chat에서 메시지를 수신하고 응답할 수 있습니다.

스크립트 실행

CLI에서 작업 디렉터리로 전환하고 스크립트를 실행합니다.

Java

mvn compile exec:java -Dexec.mainClass=Main

코드를 실행하면 애플리케이션이 Pub/Sub 주제에 게시된 메시지를 리슨하기 시작합니다.

채팅 앱 테스트

채팅 앱을 테스트하려면 앱에 채팅 메시지를 보냅니다.

  1. Google Chat을 엽니다.
  2. 앱에 채팅 메시지를 보내려면 채팅 시작 을 클릭하고 표시되는 창에서 앱 찾기를 클릭합니다.
  3. 앱 찾기 대화상자에서 '빠른 시작 앱'을 검색합니다.
  4. 앱과의 채팅 메시지를 열려면 빠른 시작 앱을 찾아 추가 > 채팅을 클릭합니다.
  5. 채팅 메시지에 Hello라고 입력하고 enter 키를 누릅니다. 채팅 앱이 사용자에게 메시지를 다시 에코합니다.

신뢰할 수 있는 테스터를 추가하고 양방향 기능 테스트에 관한 자세한 내용은 Google Chat 앱의 양방향 기능 테스트를 참고하세요.

문제 해결

Google Chat 앱 또는 카드에서 오류를 반환하면 Chat 인터페이스에 '문제 발생' 또는 '요청을 처리할 수 없습니다'라는 메시지가 표시됩니다. 채팅 UI에는 오류 메시지가 표시되지 않지만 채팅 앱 또는 카드에서 예기치 않은 결과가 발생하는 경우가 있습니다. 예를 들어 카드 메시지가 표시되지 않을 수 있습니다.

Chat UI에 오류 메시지가 표시되지 않더라도 채팅 앱에 대한 오류 기록이 사용 설정되어 있을 때 오류를 수정하는 데 도움이 되는 자세한 오류 메시지와 로그 데이터가 제공됩니다. 오류를 확인, 디버깅, 수정하는 데 도움이 필요하면 Google Chat 오류 문제 해결 및 수정하기를 참고하세요.

삭제

이 튜토리얼에서 사용한 리소스 비용이 Google Cloud 계정에 청구되지 않도록 하려면 Cloud 프로젝트를 삭제하는 것이 좋습니다.

  1. Google Cloud 콘솔에서 리소스 관리 페이지로 이동합니다. 메뉴 > IAM 및 관리자 > 리소스 관리를 클릭합니다.

    Resource Manager로 이동

  2. 프로젝트 목록에서 삭제할 프로젝트를 선택하고 삭제 를 클릭합니다.
  3. 대화상자에서 프로젝트 ID를 입력한 후 종료를 클릭하여 프로젝트를 삭제합니다.