AWS 람다(Lambda)로 실시간 추천하기 – 로켓펀치의 전문기술 정보

로켓펀치의 전문기술 정보

로켓펀치에는 다양한 종류의 비즈니스 정보들이 있습니다. 홈페이지를 보면 상단 네비게이션 바의 네트워크, 기업, 채용, 투자, 비즈니스 맵 등이 상위 항목 정보로서 여러 가지 또다른 세부 정보를 가지고 있는 것을 알 수 있습니다. 이 중에서도 사람과 기업, 채용정보는 전문기술 정보를 포함하고 있는데 이는 이용자가 각 항목에서 알고 싶은 내용들을 나타내고 있습니다. 우리가 어떤 사람이나 기업에 대해서 ‘이 분은 어떤 걸 잘 하는 걸까?’, ‘여기서 사용하는 기술들은 뭐지?’와 같은 궁금증을 가지는 것은 자연스러운 일입니다. 구인을 하는 기업 입장에서 어떤 기술을 가진 사람을 원하는지 나타내는 것도, 구직을 하는 입장에서 기업이 어떤 스킬을 가진 인재를 원하는지 알아야 하는 것도 당연합니다.

전문기술의 특징

전문기술은 다른 정보와는 조금 다른 특징을 가지고 있습니다. 개인의 경력이나 기업의 이력과 같은 정보들은 각 항목이 변하지 않는 사실을 담고 있으며 여러 가지 항목의 중요도 또한 큰 차이가 없습니다(정말 알리고 싶지 않은 사실이 있다면 이야기가 다르겠지만요). 반면에 이 전문기술이라는 정보는 그 시점과 환경에 따라 각 항목의 중요도가 달라집니다.

어떤 개발자가 자신의 전문기술 항목을 채워넣을 때를 생각해보겠습니다. 이 사람은 아마도 현재 자기가 주로 다루는 프로그래밍 언어를 입력할 겁니다. 그리고 여러 가지 라이브러리와 주로 사용하는 도구 및 서비스를 추가로 입력할 수 있겠죠. 이 때 고민거리가 생길 수 있습니다.
첫 번째는 환경이 변하는 경우입니다. 이직을 한 직후 사용하는 언어와 개발 환경이 바뀐 경우 개인정보에서 이를 수정해야 할 수 있습니다. 경력이 오래 되어 더 이상 사용하지 않게 된 기술들이 생긴 경우 그것들이 자신을 나타내는 정보라고 보기에도 석연치가 않습니다. 마찬가지로 수정이 필요합니다.
두 번째는 항목 간의 상대적 중요도 차이가 생기는 경우입니다. ‘사용할 줄은 아는데 이걸 굳이 쓰는 게 맞는 걸까? 그렇다고 빼자니 아쉽고…’ 와 같은 고민은 경험한 기술들이 많으면 많을수록 느끼기 쉽습니다. 이럴 때는 여러 가지 전문기술들 중에서 선택을 해야합니다. 기업 또한 이와 같은 일들이 생길 수 있습니다.

이와 같은 문제 때문에 전문기술 정보를 입력한다는 것은 다른 정보와 비교해 상대적으로 어려운 면이 있습니다. 하지만 위에서 말했듯이 전문기술 정보는 상당히 중요한 정보입니다. 이용자가 이를 잘 입력할 수 있도록 도와줄 수 있다면 입력하는 사람에게도 그 정보를 보는 사람에게도 많은 도움이 될 것입니다. 어떻게 하면 더 잘 입력하게 할 수 있을까요?

첫 번째 방법

많이 입력할 것 같은 전문기술들의 목록을 입력란 바로 아래에 나열해 놓고, 클릭하면 입력이 되도록 했습니다. 실제로 이용자들이 입력한 것들을 세어보면 어떤 것들이 있는지 금방 알 수 있습니다. Python, Linux, AWS, UI/UX 등등이 있겠죠. 하지만 이 방법은 명확한 한계가 있습니다. 자주 나오는 전문기술을 가진 이용자들은 쉽게 입력을 할 수 있겠지만 그렇지 않은 경우는 이 방법의 혜택을 받을 수 없습니다. 여러 가지를 입력해야 하는 경우 전부 목록에 있으면 좋겠지만 그렇지 않은 경우 자칫 입력을 빠뜨릴 수 있기도 합니다.

