글쓴이 보관물: dryrain

Spring gateway 와 eureka

attend 리팩토링을 해야 하는데 자꾸 미루네요. 저번주 주말은 캠핑을 다녀왔습니다. 너무 힘들어요.

여튼 이번주는 월요일~화요일 공부한 스프링 게이트웨이와 유레카에 대해 간단히 소개 드리겠습니다.

gateway 는 말그대로 게이트웨이입니다. 사용자가 요청하면 요청을 받고 url 을 분석해서 적절한 서버로 전달해주는 역할을 합니다.
eureka 는 마이크로 서비스 명단을 관리 해주는 역할을 합니다. 서비스 각각을 제어하지 않고 누가 살았는지 명단만 잘 관리해줍니다.

그림을 보면 이해가 쉬우실 겁니다.

순서대로 한번 보겠습니다. 일단 eureka 가 떠 있다고 가정하겠습니다.

  1. (노란색 화살표) 스프링 클라우드가 뜨면서 eureka 에 등록을 합니다.
  2. (노란색 화살표) 마이크로서비스도 뜨면서 eureka 에 다 등록이 됩니다.
  3. 현재 살아있는 서버는 api, user, file, gateway 가 되겠죠. eureka 가 정기적으로 heartbeat 를 수신 받으면서 살아있는지 체크를 합니다. 만약 heartbeat 가 안오면 죽었다고 판단을 하는 것 입니다.
  4. (빨간색 화살표) 사용자가 /api/blah/blah 로 요청을 보냅니다. spring cloud 는 url 을 읽고 api 서버로 보내야 함을 압니다.
  5. (검은색 화살표) 그리고 게이트웨이에서 살아있는 api 서버 목록을 받습니다. 살아있는 서버 중 하나로 유저의 요청을 보냅니다.

현재 조사가 안된게 “게이트웨이에서 살아있는 api 서버 목록을 받습니다” 이 부분입니다. 목록을 받는건지 살아있는 서버 중 한곳을 리턴하는건지 더 조사해야 합니다.

이런 구조의 장점은 micro service 의 scale-out이 간편하다는 것 입니다.

더 알아 보아야 할것은 로드 밸런싱 종류입니다. 기본적으로 설정되는 로드 밸런싱 방법은 round robin 입니다. 서버 사양이 다를 경우 한 서버에 부하가 심해질 수 있으므로 다른 방법을 찾아야 할 것 같습니다.

Attend 리팩토링 프로젝트 (서버편 1)

리팩토링 전에 갖고 있는 가상서버들을 정리해 보겠습니다.

최종 목표는 이겁니다. 가상 서버가 사양에 비해 금액이 적지 않다고 판단하였고 더 높은 사양을 원하기에 물리 서버를 두대 둬야한다고 생각 했습니다. 클라우드에 비해 물리서버가 죽을 확률은 높아서 프록시 서버는 클라우드에 한대 뒀습니다. 리버스 프록시에서 로드 밸런싱과 서버 health check 를 담당할 예정입니다.

우선은 북구에 놓을 서버가 없기에 Linode 60달러짜리 서버를 한대 빌렸습니다. 지금은 데탑 서버(평사)와 리노드(일본) 두대가 있습니다.

이제 자동 배포를 구축할 겁니다.
기존에는 리눅스 서비스를 만들어두고 관리하는 방식으로 했는데 코드를 변경할 때 마다 재시작 명령어를 입력해줘야 해서 배포하기가 매우 귀찮았습니다. 이걸 젠킨스를 통해 자동화 할 계획입니다.

게다가 서비스마다 각각 다른 부분이 있어 신경써야 할 부분이 많습니다. 이걸 도커로 만들어두면 서비스를 올릴 때 같은 명령으로 전부 처리할 수 있고 어느 환경에서나 동일하게 동작하기 때문에 배포할 때 뇌를 덜 사용해도 됩니다.
그리고 docker-compose를 사용할 겁니다. docker run 뒤에 오는 수많은 옵션을 관리하는 것도 상당히 귀찮은 일 입니다. docker-compose 는 이런 옵션들을 파일로 만들어뒀다고 생각하시면 됩니다. 옵션을 미리 파일로 저장해 두고 docker-compose up -d 만 입력하면 서비스가 올라갑니다.

