안녕하세요. Emadam입니다.

 

이번 포스팅에서는 Cloud Armor를 도입하면서 검토하고 공부했던 내용을 공유하려 합니다.

 

최근에 신규로 오픈한 시스템이 공격을 받았던 일이 있었습니다.

Luci Injection

 공격자는 시스템 내 PHP 파일을 스캐닝하고 SQL 인젝션 공격, 원격 코드 실행 등을 시도했습니다. 다행히 시스템에 시큐어 코딩이 적용되어 있어 큰 피해는 입지 않았지만, 공격 빈도가 잦고 공격 방식의 예측 불가능성을 고려하여 보안 강화가 시급하다는 판단에 도달했고 Cloud Armor를 도입하기로 결정하였습니다.

Cloud Armor란?

 Cloud Armor는 DDoS 공격, 크로스 사이트 스크립팅(XSS), SQL Injection과 같은 여러 공격으로부터 어플리케이션을 보호하는 GCP의 보안 서비스입니다. L3 ~ L7 사이의 속성과 일치하는 보안 규칙을 구성해 어플리케이션을 보호할 수 있습니다. 보호 대상으로 부하 분산기, 프로토콜 전달, 공개 IP가 부여된 VM 지정할 수 있으며 부하 분산기의 경우 아키텍처(하이브리드, 멀티클라우드 등)에 구애받지 않고 적용 가능합니다.

Cloud Armor는 표준 네트워크 DDoS 보호 기능인 자동 보호 기능도 제공합니다. 표준 네트워크 DDoS 보호 기능은 외부 패스 스루 네트워크 부하 분산기, 프로토콜 전달, 공개 IP 주소가 있는 VM에 대한 상시적으로 설정되는 기본 보호 기능입니다. 여기에는, Google Cloud Armor 표준이 적용되며 추가 구독이 필요하지 않습니다.

보안 정책과 규칙

 Cloud Armor에서 어플리케이션을 보호하는 가장 기본적인 방법은 보안 정책과 보안 규칙을 구성하는 것입니다. GCP 리소스에 적용 가능한 최소 단위의 보안 구성입니다. 하나의 보안 정책에는 여러 개의 보안 규칙이 포함될 수 있습니다. 예를 들어 아래와 같이 보안 정책과 규칙이 구성될 수 있으며,

  • 보안 정책 A
    • 보안 규칙 1 - X.X.X.X/24로 부터의 접근 차단
    • 보안 규칙 2 - Y.Y.Y.Y/32로 부터의 접근 허용

위 보안 정책 A를 부하 분산기의 백엔드 서비스에 연결할 수 있습니다.

그럼 보안 정책과 규칙은 어떻게 구성할까요?

보안 정책

 보안 정책은 규칙을 그룹화하여 관리하는 역할을 하며, 설정 내용은 다음과 같습니다.

  • 정책 이름
  • 정책에 대한 설명
  • 정책 유형
  • 정책 범위
  • 기본 규칙

이름과 설명은 정책을 식별하기 위한 정보이므로 적절하게 작성해주시면 됩니다. 중요한 부분은 정책 유형입니다. 정책 유형은 세 가지로 구분되어 있습니다.

  • 백엔드 보안 정책 (Backend Security Policy)
  • 에지 보안 정책 (Edge Security Policy)
  • 네트워크 에지 보안 정책 (Network Edge Security Policy)

백엔드 보안 정책

 백엔드 보안 정책은 부하 분산기 뒤에 위치한 백엔드 서비스를 보호합니다. 전역(Global) 또는 지역(Region)으로 구성할 수 있으며 전역로 구성할 경우 전역 부하 분산기의 백엔드 서비스를 대상으로 지정할 수 있고 지역으로 구성할 경우 지역 부하 분산기의 백엔드 서비스를 대상으로 지정할 수 있습니다.

 

[전역 백엔드 보안 정책]

  • 전역 외부 애플리케이션 부하 분산기
  • 기본 애플리케이션 부하 분산기
  • 전역 외부 프록시 네트워크 부하 분산기
  • 기본 프록시 네트워크 부하 분하 분산기

[지역 백엔드 보안 정책]

  • 지역 외부 애플리케이션 부하 분산기
  • 지역 내부 애플리케이션 부하 분산기

