Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
19 commits
Select commit Hold shift + click to select a range
24bc545
UPLUS-105 fix : 불필요한 디렉토리 및 파일 삭제
starboxxxx Jan 21, 2026
7fefbf0
UPLUS-105 fix : 디렉토리 및 파일 구조 재정의
starboxxxx Jan 21, 2026
8c89081
UPLUS-105 fix : Spring 설정파일 수정
starboxxxx Jan 21, 2026
7bfdc66
UPLUS-105 fix : Spring Batch 의존성 추가
starboxxxx Jan 21, 2026
140dc2f
UPLUS-105 fix : Kafka Consumer 설정 파일 수정
starboxxxx Jan 21, 2026
5ae0679
UPLUS-105 feat : 필요한 에러코드 정의
starboxxxx Jan 21, 2026
bc4d7ac
UPLUS-105 feat : 사용량 집계 배치 로직 구성
starboxxxx Jan 21, 2026
0317cde
UPLUS-105 feat : 사용량 집계 배치에 필요한 Entity 및 Dto 정의
starboxxxx Jan 21, 2026
a4eac93
UPLUS-105 feat : 사용량 집계 배치에 필요한 Entity 및 Dto 정의
starboxxxx Jan 21, 2026
5d8f848
UPLUS-105 feat : 사용량 임계치 초과 검증 배치 로직 구현
starboxxxx Jan 21, 2026
a6607f4
UPLUS-105 feat : 사용량 임계치 초과 검증 배치에 필요한 Entity 및 Dto 구현
starboxxxx Jan 21, 2026
ba0dcea
UPLUS-105 feat : 임계치 초과 사용자 Kafka 이벤트 발행 배치 로직 구현
starboxxxx Jan 21, 2026
316ef4a
UPLUS-105 feat : 임계치 초과 사용자 Kafka 이벤트 발행 배치에 필요한 Entity 및 Dto 구현
starboxxxx Jan 21, 2026
5c98e60
UPLUS-105 feat : 임계치 초과 사용자 Kafka Consumer 로직 구현
starboxxxx Jan 21, 2026
208f44f
UPLUS-105 feat : 배치 Job 실행파일 임시 구현
starboxxxx Jan 21, 2026
bc9f28f
UPLUS-105 feat : 배치 작업 기록 테이블 및 로직 구현
starboxxxx Jan 21, 2026
4c2ff1c
UPLUS-105 feat : 안쓰는 import 제거
starboxxxx Jan 21, 2026
12c1183
UPLUS-105 feat : spotless 검증
starboxxxx Jan 21, 2026
59322c7
UPLUS-105 feat : 환경변수 적용
starboxxxx Jan 21, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -61,6 +61,9 @@ dependencies {
implementation 'org.springframework.boot:spring-boot-starter-data-redis'
runtimeOnly 'org.postgresql:postgresql'

// Batch
implementation 'org.springframework.boot:spring-boot-starter-batch'

// Kafka
implementation 'org.apache.kafka:kafka-clients'
implementation 'org.springframework.kafka:spring-kafka'
Expand Down
7 changes: 2 additions & 5 deletions src/main/java/com/project/global/config/KafkaConfig.java
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,6 @@

import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.kafka.annotation.EnableKafka;
import org.springframework.kafka.config.ConcurrentKafkaListenerContainerFactory;
import org.springframework.kafka.config.KafkaListenerContainerFactory;
import org.springframework.kafka.core.ConsumerFactory;
Expand All @@ -11,7 +10,6 @@
import org.springframework.util.backoff.FixedBackOff;

@Configuration
@EnableKafka
public class KafkaConfig {

@Bean
Expand All @@ -36,14 +34,13 @@ public KafkaListenerContainerFactory<?> kafkaListenerContainerFactory(
factory.setConsumerFactory(consumerFactory);

factory.setBatchListener(true);

factory.getContainerProperties().setAckMode(ContainerProperties.AckMode.MANUAL);
factory.setAutoStartup(false); // ⭐ 핵심

factory.getContainerProperties().setAckMode(ContainerProperties.AckMode.MANUAL);

DefaultErrorHandler errorHandler =
new DefaultErrorHandler(
new FixedBackOff(1000L, 9) // 총 10번 시도(초기+9)
new FixedBackOff(1000L, 2) // 총 5번 시도(초기+9)
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

FixedBackOff 설정에 대한 주석이 실제 동작과 다릅니다. new FixedBackOff(1000L, 2)는 초기 시도를 포함하여 총 3번의 시도를 의미합니다. 하지만 주석에는 총 5번 시도(초기+9)라고 되어 있어 혼란을 줄 수 있습니다. 주석을 총 3번 시도(초기+2)와 같이 실제 동작에 맞게 수정하는 것이 좋겠습니다.

Suggested change
new FixedBackOff(1000L, 2) // 총 5번 시도(초기+9)
new FixedBackOff(1000L, 2) // 총 3번 시도(초기+2)

);

factory.setCommonErrorHandler(errorHandler);
Expand Down
16 changes: 0 additions & 16 deletions src/main/java/com/project/global/config/NotificationConsumer.java

This file was deleted.

2 changes: 2 additions & 0 deletions src/main/java/com/project/global/config/RedisConfig.java
Original file line number Diff line number Diff line change
Expand Up @@ -2,11 +2,13 @@

import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.Profile;
import org.springframework.data.redis.connection.RedisConnectionFactory;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.data.redis.serializer.GenericJackson2JsonRedisSerializer;
import org.springframework.data.redis.serializer.StringRedisSerializer;

@Profile("!batch")
@Configuration
public class RedisConfig {

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,13 @@ public enum GlobalErrorCode implements BaseErrorCode {
PLAN_CHANGE_EVENT_PRODUCE_INVALID(
HttpStatus.BAD_REQUEST, "COMMON_007", "Kafka PlanChange 이벤트 발행 과정에서 에러가 발생했습니다"),
LUA_SCRIPT_LOAD_INVALID(HttpStatus.BAD_REQUEST, "COMMON_008", "LUA 스크립트를 불러오는 과정에서 에러가 발생했습니다"),
JSON_CONVERT_INVALID(HttpStatus.BAD_REQUEST, "COMMON_009", "JSON으로 변환하는 과정에서 에러가 발생했습니다");
JSON_CONVERT_INVALID(HttpStatus.BAD_REQUEST, "COMMON_009", "JSON으로 변환하는 과정에서 에러가 발생했습니다"),
USAGE_LOG_BATCH_FAILED(HttpStatus.BAD_REQUEST, "COMMON_010", "사용자 데이터 사용량 적재 배치 시스템이 실패하였습니다"),
USAGE_NOTIFICATION_PRODUCER_FAILED(
HttpStatus.BAD_REQUEST, "COMMON_011", "UsageNotification Produce 과정에서 에러가 발생하였습니다"),
USAGE_OUTBOX_WRITER_FAILED(
HttpStatus.BAD_REQUEST, "COMMON_012", "UsageOutbox Writer 배치 시스템이 실패하였습니다"),
PLAN_NOT_VALID(HttpStatus.BAD_REQUEST, "COMMON_013", "존재하지 않는 요금제입니다");

private final HttpStatus httpStatus;
private final String customCode;
Expand Down

This file was deleted.

55 changes: 55 additions & 0 deletions src/main/java/com/project/rdb/BatchJobRunner.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
// package com.project.rdb;
//
// import org.springframework.batch.core.Job;
// import org.springframework.batch.core.JobParameters;
// import org.springframework.batch.core.JobParametersBuilder;
// import org.springframework.batch.core.launch.JobLauncher;
// import org.springframework.boot.CommandLineRunner;
// import org.springframework.stereotype.Component;
//
// import lombok.RequiredArgsConstructor;
//
// @Component
// @RequiredArgsConstructor
// public class BatchJobRunner implements CommandLineRunner {
//
// private final JobLauncher jobLauncher;
// private final Job usageAggregationJob;
// private final Job usageNotificationJob;
// private final Job notificationSendJob;
//
// ////// @Override
// ////// public void run(String... args) throws Exception {
// ////// JobParameters params = new JobParametersBuilder()
// ////// .addString("fromTime", "2025-12-01T00:00:00")
// ////// .addString("toTime", "2025-12-31T11:59:59")
// ////// .addLong("run.id", System.currentTimeMillis())
// ////// .toJobParameters();
// //////
// ////// jobLauncher.run(usageAggregationJob, params);
// ////// }
// ////
// ////// @Override
// ////// public void run(String... args) throws Exception {
// //////
// ////// JobParameters params = new JobParametersBuilder()
// ////// // 월 요금제 Step용
// ////// .addString("period", "202512")
// ////// // 일 요금제 Step용
// ////// .addString("usageDate", "20251201")
// ////// .addLong("run.id", System.currentTimeMillis())
// ////// .toJobParameters();
// //////
// ////// jobLauncher.run(usageNotificationJob, params);
// ////// }
// //
// @Override
// public void run(String... args) throws Exception {
// JobParameters params =
// new JobParametersBuilder()
// .addLong("runAt", System.currentTimeMillis()) // 중복 실행 방지
// .toJobParameters();
//
// jobLauncher.run(notificationSendJob, params);
// }
// }
Original file line number Diff line number Diff line change
@@ -0,0 +1,83 @@
package com.project.rdb.batch.model;

import java.math.BigDecimal;
import java.math.RoundingMode;
import java.time.Duration;
import java.time.LocalDateTime;
import java.time.ZoneId;
import java.util.Objects;

import org.springframework.batch.core.ExitStatus;
import org.springframework.batch.core.JobExecution;
import org.springframework.batch.core.StepExecution;
import org.springframework.batch.core.StepExecutionListener;
import org.springframework.stereotype.Component;

import com.fasterxml.jackson.databind.ObjectMapper;
import com.project.rdb.batch.model.entity.BatchExecutionReport;
import com.project.rdb.batch.model.repository.BatchExecutionReportRepository;

import lombok.RequiredArgsConstructor;

@Component
@RequiredArgsConstructor
public class BatchStepMetricsListener implements StepExecutionListener {
private final BatchExecutionReportRepository reportRepository;
private final ObjectMapper objectMapper;

@Override
public ExitStatus afterStep(StepExecution stepExecution) {

JobExecution jobExecution = stepExecution.getJobExecution();

LocalDateTime start =
Objects.requireNonNull(stepExecution.getStartTime())
.atZone(ZoneId.of("Asia/Seoul"))
.toLocalDateTime();

LocalDateTime end =
Objects.requireNonNull(stepExecution.getEndTime())
.atZone(ZoneId.of("Asia/Seoul"))
.toLocalDateTime();
Comment on lines +33 to +41
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

ZoneId.of("Asia/Seoul")과 같이 특정 시간대를 하드코딩하면, 향후 다른 지역에서 애플리케이션을 배포하거나 다른 시간대를 지원해야 할 때 문제가 발생할 수 있습니다. 서버 측의 타임스탬프는 일반적으로 UTC를 기준으로 저장하는 것이 좋습니다. ZoneId.of("UTC")를 사용하거나, 시간대를 외부 설정으로 분리하여 유연성을 확보하는 방안을 고려해 보세요.

Suggested change
LocalDateTime start =
Objects.requireNonNull(stepExecution.getStartTime())
.atZone(ZoneId.of("Asia/Seoul"))
.toLocalDateTime();
LocalDateTime end =
Objects.requireNonNull(stepExecution.getEndTime())
.atZone(ZoneId.of("Asia/Seoul"))
.toLocalDateTime();
LocalDateTime start =
Objects.requireNonNull(stepExecution.getStartTime())
.atZone(ZoneId.of("UTC"))
.toLocalDateTime();
LocalDateTime end =
Objects.requireNonNull(stepExecution.getEndTime())
.atZone(ZoneId.of("UTC"))
.toLocalDateTime();


long durationMs = Duration.between(start, end).toMillis();

long readCount = stepExecution.getReadCount();
BigDecimal tps =
durationMs > 0
? BigDecimal.valueOf(readCount)
.multiply(BigDecimal.valueOf(1000))
.divide(BigDecimal.valueOf(durationMs), 2, RoundingMode.HALF_UP)
: BigDecimal.ZERO;

String paramsJson;
try {
paramsJson =
objectMapper.writeValueAsString(
jobExecution.getJobParameters().getParameters());
} catch (Exception e) {
paramsJson = "{}";
}

reportRepository.save(
BatchExecutionReport.builder()
.jobName(jobExecution.getJobInstance().getJobName())
.stepName(stepExecution.getStepName())
.status(stepExecution.getStatus().toString())
.startedAt(start)
.endedAt(end)
.durationMs(durationMs)
.readCount(readCount)
.writeCount(stepExecution.getWriteCount())
.filterCount(stepExecution.getFilterCount())
.skipCount(stepExecution.getSkipCount())
.commitCount(stepExecution.getCommitCount())
.rollbackCount(stepExecution.getRollbackCount())
.tps(tps)
.jobParameters(paramsJson)
.createdAt(LocalDateTime.now())
.build());

return stepExecution.getExitStatus();
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
package com.project.rdb.batch.model.dto;

import java.util.concurrent.CompletableFuture;

import org.springframework.kafka.support.SendResult;

public record NotificationSendTask(
UsageNotificationEvent event, CompletableFuture<SendResult<String, String>> future) {}
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
package com.project.rdb.batch.model.dto;

public record UsageDailyAggregation(Long subId, String usageDate, long deltaBytes) {}
16 changes: 16 additions & 0 deletions src/main/java/com/project/rdb/batch/model/dto/UsageDailyKey.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
package com.project.rdb.batch.model.dto;

import java.util.Objects;

public record UsageDailyKey(Long subId, String usageDate) {
@Override
public boolean equals(Object oj) {
if (this == oj) {
return true;
}
if (!(oj instanceof UsageDailyKey that)) {
return false;
}
return Objects.equals(subId, that.subId) && Objects.equals(usageDate, that.usageDate);
}
}
Comment on lines +6 to +16
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

UsageDailyKeyrecord로 선언되어 있어 equals()hashCode() 메서드가 자동으로 생성됩니다. 현재 직접 재정의한 equals() 메서드는 자동 생성되는 코드와 동일하므로 불필요합니다. 이 코드를 제거하여 코드를 간결하게 유지하는 것을 제안합니다. 이 내용은 UsageMonthlyKey.java에도 동일하게 적용됩니다.

Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
package com.project.rdb.batch.model.dto;

import java.time.LocalDateTime;

public record UsageLogRow(Long subId, Long usedBytes, LocalDateTime eventTime) {}
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
package com.project.rdb.batch.model.dto;

public record UsageMonthlyAggregation(Long subId, String period, long deltaBytes) {}
17 changes: 17 additions & 0 deletions src/main/java/com/project/rdb/batch/model/dto/UsageMonthlyKey.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
package com.project.rdb.batch.model.dto;

import java.util.Objects;

public record UsageMonthlyKey(Long subId, String period) {

@Override
public boolean equals(Object oj) {
if (this == oj) {
return true;
}
if (!(oj instanceof UsageMonthlyKey that)) {
return false;
}
return Objects.equals(subId, that.subId) && Objects.equals(period, that.period);
}
}
Comment on lines +7 to +17
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

UsageMonthlyKeyrecord로 선언되어 있어 equals()hashCode() 메서드가 자동으로 생성됩니다. 현재 직접 재정의한 equals() 메서드는 자동 생성되는 코드와 동일하므로 불필요합니다. 이 코드를 제거하여 코드를 간결하게 유지하는 것을 제안합니다.

Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
package com.project.rdb.batch.model.dto;

public record UsageNotificationCandidate(
Long subId,
String period,
String unit,
int threshold,
int percent,
long totalUsedMb,
long allotmentMb) {}
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
package com.project.rdb.batch.model.dto;

import java.util.UUID;

public record UsageNotificationEvent(
UUID eventId,
Long id,
Long subId,
String period,
String unit,
int threshold,
int percent,
long totalUsedMb,
long allotmentMb) {}
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
package com.project.rdb.batch.model.dto;

public record UsageNotificationOutboxRow(
Long id,
Long subId,
String period,
String unit,
int threshold,
int percent,
long totalUsedMb,
long allotmentMb) {}
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
package com.project.rdb.batch.model.dto;

public record UsageNotificationSource(
Long subId, String period, String unit, long totalUsedBytes, long allotmentAmount) {}
Loading
Loading