빌드 촉발은 깃허브 푸시로 진행할 겁니다. 지금 생각하고 있는 배포 방식은 두가지 입니다.

도커 허브를 잘 활용하기 위해 깃허브에서 푸시가 들어오면 도커 빌드 → 도커 푸시 과정을 거치는것은 똑같습니다. 다만 깃허브 액션으로 빌드와 푸시를 할것인지, 도커 허브를 통해 빌드를 할것인지는 선택해야 합니다.

깃허브 액션으로 하면 다음과 같은 특징이 있습니다

  • + 빌드, 배포를 한 곳에서 관리 가능함.
  • CI Minute 을 보고 비용 신경써야 함.
  • △ 배포 명령 전달시 젠킨스를 사용해도 됨

도커 허브와 젠킨스를 사용하면 다음과 같은 특징이 있습니다

  • + 젠킨스 사용으로 자동화 자유도가 넓어짐
  • - 과정이 좀 복잡함. (깃허브 푸시 → 도커 허브 → 젠킨스 순)
  • △ 어짜피 도커 허브 프로를 샀는데 활용하자는 마인드

지금은 dockerhub 를 통해 빌드하고 있습니다. 이제 젠킨스 세팅을 알아보겠습니다.

처음 젠킨스를 설치하고 할 일은 다음과 같습니다.

  1. 웹훅 플러그인 설치, 배포(over ssh 로 검색) 플러그인 설치
  2. 대상 서버에서 ssh-keygen 을 하고 이 키를 깃허브에 등록합니다. (docker-compose 파일을 받기 위함)
  3. 배포 플러그인(Publish over SSH) 에서 ssh 서버 추가 (관리 → 시스템 설정 에 있음)
  4. 대상 서버에서 레포지토리 받기(또는 docker-compose 파일만 있어도 됨)

빌드 유발은 웹훅 입니다. 젠킨스에서 웹훅 플러그인을 깔고 토큰을 설정해 주면 됩니다.

빌드 환경 설정은 다음과 같습니다. 말이 빌드 환경일 뿐이지 사실은 서버에 접속해서 도커 재시작 해주는 명령이 전부 입니다.

cd /root/srtbot
git pull
docker-compose pull
docker-compose up -d --remove-orphans
yes | docker image prune

위 명령어를 젠킨스가 웹훅이 들어오면 실행해 줍니다.

이로써 자동 빌드 환경이 구성되었습니다. git commit 과 push 를 하게 되면 dockerhub → jenkins → 서버에서 docker-pull 과정을 거쳐서 서비스가 최신 상태로 올라오게 됩니다.

다음은 attend 프로젝트의 도커화와 cockroach 데이터베이스 연결, sqlite3 에서 cockroach 데이터베이스로 데이터 이동을 할 것 같습니다.

Attend 리팩토링 프로젝트

기존에 만들었던 Attend 서비스를 리팩토링 하려고 합니다.

DU Things 에 속하는 Attend 서비스는 출석 QR 코드를 미리 저장해서 원클릭으로 출석하는 서비스입니다.

이 서비스는 2019년 처음 개발되었습니다. 비공개로 아는 사람끼리만 쓰다가 MiscThings 동아리를 개설하면서 장애학생을 위한 서비스로 2021년 11월에 공개 했습니다.

기능은 단순합니다. 그냥 출석 QR 코드를 미리 찍어 두면 다음부터는 사이트에 접속해서 출석 버튼을 누르기만 하면 됩니다. (물론 기존과 같이 DU Wifi 에 연결되어야 함.)

출석 QR 코드를 그냥 브라우저에 저장해도 됐지만, QR 코드를 이용해 출석 링크를 생성하는 과정에서 복호화가 필요합니다.
아무래도 복호화 키를 공개하는 것은 무리가 있다고 판단했습니다. 또한 기기간 연동도 될 수 있게 하려면 사용자 관리까지 들어가야 할 것 같아서 백엔드 서버를 두기로 했습니다.
그래서 백엔드 서버의 역할은 아래와 같습니다.

  • 회원 관리 (출석 QR 데이터 저장)
  • QR 복호화
  • 이벤트 관리