두 번째 방법

이용자가 입력한 전문기술들을 바탕으로 다음에 입력할 것을 추천해 줄 수 있다면 가장 좋을 것이라고 생각했습니다. 바로 아래와 같은 모습으로요.

  1. 첫 번째 예시
    specialty-recommendation-example1
  2. 두 번째 예시
    specialty-recommendation-example2

Collaborative Filtering

위와 같은 경우에 사용할 수 있는 추천 기법은 collaborative filtering이 있습니다. 간단히 말해  “X를 좋아하는 사람은 대개 Y를 좋아합니다. 당신은 X를 좋아하시죠, Y는 어떠십니까?” 라는 방식과 비슷합니다. 이용자가 있고 각각의 이용자들이 좋아하는 아이템들의 목록이 있을 때 제일 먼저 아이템 간의 유사도를 구합니다. 아이템 개수가 N일 때 N*N 행렬로 각 아이템 간의 유사도를 나타낼 수 있습니다. 임의의 아이템들이 주어졌을 때 이 행렬을 이용해 가장 유사한 아이템이 어떤 것인지 쉽게 계산할 수 있습니다. 아래와 같은 유사도 행렬이 있다고 해보겠습니다.

specialty-recommendation-similarity-matrix
다섯 개의 아이템(i1, i2, …, i5)이 있을 때 각각의 유사도를 나타낸 행렬입니다. 유사도는 0과 1사이의 값을 가진다고 가정했습니다. i1과 i2는 0.9로 상당히 비슷하다고 할 수 있습니다. 반면 i3과 i5는 0.1로 낮은 유사도를 가지고 있습니다. 이 때 어떤 사람이 i1과 i2를 좋아한다고 가정해보겠습니다. 이 사람이 선택한 i1과 i2를 가지고 다른 아이템을 추천할 수 있습니다. i1의 유사도 row는 (1, 0.9, 0.2, 0.3, 0.8)입니다. i2는 (0.9, 1, 0.4, 0.5, 0.8)이죠. 단순히 이 둘을 더하면 (1.9, 1.9, 0.6, 0.8, 1.6)이 됩니다. i1과 i2는 이미 골랐으니 가장 높은 값을 가지는 아이템은 i5입니다. 간단하게 이런 방법으로 추천을 할 수 있습니다.

로켓펀치에서는 유사도 행렬을 계산하는 부분과, 그 행렬을 가지고 아이템을 추천하는 부분 크게 두 가지로 나누어 개발했습니다. 처음에는 이 두 가지가 모두 웹서버에 모듈로 붙어 있었습니다. 여러 가지 이슈 때문에 이를 분리해서 AWS Lambda로 옮기기로 했습니다. 비정기적으로 일어나는 task를 처리하기 위해 서버 자원을 항상 실행하고 있을 필요가 없다는 점 외에도 여러 가지 장점이 있습니다(또 다른 사례 및 자세한 내용은 이 글을 참고하시면 좋습니다). 잘만 분리한다면 꽤 괜찮은 선택이 될 것 같았고, 큰 문제없이 분리가 되었습니다. 그런데…

문제 발생

AWS Lambda는 한 번 실행이 되면 그 때부터는 초기 리소스 로딩 비용이 없이 계속 호출이 되는 특징이 있습니다. 그리고 주기적으로 초기 시작을 하는데, 이 초기 시작 때 행렬을 가져와서 메모리에 올리는데 시간이 너무 오래 걸리는 것이었습니다. 이용자가 입력했을 때 즉시 계산을 완료해 입력 칸 아래에 추천을 해야 하는데 이것이 수십초씩 걸리는 것은 안 될 말입니다. 메모리에 올리는 것 그 자체도 문제였죠. AWS Lambda는 사용한 메모리와 실행 시간에 비례해 요금을 내야하기 때문입니다.

해결 방법

