Developer/Data Science

Fast API 로 3시간만에 추천 서버 만들기 (implicit, docker, gitlabCI)

디큐로그 2021. 6. 8. 00:29
728x90

목차

1. Fast API 를 도입할 절호의 기회가 왔다.

2. Implicit 을 통한 추천 결과 얻어내기

3. 로컬 환경에서 Fast API 서버로 추천 결과 받기

4. 도커(docker) + 깃랩CI (gitlab ci) 로 배포 자동화

5. 아쉬운 점과 Future work


#FastAPI #Implicit #추천서버 #도커 #깃랩CI


1. Fast API 를 도입할 절호의 기회가 왔다

 

  소규모 스타트업이 으레 그러하듯이 새로운 언어, 새로운 기술을 사용하는 것은 개발자의 욕심이 없으면 진행조차 되기 어렵다 (legacy 살려줘...). 우리 팀도 typescript 로 모든 코드 베이스가 짜져있었고, 파이썬 코드는 내가 합류한 뒤 아주 작은 모듈 정도만 존재하고 있었다. 하지만 나 역시 python에 진심인 편이라, 팀 내 gitlab에서 파이썬 코드의 비중을 늘릴 기회를 호시탐탐 노리고 있었다. 그러던 와중, 드디어 추천 서버를 따로 서빙해야하는 시점이 왔고, Node 환경에서 데이터 사이언스 피쳐를 만들고 싶지 않았기에 과감히 Fast API 를 선택해서 실서비스에 배포하기로 한다. 

  Fast API 도입을 결정하게 된 이유는 여러가지가 있는데,

