개발

GCP Pub/Sub with Spring Boot

mycloudy 2021. 9. 8. 18:29

GCP Pub/Sub를 Spring Boot와 연결하기

작성한 코드는 github에서 확인할 수 있습니다

 

GCP Pub/Sub과 Cloud Function을 이용해 대용량 모바일 푸시 메시지 처리(FCM)를 구현하였습니다

AWS에서 SQS와 Lambda를 이용해 같은 방법으로 이용할 수 있습니다

GCP는 처음이라 해당 내용을 기록으로 남깁니다

 

Message Queue를 사용하면 다른 서버로 호출할 때 결합도를 낮출 수 있어 좋습니다. 만약에 호출한 서버가 죽더라도 메시지 큐에 메시지를 넣어두면 나중에 서버가 다시 뜰 때 메시지 큐에서 메시지를 가지고 와서 해당 내용을 호출해 좀 더 신뢰할 구조를 짤 수 있습니다.

그리고 많은 일을 처리할 때 메시지에 해야할 일을 명세해서 넣어두고 메시지를 하나씩 가져가서 처리하는 방식으로 대용량 처리를 할 수 있습니다. 특히 GCP의 Cloud Function, AWS의 Lambda를 사용하면 python, javascript 등으로 스크립트만 짜면 서버리스 구조로 처리할 수도 있습니다

 

GCP Pub/Sub을 연동하는 방법은 2가지가 있습니다

  1. Java로 직접 구현하는 방법. 이 방법은 장점은 protobuf, avro로 schema를 이용할 수 있습니다
  2. GCP Messaging 라이브러리를 이용하는 방법. Spring Boot에서 연동이 좀 더 편했습니다. configuration 넣어주는 부분, PubSubTemplate 을 이용하여 쉽게 구현할 수 있었습니다

1의 방법으로 다 만들었다가 protobuf로 schema까지 만들어서 하는 게 over-engineering이라 판단했습니다. 유지보수를 위해 2의 방법으로 바꾸었습니다.

1. Pub/Sub에서 Topic을 만듭니다

저는 "test-topic" 으로 만들었습니다

2. gradle에 설정을 추가해줍니다

Spring Initializer를 보고 설정하면 좀 더 편합니다

저는 kotlin DSL gradle을 사용해서 다음의 설정을 넣었습니다 (groovy는 문법이 자유로워 함께 협업으로 개발하기에 일관되지 않는 점이 아쉬웠고 kotlin은 문법의 강제, type hinting 지원 등이 좋아서 사용하고 있습니다)

// build.gradle.kts

extra["springCloudGcpVersion"] = "2.0.4"
extra["springCloudVersion"] = "2020.0.3"

dependencies {
	implementation("com.google.cloud:spring-cloud-gcp-starter-pubsub")
}

dependencyManagement {
    imports {
        mavenBom("com.google.cloud:spring-cloud-gcp-dependencies:${property("springCloudGcpVersion")}")
        mavenBom("org.springframework.cloud:spring-cloud-dependencies:${property("springCloudVersion")}")
    }
}

3. PubSubTemplate을 이용하여 queue에 push하는 기능을 구현합니다

PubSubTemplate은 메시지를 보내거나 수신하는 기능을 담당하는 클래스입니다

 

// PubSubController.java

@Slf4j
@RequiredArgsConstructor
@RequestMapping(produces = {MediaType.APPLICATION_JSON_VALUE})
@RestController
public class PubSubController {

    private final PubSubTemplate pubSubTemplate;
    private final ObjectMapper objectMapper;

    @Value("${spring.cloud.gcp.topic-name}")
    private String topicName;

    @PostMapping("/api/v1/sendMessage")
    public ResponseEntity<SendMessageResponse> sendMessage(
        @RequestBody MessageContent content
    ) throws JsonProcessingException {

        MessageQueueSchema queueSchema = new MessageQueueSchema(
            PushType.SINGLE_MESSAGE,
            content.getDeviceToken(),
            content.getTitle(),
            content.getBody(),
            content.getData()
        );

        String message = objectMapper.writeValueAsString(queueSchema);
        log.info("message: " + message);

        pubSubTemplate.publish(topicName, message);

        return ResponseEntity.ok(new SendMessageResponse(true));
    }
}
// MessageContent.java

@Getter
@Setter
@NoArgsConstructor
public class MessageContent {

    private String title;
    private String body;
    private String deviceToken;
    @Nullable
    private Map<String, String> data;
}
// SendMessageResponse.java

@Getter
@AllArgsConstructor
public class SendMessageResponse {

    private final boolean success;
}
// PushType.java

public enum PushType {
    SINGLE_MESSAGE(0),
    MULTIPLE_MESSAGE(1),
    ;

    public static final int SINGLE_MESSAGE_VALUE = 0;
    public static final int MULTIPLE_MESSAGE_VALUE = 1;