또한 L7에서 헤더나 페이로드 정보의 필터링이 가능하기 때문에 다른 보안 정책 유형보다 넓은 범위의 필터링 기능을 제공합니다.

에지 보안 정책

 에지 보안 정책은 부하 분산기 앞단인 인터넷 경계에서 CDN을 보호하는 정책입니다. 전역 리소스의 에지에서 작동하기 때문에 전역 부하 분산기의 백엔드 서비스 및 백엔드 버킷에서만 사용할 수 있습니다.

[지원 부하 분산기 유형]

  • 전역 외부 어플리케이션 부하 분산기
  • 기본 애플리케이션 부하 분산기

에지 보안 정책은 CDN 단에서 트래픽을 필터링하여 악성 트래픽을 차단하고, 불필요한 트래픽으로 인한 리소스 낭비를 방지합니다. 또한 백엔드 보안 정책과 적용 위치가 다르기 때문에 두 정책을 동시에 적용하여 보안 계층을 강화할 수 있습니다

네트워크 에지 보안 정책

 위 두 보안정책들은 중간에 중간 레이어와 네트워크 가장자리에 위치한 리소스를 보호하도록 설계되어있습니다. 실제 애플리케이션 서버에 직접 트래픽을 전달하기 전에 악성 트래픽을 차단합니다. 하지만 네트워크에 직접적으로 노출되는 리소스들도 있습니다. 이때는 네트워크 에지 보안 정책을 적용할 수 있습니다.

  • 외부 패스 스루 네트워크 부하 분산기
  • 프로토콜 전달
  • 공개 IP 주소가 있는 VM

(네트워크 에지 보안 정책은 지역 리소스이며, 지역 범위로 트래픽을 제어합니다.)

네트워크 에지 보안 정책을 사용하면 바이트 오프셋 필터링을 사용하여 인그레스 패킷의 TCP/UDP 바이트 오프셋 값을 기반으로 트래픽을 필터링할 수 있습니다.

네트워크 에지 보안 정책은 고급 DDoS 네트워크 기능 활성화가 선행되어야하며 이 기능은 Cloud Armor Enterprise 기능입니다. 때문에 네트워크 에지 보안 정책은 Cloud Armor Enterprise 활성화가 필수입니다.

각 정책 유형이 어떤 리소스를 지원하고 어떤 기능을 지원하는지는 아래 문서에 깔끔하게 정리되어 있습니다. Cloud Armor를 도입하시기 전이라면 아래 표를 보고 자신이 상황에 맞는 유형을 선택해주시면 됩니다.

정책 범위

 정책 범위는 리소스가 전역/지역 여부입니다. 백엔드 보안 정책의 경우 전역 또는 지역으로 선택할 수 있고 에지 보안 정책은 전역, 네트워크 에지 정책은 지역을 지정해야 합니다.

기본 규칙

 기본 규칙은 마치 프로그래밍에서 'if문'의 'else' 역할을 하는 것처럼, 보안 정책 내의 여러 규칙들이 모두 일치하지 않을 때 어떻게 처리해야 할지를 정의합니다. 기본 규칙은 자동으로 가장 높은 우선순위 값(2,147,483,647)이 부여되며 허용 및 거부를 지정할 수 있습니다.

이렇게 생성된 보안 정책은 유형에 따라 필요한 리소스를 대상으로 지정할 수 있으며 대상으로 지정한 순간부터 보안 정책 내의 규칙을 기반으로 평가 및 필터링이 수행됩니다.

보안 규칙

 보안 정책을 생성하면 기본 규칙만 존재하는 상태입니다. 때문에 모든 트래픽을 허용하거나 거부하거나 둘 중 하나의 동작만 수행하게 됩니다. 원하는 수준으로 보안을 확보하려면 조건(Condition), 작업(Action), 우선순위(Priority)가 지정된 규칙들을 직접 구성해야 합니다.

조건(Condition)

 조건은 IP를 나열하거나 CEL(Common Expression Language)를 사용해 요청 IP, Header, 본문 등을 검사합니다. 외부 요청을 검사했을 때 이 조건과 일치하면 True를 반환하며 이후에 정의한 작업을 수행하게 됩니다. 조건은 사용자가 직접 정의할 수도 있지만 GCP에서 사전 정의한 구성을 사용할 수도 있습니다. 사전 정의된 구성을 사용할 경우 OWASP 10대 위험을 쉽게 완화할 수 있습니다.

