개발
home
📨

Spring Boot와 GCP Pub/Sub 연동하기

2021-09-08 @이영훈

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")}") } }
Kotlin
복사

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)); } }
Java
복사
// MessageContent.java @Getter @Setter @NoArgsConstructor public class MessageContent { private String title; private String body; private String deviceToken; @Nullable private Map<String, String> data; }
Java
복사
// SendMessageResponse.java @Getter @AllArgsConstructor public class SendMessageResponse { private final boolean success; }
Java
복사
// 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; } }
Java
복사

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

a.
IAM & Admin에 들어가서 "Create Service Account"를 누릅니다
b.
이름과 설명을 적어줍니다
저는 이름을 pubsub-sa3로 했습니다. 설명은 안적어도 되지만 유지관리를 위해서 최대한 직관적으로 적었습니다
c.
"Pub/Sub Editor" 권한을 넣고 생성하기 (DONE) 버튼을 누릅니다
d.
생성된 Service Account를 확인합니다

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

key-filesa-nameproject-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
Bash
복사
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
YAML
복사
topic-name을 확인하는 방법입니다
topic-name이 위에서 topic 만들면서 입력했던 그 값(test-topic)이 아니었습니다. 입력했던 값은 Topic Id로 사용하고 있고 Topic Name은 스샷 화면에서처럼 따로 있었습니다
credentials.location 설정
저는 pubsub-key.json 파일이 resources 디렉토리 하위에 있기 때문에
classpath:pubsub-key.json 으로 설정했습니다

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

a.
테스트코드
잘 동작하는 지 통합 테스트를 실행해봅니다
// 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()); } }
Java
복사
MockMvc 출력 내용입니다. 테스트가 성공적으로 통과하였습니다
여기에서 의문인데 위의 테스트 코드를 실행하면 실제로 pub/sub에 메시지가 보내질 것으로 예상했는데 안보내져서 postman이나 curl, HTTP Client 등으로 API를 호출하면 정상적으로 메시지가 보내집니다 (왜 안 보내지는 지 궁금한 데 이 부분은 나중에 공부해보겠습니다)
b.
API 호출
HTTP Client로 API를 호출하였습니다.
메시지가 정상적으로 도착하였습니다
2초 정도 걸리는 것도 같이 확인할 수 있습니다
Spring Boot에서 GCP Pub/Sub 큐에 메시지를 보내는 내용이었습니다.
다음에는 GCP Pub/Sub으로 Cloud Function에 Trigger를 걸어서 FCM으로 모바일 푸시를 보내는 내용을 정리해보겠습니다

Reference