사람인 API 데이터 저장하는 로직 성능 개선

동오·2025년 2월 19일

트러블 슈팅

목록 보기
1/2

📌 스케줄러 성능 문제 분석 및 개선 방향

🚀 성능 개선을 하게된 이유

서버를 세팅할 때 처음에 사람인에서 채용공고를 가져와야 했다.

총 12,000개 가량의 데이터가 있었고 스케줄러에 기존 로직으로 저장을 했더니 평균 20분 정도 소요가 됐었다.

우리는 채용 공고를 전날 새로 올라온 데이터를 00시에 갱신해오기 때문에 채용 공고는 만아야 500개 안쪽이라 약 1분~2분 정도면 실시간성이 중요한 부분이 아닌지라 그렇게 사용자가 불편함을 겪을 수치는 아니라고 생각은 들었으나 그래도 발생하는 쿼리의 양이 만만치 않다고 느껴져서 리팩토링을 진행하게 되었다.

🏎️ 캐시를 사용한 이유

  • 사람인 채용 공고에는 직무 스킬 정보가 포함되어 있음.
  • 하지만, 사람인 API 페이지에 올라와 있는 공식 직무 스킬 코드 외에도 응답에 추가 정보가 포함됨.
  • 직무 스킬 코드와 이름의 순서가 뒤섞여 있어 임의로 추가하는 것은 리스크가 존재.
  • 따라서 공식 직무 스킬 코드만 DB에 저장하고, 없는 데이터는 저장하지 않도록 결정.
  • DB 데이터를 캐싱 저장소(Redis)에 저장하면 변동이 거의 없을 것이고,
  • 260개의 데이터는 Redis에서 부담되지 않는 수준이라 판단하여 사용.

✅ 기존 Flow

  1. 사람인 API 호출JobPosting 엔티티 변환
  2. JobPosting 전체 저장 (병목 발생 가능)
  3. 응답받은 Job 데이터를 Map에 저장 (key: Job.id, value: Job 데이터)
  4. 저장된 JobPosting을 순회하면서 추가 처리
    • JobPosting.jobId와 일치하는 Job을 Map에서 가져옴
    • Job에서 jobCode를 가져와 ","로 분리
    • 분리된 jobCode를 순회하며:
      • Job_Skill 테이블에서 jobCode 조회 (병목 발생 가능)
      • 조회된 데이터를 JobPostingJobSkillList에 추가 후 저장 (병목 발생 가능)
  5. 전체 데이터 처리 후 남은 데이터가 있으면 재귀 호출

⚠️ 주요 병목 지점

  1. JobPosting 전체 저장 → 대량의 INSERT 쿼리 발생
  2. Job_Skill 테이블에서 jobCode 조회 → N번의 SELECT 쿼리 발생
  3. JobPostingJobSkillList에 추가 후 저장 → 더티 체킹으로 인해 추가적인 UPDATE 쿼리 발생

🔍 원인 분석

  • 쿼리 호출 횟수가 많음
    • JobPosting 저장 시 1회 INSERT
    • JobCode 조회 시 최소 1회, 최대 jobCodeArray.length 만큼 추가 SELECT
    • 더티 체킹으로 인한 추가 UPDATE 발생
  • 예상 쿼리 호출량 (1,100개 기준)
    • 최소 3,300번, jobCode가 5개씩 있는 경우 7,700번 발생 가능

🚀 성능 개선 방향

  • ✅ 전체 저장시 JobSkill까지 초기화 후 저장 (더티체킹 발생하지 않게 수정)
  • RedisJobSkill을 저장해서 캐싱 처리

개선 전 -> 1,100개 - 87초

전체 저장을 마지막으로 보냈을 때 -> 1,100개 - 80초 / 7초 개선

개선 후 -> 1,100개 -> 11초 / 개선 전보다 76초 개선

결과

개선 후 87% 성능 개선이 되었다.

기존 코드

  • 기존 코드 호출 결과 (87초)
    기존 코드