작업(Action)

작업은 조건이 일치할 경우 수행하는 동작입니다. 작업은 다음과 같은 종류가 있습니다.

  • 허용(Allow)
  • 거부(Deny)
  • 리다이렉트(Redirect)
  • 제한(Throttle)
  • 비율 기반 차단(Rate-based-ban)

 조건에 일치할 경우 요청을 허용하거나 거부하거나 리다이렉트하는 것은 명확하기 때문에 제한과 비율 기반 차단은 무엇인지만 간단하게 살펴보겠습니다.

 제한 작업은 클라이언트별 요청 횟수를 제한하는 데 사용됩니다. 요청 기준을 초과하기 전까지는 요청이 허용되지만, 기준을 초과하면 설정에 따라 요청을 거부하거나 리다이렉트할 수 있습니다. 예를 들어, 각 국가별로 들어오는 트래픽이 10분당 10,000회가 넘을 경우 요청을 거부하고 싶다면 클라이언트 구분 기준을 리전 코드로 설정하고 요청 간격/횟수를 10분/10,000회로 설정합니다. 그 다음 초과 작업을 거부로 설정하고 적절한 Status Code를 지정해주면 됩니다.

 

[제한 작업 설정시 필요한 정보]

  • 클라이언트 구분 기준 (IP, Header, 지역 코드 등)
  • 요청 간격/횟수
  • 초과 작업

 비율 기반 차단은 클라이언트가 특정 시간 내에 설정된 요청 횟수를 초과하면 지정된 시간 동안 접속을 차단하는 데 사용됩니다. 예를 들어 기준시간을 10초, 요청 횟수를 60회, 차단 시간을 600초로 지정할 경우 10초동안 요청 수가 60회를 초과하면 초과한 시점부터 기준 시간이 종료된 후 600초까지 요청이 차단됩니다.

 위와 같은 설정은 예상치 못하게 클라이언트를 차단시킬수도 있기 때문에 비율 기반 차단에서는 추가적인 설정을 제공하고 있습니다. 기본 기준 설정 외 차단 기준 설정을 추가로 설정하게 되면 기본 기준 설정에 대해서는 제한(Throttle)으로 동작하지만 차단 기준 설정 값에 대해서는 위와 동일하게 차단 시간이 적용됩니다. 예를 들어 기본 기준 설정을 10초/60회, 차단 기준 설정을 100초/6000회, 차단 시간을 600초로 설정하게 되면 매 10초마다 60회를 초과하는 경우에는 10초 내에서만 차단이 적용(제한과 동일)되지만 이 요청횟수를 계속 누적시켜 100초 동안 6000회가 초과될 경우 초과된 시점부터 기준 시간 종료 후 600초 동안 접속이 차단되게 됩니다.

우선순위(Priority)

 우선순위는 각 규칙의 평가되는 순위입니다. 특정 요청이 평가될 때는 정의된 규칙들을 우선순위에 따라 순차적으로 평가하며 일치하는 규칙이 있을 경우 해당 규칙에 구성된 작업이 수행되고 평가는 중단됩니다.

단, 사전 구성된 규칙을 사용하기 위해 evaluatePreconfiguredExpr()를 사용하는 경우 POST 요청에 대해 Header와 본문이 별도로 평가됩니다. 본문을 평가하는 규칙이 Header를 평가하는 규칙보다 우선순위 값이 높은 상태에서 Header 평가가 통과(허용)하더라도 본문 검사에서 차단되면 Header만 백엔드로 전달될 수 있습니다. 이 부분은 다음 포스팅에서 직접 실습으로 확인해보겠습니다.

마무리

 이렇게 Cloud Armor의 보안 정책과 규칙에 대해 포스팅해보았습니다. 검토하고 사용할 때는 몰랐는데 막상 글로 정리하려보니까 생각보다 내용이 많고 몰랐던 부분도 많아서 공부가 많이 되었습니다. 원래는 실습이랑 모니터링 부분도 전부 작성하려했는데 글이 너무 길어져서 다음 포스팅에서 다뤄볼 예정입니다. 긴 글 봐주셔서 감사합니다.

안녕하세요. Emadam입니다.

어느덧 학교생활도 4년이 다되어가고 있습니다. 회사일과 병행해야했기 때문에 입학했을 당시만해도 어떻게 4년을 버티나 눈앞이 깜깜했는데 어느새 졸업을 한학기만 남갸두고 있습니다.

