Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

✨ Feature - 스케줄러의 Job을 Redis로 관리하는 기능 추가 #105

Merged
merged 9 commits into from
Nov 23, 2024
798 changes: 798 additions & 0 deletions http/DummyHttpRequest.http

Large diffs are not rendered by default.

573 changes: 0 additions & 573 deletions http/account/AccountControllerHttpRequest.http

Large diffs are not rendered by default.

229 changes: 1 addition & 228 deletions http/auth/AuthControllerHttpRequest.http
Original file line number Diff line number Diff line change
Expand Up @@ -94,231 +94,4 @@ Content-Type: application/json
### 2.2 탈퇴하기
// @no-log
DELETE {{host_url}}/api/v1/auth
Authorization: Bearer {{access_token}}

### 더미1 삽입
// @no-log
POST {{host_url}}/api/v1/auth/sign-up
Content-Type: multipart/form-data; boundary=boundary

--boundary
Content-Disposition: form-data; name="logo"; filename="image.jpeg"
Content-Type: image/png

< /Users/kyeom/Desktop/1_profile.jpeg

--boundary
Content-Disposition: form-data; name="banner"; filename="image.png"
Content-Type: image/png

< /Users/kyeom/Desktop/1_banner.png

--boundary
Content-Disposition: form-data; name="body"
Content-Type: application/json

{
"serial_id" : "{{dummy.dummy1.serial_id}}",
"password" : "{{dummy.dummy1.password}}",
"store_info" :
{
"onjung_tag" : ["{{dummy.dummy1.store_info.onjung_tag[0]}}"],
"title" : "{{dummy.dummy1.store_info.title}}",
"youtube_url" : "{{dummy.dummy1.store_info.youtube_url}}",
"name" : "{{dummy.dummy1.store_info.name}}",
"category" : "{{dummy.dummy1.store_info.category}}",
"introduction" : "{{dummy.dummy1.store_info.introduction}}"
},
"ocr_info" :
{
"store_name" : "{{dummy.dummy1.ocr_info.store_name}}",
"address_name" : "{{dummy.dummy1.ocr_info.address_name}}"
},
"bank_info" :
{
"name": "{{dummy.dummy1.bank_info.name}}",
"account_number" : "{{dummy.dummy1.bank_info.account_number}}"
}
}

### 더미2 삽입
// @no-log
POST {{host_url}}/api/v1/auth/sign-up
Content-Type: multipart/form-data; boundary=boundary

--boundary
Content-Disposition: form-data; name="logo"; filename="image.png"
Content-Type: image/png

< /Users/kyeom/Desktop/2_profile.jpeg

--boundary
Content-Disposition: form-data; name="banner"; filename="image.png"
Content-Type: image/png

< /Users/kyeom/Desktop/2_banner.png

--boundary
Content-Disposition: form-data; name="body"
Content-Type: application/json


{
"serial_id" : "{{dummy.dummy2.serial_id}}",
"password" : "{{dummy.dummy2.password}}",
"store_info" :
{
"onjung_tag" : ["{{dummy.dummy2.store_info.onjung_tag[0]}}"],
"title" : "{{dummy.dummy2.store_info.title}}",
"youtube_url" : "{{dummy.dummy2.store_info.youtube_url}}",
"name" : "{{dummy.dummy2.store_info.name}}",

"category" : "{{dummy.dummy2.store_info.category}}",
"introduction" : "{{dummy.dummy2.store_info.introduction}}"
},
"ocr_info" :
{
"store_name" : "{{dummy.dummy2.ocr_info.store_name}}",
"address_name" : "{{dummy.dummy2.ocr_info.address_name}}"
},
"bank_info" :
{
"name": "{{dummy.dummy2.bank_info.name}}",
"account_number" : "{{dummy.dummy2.bank_info.account_number}}"
}
}

### 더미3 삽입
// @no-log
POST {{host_url}}/api/v1/auth/sign-up
Content-Type: multipart/form-data; boundary=boundary

--boundary
Content-Disposition: form-data; name="logo"; filename="image.png"
Content-Type: image/png

< /Users/kyeom/Desktop/3_profile.jpeg

--boundary
Content-Disposition: form-data; name="banner"; filename="image.png"
Content-Type: image/png

< /Users/kyeom/Desktop/3_banner.png

--boundary
Content-Disposition: form-data; name="body"
Content-Type: application/json

