재고의 적정 유지와 발주 계산은 본래 물류 업무가 아니다

물류는 재고의 현품 관리를 하는 부문이지 재고의 적정 유지와 발주 계산, 발주 업무를 해서는 안됩니다.
재고 유지에 관한 업무는 생상 관리와 영업 업무 관리하에 있어야 하며 자산 관리를 책임지고 있는 부서가 그 업무를 해야 한다.
(기간 시스템)
즉 물류 부문은 현 품관리인 보관과 입출고 하역, 수배송이라는 실행 작업을 담당하는 부문이다.

적정한 발주 계산·보충 계산에 수요 계획이 필요

수요 계획이란 '언제 얼마나 팔고·사용하냐'에 대한 정보를 말합니다.

수요 계획이 없으면 적정한 발주 계산과 보충 계산이 불가능하기 때문에 필요한 재고를 계산할 수 없습니다.
수요 계획(수요 예측, 판매계획) → 소요량 계산(발주 계산) → 생산 수배·구매 수배
수요 계획의 종류는 아래와 같습니다.

  1. 통계적 수요 예측
    • 통계 모델을 사용해서 예측하거나 통계식을 사용합니다.
    • 이점: 개선 가능, 합리화
    • 결점: 운용비용 높음, 어려움
  2. 사람에 의한 수요 예측
    • 사람이 예측합니다.
      • 감, 경험, 담력
    • 이점: 저렴하고 간편함
    • 결점: 개선 곤란, 속인화
  3. 고객 내시(의도)
    • 고객 내시(의도)를 이용합니다.
    • 이점: 저렴하고 간편함
    • 결점: 리스크

통계 예측에 과거 실적을 사용하는 경우 과거 실적에 돌발적인 특수 수요가 포함됐다면 예측 정도가 떨어집니다.

따라서 특수 수요를 제외하고 '실질적' 실적으로 '실질적' 예측을 하지 않으면 안 됩니다.

소요량 계산이란?

소요량 계산이란 필요한 재고 수량을 구하는 것입니다.

재고를 고려하지 않은 소요량을 총 소요량, 재고를 뺀 진짜 필요량을 순 소요량이라고 합니다.
소요량 계산의 대상이 되는 판매계획은 원 단위 판매 수량인가 주 단위 판매 수량인가 일 단위 판매 수량인가에 따라 수량이 바뀝니다.

소요량 계산 시에 고려하는 기간을 버킷이라고 합니다.

기준 재고 계산과 통계적 재고 이론

발주 계산을 할 때는 기준 재고를 설정하고 순 소요량을 계산합니다.

기준 재고 = 사이클 재고 + 안전 재고로 계산합니다.
사이클 재고는 입고 타이밍 기간(버킷)의 순 필요 재고량입니다.

사이클 재고는 미래의 수요를 내다보고 준비하는 것인데, 예측이 어긋날 경우가 발생할 수 있습니다.
그래서 입고되는 사이에 예측과 실적의 차이를 조달할만큼의 여유분을 안정적으로 준비해 두는 안전 재고가 필요합니다.
발주 계산의 종류는 아래와 같습니다.

  1. 발주점 방식
    • 소요량을 계산하지 않고 간이로 발주 계산을 하는 대표적 방법
    • 준비된 재고를 사용하다가 설정된 발주점을 밑돌면 정해진 정량을 보충합니다.
    • 발주점은 발주하고 나서 납입하기까지의 리드타임 기간에 해당하는 재고를 가지면 되므로 리드타임분의 기간에 소비(출하 또는 사용)되는 수량으로 설정합니다.
  2. Min-Max법
    • 설정된 Min 값을 밑돌면 Max 값까지 보충 수량을 계산하는 방법
    • Min은 발주점에 가까운 개념입니다.
    • Max 값은 설정한 최댓값이 되도록 계산합니다.
  3. 보충 계산형
    • 발주 사이클이 되면 팔린 만큼·사용한 만큼 보충하는 간단한 방법
    • 단, 발주 리드타임이 짧아야 하는 전제가 있습니다.
  4. 더블 빈 법, 트리플 빈 법
    • 더블 빈이랑 2개의 상자(빈)에 든 재고를 준비하고 한쪽 상자(빈)의 재고가 없어지면 다음 빈을 사용하는 사이에 없어진 빈이 가득 차도록 발주하는 방법
      • 트리플 빈은 3개의 상자(빈)
    • 부가가치가 낮은 물건과 조달 리드타임이 짧아 어디에서든 바로 손에 입수할 수 있는 물건에 사용되는 가장 단순한 개념
  5. 최소 발주 수량(MOQ), 최소 포장 단위(SPQ), 최소 발주 금액(MOA)
    • 기준 재고와 발주점 방식으로 발주 수량을 계산해도 실제로 구매를 수배하는 단계가 되면 발주 비용을 낮추거나 상거래상 규정이 있기도 해서 계산한수량 자체를 발주할 수 있는 것은 아닙니다.
      • 대게는 원하는 수량보다 많이 발주해서 재고에 영향을 미칩니다.
    • Minimum Order Quantity: 최소 발주 수량이 100이면 100을 발주해야 합니다.
    • Standard Packing Quantity: 최소 발주 수량의 일종
    • Minimum Order Amount: 금액 기반의 최소 발주 단위로 주문해야 합니다.