처음에 웹 서버에 실행하는 것을 생각해 유사도 정보를 메모리에 올린 것인데 접근 방법을 조금 다르게 할 필요가 있었습니다. 해결 방법은 생각보다 복잡하지 않았습니다. 사실 추천 과정에서 모든 전문기술의 유사도를 가지고 있을 필요는 없습니다. 바로 입력 칸에 있는 전문기술들과 다른 전문기술의 유사도 목록, 즉 커다란 행렬에서 row만 있으면 됩니다. 위 유사도 행렬 예시에서 알 수 있듯이, 유저가 고른 아이템이 i1, i2 뿐이라면 그 두 아이템의 유사도 row만 있으면 계산이 가능합니다. 이해를 돕기 위해 아래에 그림과 단순화한 코드를 첨부했습니다.

specialty-recommendation-memory-usage

# 개선 전
similarity_matrix = get_similarity_matrix() # 커다란 매트릭스를 메모리에 올리는데 상당한 시간과 서버 자원을 사용
for _id in user_input:
    result += similarity_matrix[_id]

# 개선 후
for _id in user_input:
    result += get_row_from_redis(_id) # 계산에 필요한 row만 가져와서 사용

이러면 N*N의 메모리를 사용하던 것을 k*N(k는 최대 수십 개 정도)의 공간만으로 해결할 수 있습니다. 각각의 row를 elasticache(redis)에 저장한 다음, 호출이 있을 때마다 필요한 row를 불러 간단하고 빠르게 계산을 마칠 수 있습니다. 입력한 전문기술 숫자만큼 redis 호출을 해야 하지만 실제 서비스가 되는 과정에서 평균 호출 시간이 100ms 미만임을 확인했습니다. 이 정도면 괜찮은 수준입니다.

마치며

간단한 알고리즘을 로컬에서 실행해 결과를 보는 것과 실제 서비스에 적용하는 것의 간극을 극복하고, 변하는 환경에서 일어나는 문제들을 해결하는 것이 바로 소프트웨어 엔지니어가 하는 일입니다. 로켓펀치는 멋진 이용자 경험을 제공하기 위해 데이터 분석을 통한 각종 기법들을 서비스에 적용할 예정입니다.

+ 로켓펀치를 함께 만들어 갈 인재들을 기다리고 있습니다.

 

– 글쓴이 : 로켓펀치 머신러닝 엔지니어 정희동

300원에 200만뷰 소화하기 – 서버리스 아키텍처 AWS 람다(Lambda) 활용 사례

 

로켓펀치에서는 기본적인 웹 서비스 외에 제휴를 통해 로켓펀치의 채용 공고를 위젯 형식으로 노출하는 경우가 있습니다. (예 : 플래텀 오른쪽 사이드바 위젯) 이런 위젯들을 운영하다 보니 몇 가지 추가적인 요구 사항이 생겼는데요. 주로 아래의 두 가지로 요약할 수 있습니다.

  • 제휴를 맺은 곳의 갑작스러운 트래픽 증가가 로켓펀치 웹 서비스에 영향을 주지 않을 것
  • 로켓펀치에서 일시적인 오류가 발생하더라도 제휴를 맺은 곳에서 가능하면 오류가 발생하지 않을 것

이 두 가지를 만족하기 위해 최초에 기존의 서버와 분리하여 별도의 서버를 구축하는 방안을 생각했습니다. 다만 이 서버를 구축하고 유지하는 여러 가지 비용의 문제로 쉽사리 진행되지 않고 있었습니다. 그러던 중 로켓펀치에서는 AWS Lambda를 통해 이를 손쉽게 구축할 수 있다는 것을 알았고 다음과 같은 이점을 기대할 수 있었습니다.

  • 서버리스 아키텍처(Severless Architecture)를 통해 서버의 배포 및 유지에 대한 비용을 대폭 감소
  • 필요할 때만 노출되기 때문에 불필요한 자원을 소모하지 않음
  • 기존의 서버와 분리하여 혹시 모를 트래픽 증가에 기존의 서버가 영향받지 않음

AWS Lambda는 더욱 많은 장점이 있으나 위에 언급된 내용만으로도 저희가 원하던 것을 손쉽게 얻을 수 있었기 때문에 큰 고민 없이 기존의 채용 공고 위젯을 AWS Lambda로 옮기는 것을 결정했습니다.

Zappa – Serverless Python