처음 구상했던건 QR 복호화와 회원 관리 밖에 없었습니다. QR 복호화는 데이터베이스 필요 없이 값을 해독해서 응답을 주면 되었고 회원 관리는 따로 데이터베이스가 필요했습니다.
일단 사용자가 많지 않을거라고 생각해서 sqlite 로 간단하게 개발했습니다. 하지만 공개 이후 많은 사용자들에게 배포해보자 라는 욕심이 생겼고 적극적인 홍보를 통해 지금은 약 1100명이 사용하고 있습니다.

리팩토링이 필요한 이유는 다음과 같습니다.

  • 데이터베이스 관리.
  • 서버 관리

두가지 걱정거리에 대해서 알아보겠습니다. 서비스를 운영하기 위해서 1100명의 인증 정보와 강의실 정보를 저장해야 합니다. 현재 데이터베이스 구조는 sqlite 를 사용하여 KV 저장소와 비슷하게 구성되어 있습니다.
Key 는 ID 로 Value 는 암호와 강의실 정보로 이루어진 JSON 을 담고 있습니다. 지금 json 이 잘못 저장되면 복구 할 수 있는 방법이 딱히 없기도 하고 실수로 지워버리면 돌이킬 수 없기 때문에 안정성이 높은 클라우드로 이전하려 합니다.

두번째는 서버 관리입니다. 현재는 vultr 에 가상서버 1대만 구성되어 있습니다. 업데이트를 할 때나 인스턴스 장애가 일어난경우 백엔드와 프론트엔드 모두 중단되는 구조입니다. 고가용성을 위해 홈서버를 포함하여 운영해볼 계획입니다.

그럼 다시, 데이터베이스는 뭘 고르고 서버 구조는 어떻게 구성해야 할 지 고민해야 합니다.

서버 언어는 Java 로 단계적으로 전환할 겁니다. Spring, JPA 연습 겸 선택했습니다.

데이터베이스는 cockroach database 를 사용할겁니다. 고가용성에 맞게 구성된 데이터베이스기도 하고 클라우드 서비스를 제공해줍니다.

서버 구조는 아래와 같이 구성했습니다.


cockroach db 도 cloud 상에 있고 정적 파일 또한 cloudflare page 라는 cloud 상에 있습니다.
정적 페이지 상에서 서버 목록을 읽고 연결을 테스트하고 가장 빠른 서버를 선택하게 할 것입니다. 물론 이 과정이 느리다는것은 잘 알고 있습니다. 아마 클라이언트에서 아래와 같은 단계를 거칠 것입니다.

서버를 여러대 두고 고를 수 있게 한다면 세션 관리를 각 서버마다 동기화 해줘야 합니다. 이 과정은 매우 어렵고 귀찮기 때문에 jwt(Json Web Token) 를 사용할 겁니다. 서버간 비밀키만 공유되면 어디에서든 토큰 검증을 할 수 있기 때문입니다.

위와 같이 서버 선택이 가능하도록 구현한다면 하나의 서버가 죽었을 때 클라이언트에서 나머지 접속이 가능할 것입니다. 보통은 HA Proxy 같은 소프트웨어를 사용하지만 관리할 서버를 줄이기 위해 클라이언트에서 서버를 선택하게 개선할 것입니다.

백엔드 서버 구조도 개선해야 합니다. 현재는 로그인 코드에 의존되어 있는게 많은데 인증 절차를 토큰으로 대신함으로써 미들웨어에서 전부 처리할 예정입니다.

Attend 특성상 방학기간에는 사용자가 없는 서비스입니다. 방학기간 내에 리팩토링을 마치고 과정까지 업로드 하겠습니다 😘

docker swarm

docker 는 작은 단위로 앱을 배포할 수 있게 해주는 서비스 입니다.

이미지를 만들어놓고 실행하는 상태를 컨테이너라고 하는데요. 분산 처리 목적으로 여러 서버에 컨테이너를 올리려면 상당히 번거롭습니다.

그래서 컨테이너를 자동으로 배치해주고 다양한 통합 기능을 제공하는 docker swarm 에 대해 간단하게 소개 드리겠습니다.

