더보기
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에서 계속…….