현재 우리 서비스에서는 장비들을 관리하는 물류 관련 기능이 존재한다.

 

장비들이 회수 되는 경우 서비스에서 "입고 요청"이라는 기능을 통해 장비에 대한 회수 히스토리를 남기는 방식으로 진행된다.

 

입고 요청에 대해서 상태값을 전체 상태로 조회하는 경우 자동으로 입고요청일이 현재 날짜 기준으로 - 30일까지만 검색을 지원한다.
(입고 요청에 연결된 입고 장비가 많은 경우 검색 시 될 수 있기 때문에 정책적으로 제한한것으로 알고 있다.)

 

이번에 특정 조건을 기준으로 입고 요청 조회 시 Timeout이 되는 이슈가 발생했고, 이에 대한 에러 로깅 및 Slack 팀 채널에 관련 오류 내용이 공유되었다.

 

=> 아니 이렇게 말로만 설명을 하면 우리들은 제대로 이해가 가지 않는다! 이미지라도 올려서 함께 설명해주면 좋겠다.

 

그럴줄 알고 이미지를 준비했습니다.! ( 비식별화 처리해야해서 안올리고 싶었습니다 흑흑)

 

 

여기서 상태값을 "전체"에 두게 되면 자동으로 입고 요청일에 대한 필터가 설정이 되며, 상품명을 검색할때 신품/중고에 대한 필터링 없이는 약 3~4초 정도의 인터벌을 두고 응답이 반환된다.

 

 

 

하지만 문제는 추가 필터링 이후 API 호출 시 발생한다.

- 기본 조건(입고 요청 생성 날짜 + 상품명) +  추가 조건(신품/중고 여부)에 의한 검색

=> 어어어? 필터링이 겨우 한개가 추가되었다고 API 응답 시간이 4초 -> 400초가 될 수 있는건가요?

 

처음에는 "신품/중고 필터 하나 때문에 4초 정도 걸리던 작업이 400초가 된다고?" 그럴리가 없고, 다른 문제가 있을것이라고 생각했다.

 

의심이 되는 부분은 다음과 같다.

 

1. 필터링에 사용되는 메서드 코드 최적화 문제

2. 신품/중고 필드에 대한 인덱싱 문제?

3. 너무 많은 데이터를 호출하고 있어서 그런가?

 

의심 대상 1번 : 해당하는 필터에 사용되는 메서드에 대한 코드인데, 코드만 보면 그렇게 문제가 될만한 부분은 없어보였다.

- 즉 이미 쿼리 최적화가 어느정도 진행된 상황에서 필터에 사용되는 메소드의 문제는 아니라고 생각했다.

def filter_is_used(self, queryset, name, value):
      return queryset.filter(requestitem__is_used=value, requestitem__deleted__isnull=True)

 

의심 대상 2번 : 신품/중고 필드는 입고 요청 하위의 입고 요청 장비들에 대한 필드를 검색하는 문제이므로 인덱싱 문제를 의심했었다.

- 신품/중고 필드과 함께 검색되는 조건에 대해서 복합 인덱싱을 추가했으나, API 호출 후 응답까지 400초가 발생하는 문제의 원인이 아니라는것을 알게 되었다.

 

의심 대상 3번 : 입고 요청 8개에 대해서 각각 입고 요청마다 하위의 장비가 많기는 해도 신품/중고 필터 없이 4초 정도 소요되는 작업이 400초가 되는 부분에 대해서는 설명할 수 없었다.

 

 

흐음.. 필터링이 없으면 4초, 필터링을 추가하면 400초가 발생하는데 무엇이 문제인지 감이 잡히지 않는 상태였다.

 

하지만 근본적인 원인인 "필터링 과정에서 발생" 한다는 단서를 계기로 쿼리셋에서 실행되는 쿼리를 확인해보기로 결정했다.

- 필터링에 사용되는 메서드 코드는 분명 문제 없어보이는데, 실행되는 쿼리를 직접 출력해서 확인한다면 원인 파악이 가능할것이라고 예상했다.

 

```python

      print(queryset.query)

```

 

직접 쿼리를 출력해보니 문제를 확인하게 되었다. 

 

원인은 바로 "카테시안 곱 문제" 이다.

 

카테시안 곱이란? 

- From절에 2개 이상의 Table이 있을때 두 Table 사이에 유효 join 조건을 적지 않았을때 해당 테이블에 대한 모든 데이터를 전부 결합하여 Table에 존재하는 행 갯수를 곱한 만큼의 결과값이 반환되는 것

 

```sql

 

-- 기존에 이미 JOIN이 있었음

LEFT OUTER JOIN `logistics_new_storerequestitem` ON (...)

-- is_used 필터가 추가되면서 또 다른 JOIN 생성됨

INNER JOIN `logistics_new_storerequestitem` T3 ON (...)

-- WHERE 조건

WHERE (T3.`deleted` IS NULL AND T3.`is_used` = False)

 

```

 