@Slf4j
@Service
@RequiredArgsConstructor
public class SchedulerService {

    private final JobSkillRepository jobSkillRepository;
    private final JobPostingRepository jobPostingRepository;
    private final ObjectMapper objectMapper;
    private final RestTemplate restTemplate;
    private final RetryTemplate retryTemplate;


    // URI로 조합할 OPEN API URL
    private final String API_URL = "https://oapi.saramin.co.kr/job-search";

    // URI로 조합할 apiKey
    @Value("${api.key}")
    private String apiKey;

    // URI로 조합할 한 페이지당 가져올데이터 수
    @Value("${api.count}")
    private Integer count;

    /**
     * 매일 자정(00:00)에 실행될 스케줄러 메서드입니다.
     * <p>
     * - retryTemplate.execute(context -> { ... }) -> API 요청이 실패할 경우 재시도를 수행하는 `RetryTemplate`을
     * 사용합니다. - processJobPostings (totalCount, totalJobs, pageNumber) -> API에서 채용 공고 데이터를 가져와
     * 데이터베이스에 저장하는 핵심 로직을 실행합니다.
     */
    @Scheduled(cron = "0 0 0 * * ?", zone = "Asia/Seoul")
    @Transactional
    public void savePublicData() {
        retryTemplate.execute(context -> {
            int pageNumber = 0;
            int totalCount = 0;
           	int totalJobs = 1100; //1. 1,100개 기준 성능 측정

			LocalDateTime start = LocalDateTime.now();
			processJobPostings(totalCount, totalJobs, pageNumber);
			LocalDateTime end = LocalDateTime.now();

            // 시간 차이 계산
			Duration duration = Duration.between(start, end);

            // 결과값 출력
			log.info("작업 실행 시간: {} 밀리초", duration.toMillis());
			log.info("작업 실행 시간: {} 초", duration.getSeconds());
            
            return null;
        });
    }

    /**
     * - 클래스 내에서 핵심로직이며, fetchJobPostings() 메소드를 통해 가져온 채용공고 데이터들을 저장하기위한 List<JobPosting>,
     * List<JobSkill> 로 변환하여, 저장하도록 하는 메서드이다.
     * - 오늘 가져올수있는 총 공고수(totalJobs) 보다 데이텁베이스에 저장된 공고수(totalCount) 크면 callBack 함수가 멈춘다.
     *
     * @param totalCount 현재 저장된 공고수
     * @param totalJobs  오늘 총 공고 수
     * @param pageNumber 현재 페이지 번호
     */
    public void processJobPostings(int totalCount, int totalJobs, int pageNumber) {
        Jobs jobs = fetchJobPostings(pageNumber, count);

        // JobPosting 클래스로 담기
        List<JobPosting> jobPostingList = jobs.getJobsDetail().getJobList().stream()
            .map(Job::toEntity)
            .toList();

        // 전체 저장
        List<JobPosting> savedJobPostingList = saveNewJobs(jobPostingList);

        //JSON 응답 파싱
        List<Job> jobList = jobs.getJobsDetail().getJobList();
        Map<Long, Job> jobMap = jobList.stream()
            .collect(Collectors.toMap(job -> Long.parseLong(job.getId()), job -> job));

        for (JobPosting jobPosting : savedJobPostingList) {

            //채용 공고랑 jobPosting이랑 일치하는 애 찾는 if문
            // 한 페이지에 해당하는 110개의 데이터를 방금 저장한 공고들인 jobPosting과 비교하여, 손수 job-code의 code를 꺼내기 위한 작업.
            Job findJob = jobMap.get(jobPosting.getJobId());
            String jobCode = findJob.getPositionDto().getJobCode().getCode();

            //여러개면 , 기준으로 짜르기
            String[] jobCodeArray = jobCode.split(",");

            for (String s : jobCodeArray) {
                // db에 저장된 jobSkill, code로 조회
                Optional<JobSkill> jobSkillOptional = jobSkillRepository.findByCode(
                    Integer.parseInt(s.trim()));

                //jobSkill DB에 없다면
                if (jobSkillOptional.isEmpty()) {
                    continue;
                } else {
                    JobSkill jobSkill = jobSkillOptional.get();

                    //JobPosting에 jobskill 설정
                    //더티 체킹으로 인해 업데이트 쿼리 자동 발생
                    jobPosting.getJobPostingJobSkillList().add(
                        JobPostingJobSkill.builder()
                            .jobPosting(jobPosting)
                            .jobSkill(jobSkill)
                            .build());
                }
            }
        }

        //총 가져와야되는 개수 초기화
        if (totalJobs == Integer.MAX_VALUE) {
            totalJobs = Integer.parseInt(jobs.getJobsDetail().getTotal());
        }

        totalCount += jobPostingList.size();

        if (totalCount < totalJobs) {
            processJobPostings(totalCount, totalJobs, ++pageNumber);
        }
    }