발주 계산은 자산 관리이고 현품 관리와는 분리해야

발주한다는 것은 기업의 생산과 판매에 관계되는 자산을 구입하는 업무이지 물류 업무는 아닙니다.
자산으로서의 재고란 팔거나 사용하기 위한 재고로 영업 부문과 생산 관리 부문이 재고 책임을 갖고 관리하는 재고입니다.
자산 관리로서의 재고 관리는 재고를 통해 기업 수익와 자금 융통에 영향을 미치는 조직이 책임을 져야 합니다.

따라서, 재고 책임을 질 수 있는 것은 물류가 아닙니다.
물류는 현품을 관리하고 수배송에 책임을 지고 보관과 수배송에 관한 QCD 목표를 실현하기 위해 제대로 현품을 관리하는 일이 임무입니다.

소요량 계산 업무와 발주 업무는 조직을 나누어야 한다

발주 수량을 정할 수 있는 권한자가 공급자와 가격·수량·납기를 직접 고섭할 수 있다면 유착이 일어날 수 있다.
그래서 기본적으로 발주 수량을 정하는 부문이 공급자에게 직접 발주해서는 안 되는 것입니다.
마찬가지로 재고 자산을 관리하는 업무와 물류를 분리해야 합니다.
재고의 자산 관리가 가능한 조직이 현품까지 관리하면 조작과 부정이 벌어지기 때문입니다.
다만, 포장 자재 등은 물류 부문이 아니면 소요량을 알 수가 없습니다. 그 경우는 물류 부문이 소요량을 계산합니다.
그러나 이때에도 물류 부문이 직접 공급자에게 발주 의뢰를 하는 것이 아니라 구매 부분에 구매 의뢰를 해서 공급자에게 발주할 때는 구매 부분을 통해야만 합니다.

시스템 기능 배치

기간 시스템은 '수주에서 자산으로서의 재고 담당, 출하 지시', '수요 계획과 발주 계산에서 구매 발주까지를' 담당합니다.
물류 시스템은 '출하 지시를 받아 보관하고 있는 현품 재고를 할당하고 출하하는 것', '입고 예정을 건네받아 입하 수입을 하고 현품을 입고 계상하는 범위까지'를 담당합니다.
로트 넘버와 시리얼 넘버 등의 스테이터스 관리는 물류 시스템으로 수해하지만 이때의 로트 넘버와 시리얼 넘버는 타 시스템에서 취득합니다.

기간 시스템은 회사의 거래와 내부 처리를 기록하는 시스템입니다.

수주, 여신 관리, 재고 관리, 충당, 출하 지시, 매출 계상, 외상 채권 관리 등의 기능이 있습니다.
기간 시스템으로 수주하고 이용 가능 재고의 유무를 확인하고 필요시 재고를 충당해서 출하 지시를 내리고 WMS에서 연계해서 창고에 있는 현품의 출고 지시로 이어지는 흐름입니다.

기능 정리 (엔지니어가 알아야 할 물류시스템의 '지식'과 '기술' p.88, p.91 참고)

  • 수주
    • 고객(발주처)으로부터 주문을 받아 계약이 성립된 상태
    • 납기, 수량, 단가, 조건 등을 기록 → 생산 계획·출하 계획으로 연계
  • 생산 계획·발주
    • 생산 계획은 무엇을, 얼마나, 언제까지 생산할지 미리 정하는 내부 계획
    • 발주란 필요한 자재·서비스·공사 등을 외부 업체에 주문하는 행위
    • 프로세스: 발주 혹은 생산 계획 등록 → 입고 예정 데이터 작성 → WMS와 연동 (입고 예정 데이터 취득)
    • 발주 계산 방법
      1. 소요량과 기준 재고 계산
      2. Min-Max법
      3. 더블 빈 법 (소모품 관리에 주로 활용)
  • 재고 관리
    • 일반적으로 재고는 복수의 시스템으로 관리합니다.
      • 기간 시스템
        • 자산상의 재고 관리를 수행합니다.
      • WMS
        • 현품을 관리합니다.
    • 양쪽 모두 재고 정보를 갖고 있는 경우 충당 방법을 어떻게 분담할 지를 정해야 합니다.
      • 기간 시스템
        • 자산 관리상 충당
      • WMS
        • 현품 관리상 충당
  • 충당
    • WMS와의 역할 분담과 연계를 확실히 합니다.
      • 자산 관리상 충당
  • 할당
    • 이용 가능 재고 스테이터스를 신경 쓰지 않고 할당하는 데, 이를 총량 할당이라 합니다.
  • 출하 지시
    • 수주한 후 할당해서 출하 지시를 내립니다.
    • 출하는 기업 바깥으로 이동하는 것으로 출하 후에 수배송, 고객 도착으로 이어집니다.
      • 출하는 그 후 매출로 직결되는 기업 간 거래의 시작입니다.
    • 출하 계획 → 운송관리(TMS)·WMS 연계
  • 매출 계상
    • 출하 완료 기준으로 매출 인식
    • ERP 재무 모듈과 연결
  • 외상 채권 관리
    • 외상거래에 대한 채권 관리
    • 입금·미수금 관리, 연체 채권 관리
  • 여신 관리
    • 거래처 신용한도 설정 및 관리
    • 수주·출하 단계에서 신용한도 체크