컨테이너를 자동으로 배치해주고 다양한 통합 기능을 제공하는걸 컨테이너 오케스트레이션 이라고 합니다. 직접 여러 서버에 접속해서 같은 명령어를 입력하는 수고를 자동화해준다고 보시면 되겠습니다.

swarm 모드에는 manager 와 worker 가 있습니다. 매니저는 매니저 노드와 worker 노드에 컨테이너를 분산 하여 실행시킵니다.

그림에서 나오는 것 처럼 고 가용성을 위해 매니저 노드는 최소 3개를 설정해야 합니다. 이렇게 설정하면 1개 노드가 죽어도 네트워크에 액세스 할 수 있습니다.

또 하나의 swarm 특징이 overlay 네트워크인데요, 어느 노드에 접속해도 동일한 네트워크에 접속한 것 처럼 가상의 망을 구축해 줍니다.

(미완성) DU Attend 서비스에서 식별 정보 관리

서론

DU Attend 는 QR코드를 미리 휴대폰에 저장해두고 앉은자리에서 편리하게 출석체크 해 주는 서비스입니다.

시대의 흐름에 따라 개인정보의 중요성이 커져가면서 본인의 데이터가 안전한지 궁금해하는 사용자들이 많습니다. 서비스를 만드는 입장에서는 귀찮아할 일이 아니라 데이터 관리를 어떻게 하는지 적극적으로 알려드려야 할 부분 같습니다. 이러한 문화가 많이 퍼졌으면 좋겠습니다.

수집하는 식별정보

DU Attend 서비스에서는 사용자의 정보를 최소한으로 수집합니다. 순수 무료 목적으로 만들어졌기 때문에 다음과 같은 정보만 수집합니다.

  1. 아이디 -> 학번으로 유도
  2. 암호

DU Attend 서비스에서 다른 서비스들과 다른 점은 아이디를 학번이랑 동일하게 해야 합니다.
사실 학번을 사용하지 않고 무작위 8자리 숫자를 사용해도 됩니다. 학번을 사용하는 이유는 출석 시스템에서 학번을 사용하기 때문입니다. 무작위 8자리로 가입하거나 다른 사람의 학번으로 가입하게 되면 출석 기능이 제대로 동작하지 않을 것 입니다.

출석 QR과 관련된 정보는 나중에 다른 글로 정리하겠습니다.

암호는 tigers(종합정보시스템) 암호가 아닙니다. DU Attend 에서 개별적으로 사용하는 암호입니다. 현재 암호 정책은 최소 4자리 최대 36자리 로 제한하고 있습니다. 숫자 4자리로 해도 되지만 좀 더 강력한 암호를 사용하시면 좋겠습니다. 암호의 중요성은 잘 아실 거라 생각합니다.

암호 저장방법

암호 정보는 해시 함수를 사용하여 저장합니다. 해시 함수란 위키백과에 잘 정리되어 있습니다.

해시함수(hash函數)는 하나의 주어진 출력에 대하여 이 출력으로 사상시키는 하나의 입력을 찾는 것이 계산적으로 불가능하고, 하나의 주어진 입력에 대하여 같은 출력으로 사상시키는 또 다른 입력을 찾는 것이 계산적으로 불가능하다는 두 가지 성질을 만족하면서 임의의 비트열을 고정된 길이의 비트열로 사상시키는 함수이다.

한마디로 입력값을 해시 함수에 통과시키면 무작위 문자로 보이는 결과물이 출력됩니다. 해시 함수는 출력값으로 입력값을 유추할 수 없어야 합니다.

그리고 해시 함수의 알고리즘은 널리 알려져 있습니다. 알고리즘이 공개되어 있어도 많은 곳에서 사용한다는 것은 안전하다는 뜻이기도 합니다.

DU Attend 에서 사용하는 해시 함수는 bcrypt 입니다. bcrypt 의 장점은 많은 언어에서 구현되어 있어 사용하기 편리하고(개발 속도가 빠르고) 아직 알려진 문제가 없다는 점 입니다. 그리고 해시를 생성할 때 마다 다른 출력값이 나온다는 점 입니다.

