본문 바로가기

사이드 프로젝트

노래방 일본 노래 검색 서비스 | 제 9부 - 수집한 데이터 정재하기 (3)(TJ와 KY, 분열된 노래 데이터를 하나로 합치기 )

안녕하세요, 1인 사업가와 개발자 여러분! 펭귄 뮤지엄입니다. 🐧

지난 포스팅까지 우리는 제미나이(Gemini) API의 힘을 빌려 텅 비어있던 데이터에 생명을 불어넣었습니다. 하

지만 데이터 정제의 여정에는 아직 최종 보스가 남아있습니다.

바로 TJ와 KY(금영)로 나뉜 중복 데이터를 통합하고, 동일한 노래의 노래방 번호를 합치는 작업입니다.

 

상상해 보세요. 사용자가 애창곡을 저장하는데, 같은 노래를 'TJ 버전'과 'KY 버전'으로 따로 저장해야 한다면 얼마나 불편할까요?

훌륭한 사용자 경험을 위해서는 반드시 넘어야 할 산입니다.

이번 포스팅에서는 이 분열된 데이터를 어떤 전략으로 합칠 수 있는지, 그 험난하지만 의미 있는 과정을 모두 담아보려 합니다.

자, 데이터 정제의 마지막 여정을 함께 떠나보시죠!

 


 

1단계: 본격적인 통합 전, 각 진영 내부 정리하기 (내부 중복 제거)

가장 먼저 할 일은 각 데이터셋(TJ, KY) 내부에 존재하는 중복 데이터를 제거하는 것입니다.

"어째서 내부에 중복이 있나요?" 라고 물으신다면, 이는 데이터 수집(크롤링) 방식 때문입니다.

예를 들어 TJ 노래방 번호로 검색할 때 '1'을 포함하는 모든 번호(61560 등)와 '6'을 포함하는 모든 번호(61560 등)가 수집되면서, 같은 노래가 여러 번 저장된 것이죠.

 

이런 중복 데이터는 AI에게 불필요한 분석을 시켜 비용과 시간을 낭비하게 하고, 결과의 정확도를 떨어뜨릴 수 있습니다.

다행히 이 작업은 간단한 코드로 해결할 수 있습니다.

각 노래방 번호는 고유(Unique)하기 때문에, 이를 기준으로 중복을 제거하면 됩니다.

아래는 제가 사용한 Node.js 코드 예시입니다.

직접 작성하셔도 좋고, 저처럼 Gemini에게 요청하여 빠르게 만드셔도 좋습니다.

// 대상 파일과 출력 파일, 기준이 될 키를 설정합니다.
const TARGET_FILE = "./tjResult.js"; // 원본 데이터 파일
const OUTPUT_FILE = "tj.js"; // 결과물 파일
const TARGET_KEY = "tjSongNumber"; // 중복 판단 기준 키

const { song_list } = require(TARGET_FILE);
const fs = require("fs");
const path = require("path");

// Set을 활용하여 중복 여부를 효율적으로 체크합니다.
const removeDuplicateData = (data) => {
  const seenNumbers = new Set();
  const uniqueArray = data.filter((item) => {
    const isDuplicate = seenNumbers.has(item[TARGET_KEY]);
    seenNumbers.add(item[TARGET_KEY]);
    return !isDuplicate; // 중복되지 않은 아이템만 반환
  });
  return uniqueArray;
};

try {
  const uniqueArray = removeDuplicateData(song_list);

  console.log(
    `중복 제거 결과: ${song_list.length}개 -> ${uniqueArray.length}개`
  );

  const outputData = `export const tj_song_list = ${JSON.stringify(
    uniqueArray,
    null,
    2
  )};`;

  fs.writeFile(
    path.join(__dirname, OUTPUT_FILE),
    outputData,
    "utf8",
    (err) => {
      if (err) throw err;
      console.log(`✅ 성공! '${OUTPUT_FILE}' 파일로 저장되었습니다.`);
    }
  );
} catch (error) {
  console.error("🚨 처리 중 오류 발생:", error);
}

 

이 스크립트를 실행하자, 2만 개에 육박하던 데이터가 1만 개 수준으로 줄어들었습니다.

무려 50%나 감소하다니, 얼마나 많은 중복이 있었는지 실감이 나시나요?

이렇게 데이터를 '다이어트' 시켜야 다음 단계의 효율이 극대화됩니다.

 


 

2단계: 쉬운 상대부터 공략! 완벽히 일치하는 데이터 합치기 (코드 기반 병합)

내부 정리를 마쳤으니, 이제 본격적으로 두 데이터를 합쳐봅시다.

첫 번째 전략은 '노래 제목(jpName)'과 '아티스트명(jpArtistName)'이 완벽하게 일치하는 데이터를 찾아내는 것입니다.

 

이 작업 역시 AI에게 명확한 지시를 내려 코드를 생성하도록 했습니다.

 

 

[Gemini에게 요청한 프롬프트 요약]

  1. KY.js와 TJ.js 두 파일을 읽어들여.
  2. 두 파일의 배열 데이터를 비교해서, jpName과 jpArtistName이 완벽히 똑같은 데이터를 찾아줘.
  3. 똑같은 데이터를 찾으면, TJ 데이터 객체에 kySongNumber 키와 값을 추가해서 result_duplicated_list 배열에 넣어줘.
  4. 서로 짝이 없는 데이터는 각각 result_non_duplicated_ky_song_list와 result_non_duplicated_tj_song_list 배열에 따로 모아줘.
  5. 위 로직을 수행하는 Node.js 코드를 작성해줘.

 

