Skip to content

Commit 4071e77

Browse files
committed
Merge remote-tracking branch 'upstream/main'
2 parents 82284a4 + 6c87e71 commit 4071e77

13 files changed

Lines changed: 398 additions & 0 deletions

File tree

.github/workflows/main.yml

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
name: PR Agent
2+
on:
3+
pull_request:
4+
types: [opened, synchronize]
5+
jobs:
6+
pr_agent_job:
7+
runs-on: ubuntu-latest
8+
steps:
9+
- name: PR Agent action step
10+
uses: Codium-ai/pr-agent@main
11+
env:
12+
OPENAI_KEY: ${{ secrets.OPENAI_KEY }}
13+
GITHUB_TOKEN: ${{ secrets.G_TOKEN }}

README.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,7 @@ docker-compose -f ./docker/monitoring-compose.yml up
2727
Root
2828
├── apps ( spring-applications )
2929
│ ├── 📦 commerce-api
30+
│ ├── 📦 commerce-batch
3031
│ └── 📦 commerce-streamer
3132
├── modules ( reusable-configurations )
3233
│ ├── 📦 jpa
Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
dependencies {
2+
// add-ons
3+
implementation(project(":modules:jpa"))
4+
implementation(project(":modules:redis"))
5+
implementation(project(":supports:jackson"))
6+
implementation(project(":supports:logging"))
7+
implementation(project(":supports:monitoring"))
8+
9+
// batch
10+
implementation("org.springframework.boot:spring-boot-starter-batch")
11+
testImplementation("org.springframework.batch:spring-batch-test")
12+
13+
// querydsl
14+
annotationProcessor("com.querydsl:querydsl-apt::jakarta")
15+
annotationProcessor("jakarta.persistence:jakarta.persistence-api")
16+
annotationProcessor("jakarta.annotation:jakarta.annotation-api")
17+
18+
// test-fixtures
19+
testImplementation(testFixtures(project(":modules:jpa")))
20+
testImplementation(testFixtures(project(":modules:redis")))
21+
}
Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
package com.loopers;
2+
3+
import jakarta.annotation.PostConstruct;
4+
import org.springframework.boot.SpringApplication;
5+
import org.springframework.boot.autoconfigure.SpringBootApplication;
6+
import org.springframework.boot.context.properties.ConfigurationPropertiesScan;
7+
8+
import java.util.TimeZone;
9+
10+
@ConfigurationPropertiesScan
11+
@SpringBootApplication
12+
public class CommerceBatchApplication {
13+
14+
@PostConstruct
15+
public void started() {
16+
// set timezone
17+
TimeZone.setDefault(TimeZone.getTimeZone("Asia/Seoul"));
18+
}
19+
20+
public static void main(String[] args) {
21+
int exitCode = SpringApplication.exit(SpringApplication.run(CommerceBatchApplication.class, args));
22+
System.exit(exitCode);
23+
}
24+
}
Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,48 @@
1+
package com.loopers.batch.job.demo;
2+
3+
import com.loopers.batch.job.demo.step.DemoTasklet;
4+
import com.loopers.batch.listener.JobListener;
5+
import com.loopers.batch.listener.StepMonitorListener;
6+
import lombok.RequiredArgsConstructor;
7+
import org.springframework.batch.core.Job;
8+
import org.springframework.batch.core.Step;
9+
import org.springframework.batch.core.configuration.annotation.JobScope;
10+
import org.springframework.batch.core.job.builder.JobBuilder;
11+
import org.springframework.batch.core.launch.support.RunIdIncrementer;
12+
import org.springframework.batch.core.repository.JobRepository;
13+
import org.springframework.batch.core.step.builder.StepBuilder;
14+
import org.springframework.batch.support.transaction.ResourcelessTransactionManager;
15+
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
16+
import org.springframework.context.annotation.Bean;
17+
import org.springframework.context.annotation.Configuration;
18+
19+
@ConditionalOnProperty(name = "spring.batch.job.name", havingValue = DemoJobConfig.JOB_NAME)
20+
@RequiredArgsConstructor
21+
@Configuration
22+
public class DemoJobConfig {
23+
public static final String JOB_NAME = "demoJob";
24+
private static final String STEP_DEMO_SIMPLE_TASK_NAME = "demoSimpleTask";
25+
26+
private final JobRepository jobRepository;
27+
private final JobListener jobListener;
28+
private final StepMonitorListener stepMonitorListener;
29+
private final DemoTasklet demoTasklet;
30+
31+
@Bean(JOB_NAME)
32+
public Job demoJob() {
33+
return new JobBuilder(JOB_NAME, jobRepository)
34+
.incrementer(new RunIdIncrementer())
35+
.start(categorySyncStep())
36+
.listener(jobListener)
37+
.build();
38+
}
39+
40+
@JobScope
41+
@Bean(STEP_DEMO_SIMPLE_TASK_NAME)
42+
public Step categorySyncStep() {
43+
return new StepBuilder(STEP_DEMO_SIMPLE_TASK_NAME, jobRepository)
44+
.tasklet(demoTasklet, new ResourcelessTransactionManager())
45+
.listener(stepMonitorListener)
46+
.build();
47+
}
48+
}
Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
1+
package com.loopers.batch.job.demo.step;
2+
3+
import com.loopers.batch.job.demo.DemoJobConfig;
4+
import lombok.RequiredArgsConstructor;
5+
import org.springframework.batch.core.StepContribution;
6+
import org.springframework.batch.core.configuration.annotation.StepScope;
7+
import org.springframework.batch.core.scope.context.ChunkContext;
8+
import org.springframework.batch.core.step.tasklet.Tasklet;
9+
import org.springframework.batch.repeat.RepeatStatus;
10+
import org.springframework.beans.factory.annotation.Value;
11+
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
12+
import org.springframework.stereotype.Component;
13+
14+
@StepScope
15+
@ConditionalOnProperty(name = "spring.batch.job.name", havingValue = DemoJobConfig.JOB_NAME)
16+
@RequiredArgsConstructor
17+
@Component
18+
public class DemoTasklet implements Tasklet {
19+
@Value("#{jobParameters['requestDate']}")
20+
private String requestDate;
21+
22+
@Override
23+
public RepeatStatus execute(StepContribution contribution, ChunkContext chunkContext) throws Exception {
24+
if (requestDate == null) {
25+
throw new RuntimeException("requestDate is null");
26+
}
27+
System.out.println("Demo Tasklet 실행 (실행 일자 : " + requestDate + ")");
28+
Thread.sleep(1000);
29+
System.out.println("Demo Tasklet 작업 완료");
30+
return RepeatStatus.FINISHED;
31+
}
32+
}
Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
package com.loopers.batch.listener;
2+
3+
import lombok.RequiredArgsConstructor;
4+
import lombok.extern.slf4j.Slf4j;
5+
import org.springframework.batch.core.annotation.AfterChunk;
6+
import org.springframework.batch.core.scope.context.ChunkContext;
7+
import org.springframework.stereotype.Component;
8+
9+
@Slf4j
10+
@RequiredArgsConstructor
11+
@Component
12+
public class ChunkListener {
13+
14+
@AfterChunk
15+
void afterChunk(ChunkContext chunkContext) {
16+
log.info(
17+
"청크 종료: readCount: ${chunkContext.stepContext.stepExecution.readCount}, " +
18+
"writeCount: ${chunkContext.stepContext.stepExecution.writeCount}"
19+
);
20+
}
21+
}
Lines changed: 53 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,53 @@
1+
package com.loopers.batch.listener;
2+
3+
import lombok.RequiredArgsConstructor;
4+
import lombok.extern.slf4j.Slf4j;
5+
import org.springframework.batch.core.JobExecution;
6+
import org.springframework.batch.core.annotation.AfterJob;
7+
import org.springframework.batch.core.annotation.BeforeJob;
8+
import org.springframework.stereotype.Component;
9+
10+
import java.time.Duration;
11+
import java.time.Instant;
12+
import java.time.ZoneId;
13+
14+
@Slf4j
15+
@RequiredArgsConstructor
16+
@Component
17+
public class JobListener {
18+
19+
@BeforeJob
20+
void beforeJob(JobExecution jobExecution) {
21+
log.info("Job '${jobExecution.jobInstance.jobName}' 시작");
22+
jobExecution.getExecutionContext().putLong("startTime", System.currentTimeMillis());
23+
}
24+
25+
@AfterJob
26+
void afterJob(JobExecution jobExecution) {
27+
var startTime = jobExecution.getExecutionContext().getLong("startTime");
28+
var endTime = System.currentTimeMillis();
29+
30+
var startDateTime = Instant.ofEpochMilli(startTime)
31+
.atZone(ZoneId.systemDefault())
32+
.toLocalDateTime();
33+
var endDateTime = Instant.ofEpochMilli(endTime)
34+
.atZone(ZoneId.systemDefault())
35+
.toLocalDateTime();
36+
37+
var totalTime = endTime - startTime;
38+
var duration = Duration.ofMillis(totalTime);
39+
var hours = duration.toHours();
40+
var minutes = duration.toMinutes() % 60;
41+
var seconds = duration.getSeconds() % 60;
42+
43+
var message = String.format(
44+
"""
45+
*Start Time:* %s
46+
*End Time:* %s
47+
*Total Time:* %d시간 %d분 %d초
48+
""", startDateTime, endDateTime, hours, minutes, seconds
49+
).trim();
50+
51+
log.info(message);
52+
}
53+
}
Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,44 @@
1+
package com.loopers.batch.listener;
2+
3+
import jakarta.annotation.Nonnull;
4+
import lombok.RequiredArgsConstructor;
5+
import lombok.extern.slf4j.Slf4j;
6+
import org.springframework.batch.core.ExitStatus;
7+
import org.springframework.batch.core.StepExecution;
8+
import org.springframework.batch.core.StepExecutionListener;
9+
import org.springframework.stereotype.Component;
10+
import java.util.Objects;
11+
import java.util.stream.Collectors;
12+
13+
@Slf4j
14+
@RequiredArgsConstructor
15+
@Component
16+
public class StepMonitorListener implements StepExecutionListener {
17+
18+
@Override
19+
public void beforeStep(@Nonnull StepExecution stepExecution) {
20+
log.info("Step '{}' 시작", stepExecution.getStepName());
21+
}
22+
23+
@Override
24+
public ExitStatus afterStep(@Nonnull StepExecution stepExecution) {
25+
if (!stepExecution.getFailureExceptions().isEmpty()) {
26+
var jobName = stepExecution.getJobExecution().getJobInstance().getJobName();
27+
var exceptions = stepExecution.getFailureExceptions().stream()
28+
.map(Throwable::getMessage)
29+
.filter(Objects::nonNull)
30+
.collect(Collectors.joining("\n"));
31+
log.info(
32+
"""
33+
[에러 발생]
34+
jobName: {}
35+
exceptions:
36+
{}
37+
""".trim(), jobName, exceptions
38+
);
39+
// error 발생 시 slack 등 다른 채널로 모니터 전송
40+
return ExitStatus.FAILED;
41+
}
42+
return ExitStatus.COMPLETED;
43+
}
44+
}
Lines changed: 54 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,54 @@
1+
spring:
2+
main:
3+
web-application-type: none
4+
application:
5+
name: commerce-batch
6+
profiles:
7+
active: local
8+
config:
9+
import:
10+
- jpa.yml
11+
- redis.yml
12+
- logging.yml
13+
- monitoring.yml
14+
batch:
15+
job:
16+
name: ${job.name:NONE}
17+
jdbc:
18+
initialize-schema: never
19+
20+
management:
21+
health:
22+
defaults:
23+
enabled: false
24+
25+
---
26+
spring:
27+
config:
28+
activate:
29+
on-profile: local, test
30+
batch:
31+
jdbc:
32+
initialize-schema: always
33+
34+
---
35+
spring:
36+
config:
37+
activate:
38+
on-profile: dev
39+
40+
---
41+
spring:
42+
config:
43+
activate:
44+
on-profile: qa
45+
46+
---
47+
spring:
48+
config:
49+
activate:
50+
on-profile: prd
51+
52+
springdoc:
53+
api-docs:
54+
enabled: false

0 commit comments

Comments
 (0)