https://bcrypt-generator.com/ 에서 쉽게 해볼 수 있습니다. 아무 값이나 입력하고 Encrypt 버튼을 여러번 눌러 보세요. 생성할 때 마다 다른 값이 나옵니다. 자주 쓰는 패스워드를 해커가 알고 있어도 같은 결과값을 찾기는 불가능에 가깝습니다. 이것을 salt 라고 하는데요… 여기에서 보시죠.
https://ko.wikipedia.org/wiki/%EC%86%94%ED%8A%B8_(%EC%95%94%ED%98%B8%ED%95%99)

이런 방법으로 암호를 저장하면 안전합니다. 하지만 아직 많이 남았습니다.

암호 정보의 흐름

로그인 페이지에 암호를 입력하고 로그인에 성공하고 메인페이지를 보기까지 서버에서는 많은 일이 일어납니다.

대구대 정보 통합 검색 기능 개발 (1)

대구대 사이트에 있는 정보들을 하나로 통합하여 검색할 수 있으면 좋을 것 같아 개발하였습니다.

현재 대구대 사이트에는 아래와 같은 시설 검색이 가능합니다.

  • 편의 시설 정보(매점, 복사실, 편의점)
  • 학과 정보(학과 홈페이지 안내)
  • 교직원 연락처 정보(내선 번호)

추가적으로 비공개 데이터도 있습니다

  • 강의실 목록

이러한 데이터를 검색하려면 메뉴를 누르고 링크를 타고 접속해야 검색이 가능합니다.

예를 들어 전화번호를 검색하려면
① 상단 메뉴에서 대학안내 → 대학개요 → 전화번호안내 링크를 통해 접속하고
② 검색 분류를 선택한 뒤
③ 전화번호를 검색 해야 합니다.

이런 파편화된 데이터를 하나의 검색 박스에서 위 나열된 것을 검색하도록 하는것이 목표입니다.

우선 현재 데이터는 전부 JSON 으로 저장되어 있습니다. 예시 JSON 은 다음과 같습니다.

# 강의실 정보
{
  "id":"법행1100",
  "name":"1층 공통공간",
  "floor":"1층",
  "type":"17558688",
  "location":"법행정대학관"
}

# 시설 정보
{
  "id": "중앙도서관열람관",
  "sectors": "편의점",
  "name": "이마트24",
  "floor": "지하",
  "office_phone": "",
  "phone_number": "010-****-****",
  "type": "10263233"
}

# 학과 정보
{
  "id":"인문대학",
  "name":"한국어문학부",
  "url":"http://koreandu.daegu.ac.kr/",
  "image_url":"http://koreandu.daegu..."
}

# 연락처 정보
{

  "name_kr": "강**",

  "upmu": null,
  "buseo": "재활과학대학 재활건강증진학과",
  "user_upmu": "교육지원조교",
  "bojik_nm": " ",
  "sosok": null,
  "jik_id": "*****",
  "user_telno": "6095",
  "e_mail": "********@naver.com",
  "hompy_addr": null,
  }

이 정보를 키워드 형태로 검색할 수 있게 가공해야 합니다.

MariaDB 같은 RDBMS에 집어넣으려면 JSON을 표 처럼 변환해줘야 합니다. 검색과 별개로 사용자에게 보여줄 정보는 필요하기 때문에 필수적인 항목만 추려 보았습니다.

이제 위 형태로 변환시키는 코드를 만듭니다. 배열로 처리 할 수 있지만 기억력도 좋지 않은데다 표를 봐 가면서 해야하기 때문에 dataclass 로 정의해보도록 하겠습니다.

from dataclasses import dataclass
from typing import Optional

@dataclass
class CsvForm:
    idx: Optional[int] = None
    blah_id: Optional[str] = None
    name: Optional[str] = None
    location: Optional[str] = None
    tel_no: Optional[str] = None
    mobile_no: Optional[str] = None
    category: Optional[str] = None
    web_address: Optional[str] = None
    email_address: Optional[str] = None
    tags: Optional[str] = None

    def __get_data_array(self):
        return [
            self.idx, self.blah_id, self.name, self.location,
            self.tel_no, self.mobile_no, self.category, self.web_address,
            self.email_address, self.tags,
        ]

    def __getitem__(self, item):
        return self.__get_data_array()[item]

    def __repr__(self):
        return self.__get_data_array()

    def __len__(self):
        return len(self.__get_data_array())