즉 같은 테이블을 두번 JOIN 한 상황

 

- 첫 번째 JOIN : 완료 처리된 장비 개수 계산용(LEFT OUTER JOIN)

- 두 번째 JOIN : 신품/중고 필터용 (INNER JOIN, T3 별칭)

 

원활하고 빠른 이해를 위해 예시 데이터로 설명합니다.

 

# 예시 데이터

StoreRequest(id=1)

└── StoreRequestItem(id=101, is_used=False)

└── StoreRequestItem(id=102, is_used=False)

└── StoreRequestItem(id=103, is_used=True)

 

- 첫 번째 JOIN (완료 처리된 장비 개수 계산)
StoreRequest(1)[item101, item102, item103]

 

- 두 번째 JOIN (신품/중고 필터용)

StoreRequest(1)[item101, item102]

 

- 실제 결과 행은 다음과 같이 반환

StoreRequest(1) + item101 (첫번째) + item101 (두번째)

StoreRequest(1) + item101 (첫번째) + item102 (두번째)

StoreRequest(1) + item102 (첫번째) + item101 (두번째)

StoreRequest(1) + item102 (첫번째) + item102 (두번째)

StoreRequest(1) + item103 (첫번째) + item101 (두번째)

StoreRequest(1) + item103 (첫번째) + item102 (두번째)

 

즉 N개의 입고 요청, 각각 평균적인 M개의 입고 요청 장비가 연결되므로, N × M × M = N × M² 행을 처리하게 됩니다.

 

- 즉 1000개의 요청에 각각 50개의 아이템이 있다는 경우라면 

   - 정상 :1000 x 50 : 50,000행

   - 카테시안 곱 : 1000 x 50 x 50 = 2,500,000행

   - 즉 정상보다 50배의 쿼리를 검색해야하는 상황이 놓이게 된다.

   ( 동일한 상황에 놓이면서 4초 -> 400초로 응답 시간이 기하급수적으로 늘어난 것이다.)

 

 

그래서 해결 방법 중 가장 빠르게 해결할 수 있는 방법으로 JOIN을 2번 하지 않도록 id__in 서브쿼리를 적용하는 방법을 선택했다.

def filter_is_used(self, queryset, name, value):
matching_request_ids = StoreRequestItem.objects.filter(is_used=value, deleted__isnull=True).values_list(
"request_id", flat=True
)
return queryset.filter(id__in=matching_request_ids)

 

위와 같이 코드 수정 후 카테시안 곱 문제를 해결했으며, 실제 실행되는 쿼리에 대한 설명을 추가합니다.

 

-- 1단계: 별도의 독립적인 쿼리

SELECT request_id FROM storerequestitem WHERE is_used = False AND deleted IS NULL;

 

-- 2단계: 기존 쿼리에 WHERE 조건만 추가

SELECT ... FROM storerequest sr

 

-- 기존 JOIN은 그대로 (입고 완료 장비 개수 계산용)

LEFT OUTER JOIN storerequestitem sri ON (sr.id = sri.request_id) WHERE sr.id IN (1, 2, 3, 4, 5, ...) -- 서브쿼리 결과

 

 

=> 주인장 이렇게 설명해주는건 좋은데, 그래서 이게 왜 성능 개선이 되었는지 이해하기 쉽게 설명좀 해주시오

 

* 카테시안 곱으로 생성된 쿼리

→ Django가 하나의 쿼리로 모든것을 해결하려고 하기 때문에

→ queryset.filter(requestitem__is_used=value)

→ requestitem 과 연결하는 새로운 JOIN을 생성함.

 

* id__in 서브쿼리를 사용한 쿼리

→ 단계를 2단계로 분리
→ step1 = StoreRequestItem.objects.filter(...).values_list("request_id", flat=True)

→ step2 = queryset.filter(id__in=step1)

→ "이미 계산된 ID 목록으로 WHERE 조건만 추가하면 된다"

 

♣ 결론 

id__in 방식이 JOIN을 두 번 발생시키지 않는 이유는 Django ORM이 이를 완전히 별개의 두 쿼리로 처리하기 때문

첫번째 쿼리는 조건에 맞는 ID만 찾고, 두번째 쿼리는 기존 구조를 그대로 유지하면서 WHERE 조건만 추가하는 방식

즉 같은 테이블을 여러 번 JOIN하는 복잡성을 회피함

 

Django + DRF를 활용한 서비스 개발 관련 내용이 Java/Kotlin 등에 비해서 많이 부족하다고 생각하고 해당 글을 작성하게 되었습니다.

 

