Skip to content

[UPLUS-152] Cloud Tasks Queue를 통한 Job 실행 직렬화#3

Merged
arkchive merged 1 commit into
developfrom
feat/UPLUS-152
Jan 26, 2026
Merged

[UPLUS-152] Cloud Tasks Queue를 통한 Job 실행 직렬화#3
arkchive merged 1 commit into
developfrom
feat/UPLUS-152

Conversation

@swthewhite
Copy link
Copy Markdown
Contributor

  • 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로 변경
@gemini-code-assist
Copy link
Copy Markdown

Summary of Changes

Hello @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

  • 의존성 변경: build.gradle에서 google-cloud-run 의존성을 google-cloud-tasks로 교체했습니다.
  • 새로운 서비스 추가: Cloud Tasks를 통해 Cloud Run Job 실행을 처리하는 CloudTasksService를 새로 생성했습니다.
  • BatchRunService 리팩토링: BatchRunServiceJobsClient를 직접 호출하는 대신 CloudTasksService에 Job 실행을 위임하도록 변경했습니다.
  • BatchScheduleService 리팩토링: BatchScheduleService가 스케줄러 타겟을 Cloud Tasks API로 변경하고, CloudTasksService를 사용하여 요청 본문 생성 및 invMonth 추출을 처리하도록 수정했습니다.

🧠 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 Assist

The 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 /gemini <command> or @gemini-code-assist <command>. Below is a summary of the supported commands on the current page.

Feature Command Description
Code Review /gemini review Performs a code review for the current pull request in its current state.
Pull Request Summary /gemini summary Provides a summary of the current pull request in its current state.
Comment @gemini-code-assist Responds in comments when explicitly tagged, both in pull request comments and review comments.
Help /gemini help Displays a list of available commands.

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 .gemini/ folder in the base of the repository. Detailed instructions can be found here.

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

  1. Review the Privacy Notices, Generative AI Prohibited Use Policy, Terms of Service, and learn how to configure Gemini Code Assist in GitHub here. Gemini can make mistakes, so double check it and use code with caution.

Copy link
Copy Markdown

@gemini-code-assist gemini-code-assist Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Code Review

이 PR은 작업 실행 로직을 Cloud Tasks를 사용하여 직렬화하도록 리팩토링하며, 이는 좋은 아키텍처 개선입니다. 변경 사항은 직접적인 Cloud Run 호출을 새로운 CloudTasksService의 태스크 생성으로 대체하는 것을 포함합니다. BatchRunServiceBatchScheduleService의 로직은 이 새로운 서비스에 위임하도록 올바르게 업데이트되었습니다. 하지만 CloudTasksService의 구현에는 몇 가지 중요한 개선 영역이 있습니다. 특히 JSON 처리가 수동적인 문자열 조작으로 이루어지고 있어 코드가 취약하고 유지보수가 어렵습니다. 또한 일부 코드 중복도 리팩토링할 수 있습니다. 제 리뷰에는 적절한 JSON 라이브러리를 사용하고 중복 코드를 중앙 집중화하여 이러한 점들을 해결하기 위한 제안이 포함되어 있습니다.

Comment on lines +134 to +155
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;
}
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

critical

extractInvMonthFromSchedulerBody 메소드는 indexOfsubstring을 사용하여 JSON을 파싱하고 있습니다. 이 방식은 JSON의 공백이나 필드 순서 변경 등 사소한 변화에도 쉽게 깨질 수 있어 매우 취약합니다. ObjectMapperJsonNode를 사용하여 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;
    }

Comment on lines +76 to +85
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
+ "]}]}}";
}
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

high

buildCloudRunJobRequestBody 메소드에서 JSON 문자열을 수동으로 생성하고 있습니다. 이 방식은 오류가 발생하기 쉽고 유지보수가 어렵습니다. 예를 들어, 인자 중 하나에 따옴표가 포함되면 JSON 형식이 깨질 수 있습니다. 프로젝트에 이미 jackson-databind 의존성이 있으므로, ObjectMapperMap을 사용하여 객체를 만든 후 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);
        }
    }

Comment on lines +102 to +129
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
);
}
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

high

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";
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

QUEUE_LOCATION이 "asia-northeast3"으로 하드코딩되어 있습니다. PROJECT_IDREGION처럼 환경 변수에서 값을 읽어오도록 변경하면, 다른 환경(예: 개발, 스테이징)에 배포할 때 유연성이 높아집니다. GCP_QUEUE_LOCATION 같은 환경 변수를 사용하고, 이 값이 없을 경우 애플리케이션이 시작되지 않도록 하여 설정 오류를 방지하는 것을 권장합니다.

Suggested change
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() {
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

이 메소드는 BatchScheduleService에 있는 getDefaultServiceAccount 메소드와 거의 동일한 로직을 가집니다. 코드 중복을 피하고 로직을 중앙에서 관리하기 위해 이 메소드를 public으로 변경하고, BatchScheduleService에서도 이 메소드를 호출하도록 리팩토링하는 것이 좋습니다. 일관성을 위해 메소드 이름을 getDefaultServiceAccount로 변경하는 것도 고려해볼 수 있습니다.

    public static String getDefaultServiceAccountStatic() {

Copy link
Copy Markdown
Contributor

@arkchive arkchive left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Good!

@arkchive arkchive merged commit 9ba1f46 into develop Jan 26, 2026
2 checks passed
@swthewhite swthewhite deleted the feat/UPLUS-152 branch January 26, 2026 18:08
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants