- 데이터 수집 및 전처리
- TF-IDF를 사용한 키워드 추출
- 벡터화
- 코사인 유사도 계산
- 코사인 유사도의 상위 10개의 레시피 추출
- GPT API로 2차 필터링
- 최종 레시피 3개 추천
결론적으로 배치 스케줄러를 사용했다.
배치 스케줄러는 특정 작업을 미리 정해진 시간이나 주기에 따라 자동으로 실행하도록 관리하는 시스템이다.
다음은 배치 스케줄러 클래스를 별도로 만들어서 작성한 코드이다.
// 1시간 간격으로 모든 사용자의 TF-IDF 계산
@Scheduled(cron = "0 0 * * * *", zone = "Asia/Seoul")
public void scheduleUserProfileUpdate() {
log.info("사용자 프로필 업데이트 배치 시작");
userRepository.findAll().forEach(user ->
userTFIDFService.updateUserTfidf(user.getUserId())
);
log.info("사용자 프로필 업데이트 배치 완료");
}
초 분 시 일 월 요일 [년]
예시) 매우 월요일부터 금요일까지 오전 9시 45분에 실행
0 45 9 ? * MON-FRI
TF-IDF값을 정규화했다.
📌 L1 정규화 (값의 절대 합을 1로 만듦)
📌 L2 정규화 (값의 제곱 합이 1이 되도록 조정)
📌 Min-Max Scaling (0~1 범위로 변환)
다음과 같이 UPSERT를 사용한 코드를 작성하여 문제를 해결했다.
@Modifying
@Query(value = """
INSERT INTO recipe_tfidf (recipe_id, feature, tfidf_value)
VALUES (:recipeId, :feature, :value)
ON DUPLICATE KEY UPDATE
tfidf_value = VALUES(tfidf_value)
""", nativeQuery = true)
@Transactional
void upsertTfIdf(@Param("recipeId") Long recipeId, @Param("feature") String feature, @Param("value") Double value);
처음에는 사용자 활동 요소로 레시피 분류, 레시피 메인재료, 레시피 재료를 생각하였다. 그러나 각각에 대해 TF-IDF 값을 계산하여 비교할 것이 아니므로 하나를 택하는게 맞다고 판단하였다.
다음은 TF-IDF 계산에 사용한 사용자의 활동 요소이다.
첫번째 단계인 모든 레시피에 대한 TF-IDF 계산 시에도 중복키 문제가 발생했었다.
이번에는 복합키를 사용하여 문제를 해결했다.
이 벡터값도 L2 정규화 하였다.
모든 레시피와 사용자의 TF-IDF 값을 사용하여 코사인 유사도를 계산했다.
코사인 유사도를 계산하여 10개의 레시피를 필터링 하였다.
사용자 활동 벡터값이 업데이트 되었는데 왜 유사도가 0.0 일까?
사용자 활동 벡터값에 업데이트 된 요소가 선호/기피 재료 뿐이었다.
선호/기피 재료에 대한 가중치를 각각 1, -1로 두었는데, 그 외의 활동 요소을에 따라 벡터값이 업데이트가 되니까 유사도가 정상적으로 계산되었다.
💡결론적으로, 이것은 데이터의 희소성 때문이었다.
레시피 데이터 크롤링 과정에서 동일한 데이터가 수집되어 DB에 접근하여 강제로 레시피 데이터를 삭제 하였는데, 그 과정에서 문제가 발생하였다.
CASCADE로 인해 레시피 ID를 외래키로 갖고 있는 모든 데이터가 삭제 될 줄 알았는데, 직접 DB에 접근하여 데이터를 삭제하였기 때문에 CASCADE가 되지 않았다. 이로 인해 데이터 삭제가 정상적으로 이루어지지 않아서 오류가 발생하였던 것.
또한, 이 문제로 서버를 재빌드 하면서 배치 스케줄러를 적용해 놓았던 사용자 프로필 벡터 계산 메서드가 지정된 시간이 아니었음에도 실행되었고, 당연히 지정된 시간이 아니었기 때문에 postman으로 강제로 실행시켰는데 꼬임 현상으로 중복 문제가 발생하였다.
전혀 문제가 될 것이 아니라고 생각했는데, 왜냐하면 사용자 활동 벡터값을 저장할 때 UPSERT를 사용했기 때문이다.
이를 통해 외래키로 사용하는 데이터를 삭제할 때 이러한 점을 고려해야한다는 것을 다시 느끼게 되었다.⭐
아래와 같이 프롬프트를 작성하였다.
### 현재 날씨 ###
현재 서울의 날씨: °C,
### 사용자 취향 분석 ###
1. 2. 3. 4. 5
### 추천 후보 목록 ###
레시피 ID:
제목:
종류:
소요 시간:
재료:
난이도:
과정 수:
유사도:
### 요청 사항 ###
- 추천 후보 목록 중에서 현재 날씨와 사용자 취향 분석을 반영한 상위 3개 요리 선정
- 선정된 상위 3개 요리의 레시피 ID만 리스트 형태로 응답
예시 응답 형식: [1234, 5678, 9012]
여기서 사용자 취향 분석 부분에는 랜덤으로 질문을 전달하여 답변을 넣은 것이다.