카테고리 보관물: Python

대구대 정보 통합 검색 기능 개발 (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 을 사용해서 메모리에 데이터베이스를 올리고 쿼리 생성까지 만들어 보겠습니다.

파이썬 라이브러리 소개: 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)))

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

비동기 작업을 손쉽게 관리해주는 라이브러리 Celery 에 대해 간단히 소개 드리겠습니다.

파이썬에서는 비동기 모듈을 사용하지 않으면 기본적으로 동기 방식으로 작업을 처리합니다. asyncio를 사용하지 않고 비동기 작업을 원할 때 Celery 를 사용하면 됩니다.

Celery 는 라이브러리지만 broker 와 backend 가 필요합니다. broker는 큐이고 backend 는 샐러리가 처리한 결과물을 저장합니다. 아래 그림을 보시면 이해가 쉽습니다.

Celery 작동 흐름

python 에서 celery 라이브러리를 이용해 작업 요청을 하면 celery 라이브러리는 broker 에 메시지를 삽입합니다(큐에 추가). 다른 프로세스인 worker 는 대기하고 있다가 큐에 메시지가 들어오면 읽고 작업을 수행합니다. 작업이 완료되면 backend 에 작업 결과물을 저장합니다.

그림에서는 마치 동기적인 작업처럼 그려 놓았는데 작업 요청은 non-blocking 입니다. 따라서 시간이 오래 걸리는 여러 개의 작업을 먼저 요청해 두고 다른 작업을 하다가 나중에 결과 수신을 해도 되는 것이죠. 수신의 경우엔 blocking 입니다. 작업이 완료될때까지 계속 대기합니다. 이 경우 코드는 아래와 비슷해질 것입니다.

def main():
    # 시간이 오래 걸리는 작업 요청
    hw1 = heavy_work_1.delay()
    
    # 다른 작업 수행
    work2_result = work2()
    ...
    
    # 결과 수신
    hw_result = hw1.get()
    
    return hw_result, work2_result

이번에는 흔한 예제를 보겠습니다. 회원가입 예시인데요. 회원가입을 하게 되면 database 에 레코드를 추가하는 작업은 오래 걸리지 않지만 이메일 발송이 2초에서 3초 정도 소요되는 경우가 발생합니다. 이런 절차를 전부 기다리게 되면 고객이 가입 버튼을 누르고 2초에서 3초정도 기다리게 됩니다. 이 때 celery 에게 메일 발송 작업을 요청하고 고객에게 응답을 바로 전송하면 지연 시간 없이 다음 페이지로 넘어가게 됩니다. 확인 메일 발송에는 보통 반환값을 기다릴 필요가 없기 때문에 결과를 수신하는 코드는 제거했습니다.

def register_user(username, password, email):
    # 데이터베이스에 회원 레코드 삽입
    db_success = insert_to_database(username, password)
    
    if not db_success:
        return False
        
    # 확인 메일 발송 (non-blocking)
    send_verification_email.delay(username, email)
    
    # 메일 발송 기다리지 않고 응답 전송
    return True

두 가지 시나리오로 간단하게 celery 라이브러리를 알아봤는데요. 기회가 된다면 celery 의 지원 범위와 사용 예시를 포스팅하도록 하겠습니다.