[๐Ÿ”ฅTroubleShooting - MoodBuddy๐Ÿ”ฅ] ๋„ˆ์˜ ์ฟผ๋””ํ‹ฐ์•„์ด๋Š” ๋ญ๋‹ˆ?!

._mungยท2025๋…„ 3์›” 9์ผ
0

MoodBuddy

๋ชฉ๋ก ๋ณด๊ธฐ
4/9

๐Ÿ“Œ ํ”„๋กœ์ ํŠธ ์†Œ๊ฐœ

ํŒ€์› : PM(1) / Design(1) / Frontend(2) / Backend(3)
๊ธฐ๊ฐ„ : 2024.03 ~ 2025.03
๋งํฌ : https://github.com/M-ung/MoodBuddy_Server
์„œ๋น„์Šค ๋‚ด์šฉ : ์‚ฌ์šฉ์ž๊ฐ€ ์ž‘์„ฑํ•œ ์ผ๊ธฐ๋ฅผ ๋ฐ”ํƒ•์œผ๋กœ ๊ฐ์ • ๋ถ„์„ํ•˜๋Š” ์›น ์„œ๋น„์Šค
์†Œํ†ต : GitHub, Slack, Notion, Discord


๐Ÿ”ฅTroubleShooting๐Ÿ”ฅ

Problems

์šฐ๋ฆฌ ์„œ๋น„์Šค 'MoodBuddy" ์—๋Š” ํŠน๋ณ„ํ•œ ๊ธฐ๋Šฅ์ด ์žˆ๋‹ค. ๋ฐ”๋กœ "์ฟผ๋””ํ‹ฐ์•„์ด(QuddyTI)" ์ด๋‹ค.

์ฟผ๋””ํ‹ฐ์•„์ด(QuddyTI)๋ž€?
๋‹จ์ˆœํžˆ MBTI๋ฅผ ์ƒ๊ฐํ•˜๋ฉด ๋œ๋‹ค.
์‚ฌ์šฉ์ž๊ฐ€ ํ•œ ๋‹ฌ ๋™์•ˆ ์ž‘์„ฑํ•œ ์ผ๊ธฐ๋ฅผ ๊ธฐ๋ฐ˜์œผ๋กœ "์ผ๊ธฐ ์ž‘์„ฑ ํšŸ์ˆ˜, ์ผ๊ธฐ ๋ถ„์„์„ ํ†ตํ•ด ๋‚˜์˜จ ๊ฐ์ •, ์ฃผ์ œ" ๋ฅผ ๊ธฐ๋ฐ˜์œผ๋กœ ์ผ๊ธฐ ์„ฑ๊ฒฉ ์œ ํ˜•์„ ํŒ๋ณ„ํ•ด์ฃผ๋Š” ์„œ๋น„์Šค์ด๋‹ค.
์ด๋Š” ํ•œ ๋‹ฌ ๊ธฐ๋ฐ˜์œผ๋กœ ๊ฒฐ๊ณผ๊ฐ€ ๋‚˜์™€์•ผ ํ•˜๊ธฐ ๋•Œ๋ฌธ์— ๋งค๋‹ฌ 00์‹œ์— ์ฟผ๋””ํ‹ฐ์•„์ด(QuddyTI) ์ƒ์„ฑ(ํ˜„์žฌ ๋‹ฌ) ๋ฐ ์ˆ˜์ •(์ง€๋‚œ ๋‹ฌ)์ด ๋ฐœ์ƒํ•œ๋‹ค.

์ฒ˜์Œ 1์ฐจ ๋ฐฐํฌ ๋•Œ๋Š” ๋‹จ์ˆœํžˆ ์Šค์ผ€์ค„๋Ÿฌ๋ฅผ ์‚ฌ์šฉํ•ด์„œ ์•„๋ž˜์™€ ๊ฐ™์ด ๊ตฌํ˜„ํ–ˆ๋‹ค.

@Component
@RequiredArgsConstructor
public class QuddyTIScheduler {
    private final QuddyTIFacade quddyTIFacade;

    @Scheduled(cron = "0 0 0 1 * ?")
    @Transactional
    public void aggregateAndSaveDiaryData() {
        quddyTIFacade.createAndUpadteQuddyTI();
    }
}

์Šค์ผ€์ค„๋Ÿฌ๋ฅผ ๋Œ๋ ค ๋งค๋‹ฌ 00์‹œ์— ๋™์ž‘ํ•˜๋„๋ก ๊ตฌํ˜„ํ–ˆ๋‹ค.

ํ•˜์ง€๋งŒ ์•„๋ž˜์™€ ๊ฐ™์€ ๋ฌธ์ œ์ ๋“ค์ด ๋ฐœ์ƒํ–ˆ๋‹ค.