의도한건 아니었지만 어쩌다보니 4년 가까이 작업한 과제물들을 전부 보관하고 있었습니다. 이걸 그냥 가지고만 있자니 많이 아깝다는 생각이 들어 공유하려합니다. 모든 과제가 만점은 아니었지만 대부분 만점을 받았고 2점 이하로 떨어진적은 없었으니 참고용으로는 괜찮을 듯 합니다. 과제하시다가 막히는 부분이나 다른 사람들은 어떻게 해결했을까라는 의문이 드신다면 이 자료가 도움이 될 수있을 것 같네요. ㅎㅎ

과제 Github 링크 : https://github.com/ldy9037/knou-computer-science-assignment

GitHub - ldy9037/knou-computer-science-assignment: 방송통신대학교 컴퓨터과학과 과제 모음 (참고용)

방송통신대학교 컴퓨터과학과 과제 모음 (참고용). Contribute to ldy9037/knou-computer-science-assignment development by creating an account on GitHub.

github.com


감사합니다!

안녕하세요. Emadam입니다.
이번에는 Python을 사용해 멜론 콘서트 취소표 알림받는 법을 포스팅해보려 합니다.
2월 말 즈음 문득 와이프와 아이유 콘서트에 가보고 싶어 네이버에 검색해보니 3월 2일부터 주말마다 콘서트가 있다는 걸 알게 되었습니다.

물론 전부 매진이었습니다. 결국 취소표를 노려야하는 상황인데 멜론은 취소표를 노리는게 다른 플랫폼보다 많이 까다로운 편입니다.
인터파크와 같은 플랫폼은 취소표를 특정 시점에 한번에 풀어서 일명 '취켓팅'이 가능하지만 멜론은 취소표를 랜덤한 시간에 풀어 미리 대기하는 방식이 불가능합니다.
때문에 멜론으로 취소표를 노리려면 새로고침을 열심히 누르는 방법밖에는 없는데, 아이유 콘서트는 인기가 굉장히 많기 때문에 취소표가 잘 나지 않습니다. 사실상 새로고침으로 취소표를 노리는 건 불가능에 가깝습니다.
그럼 어떻게 해야 티켓을 구할 수 있을까요?
사실 전체적인 흐름은 간단합니다. 1. 멜론 홈페이지에서 자리현황을 확인하고 취소표가 발생하면 2. 메신저로 알림을 보내면 됩니다.

자리현황 API 분석

위 흐름을 구현하려면 가장 먼저 자리현황을 확인해야 합니다. 멜론 홈페이지에서는 아래와 같이 새로고침 버튼을 통해 자리현황을 갱신할 수 있습니다.

위 새로고침 버튼은 비동기로 동작하고 있습니다. API를 비동기로 호출해 데이터를 가져와서 리스트를 갱신합니다. 그렇다면 어떤 API로 데이터를 가져오고 있는지 확인해보겠습니다.

총 3가지 Json 데이터를 호출하는 것을 볼 수 있었습니다. 각 데이터의 리소스명과 응답값을 봤을 때 각 데이터의 용도를 아래와 같이 예상할 수 있었습니다.

  • getProdSellState.json -> 잘 모르겠는데.. 확실한 건 좌석정보는 아니였습니다.
  • getAreaMap.json -> 좌석도를 그리는데 사용되는 데이터로 보입니다.
  • summary.json -> callback 함수에 따라 표햔 데이터가 달라지는 것으로 보입니다. getBlockGradeSeatCountCallBack를 callback 함수로 사용할 경우 좌석 요약 데이터를, getBlockSummaryCallBack callback 함수를 사용할 경우 실제 좌석 현황을 보여주는 것으로 보입니다.
    이렇게 봤을 때 활용할 수 있는 데이터는 summary.json으로 보입니다. 또한 getBlockGradeSeatCountCallBack callback 함수에서는 잔여 좌석으로 예상되는 값을 찾을 수 없었기 때문에 getBlockSummaryCallBack을 자세하게 확인해보는 것이 좋아 보입니다.
