LYnLab

소개블로그취미로그

게시물의 썸네일 이미지

AWS를 이용한 서버리스(?) 개발기 - 태스크 리마인더 서비스

AWS Lambda + DynamoDB + CloudWatch + API Gateway를 이용한 리마인더 서비스 개발 이야기

2017-01-31#프로그래밍

💡 이 글은 작성된지 1년 이상 지났습니다. 정보글의 경우 최신 내용이 아닐 수 있음에 유의해주세요.

이 포스트는 2017년의 목표였던 1개월 1프로젝트의 첫 결과물입니다.

1. 들어가는 글

최근 회사에서의 포지션을 클라이언트 개발자에서 서버 개발자로 옮겼습니다. 현재는 스프링 프레임워크 기반의 서비스를 개발, 운영하고 있습니다. (간단한 것처럼 적어놨지만, 실제로는 온갖 라이브러리들이 붙어 규모가 엄청납니다.)

스프링은 물론 훌륭한 프레임워크입니다. 엄청난 양의 트래픽과 연산을 안정적으로 처리해주고, 오랜 역사에 걸쳐 성장해온 생태계는 다른 어떤 프레임워크도 넘볼 수 없을 만큼 방대하죠.

Spring Framework 회사에서 접하기 전까지는 아재들만 사용하는 줄 알았습니다.

하지만 역사가 깊은 만큼 신선한(?) 느낌이 없다는 점은 무척 아쉬웠습니다. 좋은 구조의 선례를 찾아 안정적으로 개발할 수는 있었지만, 이는 그저 레퍼런스를 따라간다는 느낌이 강했습니다. 그래서 호시탐탐 새로운 패러다임을 찾아보고 있었는데, 서버리스 아키텍처가 문득 눈에 들어왔습니다.

회사에서의 효율적인 할 일 관리를 하기 위해 개인적으로 사용할 도구를 만들려던 참이었기도 해서, 우선 가벼운 마음으로 할 일 리마인더 서비스를 만들어보기로 했습니다. 물론 할 일 관리를 위한 좋은 서비스나 플러그인은 이미 많이 존재하지만, 그냥 직접 만들어보고 싶었습니다. 마침 4일의 설 연휴라는 적절한 시간이 주어지기도 했으니, 각 잡고 한 번 달려보기로 했습니다.

사실 제가 만든 이 물건이 서버리스인지 아닌지 단언하지는 못합니다. 그저 인터넷 자료들을 찾아보고, 야매로 흉내 낸 결과물일 뿐이죠. 그래서 만약 글을 읽으며 심기가 불편하신 분이 있다면 가감 없이 말씀해주세요. 여러분의 거침없는 지적이 저를 무럭무럭 성장시킵니다.

2. 구조 세우기

2.1 만들 기능

이제부터 하나의 리마인드 단위를 *태스크(Task)*라고 칭하겠습니다.

필요한 주요 기능을 정리해보면 다음과 같습니다.

  • 특정 시점의 리마인더(Reminder) 기능
  • 해당 작업을 완료했음을 표시하는 기능
  • 태스크를 관리할 수 있는 간단한 관리자 웹 페이지

2.2 핵심 목표는 이것이다

사실 리마인더 시스템은 OS의 힘을 빌리면 정말 간단히 구현할 수 있습니다. 하지만 저는 OS의 관여를 최소화하고 싶었습니다. 검색을 통해 알아본 리마인더 시스템의 흔한 구현 방식은 다음과 같았습니다.

  • A안 : DB에 리마인드 시간을 작성해놓고 배치를 통해 1분마다 확인
  • B안 : 리마인드 시점까지 남은 시간을 계산하여 타이머 기능을 이용해 지연 실행

저의 선택은 A안도 B안도 아닙니다. 가장 먼저 떠올렸던 아이디어는 A안에 가까웠으나 너무 구시대적인 발상이라는 느낌을 지울 수가 없었습니다. B안의 경우 24시간 프로세스가 돌아가고 있어야 하며, 중간에 인터럽트가 발생하면 데이터가 유실될 가능성이 높죠.

기존의 사례를 통해 정리한 핵심 목표는 이것입니다.

  • 이벤트 기반의 로직 : 리마인더는 꾸준한 동작이 필요한 것이 아닌 이벤트성 작업입니다. 구조를 설계할 때 이러한 특성을 최대한 반영하고자 하였습니다. (사실 이벤트성 동작은 서버리스 구조의 핵심이기도 합니다)
  • Simple is the Best : 처음부터 다양한 기능을 추구하다 보면 정작 완성도가 떨어지는 경험을 수없이 반복해왔던 2015년부터 꾸준히 밀고 있는 컨셉입니다. 너무 심플한 나머지 별 거 아니라고 실망하실 분들께는 미리 사과드립니다.
  • AWS : 특별한 이유는 없습니다. 그냥 써보고 싶었어요. 사실 이 프로젝트를 시작하게 된 동기의 절반 이상이 'AWS를 써보자!'입니다.

