안녕하세요! 오늘은 저희가 채용 공고 관련 서비스를 추가하는 과정에서 생겼던 문제와 이를 어떻게 해결해 갔는지를 말씀드려볼까 합니다.

문제

꼬꼬면 확장공사

사실 꼬꼬면 서비스의 풀네임은 꼬리에 꼬리를 무는 면접 이었습니다. 저희가 처음 기획하던 때까지만 하더라도 면접을 대비하기 위한 플랫폼을 만들고 싶다는 의지에서 출발하게 된 서비스였습니다.

서비스가 출시하고 500분 이상의 분들이 이용해주셨고, 그 중에서 가장 열심히 이용했던 팀원들도 모두 취업을 했습니다. 비교적 제가 마지막에 취업했긴 했지만, 꼬꼬면 덕분에 꼬꼬면 서비스를 이용하는 기간에 기술면접에서 떨어진 적은 없었어요(다 마지막에 인성면접에서 떨어졌다는…주륵). 아무튼 저도 결국엔 취업을 했었고 저희 서비스를 1개월 이상동안 이용하신 사용자 분들과도 인터뷰를 했었을 때 꼬꼬면 덕분에 기술면접을 잘 통과했다는 기쁜 소식도 들었습니다😊

하지만 서비스를 계속 개발하면서 추가적으로 우려되는 부분들도 있었어요. 가장 큰 문제점은 ‘서비스 자체가 장기사용자가 없을 법하다’라는 점이었습니다. 물론 면접을 도와주긴 하지만 정작 기술 면접이 끝난다면 통과하고 난 후에는 더이상 서비스가 통과하신 분들에게 가치를 제공해드릴 수 없다는 점에서 계속 마음이 걸렸습니다. 이러한 걱정은 유저 인터뷰를 하면서 더 커졌던 것 같아요.

그래서 이번에 꼬꼬면을 조금 더 채용의 전 과정을 도와주는 파트너로서의 포지셔닝을 해야겠다! 라고 생각하고 현재 열심히 개발하고 있습니다. 가장 최근에 확장된 기능으로는 AI를 통한 채용공고 적합도 분석과 채용 공고 서비스입니다. 채용 공고들을 꼬꼬면에서 탐색하고, 여기서 봤던 공고들을 기반으로 채용 공고와 내 이력서의 적합도를 판단할 수 있게끔 서비스를 제공해드려 취준의 시작부터 도와줄 수 있는 서비스가 될 수 있도록 확장했습니다.

직행과의 파트너십

채용 공고의 경우에는 현재 팀원이 저를 포함해서 새로 들어온 한명밖에 없었기 때문에, 여러 플랫폼에서 크롤링을 통해서 채용 공고 서비스를 제공하는것은 너무 비용이 크다고 판단했습니다. 그래서 저희는 현재 폭풍 성장중인 직행 에 컨택을 요청하여 크롤링 파트너십 계약을 맺고 정식으로 직행의 데이터를 가져올 수 있게 되었습니다.

만만치 않았던 크롤링 과정에서 생긴 문제

직행의 데이터를 사용할 수는 있었지만, 직접적인 API 형태로 제공받는 것이 아닌, 크롤링을 정식으로 할 수 있게끔 허용해주는 형식인 만큼 채용 공고에 대한 데이터를 직접 처리해야 할 필요성이 있었습니다. 하지만 이러한 데이터를 크롤링 하던 중 문제가 생겼는데요, 바로 각각의 채용 상세 공고가 이미지로 되어 있었다는 점이었습니다.

아마 직행 쪽에서 데이터를 수집할 때 각각의 크롤러들이 해당 전체 페이지에 대한 스크린샷을 그대로 저장하는 형태로 운영하고 있는 것으로 보입니다. 하지만 저희의 서비스가 더 발전하기 위해서는 해당 공고를 기반으로 이후에 채용공고 적합도 확인과 같은 기능들을 추가하는 과정에서 결국 텍스트를 추출해야 할 필요성을 느끼게 되었습니다.

1. ​OCR 도입

가장 처음에 생각했던 부분은 OCR의 도입이었습니다. 현재 오픈소스로 나와 있는 OCR 중에 easyocr이나, tesseract 와 같은 메이저 OCR 오픈소스는 대부분 한국어를 지원했습니다. 따라서 이러한 OCR 모델들을 CPU 작업으로 돌리게 된다면 ec2의 작은 인스턴스에서도 돌릴 수 있지 않을까? 하는 생각으로 시도해보았습니다.

대표적으로 테스트한 채용 공고는 위와 같은 채용 공고의 캡처본입니다. 크기는 1642 × 3720 사이즈를 가지고 있는 꽤나 큰 사진 파일이었어요.

결과는 두 OCR 모델 모두 t2.micro 기준으로 5분 이상이 소요되었습니다.

여기서 큰 문제는 두 가지가 있었습니다.

  1. 매일 하루종일 돌린다고 치더라도 현재 있는 4000개 이상의 공고를 모두 파싱하는 데에는 너무 많은 시간이 들었습니다.
  2. 텍스트 추출 이후에도 캡쳐본 내의 불필요한 텍스트들을 검출하여 필터링하기에는 현실적으로 어려웠습니다. 이에 저희는 OCR보다는 AI 쪽으로 눈길을 돌렸습니다.

2. OpenAI API를 활용한 OCR

최근의 AI는 특히 OCR과 같은 부분에서도 뛰어난 능력을 발휘합니다. 저희 또한 이러한 부분을 알고 있었기에, OpenAI의 GPT-5 mini 모델을 이용해서 base64로 이미지를 보낸 후에, AI에게 채용 공고 상세 쪽만 제대로 추출해서 반환해줄 수 있도록 프롬프팅 하였습니다.