getBlockSummaryCallBack({
    "summary": [{
        "prodId": 209540,
        "scheduleNo": 100001,
        "blockId": 187,
        "seatGradeNo": 10015,
        "totSeatCntlk": 257,
        "sellSeatCntlk": 199,
        "rendrSeatCntlk": 58,
        "lockSeatCntlk": 4,
        "realSeatCntlk": 54,
        "regUserId": null,
        "regUserName": null,
        "regDate": null,
        "mdfUserId": null,
        "mdfUserName": null,
        "mdfDate": null,
        "seatGradeName": "지정석",
        "gradeColorVal": "#BEA886",
        "floorNo": null,
        "floorName": "",
        "areaNo": "[A GATE]401",
        "areaName": "구역",
        "sntvList": null,
        "sntv": ",[A GATE]401",
        "blockTypeCode": "SE0001",
        "perfStartDay": "20240414",
        "perfStartTime": "1400",
        "basePrice": 110000,
        "sejongSeatGradeCode": null
    },

    ...         

    ],
    "interlockTypeCode": "",
    "code": "0000",
    "staticDomain": null,
    "httpsDomain": null,
    "httpDomain": null
});

좌석의 영역 리스트와 summary내 배열 내용이 일치한 것을 확인했습니다. 그리고 각 배열 내 Object의 key 중 realSeatCntlk의 값이 잔여좌석의 값과 일치하는 것을 확인할 수 있었습니다. 대략 아래와 같은 값으로 예상했습니다.

  • totSeatCntlk -> 전체 좌석 수
  • sellSeatCntlk -> 판매된 좌석 수
  • rendrSeatCntlk -> 팔리지 않은 좌석 수
  • lockSeatCntlk -> 잠긴 좌석 수 (구매 진행중이라 잠긴 좌석으로 예상됩니다.)
  • realSeatCntlk -> 남은 좌석 수

남은 좌석을 확인하는 방법은 알아냈지만 남들보다 빠르게 좌석을 낚아채려면 정확한 좌석위치까지 알림으로 알려주는게 좋을 것 같습니다. 다행히 이 데이터 내에는 각 영역에 대한 설명도 적혀있습니다.

  • seatGradeName -> 좌석 등급명 (가격별로 분류된 영역 / ex R석(3회차))
  • floorNo -> 층 수 (ex 1, 2, 3, 4)
  • floorName -> 층 명 (ex 층)
  • areaNo -> 구역 번호 (ex A, B, 1, 2, 3)
  • areaName -> 구역 명 (ex 구역)
    위 값은 아래와 같이 활용해주시면 됩니다.
- {seatGradeName}, {floorNo}{floorName} {areaNo}{areaName}에 잔여좌석 {realSeatCntlk}개 발생
위 처럼 변수를 사용하면 아래와 같이 출력됩니다.
- R석(3회차), 1층 5구역에 잔여좌석 2개 발생

이제 API에서 필요한 정보는 모두 확인한 것 같습니다. 그럼 이제 이 API를 활용해서 잔여좌석을 확인하는 코드를 작성해보겠습니다.

잔여좌석 체크하기

이제 Python을 사용해 좌석 정보를 받아와 잔여좌석을 확인해보겠습니다.
좌석 데이터를 받아오는 가장 간단한 방법은 위에서 살펴본 API를 똑같이 사용하는 것입니다.
먼저 어떻게 API에 요청을 보내는지 살펴보겠습니다.

요청
요청 헤더
페이로드

뭐가 많지만 사실 모든 값이 필요하지는 않습니다. 테스트 결과 아래 정도의 값들만 필요했습니다.

  • header -> Accept, Content-Type, Cookie, Host, Referer, User-Agent
  • body -> prodId, pocCode, scheduleNo, perfDate, seatGradeNo, corpCodeNo
  • parameter -> v

header의 경우 CookieUser-Agent를 신경써주어야 합니다.

  • Cookie -> 처음 좌석선택 팝업을 띄웠을 때 나오는 자동 입력 방지 문자를 입력하면 생성되는 것으로 보입니다. 이 쿠키는 사용하지 않으면 만료되기 때문에 가장 주의해야 합니다. 이 값은 웹페이지 개발자 도구에서 복사해서 사용해야 합니다.
  • User-Agent -> Python 기본 User-Agent는 차단이 되어 있는 것 같았습니다. User-Agent를 다른 임의의 값으로 변경해서 해결할 수 있었습니다.

