GCP Pub/Sub with Spring Boot
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가지가 있습니다
- Java로 직접 구현하는 방법. 이 방법은 장점은 protobuf, avro로 schema를 이용할 수 있습니다
- 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
Reference