위의 사진을 기준으로 AI에게 부탁한 이후에는 거의 이전의 OCR 정확도가 80%라고 한다면, GPT는 95% 이상의 정확도를 보여줬습니다.

구분easyocrtesseractOpenAI
정확도85%75%95%
속도5분 이상5분 이상30초 이하
CPU 이용률90% 이상90% 이상기본적인 정도

추가적으로 5분이 걸리던 CPU 집약적 작업들도 평균 10초, 최장 30초 정도의 시간으로 단축되었으며, 무엇보다 CPU를 많이 쓰지 않고 일반 HTTP 요청을 보낼 때처럼 보내기 때문에 인스턴스가 무리해서 장애가 날 확률이 많이 줄어들었습니다.

그렇다면 이대로 OpenAI를 사용하면 됐었는데, 사실 그 과정에서도 문제가 남아 있었습니다. 바로 토큰의 수가 많아 과금이 될 수 있다는 점이었습니다.

GPT를 5달러 이상 결제 후 Data-Sharing을 통해 입출력을 OpenAI쪽에 정보 제공을 하게 해준다면, 무료로 매일 250만 토큰씩 사용할 수 있습니다. 하지만 이걸 고려하더라도, 위의 사진의 경우에는 거의 요청당 5000토큰 이상이 소모되었습니다. 그렇다면 하루에 300개만 하더라도 과금이 될 수 있는 여지를 가지게 됩니다. 따라서 최대한 비용 절약을 위한 대책을 마련했어야 했습니다.

OpenAI에서는 이러한 이미지 input에 대한 비용 산정 기준을 기재해 놓았습니다. AI 프롬프팅 과정에서 이미지는 화질과 같은 요소들은 더 비싸지는 것이 아닌, 대부분 크기에 의해 가격의 변동 폭이 가장 컸습니다. 따라서 이미지 input을 위해서는 사이즈를 어느정도 조절할 필요성을 갖게 되었습니다.

3. Sharp 모듈을 이용한 토큰 절약

Nodejs에서는 이미지에 대해서 이런저런 처리를 할 수 있는 Sharp 모듈이 오픈소스로 제공됩니다. 저는 이러한 Sharp 모듈을 이용해서 500px정도의 적정한 크기만큼 width를 줄이고 이를 기반으로 프롬프트에 base64 형태로 넣어줄 수 있도록 하였습니다.

500px로 정한 이유는 너무 너비가 작아도 제대로 글씨가 못 읽히는 경우가 많기 때문에 이러한 부분들을 고려하면서 500px정도가 어느정도 AI가 글씨를 판단하고 추출할 수 있을 정도였기 때문에 선정하게 되었어요.

500px로 줄인 이미지를 다시금 프롬프트에 넣었을 때, 결과물은 이전과 동일하게 95% 이상의 정확도를 보여주면서도 토큰은 2500정도로 50% 정도의 토큰만을 사용하여 처리할 수 있게 되었습니다. 따라서 같은 t2.micro 인스턴스를 사용하면서 비용을 추가적으로 사용하지 않고 1~20초정도 걸릴 정도로 개선하게 된 셈이죠.

관련 코드는 아래에서 보실 수 있습니다.

// OCR 일부 
  public async parseRecruitmentsFromWaitingList() {
  // connection 만들기
    const connection = await createConnection();
    try {
      const [rows] = await connection.query<OCRWaitingListRow[]>('SELECT * FROM ocr_waiting_list');
      console.log(`Found ${rows.length} recruitments to parse`);
 
      let OCR_PARSING_LIMIT = 300;
      for (const row of rows) {
        OCR_PARSING_LIMIT--;
        if (OCR_PARSING_LIMIT < 0) {
          console.log('OCR_PARSING_LIMIT reached');
          return;
        }
        const { recruit_id, image_url, id } = row;
        const imageUrl = JSON.parse(image_url)[0];
        const text = await this.parseRecruitmentFromImage(imageUrl, recruit_id.toString());
        // 하나의 채용 공고마다 트랜잭션 설정
        await connection.beginTransaction();
        await connection.query('UPDATE recruit SET content = ? WHERE id = ?', [
          text?.toString(),
          recruit_id,
        ]);
        await connection.query('DELETE FROM ocr_waiting_list WHERE id = ?', [id]);
        await connection.commit();
      }
    } catch (error) {
    // 오류시 롤백 
      console.error('Error parsing recruitments from waiting list', error);
      await connection.rollback();
    } finally {
      await connection.end();
      console.log('connection end');
    }
  }

추가적으로 이러한 작업은 Docker에 컨테이너로 띄워둔 뒤, cronjob을 설정하여 매일 자동으로 돌리게 함으로써 추가적인 인스턴스를 따로 할당하는 것이 아닌, 기존에 있던 인스턴스 내에 컨테이너를 하나만 새로 띄우는 정도로도 해결이 가능하여 결과적으로 부과해야 하는 비용은 모두 0원이었습니다.

결론

이미지를 텍스트 추출해야 하는 상황에서 처음에는 OCR로 시작했지만, 이후에는 AI를 활용하여 관련 텍스트를 정확도 높게 추출하고 추가적으로 비용을 하나도 쓰지 않고 자동화된 채용공고 OCR 파이프라인을 구축할 수 있었습니다.

비용을 크게 줄이면서도 정확도 높게 텍스트를 추출할 수 있게 되었다는 사실이 나름 뿌듯하기는 하지만, 결국 요즘에는 대부분이 AI로 문제 해결이 귀결되는 느낌이라 아무래도 조금의 찝찝함이 남아있는 것 같습니다.