AWS Lambda로 채용 공고 위젯을 옮기는 것을 결정하고 몇 가지 고민 끝에 최종적으로 Zappa를 사용하기로 했습니다. Zappa는 Python 기반으로 AWS Lambda를 손쉽게 사용할 수 있게 하고 거기에 더해서 API Gateway까지 자동으로 설정해주어서 최종적으로 저희가 원하는 웹 서버를 간단하게 구축할 수 있는 도구입니다. 물론 원래 AWS에서 제공하는 것처럼 필요한 파일들을 압축하여 업로드하고 관련된 내용을 CLI 또는 웹 콘솔을 통해서 설정하여 구현할 수 있지만, 디버깅, 환경 설정, 배포 등의 이유로 프레임워크를 사용하는 게 좋습니다. Zappa 이외에 사용 가능한 서버리스 프레임워크는 다양하게 있습니다.

  • Serverless: 가장 유명하고 제공하는 기능이 많습니다. 문서화도 잘 되어 있는 편이고 Node.js, Python, Java, Scala를 지원합니다.
  • Apex: express.js를 개발한 TJ Holowaychuk이 만든 프레임워크. 배포 시 멱등성을 보장하며 가장 다양한 언어를 지원합니다. Node.js, Golang, Python, Java, Rust, Clojure를 지원합니다.
  • Chalice: AWS에서 개발한 프레임워크. Python을 지원하며 AWS Lambda와 API Gateway를 자동으로 설정해줍니다.

위의 프레임워크 외에도 더 많은 것들이 있으나 저희는 아래와 같은 이유로 Zappa를 사용하는 것으로 결정했습니다.

– 위에서 언급한 데로 API Gateway까지 설정을 해주기 때문에 배포 후 바로 웹에서 확인 가능합니다.
– 저희는 이미 Django 와 Flask를 사용 중이고 바로 활용할 수 있습니다. 이는 디버깅도 기존에 사용하던 방식을 거의 그대로 사용할 수 있음을 의미합니다. Zappa로 개발 시 실제로 구축된 내용 중 일부는 기존의 내용을 거의 그대로 사용하여 큰 변경 없이 개발이 진행되기도 했습니다.
– C 확장 패키지들을 바로 사용 가능합니다. (AWS Lambda에서 Python을 사용하는 경우 C 확장 패키지들은 반드시 Amazon Linux에서 컴파일된 파일을 사용해야 합니다. 이런 부분이 개발 및 배포 시 불편할 수 있는데 Zappa에서는 이를 미리 컴파일된 파일을 자동으로 가져와서 배포 시 사용합니다)
– 재 수행 시 수행 시간을 느리게 만드는 원인 중 하나인 Cold Start를 방지할 수 있습니다. (설정 시 기본적으로 4분에 한 번씩 호출. 다만 저희는 그렇게 오랫동안 호출되지 않을 일이 없어서 최종적으로는 사용하지 않았습니다)

개발 및 AWS Lambda에 배포하기

Zappa로 개발하는 것은 아주 간단했습니다. 저희가 사용하던 내용 중 채용 공고 위젯의 내용을 일부 가져와서 동일하게 구현(Flask로 구현)하고 같은 개발 환경에서 테스트까지 완료되면 `zappa_settings.json` 파일만 설정하고 배포할 수 있습니다. 여기서는 `zappa_settings.json`에서 설정 가능한 일부 항목들만 간단히 소개하겠습니다.