이러한 경험을 베이스로 작성된 글들이 동일한 문제를 겪는 사람들에게 도움이 되었으면 좋겠습니다.

 

 

저의 부족한 글에 관심을 가지고 읽어주셔서 정말 감사합니다. 

마침

'Framework > Django' 카테고리의 다른 글

사업자등록번호 전체 리스트 가져오기  (0) 2025.07.09

우리팀에서 사용되는 일부 서비스 로직에서는 평일/휴일 구분에 따라서 상이하게 동작하는 로직이 존재한다.

 

평일/휴일 구분에 따라 이벤트가 다르게 발생하는데, 기존 서비스 로직은 workalendar 라는 패키지를 통해 평일/휴일을 구분하게끔 설계되었었다.

 

여기서 해당 패키지의 문제점은 "사전에 정해진 규칙과 패턴으로 휴일을 계산하는 방법"을 사용한다

 

즉 아래와 같은 고정 규칙에 대해서만 구분할 수 있다.

 

  • 매년 반복되는 공휴일 (설날, 추석 등)
  • 기본 대체공휴일 규칙 (주말과 겹칠 때)

 

 

=> 아니 고정된 규칙이 변하지 않는 이상 패키지에서 구분해주는 휴일이 잘못된게 아닌데 뭐가 문제인거냐?!

 

패키지에서 구분해주는 휴일이 잘못된 것이라는 말이 아닌 갑작스럽게 발생하는 대체 공휴일에 대한 정보를 제공해주지 않는 것이 문제

 

  • 정부가 임시로 선언하는 특별 대체공휴일
  • 갑작스러운 정책 변경
  • 특별한 상황 (코로나19, 특별기념일 등)

 

즉 갑작스럽게 발생하는 대체 공휴일은 휴일이 아닌 평일로 구분되기 때문에 서비스 로직에서 한번씩 대체 공휴일임에도 불구하고 평일처럼 동작한다는 오류 리포트를 보고 받은 케이스가 한번씩 있었다.

 

이 문제가 서비스에서 제공하는 비즈니스에서 얼마나 치명적인 부분으로 작용하는지에 따라서 그대로 쓰는 방법을 선택하거나 or 내가 하는 방법처럼 보완하는 방법을 사용하면 된다.

 

우리 서비스에서는 현장에 방문하는 인원들의 출동 여부가 결정되기 때문에 1에 1~2번만 발생하더라도 치명적으로 작용한다.

 

그래서 나는 아래와 같은 방법으로 대체 공휴일 문제를 해결했다.

  • 기존 workalendar 의존성 제거, 캘린더 데이터 DB화 + 공공 API 데이터를 활용한 추가적인 대체 공휴일 정보 업데이트

=> 오 그러면 이제 근본적인 원인이 해결된것 아닌가요? 

 

내가 제시한 방법으로 근본적인 원인을 해결한것이 맞다.

 

하지만 새로운 서비스를 개발하는 과정에서 캘린더 데이터가 필요한 경우 캘린더 데이터를 DB화 + 공공 API를 통한 주기적인 업데이트 구조를 모두 똑같이 적용해야했다.

  • 기존 서비스에서 API를 통해서 받는 방법도 있었으나, 서비스 간의 의존성을 만드는 방법은 최악의 방법이기 때문에 고려하지 않았다.

 

A, B, C 서비스가 있다고 가정할때 서비스마다 각자 캘린더 데이터가 필요한 상황이 오게 된다면 캘린더 데이터 DB화 작업을 총 3번 진행하게 된다.

 

캘린더 데이터에 대한 DB를 매번 구성하고 최신화 하는 로직을 적용하는 부분을 서비스마다 해야한다면 오히려 리소스 낭비라고 생각한다.

 

=> 아니 그러면 공통 기능을 담당하는 서비스를 따로 두면 되는것 아닙니까?!

 

공통 기능을 담당하는 서비스를 따로 두는것은 가장 현명한 방법이 맞지만, 우리팀의 한정적인 리소스를 고려하면 어려운 상황이다.

  • 공통 기능 서비스 개발 인력 및 시간 + 관리 포인트 증가
  • 캘린더 기능이 공통 기능 부분으로 들어갈만한 부분인가? ( 평일/휴일만 구분 하는 용도로 사용되므로)

그래서 공통 기능 서비스를 개발할 수 없는 상황 +  관리 포인트까지 늘리고 싶지 않은 상황이라면 어떤 방법이 있을지 고민했다.

 

그렇게 고민하다가 내가 생각해낸 해답은 "workalender 같은 패키지를 직접 만들어보자" 였다.

 

나에게는 PyPI를 통한 오픈소스 프로젝트 출시 경험은 없었지만, 과정을 확인해보니 만들기가 어렵지 않다고 판단했다.

 