{
"serial_id" : "{{dummy.dummy3.serial_id}}",
"password" : "{{dummy.dummy3.password}}",
"store_info" :
{
"onjung_tag" : ["{{dummy.dummy3.store_info.onjung_tag[0]}}"],
"title" : "{{dummy.dummy3.store_info.title}}",
"youtube_url" : "{{dummy.dummy3.store_info.youtube_url}}",
"name" : "{{dummy.dummy3.store_info.name}}",
"category" : "{{dummy.dummy3.store_info.category}}",
"introduction" : "{{dummy.dummy3.store_info.introduction}}"
},
"ocr_info" :
{
"store_name" : "{{dummy.dummy3.ocr_info.store_name}}",
"address_name" : "{{dummy.dummy3.ocr_info.address_name}}"
},
"bank_info" :
{
"name": "{{dummy.dummy3.bank_info.name}}",
"account_number" : "{{dummy.dummy3.bank_info.account_number}}"
}
}

### 더미4 삽입
// @no-log
POST {{host_url}}/api/v1/auth/sign-up
Content-Type: multipart/form-data; boundary=boundary

--boundary
Content-Disposition: form-data; name="logo"; filename="image.png"
Content-Type: image/png

< /Users/kyeom/Desktop/4_profile.png

--boundary
Content-Disposition: form-data; name="banner"; filename="image.png"
Content-Type: image/png

< /Users/kyeom/Desktop/4_banner.png

--boundary
Content-Disposition: form-data; name="body"
Content-Type: application/json

{
"serial_id" : "{{dummy.dummy4.serial_id}}",
"password" : "{{dummy.dummy4.password}}",
"store_info" :
{
"onjung_tag" : ["{{dummy.dummy4.store_info.onjung_tag[0]}}"],
"title" : "{{dummy.dummy4.store_info.title}}",
"youtube_url" : "{{dummy.dummy4.store_info.youtube_url}}",
"name" : "{{dummy.dummy4.store_info.name}}",
"category" : "{{dummy.dummy4.store_info.category}}",
"introduction" : "{{dummy.dummy4.store_info.introduction}}"
},
"ocr_info" :
{
"store_name" : "{{dummy.dummy4.ocr_info.store_name}}",
"address_name" : "{{dummy.dummy4.ocr_info.address_name}}"
},
"bank_info" :
{
"name": "{{dummy.dummy4.bank_info.name}}",
"account_number" : "{{dummy.dummy4.bank_info.account_number}}"
}
}

### 더미5 삽입
// @no-log
POST {{host_url}}/api/v1/auth/sign-up
Content-Type: multipart/form-data; boundary=boundary

--boundary
Content-Disposition: form-data; name="logo"; filename="image.png"
Content-Type: image/png

< /Users/kyeom/Desktop/5_profile.jpeg

--boundary
Content-Disposition: form-data; name="banner"; filename="image.png"
Content-Type: image/png

< /Users/kyeom/Desktop/5_banner.png

--boundary
Content-Disposition: form-data; name="body"
Content-Type: application/json

{
"serial_id" : "{{dummy.dummy5.serial_id}}",
"password" : "{{dummy.dummy5.password}}",
"store_info" :
{
"onjung_tag" : ["{{dummy.dummy5.store_info.onjung_tag[0]}}"],
"title" : "{{dummy.dummy5.store_info.title}}",
"youtube_url" : "{{dummy.dummy5.store_info.youtube_url}}",
"name" : "{{dummy.dummy5.store_info.name}}",
"category" : "{{dummy.dummy5.store_info.category}}",
"introduction" : "{{dummy.dummy5.store_info.introduction}}"
},
"ocr_info" :
{
"store_name" : "{{dummy.dummy5.ocr_info.store_name}}",
"address_name" : "{{dummy.dummy5.ocr_info.address_name}}"
},
"bank_info" :
{
"name": "{{dummy.dummy5.bank_info.name}}",
"account_number" : "{{dummy.dummy5.bank_info.account_number}}"
}
}
Authorization: Bearer {{access_token}}
Original file line number Diff line number Diff line change
Expand Up @@ -4,12 +4,14 @@
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.data.jpa.repository.config.EnableJpaRepositories;
import org.springframework.data.redis.repository.configuration.EnableRedisRepositories;
import org.springframework.scheduling.annotation.EnableAsync;

import java.util.TimeZone;