창고 관리 시스템

 

물류 시스템의 핵심입니다. 상세한 로트 넘버와 입고일 등을 세부적으로 관리합니다. 기간 시스템과 기능을 분담하고 면밀한 연계를 필요로 합니다.

기능 정리 (엔지니어가 알아야 할 물류시스템의 '지식'과 '기술' p.96 참고)

1. 하역 업무

  • 입고 관리
    • 입고 예정 데이터 취득
      • 입고 시 삭제 대상 정보가 됩니다.
      • 바코드 시스템이 있는 경우 WMS에서 바코드 단말기에 입고 예정 데이터를 전송합니다.
      • 입고 예정 데이터를 보고 원할하게 입고 업무가 진행될 수 있도록 사전준비를 합니다.
        • 입고 가능한 로케이션 공간, 입하·입고 작업자의 인원 배치 등을 계획합니다.
        • 입고 가능한 로케이션 공간 상황은 WMS에서 확인하고 로케이션 이동이나 이고가 필요한 경우는 출고 지시와 입고 지시를 하여 피킹해서 로케이션 이동과 이고를 수행합니다.
    • 입하
    • 검품
    • 입고
    • 입고 예정 삭제
  • 출고 관리
    • 수주·출하 지시 취득
    • 충당
      • 보통 출고 지시와 출하 지시 전에 출하하는 물건을 지정하기 위해 재고를 충당합니다.
        • 제품이라면 수주해서 제품 재고를 충당합니다.
      • 기간 시스템과의 역할 분담과 연계를 확실히 합니다.
        • 현품 관리상 충당
    • 할당
      • 재고 스테이터스를 고려해서 할당하는 데, 이를 실할당이라 합니다.
    • 출고 지시
      • 출고란 창고에서 보관된 상황에서 창고 밖으로 이동(출고)시키는 것을 말합니다.
      • 출고 전표 또는 피킹 리스트를 출력해서 출고 지시를 내립니다.
      • 담당자는 출고 지시, 피킹 지시를 받아 트럭 야드로 물품을 출고하고 트럭에 물건을 인도하는 타이밍에 출하 전표를 건네면 출하가 진행됩니다.
    • 피킹 표시
    • 포장 지시
    • 분류
    • 전표 인쇄
    • 출하, 화물 인도
    • 출하 지시 잔량·수주 잔량 삭제

