Skip to content

Commit

Permalink
Feat/#167 : 푸시알림 예약 발송을 구현합니다. (#176)
Browse files Browse the repository at this point in the history
* Feat : 토큰 업데이트 구현

* Feat : Controller DTO 수정

* Test : 회원가입 테스트

* Feat(batch) : batch 모듈 추가 및 초기 설정

* Feat(app) : 일기 시간 조회시 답변 타입도 같이 반환

* Feat(infra) : batch를 위한 meta DataSource 설정파일

* Feat(infra) : Schedule Repository Adapter계층 구현

* Feat(domain) : 도메인 계층 모델 정의

* Feat(domain) : 답변 작성시 알림 스케줄 이벤트 발행

- 추가적으로 SQS 로 메세지 발행 시, UserId 또한 포함하여 발송하도록 수정하였습니다.

* Chore(etc) : 사용하지 않는 파일 제거 및 형식 변경

* Feat(batch) : build.gradle

* Feat(batch) : config 추가

* Feat(batch) : Job 및 Step 구현

* Test(batch) : 일기 작성 알림 테스트

* Feat(domain) : schedule 엔티티 구현

* Feat(infra) : adapter 계층 구현

* Chore : 기타 파일 수정

* Feat(domain) : 도메인 계층 알람 전략 구현

* Feat(domain) : Alarm Creation Event & Message

* Feat(domain) : Schedule 및 Repository

* Feat(domain) : alarm publisher 및 이벤트 정의

* Feat(domain) : 이벤트 발행 DTO 이름 변경

* Feat(infra) : 의존성 추가

* Feat(infra) : MultiDataSource 설정 및 MultiTransactionManager 설정

* Feat(infra) : Adapter 메서드 명 변경

* Feat(infra) : 스케줄링 정보 등록 및 예약 구현

* Feat(batch) : 배치 미활용으로 인한 임시 보류

* Chore : 컨벤션 및 변수명 수정
  • Loading branch information
hyunw9 authored Sep 1, 2024
1 parent 0690a2b commit 5a22d9f
Show file tree
Hide file tree
Showing 46 changed files with 684 additions and 166 deletions.
6 changes: 3 additions & 3 deletions build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -58,9 +58,9 @@ subprojects {

}