AWS AWS를 사용한 서비스 개발은 인생의 숙원 사업 중 하나였습니다.

아래는 이 핵심 목표들을 반영한 액션 플로우입니다. 태스크를 등록하면 뒷단에서는 이런 순서로 일이 벌어집니다.

  1. 리마인드 날짜를 cron 형식으로 변환하여 저장합니다.
    예를 들어 2017년 1월 31일 오전 9시의 알림인 경우 cron(0 9 31 1 ? 2017)이 됩니다.
  2. 지정한 시간에 CloudWatch에서 이벤트가 발생합니다.
  3. 이벤트를 캐치한 Lambda가 실행되어 텔레그램으로 메시지를 발송합니다.
  4. 완료 처리 URL을 누르면 API Gateway를 거쳐 Lambda가 호출되고, 최종적으로 태스크가 완료 처리됩니다.
  5. 시간이 지나도 완료 처리되지 않은 태스크는 반복해서 알림을 전송합니다. 이번 단계에서는 생략합니다.

원래는 5번과 같은 고급(?) 기능도 추가하려고 했으나, Simple is the Best 목표에 어긋나는 것 같아 이번에는 포함하지 않았습니다.

2.3 스택

이벤트 발생은 Amazon CloudWatch Events가 담당합니다. CloudWatch Events에서는 주기적 이벤트 발생의 규칙(Rule)을 정의할 수 있습니다. n분 단위로 실행하거나 cron 문법을 이용할 수 있습니다. 이번 프로젝트에는 리마인드 시간을 cron 작업으로 변환하여 저장하게 됩니다.

태스크 목록 저장은 Amazon DynamoDB를 이용하였습니다. DB에는 간단히 태스크의 이름과 CloudWatch 이벤트 규칙의 ARN이 저장됩니다. 앞서 말씀드렸던 고급 기능을 관리하기 위한 변수는 이곳에 저장하려고 했었습니다.

발생한 알림은 AWS Lambda를 통해 텔레그램으로 전달됩니다. 메시지에 완료 처리를 위한 URL을 첨부하고, 해당 API가 호출되면 작업을 완료 처리하는 람다를 실행시킵니다.

정리해보면 아래 그림과 같습니다.

정리된 아키텍처는 이런 모습

2.4 목록 관리를 위한 프론트

퀵하게 구현해보고 싶었던 것은 '뒷단'의 부분입니다. 때문에 프론트에는 크게 공을 들이지 않고, 최소한의 기능인 추가, 삭제, 활성화/비활성화만 구현하는 것을 목표로 하였습니다. 2017년에 무려 jQuery를 썼다는 점에서 이미 글러먹었습니다

가장 빠르게 개발을 할 수 있는 Django 프레임워크를 이용하여, 기존의 LYnLab 프로젝트에 하나의 앱을 추가하는 형태로 개발하였습니다.

3. Step by Step

여기서는 각각의 feature들에 대한 상세한 설명이나 사용 방법 등에 대해서는 작성하지 않았습니다. AWS 공식 문서를 찾아보시는 것을 추천해 드립니다.

모든 코드는 Python 2.7로 작성하였습니다.

3.1 DynamoDB 구축

boto3 패키지를 이용하여 테이블을 생성합니다.

>>> import boto3

>>> client = boto3.client('dynamodb')
>>> client.create_table(
        TableName='ReminderTask',
        AttributeDefinitions=[
            {
                'AttributeName': 'Name',
                'AttributeType': 'S'
            }
        ],
        KeySchema=[
            {
                'AttributeName': 'Name',
                'KeyType': 'HASH'
            }
        ],
        ProvisionedThroughput={
            'ReadCapacityUnits': 1,
            'WriteCapacityUnits': 1
        }
    )

GUI를 선호하시는 분이라면 AWS 콘솔을 이용하실 수도 있습니다. 기본 키로는 태스크의 이름을 사용하였습니다.

3.2 Lambda 함수 설정

필요한 람다 함수는 세 종류입니다.

  1. reminderGenerateTask : 태스크 생성.
  2. reminderNotify : 태스크 알림 발송.
  3. reminderModifyTask : 태스크 완료 처리.

아래는 reminderGenerateTask 람다 함수 코드입니다. 이벤트 발생으로 호출된 람다는 handler 메소드를 실행시킬 것입니다.

다른 함수들도 Github에서 확인해보실 수 있습니다.

3.3 API Gateway 설정

필요한 API는 세 종류입니다.

  1. GET /reminder/tasks : 태스크 목록을 가져옵니다.
  2. POST /reminder/tasks : 태스크를 생성합니다. 파라미터로 태스크의 이름과 날짜 정보를 받습니다.
  3. GET /reminder/tasks/{name} : 태스크의 상태를 변경합니다. 파라미터로 완료 여부를 받습니다.

예를 들어, 2번 API는 위의 reminderGenerateTask 람다 함수에 연동됩니다.