2. 보관 업무

  • 로케이션 관리
    • 보관 장소를 정하고 무엇이 어느 정도 보관되어 있는지를 한눈에 알 수 있도록 관리하는 것입니다.
  • 현품 관리·스테이터스 관리
    • 현품 관리는 정해진 로케이션에 확실하게 물건이 있는 상태로 유지하는 것입니다.
    • 단순히 보관 장소에 두는 것은 보관이 아닙니다.
      • 보관되어 있는 현품의 스테이터스를 관리해야 합니다.
    • 스테이터스 종류
      • 양품
        • 검사 합격품 등의 양품은 할당 가능 재고입니다.
        • WMS에서 할당합니다.
      • 보류품
        • 검사 대기 상태여서 할당이 불가능한 상황의 스테이터스입니다.
        • 검사 결과에 따라서 양품 또는 불량품, 폐기품으로 변경합니다.
        • 유효 기간 마감 재고도 보류품이 됩니다.
      • 반품
      • 불량품
        • 구입품인 경우에는 반품합니다.
        • 자사 제조품으로 재생 가능한 경우는 공장으로 반품해서 재생합니다.
        • 재생 불가능한 불량품은 폐기품으로 구분합니다.
        • 불량 상태에 따라 등급을 정해서 판매하기도 합니다.
          • 외장 상자의 손상 정도가 경미한 물건은 B급품으로 구분해서 할당·출하할 수 있도록 판매하기도 합니다.
          • 이 때는 양품 스테이터스로 전환하지만 양품 스테이터스를 더 세분화해서 A품, B품으로 상세 스테이터스를 구분할 필요가 있습니다.
      • 폐기품
        • 재생이 불가능한 불량품은 폐기품 스테이터스가 됩니다.
        • 기간 시스템에서 재고 폐기, 재고 처분 비용이 승인되고 나서 폐기 지시가 내려오면 그 후 출고하여 폐기합니다.
    • 시대에 맞춰 고도화되는 스테이터스 관리 항목
      • 입고일
        • 현재 일자로부터 보관 기간을 산출할 수 있습니다.
      • 유효기간
        • 식품, 약품, 화학품 등 사용 기간을 정해서 관리하는 품목 관리에 사용됩니다.
      • 로트
        • 입고시 로트 넘버를 부여해서 관리합니다.
          • 동일 제품이 대량으로 재고될 때 채택됩니다.
        • 어느 고객에게 어느 로트 넘버가 출하됐는지를 기록하면 추적이 가능합니다.
        • 오래된 로트 출고 출하를 주의하여 로트 역전을 방지해야 합니다.
          • 고객별 선입선출의 철저한 준수
          • 항상 전회에 출하한 로트 넘버와 비교해서 새로운 로트 넘버의 물품이 출고되도록 출고 로직을 내장합니다.
          • 로트가 오래될 것 같을 때는 조기에 확보 상태를 해제하는 사내 규정의 작성
      • 시리얼 넘버
        • 중장비, 건설기계, 제조장치 등의 대형기기를 1대별로 관리하는 넘버를 가리키며 연번 관리라고도 할 수 있습니다.
        • 재고 보관시 시리얼 넘버를 유지합니다.
        • 어느 고객에게 어느 시리얼 넘버의 제품이 출하됐는지를 기록하면 'After Sales'에 해당하는 보수와 수리 시기를 파악하는 데 필요한 정보가 됩니다.
      • 원산지
        • 기준을 충족하지 못하는 국가의 제품을 배제할 수 있습니다.
        • 문제가 발생한 원재료를 추적하는 트레이스 백에도 중요합니다.
          • 특정 원산지에 문제 발생시 해당 원산지로부터 수입 중지 및 이미 구입한 원재료 폐기가 가능합니다.
      • 성분
        • 화학약품과 용액 등은 성분의 농도 등 성분 관리가 필요합니다.
          • 같은 1L라도 필요한 성분에 따라 유효 성분 함유량(%)이 달라집니다.
      • 내용물 중량
        • 성분 관리와 비슷합니다.
        • 필요에 따라서 입고 시에 바르게 중량을 계량해서 유지합니다.
          • 예를 들면, 포장지에 25kg이라 적혀 있어도 실제로는 24.5kg 또는 26kg일 수 있습니다.
        • 표준과 실제의 차이를 중량 오차 차이라고 합니다.
          • 매번 관리해야 하는 번거로움이 있기 때문에 WMS로 관리해야 할지의 여부는 업무 요건을 감안해서 신중히 검토할 필요가 있습니다.
      • 용적·중량
        • 포장의 짐 모양을 검토할 때나 트럭·컨테이너에 적재 시 3차원 짐짜기 계산을 할 때 용적과 짐 모양은 중요한 정보입니다.
        • 수송 가격을 검토할 때 용적과 중량 정보가 필요합니다.
      • 해비(수출 허가) 판정
        • 수출 허가를 받지 않으면 출하해서 수출하는 것이 불가능합니다.
      • 특정 유해물질 관리(RoHS 대응)
        • 납과 카드뮴 같은 특정 유해물질의 수입을 금지하는 국가도 많이 있기 때문에 보관되어 있는 물건에 이러한 유해물질이 포함되어 있는지 여부를 관리하기 위함입니다.
      • 특정 고객용
        • 특정 고객 이외의 할당을 금지하기 위함입니다.
      • 영업 확보
        • 영업 담당자가 특정 고객용으로 재고를 확보하고 그 영업 담당자 이외에 할당하지 못하게 하기 위함입니다.
        • 각 영업 담당자가 재고를 확보하고 있기 때문에 재고가 있는데도 출하하지 못하는 경우가 발생합니다.
          • 필요시 다른 영업 담당자와 서로가 확보한 재고를 대치하기도 합니다.
      • 출고 보류
        • 판매가 종료된 상품이나 판매 정지 상품 또는 신제품 중에서 판매 확보 재고 등 기업의 사정에 따라 할당을 하지 못하도록 하기 위함입니다.
  • 입출고 관리
  • 기타 상태 관리

3. 기타 중요 업무

  • 반품
  • 재고조사

채용 시장이 작아지고, 경력 대비 더 많은 스킬을 요구하는 요즘, MSA, EDA와 같은 기술을 요구하는 JD를 확인해볼 수 있습니다.

이벤트 기반 아키텍처를 통해 실시간성이 중요한 환경에서 안정적이고 빠른 시스템 운영을 기대할 수 있습니다.

 

자, 그럼 왜 이벤트 기반 아키텍처를 알아야 하고 사용할 수 있어야 할까요?

일상의 예시를 통해 알아보겠습니다.

 

우리가 점심시간에 밥을 먹고 카페에 커피를 사서 사무실로 복귀하기로 했습니다.

(점심 시간: 1시간, 남은 시간: 20분)

근처에 개인 카페가 생겨 할인 행사를 진행하고 있었고, 많은 사람들이 오고가는 이곳에서 커피를 사기로 결정합니다.

주문을 하러 갔는데, 사장님이 분주하게 메뉴를 제조하고 있었습니다. -(1)

주문 제조를 마치고 사장님이 나의 주문을 여쭤 보았고, 나의 주문을 마쳤습니다. 하지만 마침 뒤에 식사를 마치고 들어온 4명의 단체 손님이 오셔서 이들의 주문까지 받고 사장님은 음료 제조를 시작했습니다. -(2)