1. ํ•œ ๋ฒˆ์— ๋ชจ๋“  ์‚ฌ์šฉ์ž์˜ ๋ฐ์ดํ„ฐ๋ฅผ ์ฒ˜๋ฆฌํ•˜๋ ค๋‹ค ๋ณด๋‹ˆ ๋ฐ์ดํ„ฐ๊ฐ€ ๋งŽ์•„์งˆ์ˆ˜๋ก ์ฟผ๋ฆฌ ๋ถ€ํ•˜ ์ฆ๊ฐ€
2. ํŠธ๋žœ์žญ์…˜์ด ๊ธธ์–ด์ง€๊ณ , ์ „์ฒด ๋ฐ์ดํ„ฐ ์ฒ˜๋ฆฌ ์‹œ๊ฐ„์ด ๋Š˜์–ด๋‚˜๋ฉด์„œ ์„œ๋ฒ„ ๋ถ€ํ•˜ ๋ฐœ์ƒ
3. ์‚ฌ์šฉ์ž๊ฐ€ ๋งŽ์•„์งˆ์ˆ˜๋ก ๋ฐ์ดํ„ฐ ์ฆ๊ฐ€๋กœ ์ธํ•ด ํ•˜๋‚˜์˜ ์Šค์ผ€์ค„๋Ÿฌ์—์„œ ์ฒ˜๋ฆฌํ•˜๋Š” ๋ฐฉ์‹์ด ์ ์  ๋ถ€๋‹ด์Šค๋Ÿฌ์›Œ์ง


How

๊ทธ๋ž˜์„œ ์œ„ ๋ฌธ์ œ๋“ค์„ ํ•ด๊ฒฐํ•˜๊ธฐ ์œ„ํ•ด "Spring Batch" ๋ฅผ ์šฐ๋ฆฌ ์„œ๋น„์Šค์— ๋„์ž…ํ•˜๊ธฐ๋กœ ํ–ˆ๋‹ค.

Spring Batch๋ž€?
Spring Batch๋Š” ๋Œ€๋Ÿ‰์˜ ๋ฐ์ดํ„ฐ๋ฅผ ํšจ๊ณผ์ ์œผ๋กœ ์ฒ˜๋ฆฌํ•  ์ˆ˜ ์žˆ๋„๋ก ์„ค๊ณ„๋œ ๋ฐฐ์น˜ ์ฒ˜๋ฆฌ ํ”„๋ ˆ์ž„์›Œํฌ๋‹ค.
์Šค์ผ€์ค„๋Ÿฌ๊ฐ€ ๋‹จ์ˆœ ๋ฐ˜๋ณต ์‹คํ–‰์„ ํ•˜๋Š” ๊ฒƒ๊ณผ ๋‹ฌ๋ฆฌ, ๋Œ€๋Ÿ‰ ๋ฐ์ดํ„ฐ๋ฅผ ํšจ์œจ์ ์œผ๋กœ ๋‚˜๋ˆ„์–ด ์ฒ˜๋ฆฌํ•˜๊ณ , ์žฌ์‹œ๋„ ๋ฐ ์‹คํŒจ ๋ณต๊ตฌ ๊ธฐ๋Šฅ์„ ์ œ๊ณตํ•œ๋‹ค.

Spring Batch์˜ ํ•ต์‹ฌ ๊ฐœ๋…
1. Job: ํ•˜๋‚˜์˜ ๋ฐฐ์น˜ ์ž‘์—… ๋‹จ์œ„
2. Step: Job์„ ๊ตฌ์„ฑํ•˜๋Š” ๊ฐœ๋ณ„ ๋‹จ๊ณ„
3. ItemReader: ๋ฐ์ดํ„ฐ๋ฅผ ์ฝ๋Š” ์—ญํ• 
4. ItemProcessor: ๋ฐ์ดํ„ฐ๋ฅผ ๊ฐ€๊ณตํ•˜๋Š” ์—ญํ• 
5. ItemWriter: ๋ฐ์ดํ„ฐ๋ฅผ ์ €์žฅํ•˜๋Š” ์—ญํ• 

์œ„ "Spring Batch" ๋ฅผ ์ ์šฉํ•˜๊ฒŒ ๋˜๋ฉด ์•„๋ž˜์™€ ๊ฐ™์€ ๊ธฐ๋Œ€ ํšจ๊ณผ๋ฅผ ์–ป์„ ์ˆ˜ ์žˆ๋‹ค.

1. ์„ฑ๋Šฅ ๊ฐœ์„ : ์ž‘์€ ๋‹จ์œ„๋กœ ๋ฐ์ดํ„ฐ๋ฅผ ์ฒ˜๋ฆฌํ•˜์—ฌ DB ๋ถ€ํ•˜ ๊ฐ์†Œ
2. ์•ˆ์ •์„ฑ ํ–ฅ์ƒ: ์‹คํŒจํ•œ ์ž‘์—…์„ ์ž๋™์œผ๋กœ ์žฌ์‹œ๋„ ๊ฐ€๋Šฅ


