Skip to content

Commit f33d887

Browse files
authored
[Feat]: 회원 자동 충전 배치 기능 작성 (#111)
* fix: 불필요한 환경변수 제거 * feat: 회원 자동 충전을 위한 Reader 클래스 생성 * feat: sqlSessionFactory에서 mapper 메서드 인식을 위한 xml 코드 추가 * feat: 회원 자동 충전을 위한 service 메서드 추가 * feat: 회원 자동 충전을 위한 MemberService 보조 메서드 추가 * feat: 정렬을 위한 컬럼 추가 * feat: 회원 자동 충전을 위한 Writer 클래스 추가 * feat: 회원 자동 충전 이후 알림을 위한 클래스 추가 * feat: Qualifier 어노테이션 추가 * feat: 회원 자동 충전 Batch Config 추가 * feat: 분산락을 동반한 스케쥴러 추가
1 parent 7db7502 commit f33d887

16 files changed

Lines changed: 422 additions & 6 deletions

src/main/java/org/scoula/domain/member/mapper/MemberMapper.java

Lines changed: 18 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -83,7 +83,9 @@ int updateRemittanceRefsStrict(@Param("memberId") Long memberId,
8383
i.purpose,
8484
i.amount,
8585
i.transmit_fail_count,
86-
i.intermediary_bank_commission
86+
i.intermediary_bank_commission,
87+
i.created_at,
88+
i.updated_at
8789
8890
FROM member m
8991
JOIN remittance_information i ON m.remittance_information_id = i.remittance_information_id
@@ -109,4 +111,19 @@ int updateRemittanceRefsStrict(@Param("memberId") Long memberId,
109111
})
110112
List<MemberWithInformationDto> findMembersWithInfoByGroupIds(@Param("groupIds") List<Long> groupIds);
111113

114+
void updateGroupIdByMemberIds(@Param("memberIds") List<Long> memberIds,
115+
@Param("toGroupId") Long toGroupId);
116+
117+
@Select("""
118+
<script>
119+
SELECT fcm_token
120+
FROM member
121+
WHERE member_id IN
122+
<foreach item='id' collection='memberIds' open='(' separator=',' close=')'>
123+
#{id}
124+
</foreach>
125+
</script>
126+
""")
127+
List<String> findFcmTokensByMemberIds(@Param("memberIds") List<Long> memberIds);
128+
112129
}

src/main/java/org/scoula/domain/member/service/MemberService.java

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,13 @@
11
package org.scoula.domain.member.service;
22

33
import java.util.List;
4+
import java.util.Optional;
45

56
import javax.servlet.http.HttpServletRequest;
67

78
import org.scoula.domain.member.dto.MemberDTO;
89
import org.scoula.domain.member.entity.Member;
10+
import org.scoula.domain.remittancegroup.batch.dto.MemberWithInformationDto;
911