다음과 같은 과정으로 패키지를 만들고 출시하게 되었다.

 

1. GitHub 레포지토리 생성 및 Public 세팅

2. 패키지 네이밍 선정

3. 포맷터, 린트, pyproject, 디렉토리 구성 등 프로젝트의 전반적인 설정

4. 편의성 기능 개발 및 캘린더 데이터 내재화 작업

5. 테스트 코드 작성 및 자동 테스트를 위한 Codecov 연동 및 Actions Workflow 작성

6. 패키지 설명 및 관련 내용 작성 (README.md)

7. Test PyPI 선 출시 진행

8. Test PyPI 확인 및 PyPI 최초 릴리스 버전 출시 (1.0.0)


과정에 대한 내용들을 대부분 생략하였기에 별거 없다고 느껴질 수 있다. (정말 별거 없다고 느낄수도 있지만, 생각보다 손이 많이 가는 작업들이 많았다.)

 

패키지에 대한 정보가 궁금하시다면 아래 링크를 확인하시면 됩니다.

이제 "pip install your-package" 명령어만으로 전 세계 누구나 당신의 코드를 사용할 수 있습니다. 멋지지 않나요?

 

여러분의 첫 패키지 출시를 응원합니다! 🚀

 

 

저의 부족한 글에 관심을 가지고 읽어주셔서 정말 감사합니다. 

마침

현재 근무하는 회사의 팀에서는 항상 주기적으로 발생하는 이벤트가 있다.

 

그것은 API 서버 or DB의 CPU 리소스가 과부하되면서 서비스에 문제가 발생하는 이벤트이다.

 

위 상황에서는 리소스 모니터링을 위해서 AWS의 CloudWatch 같은 기본 모니터링을 통해 리소스를 체크한다.

 

하지만 기본으로 제공되는 모니터링 대시보드는 상세 매트릭 정보를 전달받지 못한다는 문제가 있다.

 

=> 무슨 소리냐? 1분 단위로 상세 매트릭도 구성이 가능하고 대시보드도 원하는 형태로 커스텀이 가능할텐데???

 

커스텀이 가능한것은 사실이지만, 상세 매트릭 정보를 전달 받지 못한다고 표현하는 이유는 우리팀은 돈에 매우 인색하기 때문이다.

 

비용 문제 + 시간 문제로 현재 팀에서는 클라우드 서버 or 온프레미스 서버 등의 하이브리드 구성에 대한 종합 모니터링 도구가 없다.

 

그래서 지금까지는 서비스를 관리하는 담당자들은 필요하다면 직접 수동으로 모니터링 했었다.

 

무한 루프마냥 반복되는 이 과정을 더이상 겪고 싶지 않아서 서버들의 리소스 모니터링 도구로 대표적으로 사용되는 조합인 Grafana + Promethues 조합을 활용해서 모니터링 시스템을 도입했다. 

 

=> 모니터링 시스템을 도입했으니 서버들의 자원 리소스 모니터링이나 상태 체크가 편해졌을건데 뭘 더 추가하냐? 너무 게으른거 아니냐?

 

리소스 모니터링 대상에 대한 기본적인 관리 방법은 리스트는 정적 형태의 설정으로 관리하는 구조이다.

 

그래서 새롭게 추가되는 서버가 생기게 될때마다 서버리스트에 직접 추가해야한다.

 

아래는 모니터링 리스트를 설정하는 Promethues에서 사용되는 config 파일인 prometheus.yml이다.

## prometheus.yml

global:
  scrape_interval: 15s

scrape_configs:
  - job_name: 'node_exporter'
    static_configs:
      - targets: ['dashboard-prod-1:9100']
        labels:
          instance: dashboard-prod-1
      - targets: ['dashboard-prod-2:9100']
        labels:
          instance: dashboard-prod-2
      - targets: ['dashboard-prod-3:9100']
        labels:
          instance: dashboard-prod-3

      # 아래로 모니터링이 필요한 신규 서버에 대한 추가 정보 입력
      ....

 

서비스를 운영하는 서버가 추가되면 모니터링 대시보드에서 보여지도록 연결하는데까지 다음의 과정이 수행된다.

 

[서버 내 리소스 수집을 위한 Exporter 설치 -> 방화벽 내 Exporter 전용 포트 개방 설정 -> 공인 IP 확인 -> config 수정 -> config 적용을 위해 모니터링 시스템 리로드]

 

=> 어 뭐야? 별거 없는 그냥 반복 작업 아닌가요?

 

제목을 보면 알다싶이 나는 매우 게으른편에 속하기 때문에 삭제하고 추가하고 하는 과정을 반복하는게 싫었다. 

 