    /**
     * 지정된 페이지 번호와 가져올 데이터 개수를 기준으로 채용공고 데이터를 가져오는 메서드입니다.
     * <p>
     * - restTemplate : 주어진 URI로 채용공고 api 서버에 GET 요청을 보내, 응답 데이터를 받아오는 역할수행 - objectMapper : JSON
     * 문자열을 Jobs 객체로 변환하는 즉 역직렬화 역할수행.
     *
     * @param pageNumber 현재 페이지 번호
     * @param count      가져올 데이터 개수
     */
    private Jobs fetchJobPostings(int pageNumber, int count) {

        URI uri = UriComponentsBuilder.fromHttpUrl(API_URL)
            .queryParam("access-key", apiKey)
            //.queryParam("published", getPublishedDate())
            .queryParam("job_mid_cd", "2")
            .queryParam("start", pageNumber) // 현재 페이지숫자
            .queryParam("count", count)
            .queryParam("fields", "count")//한 번 호출시 가지고 오는 데이터 양
            .build()
            .encode()
            .toUri();

        try {
            String jsonResponse = restTemplate.getForObject(uri, String.class);

            Jobs dataResponse = objectMapper.readValue(jsonResponse, Jobs.class);

            if (dataResponse.getJobsDetail() == null || dataResponse.getJobsDetail().getJobList()
                .isEmpty()) {
                log.error(GlobalErrorCode.NO_DATA_RECEIVED.getMessage());
                throw new GlobalException(GlobalErrorCode.NO_DATA_RECEIVED);
            }

            return dataResponse;

        } catch (JsonProcessingException e) {
            log.error("JSON 파싱 실패", e);
            throw new GlobalException(GlobalErrorCode.JSON_PARSING_FAILED);
        }

    }

    /**
     * scheduler가 자정에 실행되기 때문에 전날 데이터를 가져오게 만든 메서드
     */
    private String getPublishedDate() {
        // 전날데이터
        LocalDate today = LocalDate.now().minusDays(1);
        DateTimeFormatter formatter = DateTimeFormatter.ofPattern("yyyy-MM-dd");
        return today.format(formatter);
    }

    /**
     * JobPosting, JobSkill 데이터들을 데이터베이스에 저장하기위한 메서드
     *
     * @param newJobs 가공된 JobPosting 데이터 리스트
     */
    private List<JobPosting> saveNewJobs(List<JobPosting> newJobs) {
        try {
            List<JobPosting> savedJobPostingList = jobPostingRepository.saveAll(newJobs);
            log.info("총 {}개의 공고를 저장했습니다.", savedJobPostingList.size());
            return savedJobPostingList;
        } catch (Exception e) {
            log.error(GlobalErrorCode.DATABASE_SAVE_FAILED.getMessage(), e);
            throw new GlobalException(GlobalErrorCode.DATABASE_SAVE_FAILED);
        }
    }


}

저장 메서드 위치 수정

  • 위치 수정 후 결과 (80초)
    저장 메서드 위치 변경 후