Body의 경우 위에 6가지 키를 나열했는데 콘서트별로 사용하는 키가 다릅니다. 원하는 콘서트의 Body를 꼭 확인해주세요. Parameter에는 꼭 callback을 지정해주지 않아도 됩니다. Default으로 반환하는 데이터가 getBlockSummaryCallBack 함수를 callback으로 지정했을 때와 동일했습니다.

그럼 본격적으로 Python 코드를 작성해보겠습니다. Python에서 외부 API에 요청을 보내려면 requests라고 하는 라이브러리를 사용해야 합니다.

먼저 requests 라이브러리를 Import 합니다

import requests

그 다음 위에서 확인한 정보를 바탕으로 Melon 좌석 현황 API를 호출합니다.

def get_seats_summary() -> None:
    url = "https://ticket.melon.com/tktapi/product/block/summary.json?v=1" 

    body = {
        'prodId': '',
        'pocCode': '',
        'scheduleNo': '',
        'perfDate': '',
        'seatGradeNo': '',
        'corpCodeNo': ''
    }

    header = {
        'Accept': 'text/javascript, application/javascript, application/ecmascript, application/x-ecmascript, */*; q=0.01',
        'Content-Type': 'application/x-www-form-urlencoded; charset=UTF-8',
        'Cookie': '',
        'Host': 'ticket.melon.com',
        'Referer': 'https://ticket.melon.com/reservation/popup/stepBlock.htm',
        'User-Agent': 'X'
    }

    response = requests.post(url,headers=header,data=body)
    return response.json()

위에서 body 값과 Cookie 값만 채워주면 좌석정보를 가져오게됩니다.

좌석 데이터를 받아왔다면 이 데이터에서 realSeatCntlk 값을 확인해 잔여좌석이 남아있는지 확인해야합니다.

def check_remaining_seats(seats: list) -> list:
    result = []

    for seat in seats:
        if seat['realSeatCntlk'] > 0:
            print('남은 좌석 발생!')

# 위 함수는 아래처럼 호출합니다.
# 먼저 좌석 데이터를 가져오고
seats = get_seats_summary() 

# 좌석 데이터에서 summary(좌석 배열) 값을 잔여좌석 체크 함수의 매개변수로 넘깁니다.
check_remaining_seats(seats['summary']) 

남은 좌석을 체크하기는 했지만 어디서 몇개의 자리가 발생했는지는 알 수가 없습니다. 다행히 seats 요소에는 좌석 영역에 대한 상세한 정보가 포함되어 있어 더 구체적인 메세지 작성이 가능합니다.

# 메세지 출력 시 generate_message 함수를 호출하도록 변경합니다.
def check_remaining_seats(seats: list) -> list:
    result = []

    for seat in seats:
        if seat['realSeatCntlk'] > 0:
            print(generate_message(seat))

# 정확히 어느 위치에 몇 자리가 발생했는지 메세지를 작성해서 반환합니다.
def generate_message(seat: dict) -> str: 
    return seat['seatGradeName'] + ", " + seat['floorNo'] + seat['floorName'] + " " + seat['areaNo'] + seat['areaName'] + "에 잔여좌석 " + str(seat['realSeatCntlk']) + "개 발생! "

Slack으로 알림보내기

좌석체크를 할 수 있게 되었지만 매번 화면을 들여다보고 있을 수는 없습니다. 좌석이 생겼을 때 Slack과 같은 도구로 알림을 받을 수 있다면 좋을 것 같습니다. 좌석이 발생했을 때 Slack Webhook URL로 메세지를 보내도록 구성해보겠습니다. (Slack 계정이 있다고 가정하겠습니다.)
 
먼저 Apps 페이지로 이동 후 우측 상단에 Create New App을 클릭합니다.

그러면 App을 어떤 방식으로 생성할 것인지 선택하는 모달창이 출력됩니다. 특별한 설정이 없는 App이라 수동으로 구성해도 문제가 없을 것 같습니다. From scratch를 선택합니다.

적절하게 App Name을 선택한 뒤 Workspace를 선택해줍니다. (만약 Workspace가 없다면 새로 생성해주세요.)

App을 생성했다면 우측 메뉴에서 Incoming Webhooks를 클릭한 뒤 우측의 Activate Incoming Webhooks을 On으로 변경해줍니다.

그 다음 페이지 맨 아래로 이동해 Add New Webhook to Workspace 버튼을 클릭합니다.