1012
public interface MemberService {
1113

@@ -31,4 +33,12 @@ public interface MemberService {
3133

3234
Member getMemberByPhoneNumber(String phoneNumber, HttpServletRequest request);
3335

36+
Optional<List<Member>> getMembersByRemittanceGroup(Long RemittanceGroupId);
37+
38+
Optional<List<MemberWithInformationDto>> getMemberWithRemittanceInformationByRemittanceGroupId(
39+
Long RemittanceGroupId);
40+
41+
void changeRemittanceGroup(List<Long> memberIds, Long toGroupId);
42+
43+
void decreaseRemittanceGroupMemberCount(Long targetGroupId, int decreaseMemberCount);
3444
}

src/main/java/org/scoula/domain/member/service/MemberServiceImpl.java

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,13 +3,16 @@
33
import static org.scoula.domain.member.exception.MemberErrorCode.*;
44

55
import java.util.List;
6+
import java.util.Optional;
67
import java.util.stream.Collectors;
78

89
import javax.servlet.http.HttpServletRequest;
910

1011
import org.scoula.domain.member.dto.MemberDTO;
1112
import org.scoula.domain.member.entity.Member;
1213
import org.scoula.domain.member.mapper.MemberMapper;
14+
import org.scoula.domain.remittancegroup.batch.dto.MemberWithInformationDto;
15+
import org.scoula.domain.remittancegroup.mapper.RemittanceGroupMapper;
1316
import org.scoula.global.exception.CustomException;
1417
import org.scoula.global.kafka.dto.Common;
1518
import org.scoula.global.kafka.dto.LogLevel;
@@ -28,6 +31,7 @@ public class MemberServiceImpl implements MemberService {
2831

2932
private final MemberMapper memberMapper;
3033
private final PasswordEncoder passwordEncoder;
34+
private final RemittanceGroupMapper remittanceGroupMapper;
3135

3236
@Override
3337
public List<MemberDTO> getAllMembers() {
@@ -114,4 +118,29 @@ public Member getMemberByPhoneNumber(String phoneNumber, HttpServletRequest requ
114118
.build()));
115119
}
116120

121+
@Override
122+
public Optional<List<Member>> getMembersByRemittanceGroup(Long RemittanceGroupId) {
123+
return Optional.of(memberMapper.findMembersByRemittanceGroupId(RemittanceGroupId));
124+
}
125+
126+
@Override
127+
public Optional<List<MemberWithInformationDto>> getMemberWithRemittanceInformationByRemittanceGroupId(
128+
Long RemittanceGroupId) {
129+
return Optional.of(memberMapper.findMembersWithInformationByGroupId(RemittanceGroupId));
130+
}
131+
132+
@Override
133+
@Transactional
134+
public void changeRemittanceGroup(List<Long> memberIds, Long toGroupId) {
135+
if (memberIds == null || memberIds.isEmpty())
136+
return;
137+
memberMapper.updateGroupIdByMemberIds(memberIds, toGroupId);
138+
}
139+
140+
@Override
141+
@Transactional
142+
public void decreaseRemittanceGroupMemberCount(Long targetGroupId, int decreaseMemberCount) {
143+
remittanceGroupMapper.decreaseMemberCountById(targetGroupId, decreaseMemberCount);
144+
}
145+
117146
}
Lines changed: 78 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,78 @@
1+
package org.scoula.domain.remittancegroup.batch.config;
2+
3+
import org.mybatis.spring.batch.MyBatisPagingItemReader;
4+
import org.scoula.domain.remittancegroup.batch.reader.IdRangePartitioner;
5+
import org.scoula.domain.remittancegroup.entity.RemittanceGroup;
6+
import org.scoula.domain.remittancegroup.mapper.RemittanceGroupMapper;
7+
import org.springframework.batch.core.Job;
8+
import org.springframework.batch.core.Step;
9+
import org.springframework.batch.core.configuration.annotation.StepBuilderFactory;
10+
import org.springframework.batch.core.job.builder.JobBuilder;
11+
import org.springframework.batch.core.repository.JobRepository;
12+
import org.springframework.batch.item.ItemWriter;
13+
import org.springframework.beans.factory.annotation.Qualifier;
14+
import org.springframework.context.annotation.Bean;
15+
import org.springframework.context.annotation.Configuration;
16+
import org.springframework.core.task.TaskExecutor;
17+
import org.springframework.transaction.PlatformTransactionManager;
18+
19+
@Configuration
20+
public class RemittanceAutoJoinBatchConfig {
21+
22+
private final RemittanceGroupMapper remittanceGroupMapper;
23+
private final MyBatisPagingItemReader<RemittanceGroup> remittanceGroupReader;
24+
private final ItemWriter<RemittanceGroup> remittanceGroupWriter;
25+
26+
private final JobRepository jobRepository;
27+
private final PlatformTransactionManager transactionManager;
28+
private final StepBuilderFactory stepBuilderFactory;
29+
private final TaskExecutor taskExecutor;
30+
31+
public RemittanceAutoJoinBatchConfig(RemittanceGroupMapper remittanceGroupMapper,
32+
@Qualifier("autoJoinReader")
33+
MyBatisPagingItemReader<RemittanceGroup> remittanceGroupReader,
34+
@Qualifier("remittanceGroupAutoJoinItemWriter") ItemWriter<RemittanceGroup> remittanceGroupWriter,
35+
JobRepository jobRepository, PlatformTransactionManager transactionManager,
36+
StepBuilderFactory stepBuilderFactory,
37+
TaskExecutor taskExecutor) {
38+
this.remittanceGroupMapper = remittanceGroupMapper;
39+
this.remittanceGroupReader = remittanceGroupReader;
40+
this.remittanceGroupWriter = remittanceGroupWriter;
41+
this.jobRepository = jobRepository;
42+
this.transactionManager = transactionManager;
43+
this.stepBuilderFactory = stepBuilderFactory;
44+
this.taskExecutor = taskExecutor;
45+
}
46+
47+
private final int threadSize = 3;
48+
49+
@Bean
50+
public Job remittanceGroupAutoJoinJob() {
51+
return new JobBuilder("remittanceGroupAutoJoinJob")
52+
.start(partitionedRemittanceGroupAutoJoinStep())
53+
.repository(jobRepository)
54+
.build();
55+
}
56+
57+
@Bean
58+
public Step partitionedRemittanceGroupAutoJoinStep() {
59+
return stepBuilderFactory.get("partitionedRemittanceGroupAutoJoinStep")
60+
.partitioner("remittanceGroupAutoJoinStep", new IdRangePartitioner(remittanceGroupMapper))
61+
.step(remittanceGroupAutoJoinStep())
62+
.gridSize(threadSize)
63+
.transactionManager(transactionManager)
64+
.taskExecutor(taskExecutor)
65+
.build();
66+
}
67+
68+
@Bean
69+
public Step remittanceGroupAutoJoinStep() {
70+
return stepBuilderFactory.get("remittanceGroupAutoJoinStep")
71+
.<RemittanceGroup, RemittanceGroup>chunk(1000)
72+
.reader(remittanceGroupReader)
73+
.writer(remittanceGroupWriter)
74+
.repository(jobRepository)
75+
.transactionManager(transactionManager)
76+
.build();
77+
}
78+
}