이 생각을 하던 도중에 "정적 파일을 추가 or 삭제하는 과정을 없애버리면 되는것 아닌가?" 라는 생각이 들었고, 그래서 Consul이라는 도구를 도입하기로 결정했다. (무엇인지 궁금하시다면 설명하기 귀찮기 때문에 링크를 넣어두겠습니다.)

 

Consul 공식 사이트 : Link

 

=> 한대 맞기 전에 빨리 Consul이 무엇인지 잘 설명해봅시다. 

 

정확한 설명은 링크(Link)에서 보시고.. 대신 Consul을 사용한 목적을 빠르고 뇌에 정확히 꽃히도록 설명해드리겠습니다.

 

정적으로 관리되는 서버 목록을 동적으로 편하게 관리하기 위한 목적으로 사용했으며, 아래 이해하기 쉽도록 동작 방식을 정리했습니다.

 

이제 Consul을 도입하기 위한 과정을 아래 순서대로 작성하겠습니다.

- Grafana + Promethues + Consul 서비스에 대해서 구성했다는 전제하에 작성합니다!

 

▶ Promethues의 정적 설정 대신 Consul의 서비스 목록을 바라보도록 config 수정 (promethues.yml)

주의 : docker-compose로 구성해서 사용중이기 때문에, 도입 환경에 따라 설정값이 달라질 수 있습니다.

 

global:
  scrape_interval: 15s

scrape_configs:
  # Consul 설정
  - job_name: 'consul-discovered-services'
    consul_sd_configs:
      - server: 'consul:8500'
        datacenter: 'dc1'
        tags:
          - monitoring
    
    # 서비스 이름을 job 레이블로 설정
    relabel_configs:
      - source_labels: [__meta_consul_service]
        target_label: job
      
      # 서비스 주소와 포트 설정
      - source_labels: [__meta_consul_service_address, __meta_consul_service_port]
        target_label: __address__
        separator: ':'

 

▶ Consul 서비스가 정상적으로 동작한다는 가정하에 아래 캡처 이미지처럼 확인 가능합니다.

 

 

▶ Consul에 서비스를 등록하는 방법은 2가지가 있다.

- Consul Agent를 리소스 모니터링이 필요한 해당 서버에 설치하는 방법

- HTTP API를 통해 직접 등록하는 방법

 

나는 유연한 방식을 선호하기 때문에 API 방식으로 Consul 서비스 목록을 등록하는 방법을 사용했다. (반대로 API를 통해 서비스 목록에서 제외하는것도 가능합니다.)

서비스 목록 리스트에 동적으로 추가되는것을 확인할 수 있습니다.

 

또한 Grafana 대시보드에서 Consul에서 추가한 리스트가 자동으로 반영되는것도 확인 가능합니다.

 

 

=> 그 Node Exporter랑 Consul 서비스 목록에 등록할때 사용한 스크립트 정도는 제공해주셔야하지 않겠습니까?

 

그러실줄 알고 제가 사용했었던 설정 스크립트를 파일로 첨부했습니다! 

 

이 글을 읽는 분들도 Consul을 도입해서 리소스 모니터링에 필요한 서버 리스트를 편하게 관리할 수 있게 한번 도전해보시면 좋겠습니다.

 

 

저의 부족한 글에 관심을 가지고 읽어주셔서 정말 감사합니다. 

마침

install_node_exporter.sh
0.01MB

Docker 컨테이너에서 Python 언어를 기반으로 프로젝트를 진행하다 보면 항상 모든 Python 개발자가 똑같이 겪는 문제가 있다.

 

그것은 바로 requirements.txt로 패키지를 새롭게 설치할 때마다 기다려야 하는 그 지루한 시간이다.

 

도커 컨테이너 기반의 환경에서 requirements.txt 내의 패키지가 추가/삭제 등이 되는 과정에서 컨테이너를 리빌드 하는 과정에 항상 패키지를 인스톨하는 과정이 싫어서 직접 커맨드로 설치하고 추가하는 일이 잦았다. -> 좋지 않은 습관이다.

 

반복되는 시간이 답답해서 Poetry 같은 패키지 도구를 써볼까도 고민했었지만 컨테이너 환경에서 쓰기 썩 좋지는 않다고 생각되었기에 도입을 망설이고 있었다.

 

그러다가 최근에 Rust 언어로 개발된 파이썬 패키지 및 프로젝트 관리 도구가 있다는 이야기를 들었다!

해당 도구의 이름은 바로 uv 이다.

  • uv는 기존의 pip, venv, pyenv, poetry 등과 유사한 기능을 제공하지만, rust로 작성되어서 속도가 매우 빠르다고 설명 되어있다.
  • Link : https://docs.astral.sh/uv/pip/

패키지 관리 도구 없이 requirements.txt에 명시된 패키지를 설치하는 경우 : 약 30초