{
  // API Gateway에서 사용하는 Stage 명입니다.
  "name": {
    // 배포 시 Zip 파일을 업로드할 S3 bucket의 이름입니다.
    "s3_bucket": "bucket-name",
    // 사용할 AWS Region을 설정합니다. 설정 전 반드시 AWS Lambda, API Gateway 그리고 CloudFormation이 올바르게 사용 가능한지 확인해야 합니다.
    // 작년까지 Seoul Region에서는 AWS Lambda, API Gateway는 올바르게 사용 가능하나 CloudFormation을 통해서 API Gateway를 설정하는 데 문제가 있었기 때문에 배포가 올바르게 진행되지 않았습니다.
    "aws_region": "ap-northeast-2",
    // WSGI 애플리케이션 함수. Flask, Django 등에 따라서 내용이 다릅니다. zappa init으로 보통 자동 설정됩니다.
    "app_function": "app.app",
    // Django 로 개발 시 사용되는 Django setting 파일의 위치입니다.
    "django_settings": "your_project.settings"
    // 배포 시 압축 파일에 포함되지 않을 내용을 설정합니다.
    "exclude": ["*.gz", "*.rar", ".git", ".gitignore"],
    // 배포 시 설정될 환경 변수입니다. `os.environ.get`으로 애플리케이션 내에서 간단히 사용 가능합니다.
    "environment_variables": {"your_key": "your_value"},
    // true로 설정 시 자동으로 CloudWatch 이벤트를 추가하여 Cold Start를 방지합니다.
    "keep_warm": true,
    // AWS Lambda에서 사용할 메모리 크기를 결정합니다. AWS Lambda 비용을 결정하는 가장 큰 요소 중 하나입니다.
    "memory_size": 128,
    // AWS Lambda 함수가 수행을 완료할 수 있는 최대 시간을 설정합니다.
    "timeout_seconds": 30,
    // AWS Lambda에 VPC를 설정합니다. 이를 통해 AWS 내의 다른 자원에 대한 접근이 가능합니다.
    "vpc_config": {
      "SubnetIds": ["subnet-12345678"],
      "SecurityGroupIds": ["sg-12345678"]
    }
  }
}

위 내용을 설정 후 Zappa의 deploy(또는 update) 명령어를 사용하는 것으로 배포가 완료되었습니다.

Calling update for environment name..
Downloading and installing dependencies..
100%|██████████████████████████████████| 39/39 [00:10<00:00, 5.27pkg/s]
Packaging project as zip..
Uploading file.zip (11.0MiB)..
100%|██████████████████████████████████| 11.5M/11.5M [00:00<00:00, 21.1MB/s]
Updating Lambda function code..
Updating Lambda function configuration..
Uploading template.json (2.8KiB)..
100%|██████████████████████████████████| 2.83K/2.83K [00:00<00:00, 37.0KB/s]
Deploying API Gateway..
Your deployed Zappa deployment is live!: https://abcd1234.ap-northeast-2.amazonaws.com/name

배포 후에는 url을 통해 바로 확인할 수 있으며 수정이 필요한 경우 수정 후 `zappa update {name}`을 통해 바로 업데이트할 수 있습니다. 저희가 실제로 배포하여 사용 중인 내용 중 하나는 https://widget.rocketpunch.com/banner로 Custom Domain과 SSL을 추가로 설정하여 배포하였으며 언제든 바로 확인할 수 있습니다. AWS Lambda를 사용하기로 하고 약 3일간의 작업을 통해서 개발 및 배포까지 완전히 끝낼 수 있었으며 아래에 기술되는 내용과 더불어 아주 큰 효과를 볼 수 있었습니다.

개발 완료 후

위에 언급한 것 이외에도 AWS Lambda 사용 시 추가적으로 기대하던 것이 하나가 있었습니다. 그것은 비용 절감입니다. AWS Lambda는 자원을 사용한 만큼 지불하기 때문에 저희가 사용 중인 간단한 위젯 화면의 경우는 자원을 매우 적게 사용하기 때문에 요청이 많더라도 일반적인 서버 구축과 비교하면 비용이 매우 적을 것으로 기대했습니다.
실제로 최근 한 달간 193만 Request가 있었고 이때 AWS Lambda의 비용은 $0.19였습니다. 이는 한화로 단돈 300원에 해당합니다.

추가로 API Gateway까지 사용한 금액은 총액 $6.95로 약 8,000원에 월 200만뷰 가까이 소화하는 것을 확인했습니다.
이는 기존의 간단한 서버 구축을 위해서도 사용되는 월 몇만 원의 비용, 트래픽이 몰리는 경우 자원 사용의 문제, 거기에 더해 서버 관리를 위해 소모되는 시간까지 생각하면 아주 적은 비용으로 원하는 내용을 모두 만족하게 되었습니다. 로켓펀치에서는 현재 채용 공고 위젯 외에 비동기로 수행되는 내용의 일부를 AWS Lambda를 사용하여 구현 중이며 기대만큼의 효과를 거두고 있어 앞으로 그 활용 영역을 더욱 넓힐 예정입니다.