R로 프로덕션 개발 운영 환경 구축하기

by Minho Lee — on

cover-image

2021년 11월 19일에 한국 R Conference 2021 에서 발표했던 내용을 블로그 게시용으로 다시 정리한 결과입니다. 전체 발표는 데이터라이즈의 김상현님과 함께 준비했으며, 발표 내용 중 제가 정리했던 PART2 내용 위주로 블로그에 다시 기록합니다.

전체 발표 내용은 다음 링크에서 확인해주세요!

프로덕션 수준의 운영 환경에서 필요한 것들

저는 데이터 분석가로 커리어를 시작했기 때문에 R로 간단한 코드를 짜거나 분석은 할 줄 알았지만 서비스를 운영하기 위해 필요한 개발적 지식은 많이 부족한 상태였습니다. 하지만 종종 R을 사용해 MVP를 만들어야 하는 상황이 발생했고, 몇 번의 시행착오를 겪다보니 프로덕션에 가까운 수준으로 서비스를 운영하기 위해서는 적어도 다음과 같은 내용을 달성해야 한다는 사실을 깨닫게 되었습니다.

  • R 코드를 로컬, 서버, 쿠버네티스 등 어떤 환경에서 실행하더라도 일관적으로 동작해야 합니다.
  • 시간이 지나도 현재와 동일하게 동작해야 합니다.
  • 문제가 발생했을 때, 어디서 어떤 에러가 발생했는지 바로 확인할 수 있어야 합니다.
  • R로 동작하는 다른 서비스에 의도치 않은 영향을 미치지 않도록 환경이 서로 분리되어 있어야 합니다.

그리고 앞에서 정리한 각각의 요구사항을 만족시키기 위해서 다음과 같은 방법들을 고민하여 적용해보게 되었습니다.

  • 일관성을 위한 최고의 도구는 역시 : 도커 컨테이너
  • 예전 버전의 패키지를 사용하는 코드도 언제든 돌릴 수 있도록 : 패키지 버전 관리
  • 짜임새 있는 코드 구성을 위한 : 스크립트 모듈화
  • 에러를 기록하고 빠르게 대응할 수 있도록 : 로깅과 알림

(1) 도커 컨테이너

왜 필요할까?

  • 특정 R 버전을 사용해야 하거나, 프로젝트마다 서로 다른 R 버전이 필요한 경우가 있습니다.
  • 로컬, 서버, 쿠버네티스 환경 모두에서 일관적으로 동작해야 합니다.

어떻게 해야 할까?

  • 프로젝트의 특성에 맞는 도커 세팅을 준비해두었습니다.
    • plumber API 서버 → rocker/r-ver:4.1.1
      • 이미지 경량화를 위해 최소한으로 설치하고, R 버전을 명시합니다.
    • 분석 환경 → rocker/tidyverse:latest
      • 분석 관련된 디펜던시와 라이브러리를 최대한 많이 설치하고, 가능하면 최신 버전의 R을 사용할 수 있도록 합니다.
    • 모델링 배치 작업 → rocker/tidyverse:4.1.1 또는 r-ver + 필요한 라이브러리만 설치
      • 분석 관련된 디펜던시와 라이브러리 설치는 편리해야 하지만, R 버전은 명시합니다.
  • 필요한 환경에 맞게 docker-compose 스크립트를 분리해두고 사용합니다.
    • Prod → 실제 배포되었을 때를 기준으로 하는 배포용 설정.
    • Dev → 로컬 또는 서버에서 개발용 버전을 테스트하기 위한 설정.

(2) 패키지 버전 관리

왜 필요할까?

  • 분석용으로 주로 사용되는 R의 특성상, 가능하면 최신 버전의 라이브러리를 사용할 것을 권장하는 편입니다.
  • 라이브러리 업데이트로 인해 이전에 만들어둔 기능이 갑자기 동작하지 않으면 어떻게 해야 할까요?
  • 언제 라이브러리를 새로 설치하든지 간에, 코드를 작성할 때 설치되었던 버전을 기준으로 설치할 수 있다면 이러한 걱정을 크게 덜 수 있을 것입니다.

어떻게 해야 할까?

  • renv 를 사용했습니다.
    • renv는 lockfile을 통해 특정 시점의 라이브러리 상태를 저장합니다.
    • lockfile 에는 다음과 같은 정보가 담겨있습니다.
      • renv 라이브러리 버전
      • 프로젝트에 사용된 R 버전
      • R 레포지토리 주소
      • 패키지별 정보와 설치 방식
  • renv 가 동작하는 방식은 다음과 같습니다.
    • renv::snapshot() 으로 lockfile 을 생성합니다. → renv.lock
    • renv::restore() 를 사용해 저장된 환경에 맞게 다시 설치합니다.
  • 도커와 함께 사용하기
    • renv.lock 파일을 통해 라이브러리를 설치하도록 합니다.
    • lock 파일이 변경되지 않았다면 도커의 캐시로 인해 빌드 시간을 단축할 수 있습니다.
#### Dockerfile 내에서 renv로 라이브러리 설치하기 ####
# install dependencies
COPY ./renv.lock .
RUN Rscript -e "renv::restore()"

# copy the app
COPY app .

(3) 스크립트 모듈화

왜 필요할까?

  • 소스 코드가 길어지고 내용이 복잡해지면 유지 관리가 어려워집니다.
  • source 를 통해 분리된 R 스크립트를 불러오면 전체 스코프에 영향을 줄 수 있습니다.
  • library 는 패키지 내의 모든 함수를 attach 하기 때문에 함수가 꼬이는 경우가 발생합니다.
  • 모듈화를 통해 함수를 재사용하고 싶지만 패키지로 작성하고 싶지는 않을 때가 있습니다.