점심 시간을 5분 남겨두고 주문한 음료를 받았습니다. 그냥 메머드를 갈 걸 하고 후회를 했습니다. -(3)

 

지금의 예시가 이벤트 기반 아키텍처가 왜 필요한지를 단 번에 알 수 있는 예시가 아닐까 생각이 듭니다.

우리가 주문을 하기 위해서는 이전 주문에 대한 음료 제조가 다 끝나야 합니다.

fun 주문() {
  val 포스기에서 주문 받기 (결제 포함)
  val 상품 제조

  return 상품 전달
}

여기서 한 가지 더 생각할 것은 1개의 요청 -> 1개의 제조 뿐만 아니라 n개의 요청 -> n개의 제조도 가능하다는 것입니다.

왜냐하면, 주문을 받고 음료를 제조하는 사람이 1명이기 때문입니다.

만약, 좀 전에 주문을 잘못 받는 문제가 발생한다면? 혼자서 이것을 다시 제조하고 다음 주문을 받으려다 이후에 손님들이 환불을 요청하는 등 더 복잡한 상황이 발생할 수도 있겠습니다. (장애 발생 및 처리에 대한 복잡성)

카페 사장님의 입장에서는 상권 분석을 했겠지만, 매출이 얼마가 발생할지도 모르는 상황에서 무작정 직원과 아르바이트를 뽑는 것은 무리라고 생각했을 수도 있습니다.

 

만약, 카페에서 이벤트 기반 아키텍처를 도입한다는 것은 무엇을 말하는 것일까요?

제 생각에는 직원, 아르바이트생을 뽑는 것입니다. 그럼, 위에서 작성한 예시 (1), (2), (3)이 아래와 같이 바뀝니다.

 

주문을 하러 갔는데, 사장님이 어떤 메뉴를 드실지 나에게 질문하고, 아르바이트생은 분주하게 메뉴를 제조하고 있습니다. -(1)

나의 주문은 완료 즉시 제조가 시작됩니다. 마침 뒤에 식사를 마치고 들어온 4명의 단체 손님이 왔습니다. 사장님은 주문을 받습니다.  -(2)

1분 후에 주문한 음료를 받았습니다. 한 입 마셔보니, 마음에 들어 다음에 또 방문해야 겠다는 생각을 가지고 사무실로 복귀합니다. -(3)

fun 주문() {
  val 포스기에서 주문 받기
  이벤트 발생 요청(상품 제조)
  
  // 이제 다음 주문을 받으면 됩니다.
}

사장님은 비용을 지불하고 아르바이트생을 뽑았습니다. (이벤트 기반 아키텍처 도입)

이 비용 지출이 당장에 사장님에게 있어 마이너스일지 모릅니다. 하지만 카페에 방문했던 손님이 만족할 가능성이 훨씬 더 크기 때문에 추후에는 더 많은 기대 수입을 가져다 줄 거라는 것을 짐작할 수 있습니다.

 

다시, IT 회사의 관점으로 돌아와서...

아르바이트생과 회사 직원의 관리 비용 차이가 정말 많이 납니다.

일반적인 IT 회사 대표의 입장에서 생각해보면, 서비스로 매출이 발생하지 않더라도 매달 발생하는 지출 비용을 생각하면 이벤트 기반 아키텍처를 도입하는 것을 망설일 수 밖에 없습니다.

 

이벤트 기반 아키텍처가 필요할 만큼의 사용자, 트래픽, 데이터가 현재 발생하고 있는지, 현재 서비스에서 매출이 발생하는지, 매출이 성장하고 있는지 등을 고려할 수 있습니다.

 

하지만 모든 것은 트레이드오프가 존재합니다. 만약 지금은 무리한 아키텍처라고 생각할 수 있지만, 정말 이제 이벤트 아키텍처를 도입할 필요가 있다고 느꼈을 때, 변경을 하려면 더 큰 비용이 발생할 수도 있기 때문입니다.

프로젝트 계기

요즘 플러터 인기가 하늘을 치솟고 있다. 안드로이드, iOS 플랫폼에서 사용되는 앱을 한 번에 개발하는 건 시간 자원을 엄청나게 자원을 줄일 수 있다는 큰 장점이 있기 때문인 것 같다. 세계적인 확장성에 있어도 웹 시장보다는 모바일 어플리케이션 시장이 더 크다고 생각한다. 따라서, 앱을 개발하고 배포하는 과정을 경험하면서 플러터를 학습하고 배포 과정의 어려움을 직접 체험해보려 한다.

학습 목표

다트 언어와 플러터 그리고 구글 파이어베이스를 통해 저렴하고 빠르게 모바일 어플리케이션을 개발하고 배포한다.

프로젝트를 통해 바라는 점

(일정 관리)

"캘박해둬~"라는 말이 있다. 이것은 OS 별 내장되있거나 다운 받은 달력 앱에 우리의 약속을 저장하라는 의미이다. 그런데 달력 앱을 열면 주제와 상관없이 업무 등 다양한 일정들이 빼곡하게 있는 것을 보면 조금 어지럽다.