    private final int value;

    PushType(int value) {
        this.value = value;
    }

		public static PushType of(int value) {
        return Arrays.stream(PushType.values())
            .filter(it -> it.value == value)
            .findFirst()
            .orElse(null);
    }

    public final int getValue() {
        return value;
    }
}

4. 콘솔에서 Service Account를 만듭니다

1. IAM & Admin에 들어가서 "Create Service Account"를 누릅니다

2. 이름과 설명을 적어줍니다

저는 이름을 pubsub-sa3로 했습니다. 설명은 안적어도 되지만 유지관리를 위해서 최대한 직관적으로 적었습니다

 

3. "Pub/Sub Editor" 권한을 넣고 생성하기 (DONE) 버튼을 누릅니다

4. 생성된 Service Account를 확인합니다

5. Service Account Key 파일을 생성합니다

key-file, sa-name, project-id 세 개 필드를 상황에 맞게 바꾸시면 되니다

key-file은 생성할 파일의 경로와 확장자를 포함한 파일명입니다

sa-name은 service account의 이름입니다. 위에서 만들었던 service-account 이름을 적으시면 됩니다

project-id는 위에서 해당 프로젝트의 id값 입니다.

 

다음 명령어로 service account key를 생성합니다

# format
gcloud iam service-accounts keys create key-file \
    --iam-account=sa-name@project-id.iam.gserviceaccount.com

# my command
gcloud iam service-accounts keys create pubsub-key.json \
    --iam-account=pubsub-sa3@projecttest-325212.iam.gserviceaccount.com

 

project-id를 확인하는 방법입니다

6. application.yml 파일에 필요한 설정들을 합니다

project-id는 위에서 적었던 설정 그대로 적으시면 되고

credentials.location은 위에서 생성한 service account key의 위치입니다

 

// application.yml

spring:
  cloud:
    gcp:
      project-id: projecttest-325212
      topic-name: projects/projecttest-325212/topics/test-topic
      credentials:
        location: classpath:pubsub-key.json

 

- topic-name을 확인하는 방법입니다

topic-name이 위에서 topic 만들면서 입력했던 그 값(test-topic)이 아니었습니다. 입력했던 값은 Topic Id로 사용하고 있고 Topic Name은 스샷 화면에서처럼 따로 있었습니다

 

- credentials.location 설정

저는 pubsub-key.json 파일이 resources 디렉토리 하위에 있기 때문에

classpath:pubsub-key.json 으로 설정했습니다

 

7. 테스트 코드와 API 호출, 결과 확인

1. 테스트코드

 

잘 동작하는 지 통합 테스트를 실행해봅니다

// PubSubIntegrationTest.java

@AutoConfigureMockMvc
@SpringBootTest(classes = {PubsubStudyApplication.class})
class PubSubIntegrationTest {

    @Autowired
    private MockMvc mockMvc;

    @Test
    public void sendMessage() throws Exception {
        mockMvc.perform(
                post("/api/v1/sendMessage")
                    .contentType(MediaType.APPLICATION_JSON)
                    .content("{\"title\":\"푸시 제목\",\"body\":\"중요한 내용\",\"deviceToken\":\"FakeDeviceToken\"}")
            )
            .andExpect(status().isOk())
            .andExpect(jsonPath("$.success", is(true)))
            .andDo(print());
    }
}

 

MockMvc 출력 내용입니다. 테스트가 성공적으로 통과하였습니다

 

여기에서 의문인데 위의 테스트 코드를 실행하면 실제로 pub/sub에 메시지가 보내질 것으로 예상했는데 안보내져서 postman이나 curl, HTTP Client 등으로 API를 호출하면 정상적으로 메시지가 보내집니다 (왜 안 보내지는 지 궁금한 데 이 부분은 나중에 공부해보겠습니다)

 

2. API 호출

HTTP Client로 API를 호출하였습니다.

 

메시지가 정상적으로 도착하였습니다

2초 정도 걸리는 것도 같이 확인할 수 있습니다

 

Spring Boot에서 GCP Pub/Sub 큐에 메시지를 보내는 내용이었습니다.

다음에는 GCP Pub/Sub으로 Cloud Function에 Trigger를 걸어서 FCM으로 모바일 푸시를 보내는 내용을 정리해보겠습니다

https://mycloudy.tistory.com/46

 

GCP Cloud Function으로 FCM 메시지 보내기 + Pub/Sub

GCP Cloud Function과 Pub/Sub을 연동해서 FCM (Firebase Cloud Messaging) 사용하는 방법을 기록으로 남깁니다 모든 코드는 github에서 확인할 수 있습니다 github에 테스트 코드도 함께 작성해놓았습니다 본문에..

mycloudy.tistory.com

Reference

spring-cloud-gcp github

IAM Key 만들기