uv 도구를 활용해 requirements.txt에 명시된 패키지를 설치하는 경우 : 6초

=> 어? 제목에는 10배 이상 시간을 단축한다면서 5배밖에 단축이 안되는데요?! 라고 보는 분들이 이야기 할 수 있다.

그래서 현재 팀에서 서비스 개발 시 사용된 requirements.txt로 비교해보려고 한다.

requirements.txt만 직접 설치하는 경우

uv 도구를 통해 requirements.txt 파일에 동봉된 패키지를 설치하는 경우

직접 설치한 시간을 70초로 반올림 , uv 도구를 통해 설치한 시간을 12초로 반올림 했을때 약 5.8배 가량 시간이 단축된것을 알 수 있다.

=> 여전히 10배 이상 시간 차이가 안나잖아요!!

저는 거짓말을 하지 않았습니다. (해당 도구의 소개 페이지에 적힌 내용대로 전달했을 뿐이에요)

 

5배가 빠르더라도 충분히 여러분들의 시간을 단축시켜줄 수 있을거라고 생각합니다.

 

대신 제가 dockerfile에서 uv를 사용하기 위해 사용된 코드를 공유드립니다.


COPY --from=ghcr.io/astral-sh/uv:latest /uv /bin/uv

RUN uv pip install --system -r requirements.txt

 

저의 부족한 글이 도움이 되셨다면 감사합니다.

 

마침

상황 : 가맹점의 사업자등록번호 리스트를 수집해야하는 상황이 생겼다.

  • 사업자등록번호는 총 10자리로 구성되며, 이러한 사업자등록번호 리스트를 얻을 수 있는 곳이 존재하지 않는다.

    • ( 공공데이터 포털 같은 다양한 사이트를 참고했지만 공식적으로 리스트를 얻을 수 있는 방법이 없다는 의미)
  • 그래서 사업자등록번호 10자리의 자리마다 1~9까지 무작위로 할당해서 중복 없이 사업자등록번호의 유효성을 검증하는 방법을 사용해야한다.

  • 10자리로 무작위로 생성된 사업자등록번호를 검증하기 위해서는 국세청에서 제공하는 "국세청_사업자등록정보 진위확인 및 상태조회 서비스"를 활용할 예정

경우의 수가 너무 많지 않은가?

  • 사업자등록번호의 첫자리는 0이 아닌 1부터 시작하므로 첫자리가 0이라는 경우의 수는 제외할 수 있다.

  • 하지만 이렇게 경우의 수가 제외되더라도 첫자리는 19 나머지 자리는 09까지 할당될 수 있다.

    • 즉 이 경우에는 할당 가능한 경우의 수는 9 × 10^9 = 90억 개가 된다.
  • 사업자등록번호의 마지막 자리수는 앞 9자리의 숫자가 결정되면 자동으로 계산되어 할당되므로 실제 생성 가능한 조합은 9 × 10^8 = 9억 개로 줄어들게 된다.

  • 조합 가능한 경우의 수 9억개를 하루에 1억개씩 검사한다 하더라도 총 9일이 걸린다.
    • 추가로 1000000000 같은 10자리는 일치하지만 절대 존재할 수 없는 사업자등록번호 같이 검색이 필요 없는 케이스들도 존재한다.
  • 영화 이미테이션 게임에서 암호 해독을 위해 발명된 기계인(크리스토퍼?)를 보면 모든 설정값을 하나하나 확인해서 암호를 해독하는 시간이 상당히 소요되는것을 알 수 있다.

조건을 가지고 경우의 수를 줄여보자.

  • 사업자등록번호는 폐기되면 다시 사용되지 않는다.

  • 정부 민원 사이트를 통해서 확인한 사업자등록번호 규칙은 다음과 같다.

- 일련번호 코드(3자리) : 신규개업자에게 사용 가능한 번호 101~999를 순차적으로 부여한다.

- 개인/법인 구분코드(2자리)
    - 개인 구분 코드
        - 개인과세사업자는 01~79까지 순차적으로 부여
        - 개인면세사업자는 90~99까지 순차적으로 부여
        - 법인이 아닌 종교 단체 : 89
        - 아파트관리사무소, 다단계 판매원 등 : 80

    - 법인 구분 코드
        - 영리법인의 본점 : 81, 86, 87, 88
        - 비영리법인의 본점 및 지점 : 82
        - 국가 및 지방자치단체, 단체조합 등 : 83
        - 외국법인의 본점 및 지점 연락 사무소 : 84
        - 영리법인의 지점 : 85

- 일련번호코드(4자리)
    - 0001 ~ 9999 부여