그래서 뭔가 내 일상은 따로 분리하고 싶은 욕구가 있었다. 푸시 알림도 마찬가지다. 하나의 앱에서만 여러 알림이 오면 중첩되서 확인을 하지 못할 경우가 발생한다.

 

(가계부)

인기 있는 가계부 앱이 있지만, 뭔가 특정 시기동안 특정 액수를 사용하고 기록하고 싶은 욕구가 있었다. 마치 예전에 만원의 행복이라는 프로그램을 예로 들 수 있다. 내가 여행가서 여행비를 30만원을 가져갔는데, 이 30만원을 확인하려면 여행 통장을 개설하던가 직접 현금을 세던가 등 불편함이 생긴다.

 

이러한 이유로 해당 앱을 기획했고, 일상 생활 속에서 정말 쓸모있는 앱이 되었음 하는 바램과 적어도 내 주변 사람들이 정말 좋다고 써줬으면 좋겠다고 생각했다.

프로젝트를 통해 배운 점

(Figma)

회사를 다니면서 웹 개발에 대한 피그마 디자인을 본 경험은 없었지만, 앱 개발 시에는 피그마 디자인을 바탕으로 개발을 한 경험이 있었다. 정해진 기획안이 있다는 점이 정말 마음에 들어서 나도 피그마를 학습해서 간단한 디자인을 만들어봤다. 

 

(배포)

가상 디바이스로 했을 때는 잘 됐던 거 같은데 막상 내부 테스트로 올려보니 디바이스에 따라 화면이 깨지기도 하고 동작하던 것이 잘 안됐던 적도 있었다. 다시 배포하기 까진 생각보다 너무 귀찮아서 좀 그랬다. 이로 인해 모바일 개발에 대한 환상이 조금 사그라 들었다. 그리고 안타깝게도, 실제 배포를 모두 거절당했다. 가장 큰 이유는 역시 개인정보에 대한 규제 때문이었다. 회원 가입 시에 생년월일, 성별 등이 왜 필요한지, 랜딩페이지 여부, 개인정보 및 이용 관련 안내 등 생각보다 너무 까다로웠다. 웹 개발의 장점을 다시 한 번 뼈저리게 느꼈다. 

PS.

도메인을 등록할 때, 도메인 명을 엄청 고민하듯이 앱을 기획할 때도 앱 이름을 뭐로 할지에 대해서 정말 많이 고민했었다.

결국 나만의 플랫폼처럼 내가 다양한 서비스를 만들어 갈거라는 생각 하에 dev + kjh(이름 초성)을 섞어서 dkejvh, 아더포가 되었다.

프로젝트 계기

(AWS) 데이터센터가 있는 IDC 환경에서 이미 구축된 시스템을 운영해보거나 클라우드 서비스를 사용하더라도 거대하지 않은 플랫폼에 간단한 3-tier 구조의 시스템을 구축해 본 경험 밖에 없었다. 새로운 서비스를 개발해야 할 때 비용 및 속도를 생각하면 클라우드 컴퓨팅 서비스를 이용하는 게 당연하다. 그래서 클라우드 컴퓨팅 서비스 학습의 필요성을 느꼈고, 스타트업과 신규 서비스에 맞는 강점(온디맨드, 엘라스틱)이 있고 가장 거대한 서비스인 AWS를 경험하기로 했다.

 

(해외여행) 부모님 세대가 아닌 많은 사람들이 해외여행을 쉽게 계획하고 방문한다. 유튜브나 블로그, 검색만 해도 많은 후기들이 있기 때문이다. 하지만 그런 검색을 통해 가는 여행은 비슷한 패턴 뿐이다. 우리나라를 예시로 들면 남산이나 광화문처럼 대표 관광지를 방문하고 핫플레이스에 가서 맛있는 식사를 하는 것을 들 수 있다.

이러한 여행은 몇 번이고 다시 가라고 하면 굳이 싶은 여행이다.

게다가, 여행 관련 컨텐츠를 선택한 이유는 생각보다 많은 사람들이 여행업을 간과하고 있을거라 생각했다. 나 또한 그랬다. 왜냐하면 직접 항공편 예약하고 호텔스컴바인같은 사이트에서 숙소 잡고 액티비티 따로 예약하고 스스로 다 할 수 있는데 왜 굳이 더 돈을 주고 여행을 가야 하는지 생각할 수 있기 때문이다.

하지만 스스로 찾아보고 가는 여행에는 한계(관광버스 예약같은 단체 여행의 편리함, 다향한 컨텐츠 등)가 있다. 모두투어, 하나투어 같은 대형 관광업 말고도 중소기업에서 운영하는 여행사가 엄청나게 생각 이상으로 많다. 그리고 생각 이상의 매출과 마진이 발생한다. 왜냐하면 대게 돈에 여유가 있는 중장년층이 이러한 여행사를 많이 찾기도 하고 편리한 여행 제공과 맞춤형 컨텐츠(트래킹, 온천 등) 구성이 좀 더 돈을 지불하더라도 합당하다고 느끼기 때문이다. 여행 상품들을 잘 살펴보면 생각 이상으로 컨텐츠가 좋다.

 