Process

๋จผ์ € ๊ธฐ์กด ์Šค์ผ€์ค„๋Ÿฌ๋Š” Spring Batch์˜ ํŠธ๋ฆฌ๊ฑฐ ์—ญํ• ์„ ํ•  ์ˆ˜ ์žˆ๊ฒŒ ๋ณ€๊ฒฝํ•ด ์ฃผ์—ˆ๋‹ค.

๐Ÿ“ QuddyTIBatchScheduler

@Component
@RequiredArgsConstructor
public class QuddyTIBatchScheduler {
    private final JobLauncher jobLauncher;
    private final Job quddyTIJob;

    @Scheduled(cron = "0 0 0 1 * ?")
    public void runBatchJob() throws JobExecutionException {
        JobParameters jobParameters = new JobParametersBuilder()
                .addLong("time", System.currentTimeMillis())
                .toJobParameters();
        jobLauncher.run(quddyTIJob, jobParameters);
    }
}

๋งค์›” 00์‹œ์— ์œ„ ์Šค์ผ€์ค„๋Ÿฌ๋ฅผ ๋™์ž‘์‹œํ‚จ ํ›„ Spring Batch๊ฐ€ ๋™์ž‘ํ•˜๊ฒŒ ๋งŒ๋“ค์–ด์คฌ๋‹ค.

๊ทธ๋ฆฌ๊ณ  Spring Batch ์‹œ๋‚˜๋ฆฌ์˜ค๋Š” ์•„๋ž˜์™€ ๊ฐ™๊ฒŒ ์„ค๊ณ„ํ•ด ์ฃผ์—ˆ๋‹ค.

  1. ๋งค๋‹ฌ 1์ผ 00์‹œ์— ์Šค์ผ€์ค„๋Ÿฌ๋ฅผ ํ†ตํ•ด ํŠธ๋ฆฌ๊ฑฐ๋ฅผ ๋ฐœ์ƒํ•œ๋‹ค.
  2. Job -> Step ์ˆœ์œผ๋กœ ์‹คํ–‰ํ•œ๋‹ค.
  3. Reader์—์„œ ํ™œ๋™ ์ค‘์ธ ์‚ฌ์šฉ์ž ์ „์ฒด๋ฅผ ์กฐํšŒํ•ด ์˜จ๋‹ค.
  4. Processor์—์„œ ์‚ฌ์šฉ์ž๋“ค userId๋ฅผ ๊ธฐ๋ฐ˜์œผ๋กœ ์ฟผ๋””ํ‹ฐ์•„์ด ์ƒ์„ฑ ๋ฐ ์ˆ˜์ •์„ ์ค€๋น„ํ•œ๋‹ค.
  5. ๋งˆ์ง€๋ง‰์œผ๋กœ Writer์—์„œ ์ฟผ๋””ํ‹ฐ์•„์ด ์ƒ์„ฑ ๋ฐ ์ˆ˜์ •์„ ํ•œ๋‹ค.

๊ทธ๋ฆฌ๊ณ  ์œ„ ๊ณผ์ •์„ ์ฒ˜์Œ์—๋Š” ๋‹จ์ˆœํžˆ Spring Data JPA๋ฅผ ํ™œ์šฉํ•ด์„œ ์ƒ์„ฑ, ์ˆ˜์ •, ์กฐํšŒ๋ฅผ ํ•ด๊ฒฐํ•˜๋ ค๊ณ  ํ–ˆ๋‹ค.

ํ•˜์ง€๋งŒ Spring Data JPA๋ฅผ ํ™œ์šฉํ•œ ๋ฐฉ์‹์—๋Š” ์•„๋ž˜์™€ ๊ฐ™์€ ๋ฌธ์ œ์ ๋“ค์ด ์žˆ์—ˆ๋‹ค.

1. ๋Œ€๋Ÿ‰ ๋ฐ์ดํ„ฐ ์ฒ˜๋ฆฌ ์‹œ ์„ฑ๋Šฅ ์ €ํ•˜
2. JPA์˜ ๊ธฐ๋ณธ์ ์ธ Bulk Update/Insert์˜ ํ•œ๊ณ„

๊ทธ๋ž˜์„œ Spring Data JPA ๋ณด๋‹ค JDBC ๋ฐฉ์‹์œผ๋กœ ์ƒ์„ฑ, ์ˆ˜์ •, ์กฐํšŒ๋ฅผ ๊ตฌํ˜„ํ•˜์˜€๋‹ค.
์ฝ”๋“œ๋Š” ์•„๋ž˜์™€ ๊ฐ™๋‹ค.

๐Ÿ“ QuddyTIBatchConfig.java

@Configuration
@EnableBatchProcessing
@RequiredArgsConstructor
public class QuddyTIBatchConfig {
    private final QuddyTIBatchJDBCRepository quddyTIBatchJDBCRepository;
    private final DiaryCountService diaryCountService;
    private final JobRepository jobRepository;
    private final DataSource dataSource;
    private final PlatformTransactionManager transactionManager;

