들어가며
넷플릭스, 디즈니플러스, 티빙 등 OTT 계정을 공유하는 사람이 많습니다.
다들 OTT를 사용하는 방식이 다를 텐데요. 저희 OTT 모임 기준으로는 한 명이 계정을 만들고 결제까지 관리하고 있습니다. (그 한 명에 당첨되었습니다) 기본적으로 월정액이기 때문에 한 달마다 OTT 결제가 되고 (4인 요금제라면) 나머지 3명이 저한테 입금을 해주는 방식입니다. 처음이나 계좌 들어가면서 입금 확인하지 한 달만 지나도 누가 입금했는지, 누가 안 했는지 관리하기가 어렵습니다. 다른 사람이 입금했는지 확인할 때도 예전 계좌 내역을 찾아봐야 되는 번거로움이 생깁니다.
이 번거로움, 해결할 수 있지 않을까요? 계좌 내역 확인을 자동화하고 디스코드 봇을 사용해 알림을 전송해 보겠습니다.
🧱 커다란 벽, 은행 계좌 가져오기
전체적인 흐름을 보면
- 스케쥴러로 은행 계좌를 읽고 있다가
- OTT 입금 내역이 확인되면,
- 디스코드 봇으로 알림 전송하기
흐름 자체는 간단하지만 1번 과정부터 커다란 벽을 만나버립니다. 은행 계좌 내역을 가져올 수 있는 방법이 있을까요?
금융결제원 오픈 API(https://openapi.kftc.or.kr/main)가 존재합니다. 오픈뱅킹도 여기서 확인할 수 있습니다. 중요한 건 개인에게 제공하지 않으며, 계약 과정이 필요하고, 수수료를 청구합니다. 사이트 첫 화면부터 개인과 거리가 멀어 보입니다. 취미로 시작한 단순한 프로젝트에서 쓸만한 API는 아니었습니다.
대부분의 은행은 인터넷 뱅킹 서비스를 제공하니까 해당 서비스를 사용해서 계좌 내역을 가져오면 됩니다. 이 글을 읽는 분들도 이미 경험한 것처럼 인터넷뱅킹을 쓰기 위해서는 보안 프로그램을 설치해야 하고 로그인할 때 인증서를 요구합니다. 이것들을 고려하면서 크롤링하고 자동화하기에는 너무 어렵습니다.
아래부터는 ㅅ은행 기준으로 설명합니다. 크롤링 상세 과정은 생략해서 작성했습니다.
ㅅ은행은 간편 조회 서비스를 제공합니다. 모바일 웹에서 어떠한 보안 프로그램 없이 아이디와 비밀번호만 사용해서 계좌를 확인할 수 있습니다. 여기서 원하는 건 계좌 내역만 필요하기 때문에 조건에 맞는 서비스입니다.
이제 User-Agent를 바꿔서 모바일 환경으로 바꾸고 간편 조회 서비스에서 계좌 내역을 크롤링하면... 끝날까요?
⌨️ 두 번째 벽, 보안 키보드
보안 프로그램은 없지만 보안 키보드가 남아있습니다. 보안 키보드를 우회해야 최종적으로 계좌 내역을 가져올 수 있습니다.
쉽게 떠오르는 방법은 보안 키보드 스크린샷을 저장하고 OCR(이미지를 읽어 글자로 변환하는 기술)로 어떤 버튼인지 알아내는 방법입니다. 하지만 OCR이 들어가면서 개발 난이도가 올라가게 됩니다. 프로젝트 기반 언어는 Kotlin인데 OCR 관련 라이브러리는 Python에 많았습니다. 더 간단한 방법을 찾아봅시다.
보안 키보드의 DOM을 확인해 보면 특정 속성을 통해 보안 키보드의 버튼이 어떤 값을 뜻하는지 알 수 있습니다. 이제 원하는 값이 담긴 속성을 찾고 버튼을 누르게 하면 됩니다!
아이디, 비밀번호 로그인 그리고 계좌 비밀번호 입력에서 보안 키보드를 요구하는데 해당 방법으로 우회가 가능합니다. 하지만 완벽히 우회에 성공한 건 아닙니다.
잠깐 Sentry로 넘어가 보면,
크롤링 오류 추적을 위해 Sentry를 연결했습니다. Sentry는 오류 수집, 모니터링 도구로 크롤링하면서 발생하는 오류 (또는 기타 오류들)를 모니터링할 수 있습니다. 이 프로젝트에서 쌓이는 오류 정도는 무료 요금제로도 사용이 가능하기 때문에 적용해서 추적하고 있습니다.
보안 키보드 우회는 성공했지만 Sentry에 많은 오류가 쌓이고 있습니다. Cron 기반으로 스케쥴러를 만들어 일정 시간마다 크롤링을 하고 있는데 통계를 내보면 크롤링 10번 중에 8번은 보안 키보드 입력 과정에서 실패합니다. 물론 스케쥴러와 상관없이 실패할 때마다 수동으로 크롤링을 실행시키면 대부분 해결되지만, 사람이 개입하는 순간 번거로움을 해결하려고 했던 목적과는 거리가 멀어집니다.
Sentry의 오류 내용을 보면 아이디, 비밀번호가 맞지 않아 로그인을 실패하고 결국 크롤링 실패로 이어지고 있었습니다. 원하는 값이 있는 속성을 찾아서 버튼을 눌렀지만, 해당 속성 값과 실제 버튼 값이 다른 경우가 있었습니다. 대부분의 보안 키보드에는 재배열 기능이 있습니다. 보안 키보드 클릭 전 버튼 재배열을 해주면 속성 값과 실제 버튼 값이 다른 문제를 해결할 수 있습니다.
해결 후 장기간에 걸쳐 Sentry를 보면 로그인 실패로 인한 문제는 해결되었습니다! 크롤링 중 발생하는 오류는 아직 페이지가 완전히 로딩되지 않아서 생기는 `TimeoutException`입니다. 10번 중 8번 정도 발생했던 오류를 10번 중 2번까지 오류 발생률을 줄였습니다.
💬 데이터를 가공해서 디스코드로 보내기
크롤링을 통해 가장 중요한 데이터인 계좌 내역을 얻었습니다. 해당 프로젝트는 `Kotlin, Spring Boot, Discord4j, Jetbrains Exposed, Supabase` 기술스택으로 구성돼 있습니다. Spring Boot에서 제공하는 Spring Scheduler를 사용해 일정 시간마다 계좌 내역에서 필요한 데이터인 입금 내역만 가져오면 됩니다.
OTT 입금 내역 알림은 실시간 구현이 필요 없는 알림입니다. 짧은 간격으로 크롤링하지 않고 6시간 간격(0시, 6시, 12시, 18시)으로 계좌 내역을 크롤링하고 있습니다. 대부분의 은행은 점검 시간이 있습니다. 점검 시간대에는 크롤링을 하지 않도록 처리해 줍시다.
@Scheduled(cron = "0 0 */6 * * *")
fun schedule() {
val now = LocalDateTime.now()
if (now.isAfter(BANK_MAINTENANCE_START) && now.isBefore(BANK_MAINTENANCE_END_HOUR)) {
logger.info("은행 점검 시간입니다. 크롤링을 하지 않습니다.")
return
}
logger.info("은행 크롤링을 시작합니다.")
bankCrawler.openBrowser()
val result = bankCrawler.crawl()
bankCrawler.closeBrowser()
applicationEventPublisher.publishEvent(CompletedBankCrawlEvent(result))
// CompletedBankCrawlEvent는 뒤에서 다시 언급하겠습니다
}
코드 중 `bankCrawler.crawl()`을 더 들여다봅시다.
모든 입금 내역을 가져올 필요는 없습니다. 한 달마다 입금하기로 정한 금액 그리고 같이 사용 중인 특정 사람들의 입금 내역만 가져오면 됩니다.
계좌 번호, 계좌 비밀번호, 은행 계정, 사람 이름 등 개인정보가 담긴 내용은 Supabase Vault를 사용해 안전하게 관리하고 있고 입금 금액 같은 암호화가 필요 없는 내용은 환경변수를 사용해 평문으로 관리하고 있습니다.
val date = accountDetailHtml.selectXpath("span[2]").text()
val time = accountDetailHtml.selectXpath("span[4]").text()
val cost = accountDetailHtml.selectXpath("span[10]").text()
val normalizeCost = cost.replace(",", "").toInt()
val who = accountDetailHtml.selectXpath("span[12]").text()
val targetNames = VaultConfiguration.DEPOSIT_TARGET_NAMES
.replace("[", "").replace("]", "").replace("\"", "")
.split(", ")
if (who in targetNames && normalizeCost != 0) {
val costMonth = round(normalizeCost / bankConfiguration.cost.toDouble()).toInt().toString()
result.add(AccountData(time, date, normalizeCost.toString(), who, costMonth))
}
입금 내역, Supabase Vault 그리고 환경변수에서 가져온 정보를 조합해서 위에서 말한 조건을 만족하는 경우 결과에 크롤링 결과를 넣어줍니다. 추가적으로 웹 대시보드에서 크롤링 기록(시도 날짜, 성공 여부)을 확인할 수 있도록 데이터베이스에 기록을 남겨줍니다.
transaction {
Metrics.upsert(Metrics.key, where = { Metrics.key eq MetricsKey.LATEST_CRAWLING_STATUS.name }) {
it[key] = MetricsKey.LATEST_CRAWLING_STATUS.name
it[value] = "O"
}
Metrics.upsert(Metrics.key, where = { Metrics.key eq MetricsKey.LATEST_CRAWLING_TIME.name }) {
it[key] = MetricsKey.LATEST_CRAWLING_TIME.name
it[value] = LocalDateTime.now().format(formatter)
}
}
스케쥴러 코드에서 `CompletedBankCrawlEvent` 이벤트를 보셨나요? 크롤링이 완료되면 크롤링 결과를 담은 내부 이벤트를 발행하게 됩니다. 크롤링 외에도 웹 대시보드에서 수동으로 입금 내역을 남길 수 있는데 이 때도 결과를 담은 `AccountData` 객체를 만들어 똑같이 이벤트를 발행해 주면 됩니다. 그리고 이벤트를 받으면 디스코드에 알림을 전송하고 데이터베이스에 입금 내역을 남겨주면 됩니다.
val embed = EmbedUtil.create(
title = "넷플릭스 입금 확인",
description = "(${it.who}) ${it.costMonth}달 치 확인 완료",
date = convertStringToInstant(it.date)
)
channel.createMessage(embed).block()
DepositLog.new {
who = it.who
cost = it.cost.toInt()
date = LocalDate.parse(it.date)
costMonth = it.costMonth.toInt()
}
이제 디스코드에서 알림을 받을 수 있습니다!
💸 이전 입금 내역을 쉽게 확인해 보기
이번에는 웹 대시보드에 대한 이야기입니다.
이전 입금 내역, 예를 들어 1개월 전 크게는 6개월 전 입금 내역을 확인해야 되는 경우가 생겼습니다. 물론 디스코드로 입금 알림을 보내주긴 하지만 과거의 내용을 찾아보기는 어렵습니다. 디스코드 검색 기능이 좋은 편도 아니고요. `CompletedBankCrawlEvent` 이벤트를 받아 처리하면서 데이터베이스에 입금 내역을 저장했는데 이를 그대로 웹에 표시해 주면 됩니다. 입금 내역을 표시해 주면서 크롤링 시 함께 저장했던 Metric들도 표시해 줍시다.
프론트엔드는 Supabase Auth로 관리자 계정 관리를 하고 Next.js NextUI(현재 HeroUI로 이름 변경됨)를 사용해서 간단하게 디자인해 줍니다. 그리고 Vercel을 사용해 쉽게 배포해 주었습니다.
간혹 1개월 금액이 아니라 한꺼번에 입금을 하는 경우도 있는데요. 해당 케이스도 알아보기 쉽도록 입금 금액을 기반으로 계산해서 '입금 인정 날짜' 항목에 표시해주고 있습니다.
항상 은행 계좌로 들어오는 게 아니라 예외 케이스도 있습니다. 이런 경우는 수동 추가를 사용해 주면 됩니다. 위에서 말한 것처럼 이벤트를 발행해서 이후 과정은 크롤링과 똑같이 처리됩니다.
웹 대시보드 스크린샷을 보면 로그를 확인하는 메뉴도 있습니다.
Spring Boot 로그(또는 디스코드 봇)를 확인하기 위해서는 SSH로 서버에 접속하거나 모니터링 툴을 사용해야 됩니다. 빠르게 로그를 확인할 수 있도록 Spring Boot에서 생성되는 로그 파일을 읽어와 웹 대시보드로 뿌려줍니다.
fun getLogs(): List<String> {
val file = File("./data/logs").listFiles { file ->
file.isFile && file.name.matches(Regex("log-\\d{4}-\\d{2}-\\d{2}.\\d+\\.log"))
}?.maxByOrNull { it.lastModified() }
val filePath = file?.toPath() ?: throw ServiceException("로그 파일이 존재하지 않습니다.", 404)
val rawLog = Files.readString(filePath)
val log = rawLog.split("\n").takeLast(80)
return log
}
WebSocket, SSE(Server Sent Event) 등으로 실시간 로그를 구현할 수 있지만 장기적으로 로그를 볼 일 없기 때문에 새로고침 버튼만 만들어줬습니다.
마무리
사람이 했던 번거로운 입금 내역 확인을 이제는 크롤러와 디스코드 봇이 대신하는 이야기를 써봤습니다. 이제 저는 매번 확인할 필요 없이 생각날 때마다 디스코드를 보거나 대시보드를 확인하면 됩니다.
은행 크롤링을 하면서 최근에 생긴 OTT 인증코드도 크롤링으로 비슷하게 해결했습니다. 입금 내역에서는 대상이 은행이었다면 인증코드는 IMAP을 사용한 이메일로 변경된 것 밖에 없습니다.
글에서 소개한 Spring Boot 서버(디스코드 봇), 웹 대시보드는 오픈소스로 공개돼 있습니다!
https://github.com/SkyLightQP/NetflixChecker
GitHub - SkyLightQP/NetflixChecker: 넷플릭스 입금 확인 디스코드 봇 - 계좌 크롤링 후 입금 내역 확인 시
넷플릭스 입금 확인 디스코드 봇 - 계좌 크롤링 후 입금 내역 확인 시 디스코드로 알려줍니다. - SkyLightQP/NetflixChecker
github.com
https://github.com/SkyLightQP/NetflixChecker-admin
GitHub - SkyLightQP/NetflixChecker-admin: NetflixChecker Dashboard
NetflixChecker Dashboard. Contribute to SkyLightQP/NetflixChecker-admin development by creating an account on GitHub.
github.com
'개발 > 개발 후일담' 카테고리의 다른 글
웹에서 즐기는 보드게임 만들기 (0) | 2024.10.08 |
---|---|
NextJS와 Supabase로 포트폴리오 만들어보기 (0) | 2024.09.16 |
랜덤 한글 단어 만들기 (2) | 2021.05.04 |
학교 인트라넷 프로젝트: 수정과 개발기 (0) | 2021.03.14 |
공적마스크 재고 & 온라인 개학 시간표 알림 챗봇 개발기 (3) | 2020.05.06 |