학습 목표

간단하게 기획한 신규 서비스를 AWS 인프라 구축 + 타임리프와 HTMX를 이용한 서버 사이드 렌더링 개발 방식을 통해 저렴하고 빠르게 배포한다.

프로젝트를 통해 바라는 점

이러한 여행 상품은 인터넷에 검색한다고 찾기 매우 어렵다. 왜냐하면 이런 상품 대부분은 네이버 카페를 통해 기존 고객들에게 알리기 때문이다. 한 번 방문했던 고객이라면 카카오톡 채널이라던지, 문자로 신규 상품에 대한 안내를 받을 수 있지만, 이외에는 지인을 통해 알게 되는 것 말고는 사실상 방법이 거의 없기 때문이다.

네카라쿠배당토와 같은 IT 대기업에서는 기술 블로그를 별도로 관리하고 운영하는데, 이러한 글들을 모아서 보여주는 사이트를 자주 이용하곤 했다. 그래서 이러한 방식으로 좀 더 다양하고 재밌는 여행 컨텐츠가 많다는 것을 공유하고 싶었다. 

프로젝트를 통해 배운 점

(AWS)

가상의 네트워크 망을 구축해보면서 실제 네트워크 구조를 간접적으로 느낄 수 있었다.

특히, 여기서 사용하지 않았지만 AWS의 Role 개념에서 스프링의 의존성 주입의 느낌을 받을 수 있었다. 보안 그룹을 설정할 때, 규칙을 하나하나 추가하는 게 아니라, 규칙A에 규칙B을 추가하는 방식을 사용해 봤는데 정말 괜찮은 방식 같다.

만약 규칙A의 변경이 필요할 때, 규칙B를 갈아끼면 되는 것이고 만약 규칙B가 다양한 규칙으로 사용돼고 있어도 규칙B만 수정해도 전부 다 수정돼기 때문에 정말 용이한 것 같다.   

 

그리고 사실 로드 밸런서를 구성할 생각은 전혀 없었는데, Route 53에서 도메인을 구축하고 나니 HTTPS 통신을 하려면 ALB를 구성해야만 했다. 초반엔 생각지도 못해서 그냥 '후이즈'나 '가비아'에서 도메인을 구입해서 NGINX에 설정할 걸 하고 후회했다. 그런데 해보고 나니까 이 방식이 더 편한 것 같다.

 

(해외여행)

모바일 (반응형)
웹 브라우저

업력이 길어 만 개 이상의 데이터가 있을 줄 알았지만 데이터의 개수는 총 1천 개 정도였다. bulkInsert를 통해 데이터를 적재하기 위해 JdbcClient를 사용했다. 예전에 로컬에서 데이터 100만 개를 20만 개씩 bulkInsert 했었는데, 인텔리제이 메모리를 4048M로 설정해서 진행했었다. batchSize를 더 키웠을 때, 컴퓨터 CPU 사용량이 100%가 되는 걸 확인했다. (꽤나 좋은 PC인데..)

아무튼 그래서 프리티어 수준의 저성능 서비스를 사용하고 있기도 했고, 총 데이터 수도 적어서 batchSize는 100개로 설정해서 데이터를 생성했다. 신규 데이터는 가끔 카페 들어가서 확인하고 직접 추가했다. 휴가 시즌를 겨냥해야 일주일에 여행 상품이 3개 정도씩 등록되는 수준이어서 수동 데이터 삽입을 해도 무리가 없었다. 

 

코틀린, 스프링부트를 사용했고 특히 의존성 역전 원칙(DIP)을 고려하여 패키지 구조와 레이어드 아키텍쳐를 설계했다. 그리고 DB 접근 기술로 JPA와 JDBC를 혼용했는데, 레포지토리 어댑터 패턴을 사용해서 구조를 깨지 않고 잘 사용할 수 있었다. 

 

HTMX 관련 글이 레딧에서 자주 보여서 나도 한 번 사용해 봤다. 일반적으로 프레임워크의 서버 사이드 렌더링을 사용하면 HTML 화면을 리턴하는 API와 추가적인 API를 구성해야 하는데, HTMX 방식은 모두 HTML을 리턴하는 방식이다. 물론 데이터 변경 같은 경우에는 그렇지 않아도 된다. 예전 회사에서 JQuery를 사용해본 적이 있었는데 뭔가 더 고급지고 하이레벨의 기능을 쓰는 것 같은 느낌을 받았다. 간단한 MVP는 이렇게 2-tier (WEB, DB) 구조로 개발하면 관리 포인트도 적고 시간 소모도 적어서 정말 좋은 것 같다.

PS.