    @Bean
    public Job quddyTIJob(Step updateUserStatusStep) {
        return new JobBuilder("quddyTIJob", jobRepository)
                .start(updateUserStatusStep)
                .build();
    }

    @Bean
    public Step quddyTIStep(CompositeItemWriter<QuddyTI> compositeWriter) {
        return new StepBuilder("quddyTIStep", jobRepository)
                .<Long, QuddyTI>chunk(100, transactionManager)
                .reader(userIdReader())
                .processor(compositeProcessor())
                .writer(compositeWriter)
                .build();
    }

    @Bean
    public JdbcCursorItemReader<Long> userIdReader() {
        return new JdbcCursorItemReaderBuilder<Long>()
                .dataSource(dataSource)
                .name("userIdReader")
                .sql("SELECT user_id FROM user WHERE deleted = 0 AND user_role = 'ROLE_USER'")
                .rowMapper((rs, rowNum) -> rs.getLong("user_id"))
                .build();
    }

    @Bean
    public CompositeItemProcessor<Long, QuddyTI> compositeProcessor() {
        CompositeItemProcessor<Long, QuddyTI> processor = new CompositeItemProcessor<>();
        processor.setDelegates(Arrays.asList(quddyTICreator(), quddyTIUpdater()));
        return processor;
    }

    @Bean
    public ItemProcessor<Long, QuddyTI> quddyTICreator() {
        return userId -> QuddyTI.of(userId, DateUtil.formatYear(LocalDate.now()), DateUtil.formatMonth(LocalDate.now()));
    }

    @Bean
    public ItemProcessor<Long, QuddyTI> quddyTIUpdater() {
        return userId -> {
            LocalDate[] dates = DateUtil.getLastMonthDates();
            Map<DiaryEmotion, Long> emotionCounts = diaryCountService.getEmotionCountsByDate(dates);
            Map<DiarySubject, Long> subjectCounts = diaryCountService.getSubjectCountsByDate(dates);
            QuddyTI quddyTI = quddyTIBatchJDBCRepository.findQuddyTIByUserIdAndDate(userId, DateUtil.formatYear(dates[0]), DateUtil.formatMonth(dates[1]));
            quddyTI.update(emotionCounts, subjectCounts);
            return quddyTI;
        };
    }

    @Bean
    public CompositeItemWriter<QuddyTI> compositeWriter() {
        List<ItemWriter<? super QuddyTI>> writers = List.of(saveQuddyTI(), updateQuddyTI());
        CompositeItemWriter<QuddyTI> writer = new CompositeItemWriter<>();
        writer.setDelegates(writers);
        return writer;
    }

    @Bean
    public JdbcBatchItemWriter<QuddyTI> saveQuddyTI() {
        return new JdbcBatchItemWriterBuilder<QuddyTI>()
                .dataSource(dataSource)
                .sql("INSERT INTO quddy_ti (user_id, quddy_ti_year, quddy_ti_month, diary_frequency, daily_count, growth_count, " +
                        "emotion_count, travel_count, happiness_count, anger_count, disgust_count, fear_count, neutral_count, " +
                        "sadness_count, surprise_count, quddy_ti_type, mood_buddy_status) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)")
                .beanMapped()
                .itemPreparedStatementSetter(new ItemPreparedStatementSetter<QuddyTI>() {
                    @Override
                    public void setValues(QuddyTI item, PreparedStatement ps) throws SQLException {
                        ps.setLong(1, item.getUserId());
                        ps.setString(2, item.getQuddyTIYear());
                        ps.setString(3, item.getQuddyTIMonth());
                        ps.setInt(4, item.getDiaryFrequency());
                        ps.setInt(5, item.getDailyCount());
                        ps.setInt(6, item.getGrowthCount());
                        ps.setInt(7, item.getEmotionCount());
                        ps.setInt(8, item.getTravelCount());
                        ps.setInt(9, item.getHappinessCount());
                        ps.setInt(10, item.getAngerCount());
                        ps.setInt(11, item.getDisgustCount());
                        ps.setInt(12, item.getFearCount());
                        ps.setInt(13, item.getNeutralCount());
                        ps.setInt(14, item.getSadnessCount());
                        ps.setInt(15, item.getSurpriseCount());
                        ps.setString(16, item.getQuddyTIType());
                        ps.setString(17, item.getMoodBuddyStatus().name());
                    }
                })
                .build();
    }