가장 컸던 부분은 백재연님의 글(https://jybaek.tistory.com/890)을 보고 언젠가 꼭 실서비스에 도입해본다! 이런 목표의식(?) 

 

FastAPI 톺아보기 - 부제: python 백엔드 봄은 온다

파이썬은 인공지능, 빅데이터 분석/처리 트렌드와 함께 엄청난 인기를 얻었다. 전부터 마니아층이 두꺼웠지만, 본격적인 인기는 두 개의 트렌드와 함께했다. 파이썬이 없었다면 이 모든게 가능

jybaek.tistory.com

도 물론 있었지만, (백재연 님 좋은 글 공유 다시 한 번 감사드립니다...) 공식 문서를 보고 너무 간단해서 그냥 이거다 ! 느낌이 왔다. 

환경 설명을 먼저 간단하게 해보면...

1. 파이썬을 백엔드로 전혀 사용하고 있지 않다.

2. 테스트를 위해 빠르게 구현 후 A/B테스트를 하고 싶다.

3. admin이나, DB 테이블 관리 등은 필요없다.

 

그래서 그냥 단순하게 FastAPI 로 endpoint 1개짜리 서버를 띄우고, Node 에서는 axios 로 통신하자! 

생각하고 개발을 시작했다.

 

⚠️ 주의사항

  • 본 글은 DB 테이블로 추천 결과를 관리하거나 entity를 따로 설정하지 않음 
  • 추천 알고리즘에 대한 글이 아님
  • A/B테스트 이후 정식 피쳐가 되면 더 정교하게 다듬어야함 (빠르게 실험해보는 게 목적임)
  • 코드는 실제로 돌아가는 코드가 아닌, 컨셉을 이해시키기 위해 변형된 코드이므로 꼭 본인의 환경에 맞게 수정 후 사용

 

2. Implicit 을 통한 추천 결과 얻어내기

 

  implicit 라이브러리에 대한 자세한 설명은 이 글의 범위를 넘으므로 생략하겠다. 공식 github repo 참고 (https://github.com/benfred/implicit

 

benfred/implicit

Fast Python Collaborative Filtering for Implicit Feedback Datasets - benfred/implicit

github.com

그래도 이렇게 그냥 넘어가긴 좀 아쉬우니까 간단하게 train 하는 코드만 적어보자. 아래 파일은 recommender.py 라고 하자.

import pandas as pd
import numpy as np
from scipy.sparse import coo_matrix
from implicit.als import AlternatingLeastSquares


def model_train(items):
	model_name = 'als_model.sav'
	confidence = 40
    interaction = pd.read_sql('데이터베이스에서 df 뽑아내기')
    interaction['user_id'] = interaction['user_id'].astype('category')
    interaction['item_id'] = interaction['item_id'].astype('category')

	# sparse matrix 를 만들어줍니다.
    interaction_sparse = coo_matrix((np.ones(interaction.shape[0]),
                       (interaction['item_id'].cat.codes.copy(),
                        interaction['user_id'].cat.codes.copy())))

    model = AlternatingLeastSquares('파라미터들') 
    model.fit(confidence * interaction_sparse)
    pickle.dump(model, open(model_name, 'wb'))

Implicit 라이브러리에서 ALS를 써서 train 한 모델을 pickle.dump 로 바이너리 파일 형태로 저장해주었다. 

결과를 얻어낼 때 새로 모델을 학습하는게 아닌 이 파일에서 결과를 얻어낼 것이다.

내친김에 결과를 얻어내는 코드도 짚고 넘어가자.

def recommend(user_items, items):
    loaded_model = pickle.load(open(model_name, 'rb'))
    results = loaded_model.recommend(userid=0, user_items=user_items, recalculate_user=True, N=60)
    return [str(items[r]) for r, s in results]

사실 정말 나이브하게 짠 코드라, 학습 - 저장 - 불러오기 - 추천 결과의 과정을 수행하는데 더 효율적인 방법이 있을거라 확신한다. 

코드의 수준이 부끄러움에도 공유하는 이유는 고수 분들의 피드백을 통해 나도 성장할 수 있고, 또 어떤 독자들에게는 이런 코드도 도움이 될 수 있다는 걸 내 경험으로 알고 있기 때문이다. (미리 고맙습니다.. 힘냅시다.. 개발자분들.. )

 

이제 recommend 함수를 통해 추천된 아이템들의 ID 리스트를 얻을 수 있다. 그럼 Fast API 에서 결과를 얻어낼 준비가 된 것이다. 

 

3. 로컬 환경에서 Fast API 서버로 추천 결과 받기

root

ㄴ Dockerfile

ㄴ venv

ㄴ .env

ㄴ requirements.txt

ㄴ gitlab-ci.yml 

ㄴ app

      ㄴ main.py

      ㄴ recommender.py

      ㄴ als_model.sav

 

대략 이런 식의 폴더구조라는 걸 머릿 속에 넣어두고 시작하자.

(혹시 이거 폴더구조 이쁘게 그리는 법 아는 분 계시면 댓글로 정보 부탁드려요...)

 

main.py 는 이런식으로 생겼다. 

from fastapi import FastAPI
from recommender import recommend_api
app = FastAPI()


@app.get("/")
async def root():
    return {"message": "Hello World"}


@app.get("/recommend/{user_id}")
async def recommend(user_id: str):
    result = recommend_api(user_id)
    return {"result": result}

뭔가 글을 적다보니 end point 와 함수명 간에도 컨벤션 같은게 있을 것 같은데, 아쉬운 부분들은 Future work 에서 더 다뤄보도록 하겠다. 

 

이 코드에서 recommend_api 는 recommend 함수를 user_id 를 input으로 만들기 위해 만든 wrapper 함수라고 생각하면 좋다. (implicit 라이브러리에서 전처리 해줘야하는 부분들이 좀 더 있다. 이 글에선 다루지 않겠다.)

이제 결과를 얻어보자. 터미널에서 

cd app

uvicorn main:app --reload 

 

하면 로컬호스트에서 웹서버가 실행되고, localhost:8000 으로 접속하면 {"message": "Hello World"} 가 보여야한다.

그리고 추천 결과를 받기 위해 localhost:8000/recommend/3582912 와 같이 유저 샘플 ID를 넣어서 get 요청을 보내면,

 

 

추천 결과가 나오는걸 확인할 수 있다.

 

4. 도커(docker) + 깃랩CI (gitlab ci) 로 배포 자동화

 

  이제 배포를 하기 위해서 이 서버를 도커로 말아보자. 아래는 Dockerfile 의 내용이다.

FROM python:3.9

EXPOSE 80

COPY ./app /app

COPY .env .env

COPY requirements.txt /app/requirements.txt

WORKDIR /app

RUN python3 -m pip install --no-cache-dir --upgrade \
        setuptools \
        wheel \
        && \
    python3 -m pip install --trusted-host pypi.python.org -r requirements.txt

CMD ["uvicorn", "main:app", "--host", "0.0.0.0", "--port", "80"]

이 부분도 허접하지만, "동작하는 것"에 초점을 두고 봐주면 좋겠다.

특별한 내용은 없고, 상술한 폴더 구조를 docker로 말아서 웹서버를 실행할 수 있도록 만든 Dockerfile 이다.

아래 gitlab-ci.yml 파일을 보자.

image: python:3.9

variables:
  IMAGE_VERSION: v1.0.0
  WORK_DIR: .
  NODE_ENV: development
  DOCKER_DRIVER: overlay2
  DOCKER_FILE: Dockerfile
  DOCKER_BUILD_CONTEXT: .
  DOCKER_IMAGE: fast-recommender:${IMAGE_VERSION}
cache:
  key: '$CI_COMMIT_REF_SLUG'
stages:
  - deploy
Deploy:
  stage: deploy
  only:
    - master
  tags:
    - fast-recommender
  script:
    - docker build -t ${DOCKER_IMAGE} .

gitlab ci 도 이 글에서 다루기에는 양이 커서 모든 부분을 커버하긴 어렵지만, 이 코드를 어떻게 활용하면 되는지를 시뮬레이션 해보자. 

먼저 DOCKER_IMAGE 부분이다. 여기서 설정한 이름으로 서버에서 docker image가 생성된다. 그리고 tags 부분은 gitlab runner 중 어떤 tag를 가진 gitlab runner 가 이 CI 를 실행하는가 설정하는 부분이다. Gitlab의 레포지토리 Settings에서 CI/CD 탭으로 들어가면

위와 같이 Runners 부분이 있다. 여기에 runner 를 셋업하기 위한 URL 과 token 이 나와있으니, register 해주면 된다. 

Runner 를 등록하고 나면 위와 같이 등록된 Runner를 볼 수 있는데, 파랗게 표시되어 있는 부분이 tags 이다. gitlab-ci.yml 에서 fast-recommender 로 tags 를 지정해줬으므로, gitlab repository 에도 동일한 fast-recommender 를 태그로 갖고 있는 러너가 있어야 gitlab CI 가 동작할 것이다. 그리고 only: master 이므로, master 브랜치로 푸시했을 경우에만 CI가 동작한다.

모든 요건을 갖추어서 동작을 했다면 CI/CD 탭에서 Passed 표시를 볼 수 있을 것이다.

그리고 서버에서도 docker image 가 생성된 것을 확인할 수 있을 것이다.

그럼 이제 서버에서 실행시켜서 추천 결과를 받아올 수 있는지 확인해보자.

docker run -d --name fast-recommender -p 8080:80 fast-recommender:v1.0.0

참고로 -p 플래그는 외부(=서버)IP:내부(=도커)IP 로 쓰고, 지금 저 명령어는 도커의 80포트를 서버의 8080포트로 포워딩하겠다는 의미이다. 따라서 우리는 서버IP:8080 으로 접속했을때 {"message": "Hello World"} 를 확인할 수 있고, 추천 결과를 얻기 위해서는

서버IP:8080/recommend/{user_id} 로 get 요청을 보내야 한다는 뜻이다. 

마찬가지로 추천 결과가 잘 나오는 걸 확인할 수 있다. 

 

5. 아쉬운 점과 Future work

 

  빠른 시간 내에 실험을 런칭하다보니 아쉬웠던 점은 역시 코드 퀄리티이다. 만약 이 피쳐가 A/B테스트에서 좋은 결과를 얻어서 정식으로 서비스에 런칭해야한다면 분명 리팩토링이 필요하게 될 것이다. 개발자가 가장 싫어하는 비효율이 발생한 것이다. 다 능력이 부족한 탓이라 생각하고 더 열심히 공부하는 수 밖에 없다. 이 글에서 implicit 라이브러리를 활용한 CF 기반 추천, 백엔드, docker, CI 를 다루고 있는 만큼, 아쉬운 점도 정말 다양한 부분에 있는데, Future work 로 개인 백로그에 적기 위해서 이렇게 기록을 남기려고 한다.

 

  • implicit 모델 학습 배치화 - 현재 코드는 수동으로 implicit 모델을 학습해주어야한다. 실험 기간이 길지 않으므로 이렇게 만들었지만, 정식 피쳐가 될 경우 모델 학습 자체를 일정 주기에 따라 자동화 해주어야 한다.
  • FastAPI 네이밍 컨벤션 - FastAPI 가 자체적으로 docs, redoc 등을 제공하는 만큼 네이밍 컨벤션을 익히고 쓰는게 좋을거 같다는 생각이 들었다. 
  • docker + gitlab CI - docker 빌드 시에 속도/용량 측면에서 더 잘 할 수 있을 것 같다, gitlab ci 도 cache layer, before script 훅 등으로 최적화 시킬 수 있는 부분이 많은데, 할게 많다는 핑계로 신경을 하나도 안쓰고 있었다.
  • 배포 부분 - 지금 코드는 gitlab runner 가 docker image 를 빌드하는 것 까지만 자동화가 되어있기 때문에, 실제 서비스가 바뀌기 위해서는 기존에 있는 docker container 를 새로 만들어진 image 로 바꿔주는 작업이 필요하다. 크게 어려운 부분은 아닌 것 같은데 글을 쓰는 이 시점에는 모르는 부분이라, 자동화를 하지 못했다. 

 

FastAPI 를 알게 된게 1월이니, 반 년만에 실행에 옮긴 셈이다. 중간에 분명 시간이 있었음에도 못한 내 자신을 반성하게 된다.

사실 그간 따로 FastAPI 를 공부한 게 아니라 작업하면서 공식 문서를 처음 본거나 다름 없어서, 급하게 파이썬 서버를 띄울 일이 있을때,

FastAPI 를 추천하고 싶다. 다만, flask, django 등으로 이미 본인이 자주 쓰는 코드가 있거나, 고인물이라면 익숙한거 쓰는게 좋지 않을까... 막연하게 생각해본다. 

 

파이썬 스크립트를 Node 환경에서 바로 실행시키는 것도 고려해봤지만(ex. subprocess) 역시 효율적이지도 않고, 무엇보다 배포 단에서 너무 고통스러울게 뻔히 보였다. 대세인 MSA를 우리 팀에 적용해본다 생각하자. 시도하지 않으면 가능성은 0이다, 시도하면 1이될 가능성이 생긴다. 해보자. 

 

반응형