지피지기 백전백퇴

몇가지 잡담


개요

크게 쓸 글이 없어서 일단 작은 업데이트들을 해본다.

  • 혼자 쓰는 브라우저 익스텐션을 조금 업데이트했다. 온갖 쓸데없는 기능을 누더기처럼 덕지덕지 붙여넣은 물건인데, 이번에 추가한 기능은 특정 웹사이트에 방문했을 때 내용을 긁어서 내 서버로 보내는 기능이다. 나중에 리더로 읽을 심산이란 말이지.
    • 다만 원치 않을때 크롤링이 동작하는건 싫으니 스위치를 달아서, 해당 스위치가 켜져있을 때에만 내용을 긁도록 했다.
    • 스위치는 chrome.storage.local API를 이용하면 간편하다.
    • 스위치를 켜고 끄는 UI는 익스텐션의 팝업창을 활용했다. 기존에 간단한 키보드 네비게이션 (화살표 위아래로 메뉴를 선택해서 이동하는 간단한 UI)을 만들어서 쓰고 있었기 때문에, 해당 네비에 임의의 플래그를 켜고 끌 수 있게 추가한 정도.
  • 제대로 긁었나 확인하려고 서버에 들어갔다가 /srv/db에 가야하는데 실수로 /var/db에 들어갔다가 기겁했다. 있어야 할 데이터베이스 파일이 있는게 아니라 웬 /var/db/sudo/lectured/1000이라는 경로가 존재하는 것이다. 와 어떤 고인이 내 서버를 해킹해서 준엄한 가르침을 내렸구나 서버 어떻게 리셋시키냐… 하고 쫄았는데, 알고보니 sudo 명령어의 안내 메시지 확인 내역이었다.
    • 서버는 언제든지 털릴 수 있으니 주기적으로 백업을 하자. 물론 이렇게 말하면서도 백업은 귀찮아서 하지 않았다.
  • 해당 기능에 맞춰서 크롤링 결과물을 저장하는 서버에서도 태그 기반 인덱싱 기능을 추가했다. 익스텐션을 통해 긁은 결과물과 다른 곳에서 모은 데이터를 한곳에 짬뽕으로 모아두되, 필요한 경우 특정 태그를 필터링해서 조회할 수 있도록 하기 위해서다. 태그 기반 검색을 가능하도록 하기 위해 테이블을 다음과 같이 설계했다.
    -- 기존에 있던 테이블
    CREATE TABLE src (
      id INTEGER PRIMARY KEY AUTOINCREMENT,
      insert_date DATETIME DEFAULT CURRENT_TIMESTAMP,
      title TEXT,
      content TEXT
    );
    
    -- 새로 추가한 테이블
    CREATE TABLE tags (
      id INTEGER PRIMARY KEY AUTOINCREMENT,
      src_id INTEGER,
      tag TEXT,
      FOREIGN KEY (src_id) REFERENCES src(id) ON DELETE CASCADE
      UNIQUE (tag, src_id)
    );
    • 개념상 unique 인덱스틀 통해서 tag에 대응하는 src_id의 목록을, 또 foreign key 인덱스를 통해 src_id에 대응하는 tag의 목록을 제공할 수 있으려니 싶은데, 실제로 그렇게 동작하는 지 확인해본 것은 아니다. 어차피 데이터 1000개 단위에서 무슨 성능 최적화를 신경쓰겠는가.
    • 복잡한 태그 검색을 고려한 것은 아니라서 특정 태그가 없는 결과물만 검색한다던가, A 태그는 있지만 B 태그는 없는 것을 검색한다던가 하는 고려는 하지 않았다.
  • 위와 같이 개인용 툴에 조금씩 기능을 추가하면서 느낀점은, 내가 그렇게까지 최적화나 깔끔한 코드에는 관심이 없더라는 것이다. 어차피 개인용 툴에서 다루는 데이터의 범위라는게 수천개 남짓, 혹은 텍스트 파일 기준으로 500KB 정도라서 막상 시간복잡도가 크게 영향을 미치는 규모는 아니기도 하고, 또 설령 성능이 중요해진다고 하면 그때가서 최적화를 시도해도 충분하지 않나 싶다.
  • 업무 관련해서 pola.rs를 좀 사용해보고 있다. Python API를 제공하지만 내부는 Rust로 구현된 데이터 분석 엔진 같은건데, 제대로 짰을 때 병렬처리에 장점이 있다.
    • 다만 Python - Rust interop을 하기 때문에 Dataframe에 Python lambda 함수를 적용한다던가 하는 식의 기능을 사용하려고 하면 각 Row를 Python에 보내서 처리하고 그걸 다시 Rust로 전달하는 식으로 구현되어 있어서 성능이 크게 떨어지게 된다.
    • 대신 SQL 쿼리 실행기가 구현되어 있어서 SQL로 표현가능한 기능은 Rust 엔진 내에서 빠르게 병렬처리가 가능한 것으로 보인다. 이를테면 이런 식이다.
      pdf = pl.DataFrame({"a": [1,2,3,1,2,3], "b": [4,5,6,7,8,9]}).lazy()
      pdf.group_by("a").agg(pl.sql_expr("SUM(b)").alias("sum_B")).collect()
      shape: (3, 2)
      ┌─────┬───────┐
      │ a   ┆ sum_B │
      │ --- ┆ ---   │
      │ i64 ┆ i64   │
      ╞═════╪═══════╡
      │ 3   ┆ 15    │
      │ 2   ┆ 13    │
      │ 1   ┆ 11    │
      └─────┴───────┘
    • 대충 1~2GB정도의 데이터라면 그냥 pandas를 쓰는게 낫겠지만, 만약 메모리에 올렸을 때 10~20GB 정도 되는 데이터라면 polars도 충분히 고려해볼만 하다고 본다.