@Slf4j
@Service
@RequiredArgsConstructor
public class SchedulerService {

    private final JobSkillRepository jobSkillRepository;
    private final JobPostingRepository jobPostingRepository;
    private final ObjectMapper objectMapper;
    private final RestTemplate restTemplate;
    private final RetryTemplate retryTemplate;


    // URI로 조합할 OPEN API URL
    private final String API_URL = "https://oapi.saramin.co.kr/job-search";

    // URI로 조합할 apiKey
    @Value("${api.key}")
    private String apiKey;

    // URI로 조합할 한 페이지당 가져올데이터 수
    @Value("${api.count}")
    private Integer count;

    /**
     * 매일 자정(00:00)에 실행될 스케줄러 메서드입니다.
     * <p>
     * - retryTemplate.execute(context -> { ... }) -> API 요청이 실패할 경우 재시도를 수행하는 `RetryTemplate`을
     * 사용합니다. - processJobPostings (totalCount, totalJobs, pageNumber) -> API에서 채용 공고 데이터를 가져와
     * 데이터베이스에 저장하는 핵심 로직을 실행합니다.
     */
    @Scheduled(cron = "0 0 0 * * ?", zone = "Asia/Seoul")
    @Transactional
    public void savePublicData() {
        retryTemplate.execute(context -> {
            int pageNumber = 0;
            int totalCount = 0;
			int totalJobs = 1100; //1. 1100개 기준 성능 측정

			LocalDateTime start = LocalDateTime.now();
			processJobPostings(totalCount, totalJobs, pageNumber);
			LocalDateTime end = LocalDateTime.now();

            // 시간 차이 계산
			Duration duration = Duration.between(start, end);

            // 결과값 출력
			log.info("작업 실행 시간: {} 밀리초", duration.toMillis());
			log.info("작업 실행 시간: {} 초", duration.getSeconds());
            
            return null;
        });
    }

    /**
     * - 클래스 내에서 핵심로직이며, fetchJobPostings() 메소드를 통해 가져온 채용공고 데이터들을 저장하기위한 List<JobPosting>,
     * List<JobSkill> 로 변환하여, 저장하도록 하는 메서드이다.
     * - 오늘 가져올수있는 총 공고수(totalJobs) 보다 데이텁베이스에 저장된 공고수(totalCount) 크면 callBack 함수가 멈춘다.
     *
     * @param totalCount 현재 저장된 공고수
     * @param totalJobs  오늘 총 공고 수
     * @param pageNumber 현재 페이지 번호
     */
    public void processJobPostings(int totalCount, int totalJobs, int pageNumber) {
        Jobs jobs = fetchJobPostings(pageNumber, count);

        // JobPosting 클래스로 담기
        List<JobPosting> jobPostingList = jobs.getJobsDetail().getJobList().stream()
            .map(Job::toEntity)
            .toList();

        //JSON 응답 파싱
        List<Job> jobList = jobs.getJobsDetail().getJobList();
        Map<Long, Job> jobMap = jobList.stream()
            .collect(Collectors.toMap(job -> Long.parseLong(job.getId()), job -> job));

        for (JobPosting jobPosting : jobPostingList) {

            //채용 공고랑 jobPosting이랑 일치하는 애 찾는 if문
            // 한 페이지에 해당하는 110개의 데이터를 방금 저장한 공고들인 jobPosting과 비교하여, 손수 job-code의 code를 꺼내기 위한 작업.
            Job findJob = jobMap.get(jobPosting.getJobId());
            String jobCode = findJob.getPositionDto().getJobCode().getCode();

            //여러개면 , 기준으로 짜르기
            String[] jobCodeArray = jobCode.split(",");

            for (String s : jobCodeArray) {
                // db에 저장된 jobSkill, code로 조회
                Optional<JobSkill> jobSkillOptional = jobSkillRepository.findByCode(
                    Integer.parseInt(s.trim()));

                //jobSkill DB에 없다면
                if (jobSkillOptional.isEmpty()) {
                    continue;
                } else {
                    JobSkill jobSkill = jobSkillOptional.get();

                    //JobPosting에 jobskill 설정
                    jobPosting.getJobPostingJobSkillList().add(
                        JobPostingJobSkill.builder()
                            .jobPosting(jobPosting)
                            .jobSkill(jobSkill)
                            .build());
                }
            }
        }

        // 전체 저장 (위치 변경)
        List<JobPosting> savedJobPostingList = saveNewJobs(jobPostingList);

        //총 가져와야되는 개수 초기화
        if (totalJobs == Integer.MAX_VALUE) {
            totalJobs = Integer.parseInt(jobs.getJobsDetail().getTotal());
        }

        totalCount += jobPostingList.size();

        if (totalCount < totalJobs) {
            processJobPostings(totalCount, totalJobs, ++pageNumber);
        }
    }