Gemini는 이 지시에 따라 정확하게 동작하는 코드를 만들어주었고, 저는 1차적으로 중복된 데이터를 손쉽게 병합할 수 있었습니다.

하지만 현실은 동화가 아니죠.

각 노래방에서 아티스트 이름과 제목을 미묘하게 다르게 표기하는 경우가 허다합니다. 이제 진짜 어려운 상대가 남았습니다.

 


 

3단계: 최종 보스 등판! 미묘하게 다른 데이터, AI로 합치기 (AI 기반 병합)

(愛)という名の誇り 와 愛という名の誇り. 사람은 같은 노래임을 쉽게 알지만,

코드는 다른 데이터로 인식합니다. 바로 이럴 때, 문맥을 이해하는 AI의 '유사성 판단 능력'이 필요합니다.

남아있는 non_duplicated 리스트들을 비교하여 숨어있는 중복 데이터를 찾아내기 위해, 저는 다시 한번 Gemini에게 복잡한 임무를 부여했습니다.

 

 

[Gemini에게 요청한 프롬프트 요약]

  1. 파이썬 코드를 작성해줘. result.js 파일을 읽어서 3개의 배열(duplicated_list, non_duplicated_ky_song_list, non_duplicated_tj_song_list)을 파싱해.
  2. non_duplicated_tj_song_list에서 노래를 하나씩 꺼내.
  3. non_duplicated_ky_song_list에서 노래를 20개씩 묶어서, TJ 노래와 의미/문맥상 동일한 노래가 있는지 Gemini API로 판단해줘.
  4. 만약 중복을 찾으면, TJ 데이터를 기준으로 KY의 kySongNumber를 합쳐 duplicated_list에 추가하고, 각 non_duplicated 리스트에서는 해당 데이터를 제거해줘.
  5. 진짜 중복이었는지 내가 2차 검증을 할 수 있도록, 중복 처리된 노래 정보를 duplicate.log 파일에 기록해줘.
  6. tqdm 라이브러리로 전체 진행도를 시각적으로 보여줘.

 

 

🚨 예상치 못한 에러와 해결책: Safety Settings

그런데 위 프롬프트로 생성된 코드를 실행하자, 다음과 같은 에러가 발생했습니다.

Invalid operation: The `response.text` quick accessor requires the response to contain a valid `Part`, but none were returned. The candidate's [finish_reason](https://ai.google.dev/api/generate-content#finishreason) is 2.

 

조사해보니, 이는 Gemini가 노래 제목이나 가사 일부를 유해 콘텐츠나 저작권 위배 가능성이 있는 내용으로 판단하여 응답을 차단하는 안전 설정(Safety Settings) 때문이었습니다.

이 문제를 해결하기 위해 모델을 생성하는 부분에 안전 설정을 '차단 안 함'으로 명시하는 코드를 추가했습니다.

safety_settings = [
    {"category": "HARM_CATEGORY_HARASSMENT", "threshold": "BLOCK_NONE"},
    {"category": "HARM_CATEGORY_HATE_SPEECH", "threshold": "BLOCK_NONE"},
    {"category": "HARM_CATEGORY_SEXUALLY_EXPLICIT", "threshold": "BLOCK_NONE"},
    {"category": "HARM_CATEGORY_DANGEROUS_CONTENT", "threshold": "BLOCK_NONE"},
]
        
model = genai.GenerativeModel(
    model_name=GEMINI_MODEL,
    # ... 다른 설정들
    safety_settings=safety_settings
)

 

이 코드를 추가하자 에러는 말끔히 해결되었습니다.

혹시 비슷한 문제를 겪으신다면 꼭 참고하세요!

 


 

😱 장대한 여정의 끝? 현실의 벽에 부딪히다

하지만 진짜 문제는 다른 곳에 있었습니다.

바로 '시간'입니다. 남아있는 데이터는 TJ가 약 5,300개, KY가 약 4,000개. 제가 설계한 로직대로 예상 소요 시간을 계산해 보았습니다.

  • 총 비교 횟수: 5,307개 (TJ) × (3,984개 (KY) / 20개씩 묶음) ≈ 1,057,154회
  • 예상 소요 시간: 1,057,154회 × 10초/회 ≈ 10,571,544초
  • 시간으로 변환: 약 2,936시간 (약 122일)

네, 그렇습니다. 이 스크립트 실행 버튼을 누르고, 4달 뒤에 결과를 확인해야 하는 엄청난 시간이었습니다. 😅

 


 

💬 불만족스러운 결과, 그리고 여러분께 던지는 질문

솔직히 이번 포스팅은 만족스럽지 못합니다.

데이터를 병합하는 로직은 완성했지만, 현실적으로 실행하기 어려운 시간이라는 큰 벽에 부딪혔기 때문입니다.

일단 스크립트를 실행시켜 두고 다른 작업을 진행하고는 있지만, 마음 한구석이 편치 않네요.

 

이 엄청난 시간을 줄일 수 있는 더 좋은 아이디어가 있으신 분은 저에게 꼭 좀 알려주시면 정말 감사하겠습니다.

이것으로 길고 길었던 데이터 정제 및 병합의 여정은 일단락되었습니다.

비록 아쉬움이 남지만, 이 또한 개발 과정의 일부라 생각합니다.

여러분의 소중한 의견을 기다리며, 다음 포스팅으로 찾아뵙겠습니다!