추가적으로 예전에 '페이히어' 면접 때 당시 근무하고 있던 회사에서 인프라 비용이 너무 많이 나와 고민이 있어서 조언을 부탁드렸는데, 서비리스 아키텍처를 고려해 보라는 말을 해주셨었다. 당시에는 사실 '서버리스면, Supabase나 Firebase같은 서비스 아니야?' 라고 생각하고 말았었다. AWS를 사용해 보고 나니까 서버리스를 적용해서 비용적으로나 관리적으로 많은 이득을 가질 수 있겠다고 생각했다. 특히, 스타트업이나 신규 서비스의 경우에 말이다. 기능이 적기 때문에 S3와 람다를 통해서 충분히 MVP 서비스를 개발할 수 있겠다고 생각했다. 그래서 다음 프로젝트를 한다면 서버리스 아키텍처로 구현해볼 생각이다.

import os
import tempfile

# response
# return StreamingResponse(image_data, media_type="image/png")

with tempfile.NamedTemporaryFile("wb", delete=False) as f:
    f.write(response.content)
    print(f"새로운 파일: {f.__dict__}")
		"""{
			'file': <_io.BufferedWriter name='C:\\Users\\temp\\AppData\\Local\\Temp\\tmprk5y2mf6'>, 
			'name': 'C:\\Users\\temp\\AppData\\Local\\Temp\\tmprk5y2mf6', 
			'delete': False, 
			'_closer': <tempfile._TemporaryFileCloser object at 0x00000199A04B6610>, 
			'write': <function BufferedWriter.write at 0x00000199A0756980>
		}
		"""
    from uuid import uuid4
		
    file_name = uuid4()
    file_path = f"my/bucket/adderss/img_{file_name}.png"
    # 업로드_함수에서 temp 파일 경로인 f.name open(임시파일경로, 'rb')로 읽어 처리한다.
    image_url = await 업로드_함수(f.name, file_path)

os.remove(f.name)

[참고 - Docs]

https://pycryptodome.readthedocs.io/en/latest/src/cipher/aes.html

 

[MODE_EAX]

# pip install pycryptodome
import base64

from Crypto.Cipher import AES


class AES256:
    def __init__(self, key: str):
        self.key = base64.b64decode(key.encode())
        pass

    def encrypt(self, plain_text: str):
        """AES256 암호화

        Returns:
            * tuple (cipher_text, tag, nonce)
        """
        cipher = AES.new(key=self.key, mode=AES.MODE_EAX)
        nonce = cipher.nonce
        cipher_text, tag = cipher.encrypt_and_digest(plain_text.encode())
        return cipher_text, tag, nonce

    def get_encrypted(self, plain_text: str) -> str:
        """AES256 암호화된 데이터를 문자열로 반환"""
        cipher_text = self.encrypt(plain_text=plain_text)[0]
        return base64.b64encode(cipher_text).decode()

    def decrypt(
        self,
        cipher_text: bytes,
        nonce: bytes,
        tag: bytes,
    ):
        cipher = AES.new(self.key, AES.MODE_EAX, nonce=nonce)
        plain_text = cipher.decrypt(cipher_text)
        try:
            cipher.verify(tag)
            return plain_text.decode()
        except Exception as e:
            print(f"aes256 decode error: {e}")
            return False

[실습]

import base64
from uuid import uuid4

aes256 = AES256("key")

cipher_text, tag, nonce = aes256.encrypt("plaintext")
print(f"cipher_text: {cipher_text}")
print(f"b64: {base64.b64encode(cipher_text).decode()}")

result = aes256.decrypt(cipher_text=cipher_text, nonce=nonce, tag=tag)
print(f"result: {result}")  # plaintext

 

[MODE_CBC]

import binascii
from base64 import b64decode, b64encode

from Crypto.Cipher import AES
from Crypto.Util.Padding import pad, unpad

from ..configs import settings


class AES256_MODE_CBC:
    def __init__(self, password):
        self.cipher_pass = password
        self.transformation = "AES/CBC/PKCS5Padding"
        self.iv = bytes([0] * 16)

        sk = self.to_hex_string(self.cipher_pass.encode())
        key_data = self.to_bytes(sk, 16)
        self.key = self.generate_key("AES", key_data)

    def encrypt(self, encrypt_str):
        cipher = AES.new(self.key, AES.MODE_CBC, self.iv)
        plain_bytes = encrypt_str.encode("utf-8")
        encrypted_bytes = cipher.encrypt(pad(plain_bytes, AES.block_size))
        return b64encode(encrypted_bytes).decode("utf-8")

    def decrypt(self, decrypt_str):
        cipher = AES.new(self.key, AES.MODE_CBC, self.iv)
        decrypted_bytes = cipher.decrypt(b64decode(decrypt_str))
        plain_text = unpad(decrypted_bytes, AES.block_size).decode("utf-8")
        return plain_text

    def generate_key(self, algorithm, key_data):
        if algorithm == "AES":
            return key_data
        raise ValueError("Unsupported algorithm")

    def to_hex_string(self, byte_data):
        return binascii.hexlify(byte_data).decode()

    def to_bytes(self, hex_str, radix):
        if radix != 16:
            raise ValueError(f"Unsupported radix: {radix}")
        return bytes.fromhex(hex_str)


aes256_mode_cbc = AES256_MODE_CBC(settings.NICE_EPAY_SECRET_KEY)

+ Recent posts