Troubleshooting: 무엇이 문제였는가?/취하여 프로젝트

1주 차: 알림 기능 찾아 삼만리 Day 2 - Spring Scheduler를 어떻게 고칠지 다시 정리하다

writingforever162 2025. 2. 16. 19:49

[알림 기능 찾아 삼만리 Day 1 링크]

더보기
package com.project.cheerha.domain.notice;

import com.project.cheerha.domain.notice.dto.JobOpeningDto;
import com.project.cheerha.domain.notice.dto.JobOpeningKeywordDto;
import com.project.cheerha.domain.notice.dto.UserDto;
import com.project.cheerha.domain.notice.dto.UserKeywordDto;
import com.project.cheerha.domain.notice.service.NoticeCreationService;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.scheduling.annotation.Scheduled;
import org.springframework.stereotype.Component;
import org.springframework.transaction.annotation.Transactional;

@Slf4j
@Component
@RequiredArgsConstructor
public class NoticeCreationScheduler {

    private final NoticeCreationService service;

    @Scheduled(cron = "*/30 * * * * *")
    @Transactional
    public void sendJobOpeningMatchingNotices() {

        // JobOpeningId 목록 및 keywordId 목록 조회
        List<JobOpeningKeywordDto> jobOpeningKeywordDtoList = service.findAllJobOpeningKeywords();

        // userId 목록 및 keywordId 목록 조회
        List<UserKeywordDto> userKeywordDtoList = service.findAllUserKeywords();

        // userId 목록 및 email 목록 조회
        List<UserDto> userDtoList = service.findAllUsers();

        // jobOpeningId 목록 및 url 목록 조회
        List<JobOpeningDto> jobOpeningDtoList = service.findAllJobOpenings();

        // 사용자별로 선택한 키워드 ID 목록을 저장할 Map
        Map<Long, List<Long>> userKeywordMap = new HashMap<>();

        // 사용자별로 선택한 키워드 매핑(mapping)
        for (UserKeywordDto dto : userKeywordDtoList) {
            userKeywordMap.computeIfAbsent(
                dto.userId(),
                // 사용자 ID를 기준으로
                keywordId -> new ArrayList<>()
                // 해당 사용자의 키워드 ID 목록 생성
            ).add(dto.keywordId());
            // 키워드 ID 추가
        }

        log.info("사용자별 키워드 목록: {}", userKeywordMap);

        // 채용 공고별로 포함된 키워드 ID 목록을 저장할 Map
        Map<Long, List<Long>> jobOpeningKeywordMap = new HashMap<>();

        // 채용 공고별로 포함된 키워드 매핑
        for (JobOpeningKeywordDto dto : jobOpeningKeywordDtoList) {
            jobOpeningKeywordMap.computeIfAbsent(
                dto.jobOpeningId(),
                // 채용 공고 ID를 기준으로
                keywordId -> new ArrayList<>()
                // 해당 채용 공고의 키워드 ID 목록 생성
            ).add(dto.keywordId());
            // 키워드 ID 추가
        }

        log.info("채용 공고별 키워드 목록: {}", jobOpeningKeywordMap);

        // 사용자별로 키워드를 확인하여 일치하는 채용 공고 찾기
        for (Map.Entry<Long, List<Long>> entry : userKeywordMap.entrySet()) {
            Long userId = entry.getKey();
            // 사용자 ID

            Set<Long> keywordIdSetChosenByUser = new HashSet<>(entry.getValue());
            // 사용자가 선택한 키워드 ID 집합

            // 사용자가 선택한 키워드와 일치하는 채용 공고 ID 목록 찾기
            List<Long> jobOpeningIdList = jobOpeningKeywordMap.entrySet().stream()
                .filter(jobOpening -> jobOpening.getValue().stream()
                    .anyMatch(keywordIdSetChosenByUser::contains))
                // 키워드 일치 여부 확인
                .map(Map.Entry::getKey)
                // 키워드가 일치하는 채용 공고 ID 추출
                .toList();

            // 키워드가 일치하는 채용 공고의 URL 추출 및 목록 생성
            List<String> jobOpeningUrlList = jobOpeningDtoList.stream()
                .filter(jobOpening -> jobOpeningIdList.contains(
                    jobOpening.jobOpeningId()))
                .map(JobOpeningDto::url)
                .toList();

            log.info("사용자 ID: {} - 매칭된 채용 공고 URL 목록: {}", userId, jobOpeningUrlList);

            // 사용자의 이메일로 채용 공고 url 보낼 준비
            userDtoList.stream()
                .filter(userDto -> userDto.userId().equals(userId))
                .forEach(userDto -> {
                        log.info("이메일 전송 준비: {}", userDto.email());
                    }
                );
        }
    }
}

처음에는 DTO를 총 4개 만들어서 각각 필요한 데이터를 가져왔는데, 이메일에 넣을 채용 공고의 URL 목록과 사용자의 이메일을 추출하고 연결한 순간, 코드 가독성이 곤두박질쳤다. 잠에 막 들 무렵에 전체 코드에서 합쳐도 되겠다 싶은 부분이 번뜩했다. 기억이 증발할세라, 보기 편하게 표로 우선 DTO마다 어떤 데이터가 있는지 정리했다. 겹치는 데이터를 같은 글자 색으로 칠해 놓으니, 중구난방으로 튀어나온 생각의 양이 확 줄었다. 

 

(1) UserDto와 UserKeywordDto를 하나로 합치고 총 세 종류 데이터 가져오기: keywordId, userId, email

(2) JobOpeningDto와 JobOpeningKeywordDto를 하나로 합치고 총 세 종류 데이터 가져오기: keywordId, jobOpeningId, url

이렇게 하면 데이터베이스와 소통하는 횟수도 절반으로 줄이고 불필요한 로직을 없앨 수 있을 듯했다.

 

Day 3에서 계속…….