    @Bean
    public JdbcBatchItemWriter<QuddyTI> updateQuddyTI() {
        return new JdbcBatchItemWriterBuilder<QuddyTI>()
                .dataSource(dataSource)
                .sql("UPDATE quddy_ti SET diary_frequency = ?, daily_count = ?, growth_count = ?, emotion_count = ?, " +
                        "travel_count = ?, happiness_count = ?, anger_count = ?, disgust_count = ?, fear_count = ?, neutral_count = ?, " +
                        "sadness_count = ?, surprise_count = ?, quddy_ti_type = ? WHERE id = ? AND user_id = ? AND quddy_ti_year = ? AND quddy_ti_month = ?")
                .beanMapped()
                .itemPreparedStatementSetter(new ItemPreparedStatementSetter<QuddyTI>() {
                    @Override
                    public void setValues(QuddyTI item, PreparedStatement ps) throws SQLException {
                        ps.setInt(1, item.getDiaryFrequency());
                        ps.setInt(2, item.getDailyCount());
                        ps.setInt(3, item.getGrowthCount());
                        ps.setInt(4, item.getEmotionCount());
                        ps.setInt(5, item.getTravelCount());
                        ps.setInt(6, item.getHappinessCount());
                        ps.setInt(7, item.getAngerCount());
                        ps.setInt(8, item.getDisgustCount());
                        ps.setInt(9, item.getFearCount());
                        ps.setInt(10, item.getNeutralCount());
                        ps.setInt(11, item.getSadnessCount());
                        ps.setInt(12, item.getSurpriseCount());
                        ps.setString(13, item.getQuddyTIType());
                        ps.setLong(14, item.getId());
                        ps.setLong(15, item.getUserId());
                        ps.setString(16, item.getQuddyTIYear());
                        ps.setString(17, item.getQuddyTIMonth());
                    }
                })
                .build();
    }
}

ํ•˜์ง€๋งŒ ์œ„์ฒ˜๋Ÿผ ๊ตฌํ˜„ํ–ˆ์„ ๋•Œ, ์—๋Ÿฌ๋„ ๋งŽ์ด ๋ฐœ์ƒํ–ˆ๊ณ  ํ•˜๋‚˜์˜ Reader์— ๋‘ ๊ฐœ์˜ Processor, Writer๋ฅผ ๊ด€๋ฆฌํ•˜๊ธฐ์— ์–ด๋ ค์› ๋‹ค. ๋˜ ํ•œ ๋ฒˆ์— ๋งŽ์€ DB ์ ‘๊ทผ์„ ํ•˜๊ธฐ์—๋Š” ๋ถ€ํ•˜๊ฐ€ ํด ๊ฒƒ ๊ฐ™์•˜๋‹ค.

๊ทธ๋ž˜์„œ ์ด๋ฅผ ๋‘ ๊ฐœ๋กœ ๋‚˜๋ˆ„๊ธฐ๋กœ ํ–ˆ๋‹ค. ์‹œ๋‚˜๋ฆฌ์˜ค๋Š” ์•„๋ž˜์™€ ๊ฐ™๊ฒŒ ์„ค๊ณ„ํ•ด ์ฃผ์—ˆ๋‹ค.

์Šค์ผ€์ค„๋Ÿฌ๋Š” ๋‘ ๊ฐœ๋กœ ๋‚˜๋ˆด๋”๋ผ๋„ ๊ฐ™์€ ์‹œ๊ฐ„์— ์‹คํ–‰ํ•˜๋ฉด ์œ„์™€ ๊ฐ™์€ ๋ฌธ์ œ๊ฐ€ ๋ฐœ์ƒํ•  ์ˆ˜ ์žˆ๊ธฐ ๋•Œ๋ฌธ์— ์ƒ์„ฑ๊ณผ ์ˆ˜์ • ์Šค์ผ€์ค„๋Ÿฌ๋ฅผ ๊ฐ๊ฐ ๋‹ค๋ฅธ ๋งค๋‹ฌ ๋ง์ผ๊ณผ 1์ผ๋กœ ๋ถ„๋ฆฌํ•ด์„œ ์‹คํ–‰ํ•˜๋„๋ก ์„ค๊ณ„ํ–ˆ๋‹ค.

๐Ÿ“ ์ฟผ๋””ํ‹ฐ์•„์ด ์ƒ์„ฑ
1. ๋งค๋‹ฌ ๋ง ์ผ 23์‹œ 58๋ถ„์— ์Šค์ผ€์ค„๋Ÿฌ๋ฅผ ํ†ตํ•ด ํŠธ๋ฆฌ๊ฑฐ๋ฅผ ๋ฐœ์ƒํ•œ๋‹ค.
2. Job -> Step ์ˆœ์œผ๋กœ ์‹คํ–‰ํ•œ๋‹ค.
3. Reader์—์„œ ํ™œ๋™ ์ค‘์ธ ์‚ฌ์šฉ์ž ์ „์ฒด๋ฅผ ์กฐํšŒํ•ด ์˜จ๋‹ค.
4. Processor์—์„œ ์‚ฌ์šฉ์ž๋“ค userId๋ฅผ ๊ธฐ๋ฐ˜์œผ๋กœ ์ฟผ๋””ํ‹ฐ์•„์ด ์ƒ์„ฑ์„ ์ค€๋น„ํ•œ๋‹ค.
5. ๋งˆ์ง€๋ง‰์œผ๋กœ Writer์—์„œ ์ฟผ๋””ํ‹ฐ์•„์ด ์ƒ์„ฑ์„ ํ•œ๋‹ค.