어떻게 해야 할까?

  • modules 또는 box 를 사용해 모듈화할 수 있습니다.
    • modules 는 깔끔하고 직관적인 문법이 장점입니다.
    • box 는 Python의 내부 모듈 시스템과 유사하여 다른 언어에서 개발을 경험해본 사람들이 쉽게 익숙해질 수 있고, 더 다양한 기능을 제공합니다.
  • 개인적으로는 1버전까지 올라가있으며, 현재 활발히 개발 및 유지보수가 진행중인 box 를 선호합니다.

modules

# src/data.R
import("DBI")
import("RMySQL")
import("lubridate")
import("dplyr")

export("get_data_from_db")

get_data_from_db <- function() {
  # ...
}
# run.R
Module <- modules::use("src/data.R")
data <- Module$get_data_from_db()

box

# src/data.R
box::use(
  DBI[dbGetQuery],
  RMySQL,
  lubridate,
  dplyr
)

#' @export
get_data_from_db <- function() {
  # ...
}
# run.R
box::use(Module = src/data)
data <- Module$get_data_from_db()

모듈화에 대한 고민

회사 내의 R 사용자가 점점 늘어나면서 모듈화에 대한 고민이 더 많아졌습니다. 현재는 다음과 같은 정도로 사내 코드를 관리하고 있습니다.

  • 회사 내 모든 구성원들이 사용하는 기능은 패키지로 모아서 작성해둡니다.
    • DB 컨넥션 및 공통적으로 사용되는 데이터 전처리 함수
    • 그래프 디자인 템플릿
    • Rmarkdown 문서 디자인 템플릿
  • 특정 프로젝트 내에서 반복적으로 사용되는 함수는 기능별로 모아서 스크립트를 분리합니다.
    • 기능 단위로 스크립트를 나누어 두면 프로젝트가 복잡해지더라도 원하는 부분을 쉽게 찾을 수 있습니다.
    • 파일 단위로 쪼개지면 Git 으로 협업하기 용이해집니다.
  • 스크립트를 작성할 때 가능하면 라이브러리 전체를 임포트하는 것을 피하고, 라이브러리의 특정 함수를 임포트하도록 합니다.
    • 모든 함수를 attach하면, 현재 스크립트에서 해당 라이브러리가 실제로 사용되고 있는지 확인하는 것이 어렵습니다.
    • 따라서 조금 불편하더라도 최대한 명시적으로 함수가 어느 라이브러리를 통해 임포트되었는지 코드에 기록하도록 합니다.
      • 별도의 모듈화 라이브러리를 사용하지 않는다면, 자주 사용하지 않는 라이브러리는 library::function_name 형태로 호출하도록 합니다.
      • box 등 모듈화 라이브러리를 사용할 경우, 스크립트 상단에서 필요한 함수만 import 합니다.

(4) 로깅과 알림

왜 필요할까?

  • 분석을 할 때는 직접 보면서 에러가 발생하지 않도록 해결하면 됩니다.
  • 운영 환경에서는 정해진 주기로 자동으로 동작하거나 실시간으로 돌아가다보니, 언제 어디서 어떤 에러가 발생했는지 담당자가 빠르게 확인할 수 있어야 합니다.

어떻게 해야 할까?

  • logger 로 출력해두기
    • 도커와 쿠버네티스 환경을 사용하다보니, 에러가 발생할 때 콘솔에 출력해두면 문제가 생겼을 때 확인할 수 있습니다.
    • logger 라이브러리를 통해 에러가 발생한 시간, 로그 레벨 등을 쉽게 출력할 수 있습니다.
logger::log_info('Loading data')
logger::log_warn('The function is deprecated')
logger::log_error('Failed to save RDS file')
# INFO [2021-11-04 11:16:41] Loading data
# WARN [2021-11-04 11:16:41] The function is deprecated
# ERROR [2021-11-04 11:16:41] Failed to save RDS file

name <- 'conference'
logger::log_info('Loading data for {name}')
# INFO [2021-11-04 11:17:57] Loading data for conference
  • 슬랙을 사용한 에러 메시지 발송
    • 현재는 주기적으로 동작하는 배치 작업을 Airflow 로 관리하고 있습니다.
    • 작업이 실패할 경우 Airflow 에서 슬랙으로 메시지를 보내도록 하고 있으며, 첨부된 Log URL에서 출력해둔 에러 메시지를 확인할 수 있습니다.

R로 개발 운영 환경을 구성하면서 느낀점

  • 여전히 개발 운영에 필요한 도구가 많이 부족하지만, 그래도 조금씩 생태계가 확장되고 있다는 것을 체감하고 있습니다.
  • 다른 언어를 사용하다 돌아와보니, R 고유의 특성과 생태계로 인해 개발하는 것이 쉽지 않습니다.
    • 특히 인터렉티브 환경에서 데이터를 다루는데 특화되었다는 점 때문에, 개발의 안정성과 같은 부분에 대한 니즈가 다른 언어 대비 크지 않았습니다.
    • 인덱스가 1부터 시작한다는 점, 모듈 시스템, 평가 방식 등 타 언어 대비 매우 다른 특성을 가지고 있어서 개발이 매우 어렵습니다.
  • 하지만 시각화나 rmarkdown 등 R의 강점을 온전히 누리기 위해 많은 사람들이 고민하고 있으며, 그 결과 R로도 다양한 개발 프로젝트를 시도할 수 있는 환경이 갖추어지고 있는 것 같습니다.
    • 파이썬 등 다른 언어의 라이브러리들에서 R의 강점을 흡수하려는 시도도 많이 보이고 있지만, 아직까지는 퀄리티 면에서 차이가 보인다고 생각합니다.
    • 하지만 장기적으로는 R도 개발 언어로서 많은 발전이 있었으면 좋겠습니다.

Comments