    /**
     * 지정된 페이지 번호와 가져올 데이터 개수를 기준으로 채용공고 데이터를 가져오는 메서드입니다.
     * <p>
     * - restTemplate : 주어진 URI로 채용공고 api 서버에 GET 요청을 보내, 응답 데이터를 받아오는 역할수행 - objectMapper : JSON
     * 문자열을 Jobs 객체로 변환하는 즉 역직렬화 역할수행.
     *
     * @param pageNumber 현재 페이지 번호
     * @param count      가져올 데이터 개수
     */
    private Jobs fetchJobPostings(int pageNumber, int count) {

        URI uri = UriComponentsBuilder.fromHttpUrl(API_URL)
            .queryParam("access-key", apiKey)
            // .queryParam("published", getPublishedDate())
            .queryParam("job_mid_cd", "2")
            .queryParam("start", pageNumber) // 현재 페이지숫자
            .queryParam("count", count)
            .queryParam("fields", "count")//한 번 호출시 가지고 오는 데이터 양
            .build()
            .encode()
            .toUri();

        try {
            String jsonResponse = restTemplate.getForObject(uri, String.class);

            Jobs dataResponse = objectMapper.readValue(jsonResponse, Jobs.class);

            if (dataResponse.getJobsDetail() == null || dataResponse.getJobsDetail().getJobList()
                .isEmpty()) {
                log.error(GlobalErrorCode.NO_DATA_RECEIVED.getMessage());
                throw new GlobalException(GlobalErrorCode.NO_DATA_RECEIVED);
            }

            return dataResponse;

        } catch (JsonProcessingException e) {
            log.error("JSON 파싱 실패", e);
            throw new GlobalException(GlobalErrorCode.JSON_PARSING_FAILED);
        }

    }

    /**
     * scheduler가 자정에 실행되기 때문에 전날 데이터를 가져오게 만든 메서드
     */
    private String getPublishedDate() {
        // 전날데이터
        LocalDate today = LocalDate.now().minusDays(1);
        DateTimeFormatter formatter = DateTimeFormatter.ofPattern("yyyy-MM-dd");
        return today.format(formatter);
    }

    /**
     * JobPosting, JobSkill 데이터들을 데이터베이스에 저장하기위한 메서드
     *
     * @param newJobs 가공된 JobPosting 데이터 리스트
     */
    private List<JobPosting> saveNewJobs(List<JobPosting> newJobs) {
        try {
            List<JobPosting> savedJobPostingList = jobPostingRepository.saveAll(newJobs);
            log.info("총 {}개의 공고를 저장했습니다.", savedJobPostingList.size());
            return savedJobPostingList;
        } catch (Exception e) {
            log.error(GlobalErrorCode.DATABASE_SAVE_FAILED.getMessage(), e);
            throw new GlobalException(GlobalErrorCode.DATABASE_SAVE_FAILED);
        }
    }
}

Redis 캐시 도입

  • Redis 캐시 도입 후 결과 (11초)
    Redis 캐시 도입 후 결과
@Slf4j
@Service
@RequiredArgsConstructor
public class SchedulerService {