그러면 Workspace 내 채널에 대한 액세스 권한을 요청합니다. 알림을 받고자 하는 채널을 선택 후 허용해줍니다. (채널이 없다면 생성해주세요.)

이제 Webhook URL이 생성되었습니다. 이제 이 Webhook URL을 사용해 잔여좌석 알림을 받을 수 있습니다.

아래와 같이 코드를 추가합니다.

# 메세지를 출력하는 대신 배열에 저장한 뒤 반환합니다.
def check_remaining_seats(seats: list) -> list:
    result = []

    for seat in seats:
        if seat['realSeatCntlk'] > 0:
            result.append(generate_message(seat))

    return result

# 메세지 목록을 받아 Slack Webhook URL로 메세지를 전달합니다.
def send_message(messages: list) -> None:
    slack_webhook_url = ""
    for message in messages:
        response = requests.post(slack_webhook_url, json={'text' : message})

# 위 함수는 아래처럼 호출합니다.
# 좌석 데이터를 받아옵니다.
seats = get_seats_summary()

# 좌석 중 잔여 좌석 정보에 대한 메세지 배열을 받아옵니다.
messages = check_remaining_seats(seats['summary'])

# 메세지 목록을 매개변수로 전달합니다.
send_message(messages)

crontab을 사용해 자동으로 확인하기

알림도 받을 수 있게 되었지만 현재 상태라면 Melon 홈페이지에서 새로고침 버튼을 누르는 것과 크게 다르지 않습니다. (스크롤하면서 빈 좌석을 확인하지 않아도 되는 점이 다르겠네요.)
crontab으로 스케줄링을 걸어 주기적으로 확인하게끔 설정한다면 편할 것 같습니다.
MacOS에서는 간단하게 crontab을 설정할 수 있습니다. 먼저 crontab 설정 파일을 오픈합니다.

$ crontab -e 

그 다음 아래와 같이 내용을 작성후 저장합니다.

* * * * * <PYTHON_PATH> <PYTHON_FILE_PATH>

PYTHON_PATH에는 python 바이너리 파일의 전체 경로를 입력합니다. PYTHON_FILE_PATH에는 실행할 파일 경로를 작성해주세요.
위 설정(* * * * *)은 매 분마다 파이선 파일을 실행하게 합니다. 이 간격은 더 늘릴 수 있는데 티켓팅 특성상 주기가 짧을수록 좋기 때문에 매분으로 설정하는 것이 좋습니다. (다른 주기로 설정하고 싶다면 이 링크에서 쉽게 표현식을 만들 수 있습니다.)

 
그런데 사실 인기가 많은 콘서트의 경우 1분으로는 부족할 수 있습니다. 제가 노렸던 아이유 콘서트의 경우 잔여좌석 발생 후 2~8초 사이에 매진이 됐기 때문에 주기를 더 짧게 설정해줘야 하지만 crontab은 최소 분단위만 지원합니다.

이런 경우 python의 time 라이브러리의 sleep 기능을 사용하면 간단하게 해결할 수 있습니다. 예시로 2초마다 실행하도록 코드를 작성해보겠습니다.

import time

def main() -> None:
    for i in range(30):
        seats = get_seats_summary()
        messages = check_remaining_seats(seats['summary'])
        send_message(messages)
        time.sleep(2)

...

crontab 주기인 1분 동안 잔여좌석 체크 함수를 30번 실행시키고 잔여좌석을 한번 체크할 때마다 2초씩 정지합니다. 이렇게 하면 1분동안 2초간격으로 함수를 실행하게 됩니다.
 
여태까지 진행한 전체코드는 Github에 배포해두었으니 참고해주세요!
- ldy9037/melon-ticket-alert

마무리

이렇게 Python, Slack, crontab을 사용해 Melon 콘서트 취소표를 체크하는 프로그램을 만들어 보았습니다. 만들기 전에는 잘될까 싶었는데 만들고 나니 생각보다 잘 작동해서 만족스러웠던 프로그램이었습니다. 이 프로그램으로 꼭 티켓을 구매해서 블로그에 인증하고 싶었는데.. 1인 1매 한정이라 중간에 포기해서 많이 아쉬웠습니다. 다른 분이라도 꼭 원하는 콘서트 티켓을 얻어가시길 바라겠습니다.
감사합니다!
 

+ Recent posts