๐Ÿ“ ์ฟผ๋””ํ‹ฐ์•„์ด ์ˆ˜์ •
1. ๋งค๋‹ฌ 1์ผ 00์‹œ์— ์Šค์ผ€์ค„๋Ÿฌ๋ฅผ ํ†ตํ•ด ํŠธ๋ฆฌ๊ฑฐ๋ฅผ ๋ฐœ์ƒํ•œ๋‹ค.
2. Job -> Step ์ˆœ์œผ๋กœ ์‹คํ–‰ํ•œ๋‹ค.
3. Reader์—์„œ ์ง€๋‚œ๋‹ฌ ์ฟผ๋””ํ‹ฐ์•„์ด ์ „์ฒด๋ฅผ ์กฐํšŒํ•ด ์˜จ๋‹ค.
4. Processor์—์„œ ์ฟผ๋””ํ‹ฐ์•„์ด quddy_ti_id๋ฅผ ๊ธฐ๋ฐ˜์œผ๋กœ ์ฟผ๋””ํ‹ฐ์•„์ด ์ˆ˜์ •์„ ์ค€๋น„ํ•œ๋‹ค.
5. ๋งˆ์ง€๋ง‰์œผ๋กœ Writer์—์„œ ์ฟผ๋””ํ‹ฐ์•„์ด ์ˆ˜์ •์„ ํ•œ๋‹ค.

์‹œ๋‚˜๋ฆฌ์˜ค์— ๋งž๊ฒŒ ๊ตฌํ˜„ํ•œ ์ฝ”๋“œ๋Š” ์•„๋ž˜์™€ ๊ฐ™๋‹ค.

๐Ÿ“ QuddyTICreateBatchScheduler.java

@Component
@RequiredArgsConstructor
public class QuddyTICreateBatchScheduler {
    private final JobLauncher jobLauncher;
    private final Job quddyTICreateJob;

    @Scheduled(cron = "0 58 23 L * ?")
    public void runBatchJob() throws JobExecutionException {
        JobParameters jobParameters = new JobParametersBuilder()
                .addLong("time", System.currentTimeMillis())
                .toJobParameters();
        jobLauncher.run(quddyTICreateJob, jobParameters);
    }
}

๐Ÿ“ QuddyTICreateBatchConfig.java

@Configuration
@RequiredArgsConstructor
@EnableTransactionManagement
public class QuddyTICreateBatchConfig {

    private final JobRepository jobRepository;
    private final DataSource dataSource;
    private final QuddyTIBatchJDBCRepository quddyTIBatchJDBCRepository;
    private final PlatformTransactionManager transactionManager;

    @Bean
    public Job quddyTICreateJob(Step quddyTICreateStep) {
        return new JobBuilder("quddyTICreateJob", jobRepository)
                .start(quddyTICreateStep)
                .build();
    }

    @Bean
    public Step quddyTICreateStep() {
        return new StepBuilder("quddyTICreateStep", jobRepository)
                .<Long, QuddyTI>chunk(100, transactionManager)
                .reader(userIdReader())
                .processor(createQuddyTIProcessor())
                .writer(saveQuddyTIWriter())
                .build();
    }

    @Bean
    public JdbcCursorItemReader<Long> userIdReader() {
        return new JdbcCursorItemReaderBuilder<Long>()
                .dataSource(dataSource)
                .name("userIdReader")
                .sql("SELECT id FROM user WHERE deleted = 0")
                .rowMapper((rs, rowNum) -> rs.getLong("id"))
                .fetchSize(100)
                .saveState(false)
                .build();
    }

    @Bean
    public ItemProcessor<Long, QuddyTI> createQuddyTIProcessor() {
        return userId -> QuddyTI.of(
                userId,
                DateUtil.formatYear(LocalDate.now()),
                DateUtil.formatMonth(LocalDate.now())
        );
    }

    @Bean
    public ItemWriter<QuddyTI> saveQuddyTIWriter() {
        return items -> quddyTIBatchJDBCRepository.bulkSave(items.getItems());
    }
}

๐Ÿ“ QuddyTIUpdateBatchScheduler.java

@Component
@RequiredArgsConstructor
public class QuddyTIUpdateBatchScheduler {
    private final JobLauncher jobLauncher;
    private final Job quddyTIUpdateJob;

    @Scheduled(cron = "0 0 0 1 * ?")
    public void runBatchJob() throws JobExecutionException {
        JobParameters jobParameters = new JobParametersBuilder()
                .addLong("time", System.currentTimeMillis())
                .toJobParameters();
        jobLauncher.run(quddyTIUpdateJob, jobParameters);
    }
}