src/main/java/org/scoula/domain/remittancegroup/batch/config/RemittanceGroupBatchConfig.java

Lines changed: 16 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -10,15 +10,13 @@
1010
import org.springframework.batch.core.repository.JobRepository;
1111
import org.springframework.batch.item.ItemReader;
1212
import org.springframework.batch.item.ItemWriter;
13+
import org.springframework.beans.factory.annotation.Qualifier;
1314
import org.springframework.context.annotation.Bean;
1415
import org.springframework.context.annotation.Configuration;
1516
import org.springframework.core.task.TaskExecutor;
1617
import org.springframework.transaction.PlatformTransactionManager;
1718

18-
import lombok.RequiredArgsConstructor;
19-
2019
@Configuration
21-
@RequiredArgsConstructor
2220
public class RemittanceGroupBatchConfig {
2321

2422
private final ItemReader<RemittanceGroup> remittanceGroupReader;
@@ -32,6 +30,21 @@ public class RemittanceGroupBatchConfig {
3230

3331
private final int threadSize = 3;
3432

33+
public RemittanceGroupBatchConfig(RemittanceGroupMapper remittanceGroupMapper,
34+
ItemReader<RemittanceGroup> remittanceGroupReader,
35+
@Qualifier("remittanceGroupItemWriter") ItemWriter<RemittanceGroup> remittanceGroupWriter,
36+
JobRepository jobRepository, PlatformTransactionManager transactionManager,
37+
StepBuilderFactory stepBuilderFactory,
38+
TaskExecutor taskExecutor) {
39+
this.remittanceGroupMapper = remittanceGroupMapper;
40+
this.remittanceGroupReader = remittanceGroupReader;
41+
this.remittanceGroupWriter = remittanceGroupWriter;
42+
this.jobRepository = jobRepository;
43+
this.transactionManager = transactionManager;
44+
this.stepBuilderFactory = stepBuilderFactory;
45+
this.taskExecutor = taskExecutor;
46+
}
47+
3548
@Bean
3649
public Job remittanceGroupJob() {
3750
return new JobBuilder("remittanceGroupJob")

src/main/java/org/scoula/domain/remittancegroup/batch/dto/MemberWithInformationDto.java

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22

33
import java.math.BigDecimal;
44
import java.time.LocalDate;
5+
import java.time.LocalDateTime;
56

67
import org.scoula.domain.remmitanceinformation.entity.IntermediaryBankCommission;
78
import org.scoula.global.constants.Currency;
@@ -41,4 +42,6 @@ public class MemberWithInformationDto {
4142
private BigDecimal amount; // 환전을 원하는 금액(원화)
4243
private Integer transmitFailCount; // 송금 실패 카운트 (2회 이상 시 그룹 탈퇴)
4344
private IntermediaryBankCommission intermediaryBankCommission; // 중계 수수료 부담 방식
45+
private LocalDateTime createdAt;
46+
private LocalDateTime updatedAt;
4447
}
Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,36 @@
1+
package org.scoula.domain.remittancegroup.batch.reader;
2+
3+
import java.util.Map;
4+
5+
import org.apache.ibatis.session.SqlSessionFactory;
6+
import org.mybatis.spring.batch.MyBatisPagingItemReader;
7+
import org.scoula.domain.remittancegroup.entity.RemittanceGroup;
8+
import org.springframework.batch.core.configuration.annotation.StepScope;
9+
import org.springframework.beans.factory.annotation.Qualifier;
10+
import org.springframework.beans.factory.annotation.Value;
11+
import org.springframework.context.annotation.Bean;
12+
import org.springframework.context.annotation.Configuration;
13+
14+
@Configuration
15+
public class RemittanceGroupAutoJoinReader {
16+
17+
@Bean
18+
@StepScope
19+
public MyBatisPagingItemReader<RemittanceGroup> autoJoinReader(
20+
@Value("#{stepExecutionContext['startId']}") Long startId,
21+
@Value("#{stepExecutionContext['endId']}") Long endId,
22+
@Qualifier("simpleSqlSessionFactory")
23+
SqlSessionFactory sqlSessionFactory) {
24+
MyBatisPagingItemReader<RemittanceGroup> reader = new MyBatisPagingItemReader<>();
25+
26+
reader.setQueryId(
27+
"org.scoula.domain.remittancegroup.mapper.RemittanceGroupMapper.findByIdRangeBenefitStatusOn");
28+
reader.setParameterValues(Map.of("startId", startId, "endId", endId));
29+
reader.setSqlSessionFactory(sqlSessionFactory);
30+
reader.setPageSize(1000);
31+
reader.setSaveState(false);
32+
reader.setName("autoJoinReader");
33+
34+
return reader;
35+
}
36+
}

src/main/java/org/scoula/domain/remittancegroup/batch/scheduler/RemittanceGroupScheduler.java

Lines changed: 18 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -20,15 +20,18 @@ public class RemittanceGroupScheduler {
2020
private final JobLauncher jobLauncher;
2121
private final Job remittanceGroupJob;
2222
private final RemittanceService remittanceService;
23+
private final Job remittanceGroupAutoJoinJob;
2324

2425
public RemittanceGroupScheduler(
2526
JobLauncher jobLauncher,
2627
@Qualifier("remittanceGroupJob") Job remittanceGroupJob,
27-
RemittanceService remittanceService
28+
RemittanceService remittanceService,
29+
@Qualifier("remittanceGroupAutoJoinJob") Job remittanceGroupAutoJoinJob
2830
) {
2931
this.jobLauncher = jobLauncher;
3032
this.remittanceGroupJob = remittanceGroupJob;
3133
this.remittanceService = remittanceService;
34+
this.remittanceGroupAutoJoinJob = remittanceGroupAutoJoinJob;
3235
}
3336

3437
@Scheduled(cron = "0 0 3 * * *", zone = "Asia/Seoul")
@@ -45,6 +48,20 @@ public void runRemittanceGroupJob() {
4548
}
4649
}
4750

51+
@Scheduled(cron = "0 30 3 * * *", zone = "Asia/Seoul")
52+
@SchedulerLock(name = "runRemittanceGroupAutoJoinJobLock", lockAtMostFor = "PT10M") // 락 10분간 유지
53+
public void runRemittanceGroupAutoJoinJob() {
54+
try {
55+
JobParameters jobParameters = new JobParametersBuilder()
56+
.addLong("timestamp", System.currentTimeMillis())
57+
.toJobParameters();
58+
59+
jobLauncher.run(remittanceGroupAutoJoinJob, jobParameters);
60+
} catch (Exception e) {
61+
throw new RuntimeException(e);
62+
}
63+
}
64+
4865
@Scheduled(cron = "0 0 8 * * *", zone = "Asia/Seoul")
4966
@SchedulerLock(name = "runRemittanceGroupAlarmLock", lockAtMostFor = "PT10M") // 락 10분간 유지
5067
public void remittanceGroupAlarm() {
Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,46 @@
1+
package org.scoula.domain.remittancegroup.batch.writer;
2+
3+
import java.util.ArrayList;
4+
import java.util.Comparator;
5+
import java.util.List;
6+
7+
import org.scoula.domain.remittancegroup.entity.RemittanceGroup;
8+
import org.scoula.domain.remittancegroup.service.RemittanceService;
9+
import org.springframework.batch.item.ItemWriter;
10+
import org.springframework.context.annotation.Bean;
11+
import org.springframework.context.annotation.Configuration;
12+
13+
import lombok.RequiredArgsConstructor;
14+
import lombok.extern.slf4j.Slf4j;
15+
16+
@Configuration
17+
@RequiredArgsConstructor
18+
@Slf4j
19+
public class RemittanceGroupAutoJoinWriter {
20+
21+
private final RemittanceService remittanceService;
22+
23+
@Bean("remittanceGroupAutoJoinItemWriter")
24+
public ItemWriter<RemittanceGroup> remittanceGroupAutoJoinItemWriter() {
25+
return items -> {
26+
List<RemittanceGroup> sorted = new ArrayList<>(items);
27+
sorted.sort(Comparator.comparingInt(g ->
28+
g.getMemberCount() != null ? g.getMemberCount() : 0
29+
));
30+
31+
if (log.isDebugEnabled()) {
32+
for (RemittanceGroup g : sorted) {
33+
log.debug("memberCount={}", g.getMemberCount());
34+
}
35+
}
36+
37+
for (RemittanceGroup g : sorted) {
38+
// 기존 그룹에 이탈자가 없을 경우 패스
39+
if (g.getMemberCount() != null && g.getMemberCount() >= 30)
40+
continue;
41+
42+
remittanceService.changeRemittanceGroup(g);
43+
}
44+
};
45+
}
46+
}

0 commit comments

Comments
 (0)