- 검증번호 (1자리)
  • 앞 3자리 : 899개

  • 가운데 2자리 : 97개

  • 뒤 4자리 : 9999개

  • 마지막 1자리 : 앞 9자리마다 오직 1개만 유효하

  • 줄일 수 있는 경우의 수는 최대 8억 7천개로 좁혀진다.

소요 시간

  • 소요 시간은 8.7일을 소요해야한다.
  • 사업자등록번호조회 호출이 가능한 API 계정을 1개 더 늘리는 방법을 사용하면 소요 시간을 약 4.3일로 줄일 수 있을것이다.

cron -> Lambda + EventBridge 전환을 선택한 이유

  • 나는 현재 서비스 개발 과정에서 AWS 같은 환경에서의 비용 발생을 최소화하기 위해 Ubuntu OS 가 설치된 VM 환경에서 개발을 진행하고 있다.
  • 현재 집에 구축된 홈랩에서 개발 환경을 제공하고 있으나 전기 공급 문제 or 인터넷 일시적 단절 문제 같은 24시간 안정성을 해치는 문제가 존재한다.

Linux 서버 자체에서 제공하는 스케줄링은 다음과 같은 문제로 피하려고 한다.

  • ⚡ 전력 공급 불안정 → 서버 다운 → cron 중단
  • 🌐 인터넷 연결 불안정 → 외부 API 호출 실패
  • 🔥 하드웨어 장애 → 전체 서비스 중단
  • 😱 외부에 있는 경우 → 장애 발생시 조치 어려움

위 문제로 cron을 사용하지 않는다고 하면? -> "홈랩을 기준으로 프로덕션 서비스를 운영하기 위한 서버에서 안정성이 고민된다면 AWS 같은 클라우드 환경에서 동작시키면 문제가 없는것 아닌가" 라는 질문이 100% 떠오르게 될것이다.

클라우드 환경의 프로덕션에서 만약 상황에 따라 다중 인스턴스 같은 Replica를 고려해야하는 상황이 찾아오게 된다면?

Load Balancer
├── Web Server 1 (cron 실행) ← 여기서만 실행해야 함
├── Web Server 2 (cron 실행 X)
└── Web Server 3 (cron 실행 X)

  • 모든 서버에서 cron 실행 → 중복 작업 발생 😱
  • 한 서버에서만 실행 → 해당 서버 다운 시 작업 중단 😱
  • 서버 추가/제거 시마다 cron 설정 수정 필요 😱

내가 만드는 서비스에서는 이러한 오토 스케일링 같은 부분은 사실 필요할 일이 없을 수 있다. -> 100% 확신을 가지고 이야기 할 수 없는 부분이라면 이러한 변수까지 고려해서 방안을 찾는게 맞다고 생각한다.

내가 만드는 서비스의 프레임워크인 Django 의 Django-cron을 사용하더라도, 각 서버의 프로세스가 각자 크론을 따로 실행시키는것을 피할 수 없는것도 사실이다.


INSTALLED_APPS = [
    'django_cron',
]

# 여전히 각 인스턴스에서 실행됨
@CronJobBase
class MyCronJob(CronJobBase):
    schedule = Schedule(run_every_mins=5)
    code = 'app.my_cron_job'

    def do(self):

로그 관리와 디버깅 또한 이러한 다중 인스턴스 같은 부분에서도 고려할 대상이 되어버린다.

# 서버 1
tail -f /var/log/cron.log | grep my_job

# 서버 2  
ssh server2 "tail -f /var/log/cron.log | grep my_job"

# 서버 3
ssh server3 "tail -f /var/log/cron.log | grep my_job"
  • 😱 어느 서버에서 실행됐는지, 왜 실패했는지 파악하기 어려움

다중 환경에서의 배포 및 롤백의 복잡성이 증가하는 부분도 문제가 된다.

  • cron 업데이트 시나리오
      1. 코드 수정
      1. 각 서버에 개별 배포
      1. cron 재시작
      1. 문제 발생 시 각 서버 개별 롤백
  • 😱 다중 서버 중 1개의 서버라도 실패하는 경우 일관성 깨짐

위에 언급한 문제 말고도 모니터링 + 비용 + 서버 관리 + 개발/스테이징/프로덕션 등의 환경 관리 등등 많은 포인트들을 고려해야한다.


Lambda + EventBridge로 전환하게 된다면?


# 모든 로그가 CloudWatch에 중앙화
aws logs tail /aws/lambda/my-function --follow

💰 비용 효율성 - 서버 운영 vs 서버리스