tasks.withType<BootJar> {
enabled = false;
}
// tasks.withType<BootJar> {
// enabled = false;
// }
tasks.withType<Jar> {
enabled = true;
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -30,8 +30,6 @@ public class UserControllerImpl implements UserController {
private final UserUpdateUsecase userUpdateUsecase;
private final UserDeletionUsecase userDeletionUsecase;



@Override
@GetMapping("/user/info")
public ResponseEntity<ApiResponse<UserInfoGetResponse>> getUserInfo() {
Expand Down
19 changes: 16 additions & 3 deletions clody-batch/build.gradle.kts
Original file line number Diff line number Diff line change
@@ -1,27 +1,32 @@
import io.spring.gradle.dependencymanagement.dsl.DependencyManagementExtension
import org.gradle.internal.impldep.org.junit.experimental.categories.Categories.CategoryFilter.include
import org.springframework.boot.gradle.tasks.bundling.BootJar

plugins {
java
id("org.springframework.boot") version "3.3.0"
// id("io.spring.dependency-management") version "1.1.5"

}

group = "com.clody"
version = "0.0.1-SNAPSHOT"
group = "com.clody-batch"
version = "1.0.0"

java {
toolchain {
languageVersion = JavaLanguageVersion.of(21)
}
}


repositories {
mavenCentral()
}

dependencies {
implementation(project(":clody-domain"))
implementation(project(":clody-infra"))
// implementation(project(":clody-infra"))
// implementation(project(":clody-support"))
implementation("org.springframework.boot:spring-boot-starter-batch")
testImplementation("org.springframework.boot:spring-boot-starter-test")
testImplementation("org.springframework.batch:spring-batch-test")
Expand All @@ -40,6 +45,14 @@ tasks.withType<Test> {
useJUnitPlatform()
}

tasks.named<BootJar>("bootJar") {
enabled = true
}

tasks.named<Jar>("jar") {
enabled = true
}

the<DependencyManagementExtension>().apply {
imports {
mavenBom("org.springframework.cloud:spring-cloud-dependencies:${property("springCloudDependenciesVersion")}")
Expand Down
Empty file modified clody-batch/gradle/wrapper/gradle-wrapper.properties
100644 → 100755
Empty file.
Empty file modified clody-batch/gradlew
100644 → 100755
Empty file.
Empty file modified clody-batch/gradlew.bat
100644 → 100755
Empty file.
Original file line number Diff line number Diff line change
@@ -1,22 +1,26 @@
package com.clody.clodybatch;

import lombok.extern.slf4j.Slf4j;
import static com.clody.clodybatch.config.BatchConfig.BATCH_DATASOURCE;
import static com.clody.clodybatch.config.BatchConfig.BATCH_TRANSACTION_MANAGER;

import com.clody.clodybatch.config.BatchConfig;
import org.springframework.batch.core.configuration.annotation.EnableBatchProcessing;
import org.springframework.batch.core.configuration.support.DefaultBatchConfiguration;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.WebApplicationType;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean;
import org.springframework.boot.builder.SpringApplicationBuilder;
import org.springframework.context.annotation.Import;

@EnableBatchProcessing
@Import({BatchConfig.class})
@EnableBatchProcessing(dataSourceRef = BATCH_DATASOURCE, transactionManagerRef = BATCH_TRANSACTION_MANAGER)
@SpringBootApplication
@Slf4j
@ConditionalOnMissingBean(value = DefaultBatchConfiguration.class, annotation = EnableBatchProcessing.class)
public class ClodyBatchApplication {

public static void main(String[] args) {
int exit = SpringApplication.exit(SpringApplication.run(ClodyBatchApplication.class, args));
log.info("exit = {}", exit);
System.exit(exit);
System.setProperty("spring.config.name","application-batch");
SpringApplication springApplication = new SpringApplicationBuilder(ClodyBatchApplication.class).web(
WebApplicationType.NONE).build();
springApplication.run(args);
}

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
package com.clody.clodybatch.config;

import org.springframework.batch.core.configuration.annotation.EnableBatchProcessing;
import org.springframework.batch.core.configuration.support.DefaultBatchConfiguration;
import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean;

@ConditionalOnMissingBean(value = DefaultBatchConfiguration.class, annotation = EnableBatchProcessing.class)
public class BatchAutoConfiguration {
}
Original file line number Diff line number Diff line change
@@ -1,26 +1,75 @@
package com.clody.clodybatch.config;

import com.clody.infra.meta.JpaScheduleMetaRepository;
import com.clody.meta.Schedule;
import java.time.LocalDateTime;
import java.time.temporal.ChronoUnit;
import java.util.Collections;
import org.springframework.batch.core.configuration.support.DefaultBatchConfiguration;
import org.springframework.batch.item.data.RepositoryItemReader;
import com.zaxxer.hikari.HikariDataSource;
import java.util.Collection;
import javax.sql.DataSource;
import lombok.RequiredArgsConstructor;
import org.springframework.batch.core.Job;
import org.springframework.batch.core.explore.JobExplorer;
import org.springframework.batch.core.launch.JobLauncher;
import org.springframework.batch.core.repository.JobRepository;
import org.springframework.beans.factory.ObjectProvider;
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.boot.autoconfigure.batch.BatchDataSource;
import org.springframework.boot.autoconfigure.batch.BatchDataSourceScriptDatabaseInitializer;
import org.springframework.boot.autoconfigure.batch.BatchProperties;
import org.springframework.boot.autoconfigure.batch.JobLauncherApplicationRunner;
import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean;
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.boot.context.properties.EnableConfigurationProperties;
import org.springframework.boot.jdbc.DataSourceBuilder;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.Primary;
import org.springframework.jdbc.datasource.DataSourceTransactionManager;
import org.springframework.transaction.PlatformTransactionManager;
import org.springframework.util.StringUtils;

@Configuration
public class BatchConfig extends DefaultBatchConfiguration {
@RequiredArgsConstructor
@EnableConfigurationProperties(BatchProperties.class)
public class BatchConfig {

public static final String BATCH_DATASOURCE = "batchDataSource";
public static final String BATCH_TRANSACTION_MANAGER = "batchTransactionManager";

@Bean
@ConditionalOnMissingBean
@ConditionalOnProperty(prefix = "spring.batch.job", name = "enabled", havingValue = "true", matchIfMissing = true)
public JobLauncherApplicationRunner jobLauncherApplicationRunner(JobLauncher jobLauncher, JobExplorer jobExplorer,
JobRepository jobRepository, BatchProperties properties, Collection<Job> jobs) {
JobLauncherApplicationRunner runner = new JobLauncherApplicationRunner(jobLauncher, jobExplorer, jobRepository);
String jobNames = properties.getJob().getName();
if (StringUtils.hasText(jobNames)) {
if (jobs.stream().map(Job::getName).noneMatch(s -> s.equals(jobNames))){
throw new IllegalArgumentException(jobNames + "는 등록되지 않은 job name입니다. job name을 확인하세요.");
}
runner.setJobName(jobNames);
}
return runner;
}

@Bean
public RepositoryItemReader<Schedule> metaScheduleReader(
JpaScheduleMetaRepository scheduleMetaRepository){
RepositoryItemReader<Schedule> reader = new RepositoryItemReader<>();
reader.setRepository(scheduleMetaRepository);
reader.setMethodName("findByNotificationTime");
reader.setArguments(Collections.singletonList(LocalDateTime.now().truncatedTo(ChronoUnit.SECONDS)));
reader.setPageSize(10);
return reader;
@ConditionalOnMissingBean(BatchDataSourceScriptDatabaseInitializer.class)
BatchDataSourceScriptDatabaseInitializer batchDataSourceInitializer(DataSource dataSource,
@BatchDataSource ObjectProvider<DataSource> batchDataSource, BatchProperties properties) {
return new BatchDataSourceScriptDatabaseInitializer(
batchDataSource.getIfAvailable(() -> dataSource),
properties.getJdbc());
}

@Primary
@BatchDataSource
@Bean(BATCH_DATASOURCE)
@ConfigurationProperties(prefix = "spring.datasource.batch.hikari")
public DataSource batchDataSource() {
return DataSourceBuilder.create().type(HikariDataSource.class).build();
}

@Bean(BATCH_TRANSACTION_MANAGER)
public PlatformTransactionManager batchTransactionManager(
@Qualifier(BATCH_DATASOURCE) DataSource batchDataSource) {
return new DataSourceTransactionManager(batchDataSource);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
//package com.clody.clodybatch.config;
//
//import jakarta.annotation.PostConstruct;
//import java.util.Map;
//import lombok.RequiredArgsConstructor;
//import org.springframework.batch.core.Job;
//import org.springframework.context.ApplicationContext;
//import org.springframework.stereotype.Component;
//
//@Component
//@RequiredArgsConstructor
//public class Inspector {
//
// private final ApplicationContext applicationContext;
//
// @PostConstruct
// public void printAllJobs() {
// Map<String, Job> jobs = applicationContext.getBeansOfType(Job.class);
// System.out.println("Registered Jobs:");
// jobs.forEach((name, job) -> System.out.println("Job name: " + name));
// }
//}
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
package com.clody.clodybatch.config;

import static com.clody.clodybatch.config.ReplyNotificationStepConfig.STEP_NAME;

import org.springframework.batch.core.Job;
import org.springframework.batch.core.Step;
import org.springframework.batch.core.job.builder.JobBuilder;
import org.springframework.batch.core.launch.support.RunIdIncrementer;
import org.springframework.batch.core.repository.JobRepository;
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

@Configuration
public class ReplyJobConfig {

public static final String JOB_NAME= "REPLY_NOTIFICATION_JOB";
private final Step replyNotificationStep;

public ReplyJobConfig(@Qualifier(STEP_NAME) Step replyNotificationStep) {
this.replyNotificationStep = replyNotificationStep;
}

@Bean(name = JOB_NAME)
public Job ReplyNotificationJob(JobRepository jobRepository){
return new JobBuilder(JOB_NAME, jobRepository)
.incrementer(new RunIdIncrementer())
.start(replyNotificationStep)
.build();
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
package com.clody.clodybatch.config;

import java.time.LocalDateTime;
import java.time.format.DateTimeFormatter;
import lombok.Getter;
import lombok.NoArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.batch.core.configuration.annotation.JobScope;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Component;

@Component
@JobScope
@NoArgsConstructor
@Getter
@Slf4j
public class ReplyNotificationJobParameter {

private LocalDateTime dateTime;

@Value("${chunk-size:1000}")
private int chunkSize;

@Value("#{jobParameters[dateTime]}")
public void setDateTime(String dateTime){
log.info("Job Parameter dateTime: {}", dateTime);
DateTimeFormatter formatter = DateTimeFormatter.ofPattern("yyyy-MM-dd'T'HH:mm");
this.dateTime = LocalDateTime.parse(dateTime, formatter);
}

}
Original file line number Diff line number Diff line change
@@ -1,30 +1,70 @@
package com.clody.clodybatch.config;

import com.clody.clodybatch.service.ReplyNotificationService;
import com.clody.clodybatch.service.ReplyScheduleService;
import com.clody.meta.Schedule;
import jakarta.persistence.EntityManagerFactory;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.batch.core.Step;
import org.springframework.batch.core.configuration.annotation.EnableBatchProcessing;
import org.springframework.batch.core.configuration.annotation.JobScope;
import org.springframework.batch.core.configuration.annotation.StepScope;
import org.springframework.batch.core.repository.JobRepository;
import org.springframework.batch.core.step.builder.StepBuilder;
import org.springframework.batch.core.step.tasklet.Tasklet;
import org.springframework.batch.item.ItemReader;
import org.springframework.batch.item.ItemWriter;
import org.springframework.batch.item.database.builder.JpaPagingItemReaderBuilder;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.transaction.PlatformTransactionManager;

@Configuration
@EnableBatchProcessing
@RequiredArgsConstructor
@Slf4j
//@EnableBatchProcessing
public class ReplyNotificationStepConfig {

@Bean
public Tasklet replyNotificationTasklet() {
return (contribution, chunkContext) -> {
System.out.println("Sending notification to user: " + chunkContext.getStepContext().getStepExecution().getJobExecution().getExecutionContext().get("fcmToken"));
return null;
};
public static final String STEP_NAME = ReplyJobConfig.JOB_NAME + ".STEP";

private final ReplyNotificationService replyNotificationService;
private final ReplyScheduleService replyScheduleService;
private final EntityManagerFactory entityManagerFactory;
private final ReplyNotificationJobParameter jobParameter;
// private final ScheduleMetaRepository scheduleMetaRepository;

@Bean(STEP_NAME)
@JobScope
public Step replyNotificationStep(
JobRepository jobRepository, PlatformTransactionManager platformTransactionManager) {
log.info(">>>>> This is Step1");

return new StepBuilder(STEP_NAME, jobRepository)
.<Schedule, Schedule>chunk(jobParameter.getChunkSize(), platformTransactionManager)
.reader(scheduleItemReader())
.writer(changeScheduleStatus())
.build();
}

@Bean
public Step myStep(JobRepository jobRepository, Tasklet replyNotificationTasklet, PlatformTransactionManager transactionManager) {
return new StepBuilder("myStep", jobRepository)
.tasklet(replyNotificationTasklet,transactionManager)
@StepScope
public ItemReader<Schedule> scheduleItemReader() {
return new JpaPagingItemReaderBuilder<Schedule>()
.name("ScheduleItemReader")
.entityManagerFactory(entityManagerFactory)
.pageSize(jobParameter.getChunkSize())
.queryString(
"SELECT s FROM Schedule s WHERE s.isSent = false AND s.notificationTime <= :now")
.build();
}

@Bean
public ItemWriter<Schedule> changeScheduleStatus(){
return schedules ->{
for (Schedule schedule : schedules){
schedule.notifySent();
// replyScheduleService.updateSchedulesAsSent(schedule);
}
};
}

}
Loading

0 comments on commit 5a22d9f

Please sign in to comment.