	private final JobSkillRepository jobSkillRepository;
	private final JobPostingRepository jobPostingRepository;
	private final ObjectMapper objectMapper;
	private final RestTemplate restTemplate;
	private final RetryTemplate retryTemplate;
	private final RedisRepository redisRepository;


	// URI로 조합할 OPEN API URL
	private final String API_URL = "https://oapi.saramin.co.kr/job-search";

	// URI로 조합할 apiKey
	@Value("${api.key}")
	private String apiKey;

	// URI로 조합할 한 페이지당 가져올데이터 수
	@Value("${api.count}")
	private Integer count;

	/**
	 * 매일 자정(00:00)에 실행될 스케줄러 메서드입니다.
	 * <p>
	 * - retryTemplate.execute(context -> { ... }) -> API 요청이 실패할 경우 재시도를 수행하는 `RetryTemplate`을
	 * 사용합니다. - processJobPostings (totalCount, totalJobs, pageNumber) -> API에서 채용 공고 데이터를 가져와
	 * 데이터베이스에 저장하는 핵심 로직을 실행합니다.
	 */
	@Scheduled(cron = "0 0 0 * * ?", zone = "Asia/Seoul")
	@Transactional
	public void savePublicData() {
		retryTemplate.execute(context -> {
			int pageNumber = 0;
			int totalCount = 0;
//            int totalJobs = Integer.MAX_VALUE;
			int totalJobs = 1000; //1. 1000개 기준 성능 측정
//            int totalJobs = 10000; //2. 10000개 기준 성능 측정

			LocalDateTime start = LocalDateTime.now();
			processJobPostings(totalCount, totalJobs, pageNumber);
			LocalDateTime end = LocalDateTime.now();

            // 시간 차이 계산
			Duration duration = Duration.between(start, end);

			// 결과값 출력
			log.info("작업 실행 시간: {} 밀리초", duration.toMillis());
			log.info("작업 실행 시간: {} 초", duration.getSeconds());

            return null;

        });
    }

	/**
	 * - 클래스 내에서 핵심로직이며, fetchJobPostings() 메소드를 통해 가져온 채용공고 데이터들을 저장하기위한 List<JobPosting>,
	 * List<JobSkill> 로 변환하여, 저장하도록 하는 메서드이다. - 오늘 가져올수있는 총 공고수(totalJobs) 보다 데이텁베이스에 저장된
	 * 공고수(totalCount) 크면 callBack 함수가 멈춘다.
	 *
	 * @param totalCount 현재 저장된 공고수
	 * @param totalJobs  오늘 총 공고 수
	 * @param pageNumber 현재 페이지 번호
	 */
	public void processJobPostings(int totalCount, int totalJobs, int pageNumber) {
		Jobs jobs = fetchJobPostings(pageNumber, count);

		// JobPosting 클래스로 담기
		List<JobPosting> jobPostingList = jobs.getJobsDetail().getJobList().stream()
			.map(Job::toEntity)
			.toList();

		//JSON 응답 파싱
		List<Job> jobList = jobs.getJobsDetail().getJobList();
		Map<Long, Job> jobMap = jobList.stream()
			.collect(Collectors.toMap(job -> Long.parseLong(job.getId()), job -> job));

		for (JobPosting jobPosting : jobPostingList) {
			// JobId로 분류된 JobMap에서 Job 꺼내기
			Job findJob = jobMap.get(jobPosting.getJobId());

			//꺼내온 Job 안에 JobCode 꺼내기
			String jobCode = findJob.getPositionDto().getJobCode().getCode();

			//여러개면 , 기준으로 짜르기
			String[] jobCodeArray = jobCode.split(",");

			for (String s : jobCodeArray) {
				String key = JobSkillConstant.JOB_SKILL_REDIS_KEY.getKey() + s;

				//Redis에서 KEY값이 있는지 없는지 조회
				//exists
				boolean hasKeyResult = redisRepository.hasKey(key);

				//만약 있다면 Redis에서 VALUE 조회해서 jobSkill 객체 생성
				if (hasKeyResult) {
					//JobSkillId 가져오는 로직
					Long jobSkillId = Long.valueOf(redisRepository.get(key).toString());

					//JobSkill 생성
					JobSkill jobSkill = JobSkill.builder()
						.id(jobSkillId)
						.build();

					jobPosting.getJobPostingJobSkillList().add(
						JobPostingJobSkill.builder()
							.jobPosting(jobPosting)
							.jobSkill(jobSkill)
							.build());
				}
			}
		}

		// 전체 저장
		List<JobPosting> savedJobPostingList = saveNewJobs(jobPostingList);

		//총 가져와야되는 개수 초기화
		if (totalJobs == Integer.MAX_VALUE) {
			totalJobs = Integer.parseInt(jobs.getJobsDetail().getTotal());
		}

		totalCount += savedJobPostingList.size();

		if (totalCount < totalJobs) {
			processJobPostings(totalCount, totalJobs, ++pageNumber);
		}
	}