어쨌든 CSV 로 변환해야 하기 때문에 class 형태지만 배열처럼 동작해야 합니다. __getitem__()__len__() 을 구현해 줌으로써 배열 처럼 동작하게 할 수 있습니다.

(2) 에서는 sqlite3 을 사용해서 메모리에 데이터베이스를 올리고 쿼리 생성까지 만들어 보겠습니다.

Elasticsearch bool query

새로 배운 내용


집계나 조건 검색 할때 query 보다 필터가 더 빠름
이유는 필터는 100% 일치하는 문서만 반환하는 데 비해 쿼리는 입력값과 비슷한 문서도 검색 하고 점수계산을 시행함.

(+) 필터의 이점은 자주 사용하는 필터는 엘라스틱이 자동으로 캐시해준다.
https://www.elastic.co/guide/en/elasticsearch/reference/current/query-filter-context.html

여러개의 필터(조건)를 걸려면 부울 쿼리 사용
https://www.elastic.co/guide/en/elasticsearch/reference/current/query-dsl-bool-query.html

{
  "bool" : {
    "must" : [],
    "should" : [],
    "must_not" : [],
    "filter": []
  }
}

부울 쿼리에서 사용되는 모드

  • must
    • All of these clauses must match. The equivalent of AND.
    • must 안에 있는 쿼리들은 모두 일치해야 함.
  • must_not
    • All of these clauses must not match. The equivalent of NOT.
    • must_not 안에 있는 쿼리들은 일치하지 않아야 함
  • should
    • At least one of these clauses must match. The equivalent of OR.
    • should 쿼리중 n개 이상 매치되어야 함. minimum_should_match 로 최소 몇개가 매치되어야 하는지 조절할 수 있음.
  • filter
    • Clauses that must match, but are run in non-scoring, filtering mode.
    • must match 같이 동작하지만 점수 계산 안 함.

파이썬 라이브러리 소개: diskcache

diskcache 는 말 그대로 disk 에 cache 데이터를 저장시켜주는 라이브러리 입니다.

일반 캐시와 deque 등 다양한 자료구조를 지원합니다.

기본적인 캐시 선언입니다.

from diskcache import Cache
cache = Cache()

다음과 같이 사용할 수 있습니다. 자료를 get/set 하고 만료기한을 설정할 수 있습니다.

cache.pop('alice')
# 1
cache.pop('dave', default='does not exist')
# 'does not exist'
cache.set('dave', 0, expire=None, tag='admin')
# True
result = cache.pop('dave', expire_time=True, tag=True)
value, timestamp, tag = result
value
# 0
print(timestamp)
# None
print(tag)
# admin

데이터베이스를 샤딩하는 fanout cache 도 지원합니다. 샤딩이 되기 때문에 쓰기가 겹칠 확률이 줄어듭니다. (읽기는 동시에 해도 상관 없음) 아래 예시는 샤드를 4개 하고 타임아웃이 1초인 경우입니다.

from diskcache import FanoutCache
cache = FanoutCache(shards=4, timeout=1)

함수 리턴값을 캐시할 수 있습니다.

from diskcache import FanoutCache
cache = FanoutCache()

@cache.memoize(typed=True, expire=1, tag='fib')
def fibonacci(number):
    if number == 0:
        return 0
    elif number == 1:
        return 1
    else:
        return fibonacci(number - 1) + fibonacci(number - 2)
print(sum(fibonacci(value) for value in range(100)))

디지털액자 (4)

알림 기능을 구현해 보겠습니다.

실시간 알림은 websocket 등으로 구현 하면 되지만 디지털액자는 그렇게까지 실시간으로 알림을 보지 않아도 되기 때문에 2~5초 간격으로 서버에 ajax 요청을 보냅니다. 서버는 알림 배열을 반환합니다.

알림 API 응답

클라이언트는 이 배열을 읽고 알림 표시 기한(expire)이 지났는지 확인한 후 알림을 display_time 초 동안 표시합니다. 랜덤 id 값이 부여되기 때문에 클라이언크는 알림을 표시했으면 localstorage 에 이 id 값을 저장합니다.

localstorage 에 읽을 알림 id 저장

이 localstorage 는 새로고침해도 데이터가 지워지지 않습니다. 따라서 한번 표시된 알림은 다시 표시되지 않습니다.

