[UPLUS-152] Cloud Tasks Queue를 통한 Job 실행 직렬화#3
Conversation
swthewhite
commented
Jan 26, 2026
- Cloud Tasks 의존성 추가 (google-cloud-run → google-cloud-tasks)
- CloudTasksService 신규 생성: 큐를 통한 Cloud Run Job 실행
- BatchRunService: JobsClient 직접 호출 → CloudTasksService 위임
- BatchScheduleService: Scheduler 타겟을 Cloud Tasks API로 변경
- Cloud Tasks 의존성 추가 (google-cloud-run → google-cloud-tasks) - CloudTasksService 신규 생성: 큐를 통한 Cloud Run Job 실행 - BatchRunService: JobsClient 직접 호출 → CloudTasksService 위임 - BatchScheduleService: Scheduler 타겟을 Cloud Tasks API로 변경
Summary of ChangesHello @swthewhite, I'm Gemini Code Assist1! I'm currently reviewing this pull request and will post my feedback shortly. In the meantime, here's a summary to help you and other reviewers quickly get up to speed! 이 Pull Request는 배치 작업 실행 및 스케줄링 메커니즘을 Google Cloud Tasks를 활용하도록 리팩토링합니다. 이전에는 Cloud Run 작업이 직접 호출되었으나, 이제는 Cloud Tasks 큐를 통해 작업 실행이 직렬화되어 배치 처리의 안정성과 제어력을 향상시킵니다. 이를 위해 Cloud Tasks 의존성을 추가하고, Cloud Tasks 상호작용을 위한 전용 서비스를 생성하며, 기존 서비스들이 즉각적인 작업 실행과 스케줄링된 작업 생성/업데이트 모두를 이 새로운 Cloud Tasks 서비스에 위임하도록 업데이트했습니다. Highlights
🧠 New Feature in Public Preview: You can now enable Memory to help Gemini Code Assist learn from your team's feedback. This makes future code reviews more consistent and personalized to your project's style. Click here to enable Memory in your admin console. Using Gemini Code AssistThe full guide for Gemini Code Assist can be found on our documentation page, here are some quick tips. Invoking Gemini You can request assistance from Gemini at any point by creating a comment using either
Customization To customize Gemini Code Assist for GitHub experience, repository maintainers can create a configuration file and/or provide a custom code review style guide (such as PEP-8 for Python) by creating and adding files to a Limitations & Feedback Gemini Code Assist may make mistakes. Please leave feedback on any instances where its feedback is incorrect or counter productive. You can react with 👍 and 👎 on @gemini-code-assist comments. If you're interested in giving your feedback about your experience with Gemini Code Assist for Github and other Google products, sign up here. You can also get AI-powered code generation, chat, as well as code reviews directly in the IDE at no cost with the Gemini Code Assist IDE Extension. Footnotes
|
There was a problem hiding this comment.
Code Review
이 PR은 작업 실행 로직을 Cloud Tasks를 사용하여 직렬화하도록 리팩토링하며, 이는 좋은 아키텍처 개선입니다. 변경 사항은 직접적인 Cloud Run 호출을 새로운 CloudTasksService의 태스크 생성으로 대체하는 것을 포함합니다. BatchRunService와 BatchScheduleService의 로직은 이 새로운 서비스에 위임하도록 올바르게 업데이트되었습니다. 하지만 CloudTasksService의 구현에는 몇 가지 중요한 개선 영역이 있습니다. 특히 JSON 처리가 수동적인 문자열 조작으로 이루어지고 있어 코드가 취약하고 유지보수가 어렵습니다. 또한 일부 코드 중복도 리팩토링할 수 있습니다. 제 리뷰에는 적절한 JSON 라이브러리를 사용하고 중복 코드를 중앙 집중화하여 이러한 점들을 해결하기 위한 제안이 포함되어 있습니다.
| public static String extractInvMonthFromSchedulerBody(String body) { | ||
| try { | ||
| int bodyStart = body.indexOf("\"body\":"); | ||
| if (bodyStart == -1) return null; | ||
|
|
||
| int valueStart = body.indexOf("\"", bodyStart + 7) + 1; | ||
| int valueEnd = body.indexOf("\"", valueStart); | ||
| if (valueStart == 0 || valueEnd == -1) return null; | ||
|
|
||
| String base64Body = body.substring(valueStart, valueEnd); | ||
| String decodedBody = new String(Base64.getDecoder().decode(base64Body), StandardCharsets.UTF_8); | ||
|
|
||
| if (decodedBody.contains("--invMonth=")) { | ||
| int start = decodedBody.indexOf("--invMonth=") + 11; | ||
| int end = decodedBody.indexOf("\"", start); | ||
| if (end != -1) return decodedBody.substring(start, end); | ||
| } | ||
| } catch (Exception e) { | ||
| System.err.println("[CloudTasksService] Failed to extract invMonth: " + e.getMessage()); | ||
| } | ||
| return null; | ||
| } |
There was a problem hiding this comment.
extractInvMonthFromSchedulerBody 메소드는 indexOf와 substring을 사용하여 JSON을 파싱하고 있습니다. 이 방식은 JSON의 공백이나 필드 순서 변경 등 사소한 변화에도 쉽게 깨질 수 있어 매우 취약합니다. ObjectMapper와 JsonNode를 사용하여 JSON 문자열을 파싱한 후, 필요한 값을 안전하게 추출하는 것이 올바른 접근 방식입니다. 이는 코드의 안정성과 신뢰성을 크게 향상시킵니다.
public static String extractInvMonthFromSchedulerBody(String body) {
try {
com.fasterxml.jackson.databind.ObjectMapper objectMapper = new com.fasterxml.jackson.databind.ObjectMapper();
com.fasterxml.jackson.databind.JsonNode rootNode = objectMapper.readTree(body);
String base64Body = rootNode.path("task").path("httpRequest").path("body").asText(null);
if (base64Body == null) {
return null;
}
byte[] decodedBytes = java.util.Base64.getDecoder().decode(base64Body);
String decodedBody = new String(decodedBytes, java.nio.charset.StandardCharsets.UTF_8);
com.fasterxml.jackson.databind.JsonNode innerRootNode = objectMapper.readTree(decodedBody);
com.fasterxml.jackson.databind.JsonNode argsNode = innerRootNode.path("overrides").path("containerOverrides").get(0).path("args");
if (argsNode == null || argsNode.isMissingNode() || !argsNode.isArray()) {
return null;
}
for (com.fasterxml.jackson.databind.JsonNode argNode : argsNode) {
String arg = argNode.asText();
if (arg.startsWith("--invMonth=")) {
return arg.substring("--invMonth=".length());
}
}
} catch (Exception e) {
System.err.println("[CloudTasksService] Failed to extract invMonth: " + e.getMessage());
}
return null;
}| private String buildCloudRunJobRequestBody(BatchJobType jobType, String invMonth) { | ||
| String jobArg = "\"--spring.batch.job.name=" + jobType.springBatchJobName() + "\""; | ||
| String monthArg = "\"--invMonth=" + invMonth + "\""; | ||
| String enabledArg = "\"--spring.batch.job.enabled=true\""; | ||
| String webTypeArg = "\"--spring.main.web-application-type=none\""; | ||
|
|
||
| return "{\"overrides\": {\"containerOverrides\": [{\"args\": [" | ||
| + jobArg + ", " + monthArg + ", " + enabledArg + ", " + webTypeArg | ||
| + "]}]}}"; | ||
| } |
There was a problem hiding this comment.
buildCloudRunJobRequestBody 메소드에서 JSON 문자열을 수동으로 생성하고 있습니다. 이 방식은 오류가 발생하기 쉽고 유지보수가 어렵습니다. 예를 들어, 인자 중 하나에 따옴표가 포함되면 JSON 형식이 깨질 수 있습니다. 프로젝트에 이미 jackson-databind 의존성이 있으므로, ObjectMapper나 Map을 사용하여 객체를 만든 후 JSON 문자열로 직렬화하는 것이 더 안전하고 깔끔합니다.
private String buildCloudRunJobRequestBody(BatchJobType jobType, String invMonth) {
try {
com.fasterxml.jackson.databind.ObjectMapper objectMapper = new com.fasterxml.jackson.databind.ObjectMapper();
java.util.Map<String, Object> root = java.util.Map.of(
"overrides", java.util.Map.of(
"containerOverrides", java.util.List.of(
java.util.Map.of(
"args", java.util.List.of(
"--spring.batch.job.name=" + jobType.springBatchJobName(),
"--invMonth=" + invMonth,
"--spring.batch.job.enabled=true",
"--spring.main.web-application-type=none"
)
)
)
)
);
return objectMapper.writeValueAsString(root);
} catch (com.fasterxml.jackson.core.JsonProcessingException e) {
throw new RuntimeException("Failed to build Cloud Run job request body", e);
}
}| public static String buildSchedulerTaskRequestBody(BatchJobType jobType, String invMonth) { | ||
| String cloudRunJobUrl = String.format( | ||
| "https://%s-run.googleapis.com/apis/run.googleapis.com/v1/namespaces/%s/jobs/%s:run", | ||
| REGION, PROJECT_ID, CLOUD_RUN_JOB_NAME | ||
| ); | ||
|
|
||
| String serviceAccountEmail = getDefaultServiceAccountStatic(); | ||
|
|
||
| String jobArg = "\"--spring.batch.job.name=" + jobType.springBatchJobName() + "\""; | ||
| String monthArg = (invMonth != null && !invMonth.isBlank()) | ||
| ? ", \"--invMonth=" + invMonth + "\"" : ""; | ||
|
|
||
| String innerBody = "{\"overrides\": {\"containerOverrides\": [{\"args\": [" | ||
| + jobArg + monthArg + "]}]}}"; | ||
| String encodedBody = Base64.getEncoder().encodeToString( | ||
| innerBody.getBytes(StandardCharsets.UTF_8)); | ||
|
|
||
| return String.format( | ||
| "{\"task\": {\"httpRequest\": {" + | ||
| "\"url\": \"%s\", " + | ||
| "\"httpMethod\": \"POST\", " + | ||
| "\"headers\": {\"Content-Type\": \"application/json\"}, " + | ||
| "\"body\": \"%s\", " + | ||
| "\"oidcToken\": {\"serviceAccountEmail\": \"%s\", \"audience\": \"%s\"}" + | ||
| "}}}", | ||
| cloudRunJobUrl, encodedBody, serviceAccountEmail, cloudRunJobUrl | ||
| ); | ||
| } |
There was a problem hiding this comment.
buildSchedulerTaskRequestBody 메소드에서도 JSON 문자열을 수동으로 생성하고 있습니다. 특히 String.format을 사용하여 복잡한 중첩 JSON 구조를 만드는 것은 가독성을 해치고 오류를 유발하기 쉽습니다. 이 메소드 또한 ObjectMapper를 사용하여 타입-세이프하게 JSON을 생성하는 것이 좋습니다. 내부 body를 Base64로 인코딩해야 하는 점을 감안하더라도, 객체 구조를 먼저 만든 후 직렬화하는 것이 더 안전합니다.
public static String buildSchedulerTaskRequestBody(BatchJobType jobType, String invMonth) {
try {
com.fasterxml.jackson.databind.ObjectMapper objectMapper = new com.fasterxml.jackson.databind.ObjectMapper();
java.util.List<String> args = new java.util.ArrayList<>();
args.add("--spring.batch.job.name=" + jobType.springBatchJobName());
if (invMonth != null && !invMonth.isBlank()) {
args.add("--invMonth=" + invMonth);
}
java.util.Map<String, Object> innerBodyMap = java.util.Map.of(
"overrides", java.util.Map.of(
"containerOverrides", java.util.List.of(
java.util.Map.of("args", args)
)
)
);
String innerBodyJson = objectMapper.writeValueAsString(innerBodyMap);
String encodedBody = java.util.Base64.getEncoder().encodeToString(
innerBodyJson.getBytes(java.nio.charset.StandardCharsets.UTF_8));
String cloudRunJobUrl = String.format(
"https://%s-run.googleapis.com/apis/run.googleapis.com/v1/namespaces/%s/jobs/%s:run",
REGION, PROJECT_ID, CLOUD_RUN_JOB_NAME
);
String serviceAccountEmail = getDefaultServiceAccountStatic();
java.util.Map<String, Object> taskMap = java.util.Map.of(
"task", java.util.Map.of(
"httpRequest", java.util.Map.of(
"url", cloudRunJobUrl,
"httpMethod", "POST",
"headers", java.util.Map.of("Content-Type", "application/json"),
"body", encodedBody,
"oidcToken", java.util.Map.of(
"serviceAccountEmail", serviceAccountEmail,
"audience", cloudRunJobUrl
)
)
)
);
return objectMapper.writeValueAsString(taskMap);
} catch (com.fasterxml.jackson.core.JsonProcessingException e) {
throw new RuntimeException("Error creating JSON for scheduler task request", e);
}
}| private static final String PROJECT_ID = System.getenv("GCP_PROJECT"); | ||
| private static final String REGION = System.getenv("GCP_REGION"); | ||
| private static final String QUEUE_NAME = "telecom-app-batch-queue"; | ||
| private static final String QUEUE_LOCATION = "asia-northeast3"; |
There was a problem hiding this comment.
QUEUE_LOCATION이 "asia-northeast3"으로 하드코딩되어 있습니다. PROJECT_ID나 REGION처럼 환경 변수에서 값을 읽어오도록 변경하면, 다른 환경(예: 개발, 스테이징)에 배포할 때 유연성이 높아집니다. GCP_QUEUE_LOCATION 같은 환경 변수를 사용하고, 이 값이 없을 경우 애플리케이션이 시작되지 않도록 하여 설정 오류를 방지하는 것을 권장합니다.
| private static final String QUEUE_LOCATION = "asia-northeast3"; | |
| private static final String QUEUE_LOCATION = System.getenv("GCP_QUEUE_LOCATION"); |
| return getDefaultServiceAccountStatic(); | ||
| } | ||
|
|
||
| private static String getDefaultServiceAccountStatic() { |
There was a problem hiding this comment.
이 메소드는 BatchScheduleService에 있는 getDefaultServiceAccount 메소드와 거의 동일한 로직을 가집니다. 코드 중복을 피하고 로직을 중앙에서 관리하기 위해 이 메소드를 public으로 변경하고, BatchScheduleService에서도 이 메소드를 호출하도록 리팩토링하는 것이 좋습니다. 일관성을 위해 메소드 이름을 getDefaultServiceAccount로 변경하는 것도 고려해볼 수 있습니다.
public static String getDefaultServiceAccountStatic() {