	/**
	 * 지정된 페이지 번호와 가져올 데이터 개수를 기준으로 채용공고 데이터를 가져오는 메서드입니다.
	 * <p>
	 * - restTemplate : 주어진 URI로 채용공고 api 서버에 GET 요청을 보내, 응답 데이터를 받아오는 역할수행 - objectMapper : JSON
	 * 문자열을 Jobs 객체로 변환하는 즉 역직렬화 역할수행.
	 *
	 * @param pageNumber 현재 페이지 번호
	 * @param count      가져올 데이터 개수
	 */
	private Jobs fetchJobPostings(int pageNumber, int count) {

		URI uri = UriComponentsBuilder.fromHttpUrl(API_URL)
			.queryParam("access-key", apiKey)
			// .queryParam("published", getPublishedDate())
			.queryParam("job_mid_cd", "2")
			.queryParam("start", pageNumber) // 현재 페이지숫자
			.queryParam("count", count)
			.queryParam("fields", "count")//한 번 호출시 가지고 오는 데이터 양
			.build()
			.encode()
			.toUri();

		try {
			String jsonResponse = restTemplate.getForObject(uri, String.class);

			Jobs dataResponse = objectMapper.readValue(jsonResponse, Jobs.class);

			if (dataResponse.getJobsDetail() == null || dataResponse.getJobsDetail().getJobList()
				.isEmpty()) {
				log.error(GlobalErrorCode.NO_DATA_RECEIVED.getMessage());
				throw new GlobalException(GlobalErrorCode.NO_DATA_RECEIVED);
			}

			return dataResponse;

		} catch (JsonProcessingException e) {
			log.error("JSON 파싱 실패", e);
			throw new GlobalException(GlobalErrorCode.JSON_PARSING_FAILED);
		}

	}

	/**
	 * scheduler가 자정에 실행되기 때문에 전날 데이터를 가져오게 만든 메서드
	 */
	private String getPublishedDate() {
		// 전날데이터
		LocalDate today = LocalDate.now().minusDays(1);
		DateTimeFormatter formatter = DateTimeFormatter.ofPattern("yyyy-MM-dd");
		return today.format(formatter);
	}

	/**
	 * JobPosting, JobSkill 데이터들을 데이터베이스에 저장하기위한 메서드
	 *
	 * @param newJobs 가공된 JobPosting 데이터 리스트
	 */
	private List<JobPosting> saveNewJobs(List<JobPosting> newJobs) {
		try {
			List<JobPosting> savedJobPostingList = jobPostingRepository.saveAll(newJobs);
			log.info("총 {}개의 공고를 저장했습니다.", savedJobPostingList.size());
			return savedJobPostingList;
		} catch (Exception e) {
			log.error(GlobalErrorCode.DATABASE_SAVE_FAILED.getMessage(), e);
			throw new GlobalException(GlobalErrorCode.DATABASE_SAVE_FAILED);
		}
	}
}

0개의 댓글