이제 notification 을 자동으로 넣어주는 코드를 짜야 합니다. 우선 일정 시작 전 알려주는 코드를 짭니다.

매 1분마다 작업을 돌려줘야 하기 때문에 fastapi_utils 라이브러리를 사용했습니다.

@app.on_event("startup")
@repeat_every(seconds=60 * 1)  # 1 minute
def event_notification() -> None:
    now = datetime.datetime.now(tz=tzoffset(None, 32400))
    evt_list = get_event_list()
    expire = 60 * 10  # 10 분
    display_time = 30  # 30 초

    for evt in evt_list:
        for reminders in evt.reminders:
            reminder_at = get_evt_start_date(evt) - datetime.timedelta(minutes=reminders.minutes_before_start)
            if now <= reminder_at < now + datetime.timedelta(minutes=1):
                start_date = get_evt_start_date(evt).strftime("%m-%d %H:%M")
                add_notification(f"{start_date}<br>{evt.summary}<br>일정이 {reminders.minutes_before_start}분 후 시작합니다.",
                                 expire=now.timestamp() + expire, display_time=display_time, show_notiboard=False)

        if len(evt.reminders) == 0:
            reminder_at = get_evt_start_date(evt) - datetime.timedelta(minutes=30)
            if now <= reminder_at < now + datetime.timedelta(minutes=1):
                start_date = get_evt_start_date(evt).strftime("%m-%d %H:%M")
                add_notification(f"{start_date}<br>{evt.summary}<br>일정이 {30}분 후 시작합니다.",
                                 expire=now.timestamp() + expire, display_time=display_time, show_notiboard=False)

임의의 초에 1분마다 실행되는 것이기 때문에 (현재시각 <= 리마인더시각 < 현재시각 + 1분) 범위로 잡아야 알림을 빠짐없이 잡을 수 있습니다.

다음은 학사·학과공지 코드로 알림을 구현할 예정입니다.

디지털액자 (3)

하드웨어를 구성했으니 웹 서버를 만들 차례입니다.

FastAPI 와 Jinja2 템플릿을 사용하여 빠르게 만들겠습니다.

FastAPI: https://fastapi.tiangolo.com/ko/

레이아웃은 아래와 같이 할겁니다

눈이 부시면 안 되기 때문에 다크 테마로 적용시켰고 미래 일정까지 다 보이도록 일정 공간을 넓게 잡았습니다. 그리고 매주 금요일마다 서울에 다녀와야 해서 예매 까먹는걸 방지하기 위해 예매 내역을 조회할 수 있는 칸을 만들었습니다.

그리고 웹 페이지기 때문에 주기적으로 데이터를 변경해줘야 합니다. 아래 코드로 120초마다 페이지를 새로 고칩니다.

function reloadFunction() {
    window.location.reload();
}

let timeout = window.setTimeout(reloadFunction, 120 * 1000);

이제 데이터를 만들어 넣어야 합니다.

파이썬 srt 라이브러리로 티켓 가져오는 코드를 짭니다.

def srt_get_reservations() -> List:
    srt = SRT(srt_id=srt_id, srt_pw=srt_pw)
    reservations = srt.get_reservations()
    return reservations[0:3]

레이아웃이 깨지지 않게 하려면 3개까지 보여지게 합니다.

페이지가 120초마다 새로고침 되므로 위 코드도 계속 실행되면 SRT 서버에 부하를 발생시키고 심하면 차단당할 것입니다. 따라서 적절하게 캐시를 사용해야 합니다. 함수 실행결과를 disk 에 캐시해주는 라이브러리인 diskcache 를 사용하였습니다.

from diskcache import FanoutCache
cache = FanoutCache("./.cache/memoize")

@cache.memoize(expire=60 * 30)
def srt_get_reservations() -> List:
    ...

위와 같이 어노테이션 한줄만으로 쉽게 캐시할 수 있습니다.

마찬가지로 구글 캘린더도 gcsa 라이브러리를 사용하여 작업합니다. 타임존 관련으로 어려운 부분이 있었는데 이 부분은 따로 포스팅 하겠습니다.

다음은 일정이 임박하면 소리와 함께 알려주는 코드를 작성해 보겠습니다.