@EnableAsync
@EnableJpaRepositories(basePackages = "com.daon.onjung.*.repository.mysql")
@EnableRedisRepositories(basePackages = "com.daon.onjung.*.repository.redis")
@SpringBootApplication
public class OnjungMainServerApplication {
@PostConstruct
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -14,14 +14,14 @@ public class SchedulerConfig {
@Bean
public SchedulerFactoryBean schedulerFactoryBean(AutowiringSpringBeanJobFactory jobFactory) {
SchedulerFactoryBean factoryBean = new SchedulerFactoryBean();
factoryBean.setJobFactory(jobFactory); // Spring 빈으로 관리되는 JobFactory 주입
factoryBean.setJobFactory(jobFactory);
return factoryBean;
}

@Bean
public AutowiringSpringBeanJobFactory jobFactory(AutowireCapableBeanFactory beanFactory) {
AutowiringSpringBeanJobFactory jobFactory = new AutowiringSpringBeanJobFactory();
jobFactory.setBeanFactory(beanFactory); // Spring의 BeanFactory 주입
jobFactory.setBeanFactory(beanFactory);
return jobFactory;
}

Expand All @@ -32,7 +32,7 @@ public static class AutowiringSpringBeanJobFactory extends SpringBeanJobFactory
@Override
protected Object createJobInstance(TriggerFiredBundle bundle) throws Exception {
Object job = super.createJobInstance(bundle);
beanFactory.autowireBean(job); // Spring 의존성 주입
beanFactory.autowireBean(job);
return job;
}
}
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
package com.daon.onjung.core.domain;

import lombok.AccessLevel;
import lombok.Builder;
import lombok.Getter;
import lombok.NoArgsConstructor;
import org.springframework.data.annotation.Id;
import org.springframework.data.redis.core.RedisHash;

import java.time.LocalDateTime;
import java.util.UUID;

@Getter
@NoArgsConstructor(access = AccessLevel.PROTECTED)
//@RedisHash(value = "ScheduledJob", timeToLive = 60 * 60 * 24 * 14) // 14일
@RedisHash(value = "ScheduledJob", timeToLive = 60 * 5) // 테스트용 5분
public class ScheduledEventJob {
@Id
private String jobId;

private Long eventId;

private LocalDateTime scheduledTime;

@Builder
public ScheduledEventJob(Long eventId, LocalDateTime scheduledTime) {
this.jobId = UUID.randomUUID().toString();
this.eventId = eventId;
this.scheduledTime = scheduledTime;
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
package com.daon.onjung.core.domain.service;

import com.daon.onjung.core.domain.ScheduledEventJob;
import org.springframework.stereotype.Service;

import java.time.LocalDateTime;

@Service
public class ScheduledEventJobService {

public ScheduledEventJob createScheduledJob(
Long eventId,
LocalDateTime scheduledTime
) {
return ScheduledEventJob.builder()
.eventId(eventId)
.scheduledTime(scheduledTime)
.build();
}
}
Original file line number Diff line number Diff line change
@@ -1,9 +1,10 @@
package com.daon.onjung.core.listener;

import com.daon.onjung.core.domain.ScheduledEventJob;
import com.daon.onjung.core.exception.error.ErrorCode;
import com.daon.onjung.core.exception.type.CommonException;
import com.daon.onjung.core.repository.redis.ScheduledEventJobRepository;
import com.daon.onjung.event.application.controller.consumer.EventSchedulerConsumerV1Controller;
import com.daon.onjung.event.domain.event.EventScheduled;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.quartz.*;
Expand All @@ -19,23 +20,26 @@ public class AppEventListener {

private final Scheduler scheduler;

private final ScheduledEventJobRepository scheduledEventJobRepository;

@EventListener
public void handleEventScheduled(EventScheduled eventScheduled) {
public void handleEventScheduled(ScheduledEventJob scheduledEventJob) {
JobDetail jobDetail = JobBuilder.newJob(EventSchedulerConsumerV1Controller.class)
.withIdentity("eventJob-" + eventScheduled.eventId(), "eventGroup")
.usingJobData("eventId", eventScheduled.eventId())
.withIdentity("eventJob-" + scheduledEventJob.getJobId(), "eventGroup")
.usingJobData("eventId", scheduledEventJob.getEventId())
.build();
log.info("Job 등록 완료. JobKey: {}", jobDetail.getKey());
log.info("Job 등록 완료. eventId: {}", scheduledEventJob.getEventId());

Trigger trigger = TriggerBuilder.newTrigger()
.withIdentity("eventTrigger-" + eventScheduled.eventId(), "eventGroup")
.startAt(Timestamp.valueOf(eventScheduled.scheduledTime()))
.withIdentity("eventTrigger-" + scheduledEventJob.getJobId(), "eventGroup")
.startAt(Timestamp.valueOf(scheduledEventJob.getScheduledTime()))
.build();
log.info("Trigger 등록 완료. TriggerKey: {}", trigger.getKey());
log.info("eventId {}에 대한 Trigger 등록 완료.", scheduledEventJob.getEventId());
log.info("Trigger 시작 시간: {}", trigger.getStartTime());

try {
scheduler.scheduleJob(jobDetail, trigger);
scheduledEventJobRepository.save(scheduledEventJob);
} catch (SchedulerException e) {
throw new CommonException(ErrorCode.SCHEDULER_ERROR);
}
Expand Down
Loading
Loading