๐Ÿ“ QuddyTIUpdateBatchConfig.java

@Configuration
@RequiredArgsConstructor
@EnableTransactionManagement
public class QuddyTIUpdateBatchConfig {
    private final DataSource dataSource;
    private final JobRepository jobRepository;
    private final DiaryCountService diaryCountService;
    private final QuddyTIBatchJDBCRepository quddyTIBatchJDBCRepository;
    private final PlatformTransactionManager transactionManager;

    @Bean
    public Job quddyTIUpdateJob(Step quddyTIUpdateStep) {
        return new JobBuilder("quddyTIUpdateJob", jobRepository)
                .start(quddyTIUpdateStep)
                .build();
    }

    @Bean
    public Step quddyTIUpdateStep() {
        return new StepBuilder("quddyTIUpdateStep", jobRepository)
                .<QuddyTI, QuddyTI>chunk(100, transactionManager)
                .reader(quddyTIReader())
                .processor(findCountProcessor())
                .writer(updateQuddyTIWriter())
                .build();
    }

    @Bean
    public JdbcCursorItemReader<QuddyTI> quddyTIReader() {
        LocalDate[] dates = DateUtil.getLastMonthDates();
        return new JdbcCursorItemReaderBuilder<QuddyTI>()
                .dataSource(dataSource)
                .name("quddyTIReader")
                .sql(
                """
                SELECT id, user_id, quddy_ti_year, quddy_ti_month, mood_buddy_status
                FROM quddy_ti 
                WHERE quddy_ti_year = ? AND quddy_ti_month = ? AND mood_buddy_status = ?
                """
                )
                .queryArguments(
                        DateUtil.formatYear(dates[0]),
                        DateUtil.formatMonth(dates[1]),
                        MoodBuddyStatus.DIS_ACTIVE.name()
                )
                .rowMapper(this::mapQuddyTI)
                .fetchSize(50)
                .saveState(false)
                .build();
    }

    private QuddyTI mapQuddyTI(ResultSet rs, int rowNum) throws SQLException {
        return QuddyTI.builder()
                .id(rs.getLong("id"))
                .userId(rs.getLong("user_id"))
                .quddyTIYear(rs.getString("quddy_ti_year"))
                .quddyTIMonth(rs.getString("quddy_ti_month"))
                .moodBuddyStatus(MoodBuddyStatus.valueOf(rs.getString("mood_buddy_status")))
                .build();
    }

    @Bean
    public ItemProcessor<QuddyTI, QuddyTI> findCountProcessor() {
        return quddyTI -> {
            LocalDate[] dates = DateUtil.getLastMonthDates();
            Map<DiaryEmotion, Long> emotionCounts = diaryCountService.getEmotionCountsByDate(quddyTI.getUserId(), dates);
            Map<DiarySubject, Long> subjectCounts = diaryCountService.getSubjectCountsByDate(quddyTI.getUserId(), dates);
            quddyTI.update(emotionCounts, subjectCounts);
            return quddyTI;
        };
    }

    @Bean
    public ItemWriter<QuddyTI> updateQuddyTIWriter() {
        return items -> quddyTIBatchJDBCRepository.bulkUpdate(items.getItems());
    }
}

์ •์ƒ์ ์œผ๋กœ ๋Œ์•„๊ฐ€๋Š”์ง€ ํ™•์ธํ•˜๊ธฐ ์œ„ํ•ด ๋”๋ฏธ ๋ฐ์ดํ„ฐ๋ฅผ ๋„ฃ๊ณ  ์‹คํ–‰ํ•œ ๊ฒฐ๊ณผ, ์ •์ƒ์ ์œผ๋กœ ๋Œ์•„๊ฐ€๋Š” ๊ฑธ ํ™•์ธํ•  ์ˆ˜ ์žˆ๋‹ค.


Result

๋ฌธ์ œ ํ•ด๊ฒฐ๋กœ ์•„๋ž˜์™€ ๊ฐ™์€ ๊ฒฐ๊ณผ๋ฅผ ์–ป์—ˆ๋‹ค.
1. ์‚ฌ์šฉ์ž 10,000๋ช… ๊ธฐ์ค€, ์ฟผ๋””ํ‹ฐ์•„์ด ์ƒ์„ฑ ํ‰๊ท  ์•ฝ 919ms, ์ˆ˜์ • ํ‰๊ท  ์•ฝ 14,528ms๋กœ ์„ฑ๋Šฅ ๊ฐœ์„ .
2. ์Šค์ผ€์ค„๋Ÿฌ โ†’ Spring Batch ํŠธ๋ฆฌ๊ฑฐ ๋ฐฉ์‹์œผ๋กœ ์ „ํ™˜ํ•˜์—ฌ ์œ ์ง€๋ณด์ˆ˜์„ฑ์„ ํ–ฅ์ƒ.
3. JPA ๋Œ€์‹  JDBC ์‚ฌ์šฉ์œผ๋กœ ๋”ํ‹ฐ ์ฒดํ‚น ์˜ค๋ฒ„ํ—ค๋“œ๋ฅผ ์ œ๊ฑฐํ•˜๊ณ  batch ์„ฑ๋Šฅ ์ตœ์ ํ™”.
4. ์ฟผ๋””ํ‹ฐ์•„์ด ์ƒ์„ฑ๊ณผ ์ˆ˜์ • ์ž‘์—…์„ ๋ถ„๋ฆฌํ•˜์—ฌ DB ๋ถ€ํ•˜๋ฅผ ๋ถ„์‚ฐ