만들어진 API는 이런 모습 만들어진 API는 이런 모습입니다.

3번 API의 경우 원래 PUT 메소드를 사용하려고 하였으나, 텔레그렘에서 URL을 호출 시 PUT을 사용할 수 없으므로 불가피하게 GET을 사용하였습니다. 혹시 이와 같은 구조에서 RESTful API 설계 규칙을 어기지 않기 위해 어떤 방법을 사용하는지에 대해 아시는 분은 알려주시면 감사드리겠습니다.

3.4 텔레그램 설정

마지막으로, 텔레그램 봇을 만들어 토큰을 얻은 후 람다의 환경 변수로 등록합니다.

텔레그램 봇은 정말 간단하게 만들 수 있지만, 토큰을 얻어내는 과정에서 헷갈리는 부분이 많았습니다. 이와 관련해서는 별도의 포스트로 정리하고자 합니다.

4. 결과물

DaumBatch라는 이름의 태스크를 만들어 2017년 1월 31일 오전 10시 30분에 리마인드 하도록 설정하였습니다.

DynamoDB 완료 처리를 한 이후에 스크린샷을 찍어 IsDone에 이미 true가 들어가있습니다

DynamoDB 테이블에는 해당 태스크가 잘 등록되었습니다.

대망의 31일 오전 10시 30분, 부푼 마음을 안고 기다리고 있었지만... 아쉽게도 알림은 단박에 오지 않았습니다. 심지어 CloudWatch 콘솔에서 로그조차 찾아볼 수가 없었습니다.

문제의 원인은 의외로 시시한 곳에 있었습니다. CloudWatch 이벤트 스케줄 cron은 AWS 리전과 상관없이 GMT 시간대를 사용합니다. 좀 더 정확히 말하자면, 시간대를 지정하는 기능이 없습니다. 즉 제가 람다에서 2017-01-31 10:30:00+0900 으로 날짜를 전송해도, CloudWatch는 시간대는 쏙 빼먹고 GMT 기준 10시 30분으로 저장해버립니다. 따라서 이 알림은 한국 시각으로 31일 오전 1시 30분에 나갔어야 했지만, 저는 이 태스크를 2시쯤 등록했으니 사실상 아무런 동작이 없었던 것입니다.

텔레그램 캡처 저의 모든 알림은 LYnBot이 전해줍니다.

cron 설정을 9시간 당기고, 테스트를 위해 약 3분 정도 늦추어 설정한 결과 위와 같이 성공적으로 알림이 발송된 것을 볼 수 있었습니다.

CloudWatch 로그

위는 CloudWatch가 남긴 로그입니다. 수행 시간은 약 1.4초로, 단순히 메시지를 보내는 작업치고는 다소 아쉬운 길이입니다. 람다가 느린 것인지, 아니면 HTTP 요청에서 지연된 것인지 확인이 필요할 것 같습니다.

어드민 페이지 Semantic UI는 사랑입니다.

참고로 어드민 페이지는 이런 모습입니다. 최초 단계에서는 최대한 간소하게 만들었지만, 앞으로 사용하면서 불편한 부분은 조금씩 고쳐나갈 예정입니다.

5. What's next?

설 당일을 제외하고 연휴 내내 개발에 매달렸는데, 꽤 재밌었습니다. 앞으로도 시간 나는 틈틈이 이런 소규모 프로젝트를 진행해보게 될 것 같습니다.

2월의 프로젝트로는 업무 시간 트래킹 앱을 만들어보고자 합니다. 회사에서 소모하는 개발 시간, 회의 시간, 노는 시간 등을 트래킹하여 우선은 로우(Raw) 데이터를 축적하는 것이 목적입니다. 이번에는 제 특기 분야인 안드로이드 개발 경험을 살려 반듯한 클라이언트까지 만들어 볼 계획입니다.

관련된 글

Rails와 GitHub Actions에 커버리지 레포트를 달아보자

이 블로그의 CMS이기도 한 Shiori를 대폭 리팩토링하면서 테스트가 얼마나 잘 작성되어있는지 궁금해졌습니다.

Rails Global ID로 전역 객체 식별하기

Global ID는 Rails의 모든 객체를 식별할 수 있는 URI(Uniform Resource Identifier)입니다.

Ruby on WebAssembly: 살짝 맛보기

Ruby 3.2에 추가된 WebAssembly 지원을 간단하게 테스트해봅시다.

작성한 댓글은 giscus를 통해 GitHub Discussion에 저장됩니다.

크리에이티브 커먼즈 라이선스크리에이티브 커먼즈 저작자표시크리에이티브 커먼즈 동일조건변경허락

본 사이트의 저작물은 별도의 언급이 없는 한 크리에이티브 커먼즈 저작자표시-동일조건변경허락 4.0 국제 라이선스에 따라 이용할 수 있습니다.

© 2011 - 2024 Hoerin Doh, All rights reserved.

LYnLab 로고About MeGitHubTwitterInstagram