cron 스케줄링을 위한 추가적인 cron 서버를 운영하는 경우 다음과 같은 비용이 발생한다.

  • EC2 t3.small: $16.79/월 (만약 사양이 부족하다면 여기서 추가적인 비용 발생 )
    • 1분마다 크롤링 작업 실행 (24시간 풀 가동 필요)
    • 서버 자체 리소스 사용은 정당함
  • 추가 관리 비용: (저는 실제로 적용하지 않았지만 이러한 비용까지 발생할 수 있다는 점을 참고해주세요.)
    • 서버 모니터링 도구: $15/월 (DataDog 등)
    • 백업 스토리지: $5/월
    • 관리 시간 (월 4시간 × $50): $200/월
  • 연간 총 비용: $2,838

Lambda + EventBridge로 운영되는 경우 다음과 같은 비용이 발생한다.

  • Lambda 요청: 월 43,200회 (1분마다) = 무료 (100만 요청 한도 내)
  • Lambda 실행 시간: 월 720분 (평균 1초/회) = 무료 (40만 GB-초 한도 내)
  • EventBridge: 월 43,200회 = $0.43
  • 관리 시간: 거의 0시간
  • 연간 총 비용: $5.16

연간 최대 절약 비용 : $2,833(99.8% 절약!)

관리 포인트의 극적인 단순화

  • cron 환경

서버 관리:
✋ OS 보안 패치 (월 1회)
✋ 서버 상태 모니터링 (일일 체크)
✋ 디스크 용량 관리 (로그 정리)
✋ 네트워크 및 방화벽 설정
✋ cron 서비스 상태 확인

다중 인스턴스 관리 (확장 시):
✋ 어느 서버에서 cron 실행할지 결정
✋ 서버 추가/제거 시 cron 재배치
✋ 각 서버별 로그 수집 및 분석
✋ 로드밸런서와 cron 서버 분리 고려

배포 및 업데이트:
✋ 각 서버에 개별 배포
✋ 배포 실패 시 롤백 프로세스
✋ 환경별(dev/staging/prod) 개별 관리
  • Lambda + EventBridge 환경

관리할 것들:
✅ 코드 작성
✅ 배포 (1회 명령)
✅ 끝.

AWS가 자동 관리:
🤖 서버 패치 및 보안
🤖 가용성 및 확장
🤖 모니터링 및 로깅
🤖 장애 복구
🤖 백업 및 재해복구

관리 복잡도의 실질적인 비교 예시

  • cron 환경
1.  스크립트 파일 작성 (30분)
2.  각 서버에 파일 배포 (15분)
3.  crontab 수정 (10분)
4.  권한 및 환경변수 설정 (15분)
5.  로그 로테이션 설정 (10분)
6.  모니터링 알람 설정 (30분)
7.  문서화 (20분)

총 소요 시간: 2시간 10분  
관리 포인트: 7개 서버 × 7개 설정 = 49개 체크리스트
  • Lambda + EventBridge 환경
1. 함수 코드 작성 (30분)
2. 배포 (5분)
3. EventBridge 규칙 생성 (5분)

총 소요 시간: 40분
관리 포인트: 3개 체크리스트

장애 대응 시나리오 비교 예시

  • cron 환경
시나리오: 크롤링 작업이 갑자기 동작하지 않음

1.  어느 서버에서 문제인지 확인 (10분)  
    ssh server1 "sudo systemctl status cron"  
    ssh server2 "sudo systemctl status cron"  
    ssh server3 "sudo systemctl status cron"
2.  각 서버 로그 확인 (15분)  
    ssh server1 "tail -100 /var/log/cron.log"  
    ssh server2 "tail -100 /var/log/cron.log"  
    ssh server3 "tail -100 /var/log/cron.log"
3.  스크립트 실행 확인 (10분)  
    ssh server1 "cd /opt && python3 crawler.py"
4.  환경변수/권한 확인 (10분)
5.  수정 후 각 서버 재배포 (20분)

총 복구 시간: 65분
  • Lambda + EventBridge 환경
시나리오: Lambda 함수 실행 실패

1. CloudWatch에서 에러 로그 확인 (2분)
2. 로컬에서 코드 수정 (15분)  
3. 배포 (1분)

총 복구 시간: 18분

핵심 요약 정리

  • 📊 비용: 연간 $2,833 → $5.16 (99.8% 절약)
  • ⏰ 관리 시간: 2시간 10분 → 40분 (70% 단축)
  • 🚨 장애 복구: 65분 → 18분 (72% 단축)
  • 🎯 관리 포인트: 49개 → 3개 (94% 단순화)

결론적으로, 일정 단위로 동작하는 스케줄링이라면 서버를 기준으로 24시간 가동은 당연히 필요한 부분,
하지만 그로 인해 발생하는 남는 리소스 + 관리 오버헤드가 문제였고, Lambda + EventBridge를 통해 해결할 수 있었습니다.

이제 서버 관리 대신 실제 비즈니스 로직 개발에만 집중하시면 됩니다! 💪

+ Recent posts