Thoughts

์ด๋ฒˆ Spring Batch ๋„์ž…์„ ํ†ตํ•ด ์Šค์ผ€์ค„๋Ÿฌ ๊ธฐ๋ฐ˜์˜ ๋Œ€๋Ÿ‰ ๋ฐ์ดํ„ฐ ์ฒ˜๋ฆฌ๋ฅผ ๊ฐœ์„ ํ•˜๋Š” ๊ฒฝํ—˜์„ ํ•  ์ˆ˜ ์žˆ์—ˆ๋‹ค.

์ฒ˜์Œ ์ ์šฉํ•œ ์Šค์ผ€์ค„๋Ÿฌ ๋ฐฉ์‹์—์„œ๋Š” ๋Œ€๋Ÿ‰ ๋ฐ์ดํ„ฐ ์กฐํšŒ ๋ฐ ์—…๋ฐ์ดํŠธ๋กœ ์ธํ•ด ํŠธ๋žœ์žญ์…˜์ด ๊ธธ์–ด์ง€๊ณ  ์„ฑ๋Šฅ ์ €ํ•˜ ๋ฌธ์ œ๊ฐ€ ๋ฐœ์ƒํ–ˆ์ง€๋งŒ, Spring Batch๋ฅผ ํ™œ์šฉํ•˜์—ฌ ์ž‘์€ ๋‹จ์œ„๋กœ ๋‚˜๋ˆ„์–ด ์ฒ˜๋ฆฌํ•  ์ˆ˜ ์žˆ์—ˆ์œผ๋ฉฐ ์„ฑ๋Šฅ์„ ๊ฐœ์„ ํ•˜๊ณ  ์•ˆ์ •์„ฑ์„ ํ™•๋ณดํ•  ์ˆ˜ ์žˆ์—ˆ๋‹ค.

์œ„ ๊ฒฝํ—˜๋“ค์„ ํ†ตํ•ด ๋Œ€๋Ÿ‰ ๋ฐ์ดํ„ฐ ์ฒ˜๋ฆฌ๋Š” ๋‹จ์ˆœํžˆ ์Šค์ผ€์ค„๋Ÿฌ๋กœ ์ ์šฉํ•ด์•ผ๊ฒ ๋‹ค๋Š” ์ƒ๊ฐ๋ณด๋‹จ Spring Batch์™€ ๊ฐ™์€ ํ”„๋ ˆ์ž„์›Œํฌ๋ฅผ ์ ์šฉํ•˜๋Š” ๊ฒƒ์ด ํ•„์ˆ˜์ ์ด๋ผ๋Š” ์ƒ๊ฐ์„ ํ–ˆ๋‹ค.

๋˜ Spring Data JPA์˜ ํŽธ๋ฆฌ์„ฑ์œผ๋กœ ์ธํ•ด ์ด์— ์˜์กดํ•˜๊ธฐ๋ณด๋‹จ ์•Œ๊ณ  ์“ฐ๋Š” ๊ฒƒ์ด ์ค‘์š”ํ•˜๋‹ค๋Š” ๊ฒƒ์„ ๊นจ๋‹ฌ์•˜๋‹ค. ๋ถˆํŽธํ•˜๋”๋ผ๋„ ์„ฑ๋Šฅ์ ์œผ๋กœ ๋–จ์–ด์ง€๋ฉด JDBC์™€ ๊ฐ™์€ ๊ธฐ์ˆ ์„ ์ ์šฉํ•  ์ˆ˜ ์žˆ๋Š” ์„ค๊ณ„ ๋Šฅ๋ ฅ์„ ๊ฐ–์ถ˜ ๊ฐœ๋ฐœ์ž๊ฐ€ ๋˜์–ด์•ผ๊ฒ ๋‹ค๊ณ  ์ƒ๊ฐํ–ˆ๋‹ค.


profile
๐Ÿ’ป ๐Ÿ’ป ๐Ÿ’ป

0๊ฐœ์˜ ๋Œ“๊ธ€

๊ด€๋ จ ์ฑ„์šฉ ์ •๋ณด