티스토리 뷰

 


이번에 세금계산서 자동화를 기능을 맡으면서, 어떤 데이터 기반으로 자동화를 하고 어떻게 설계를 하고 어떻게 자동화를 했는지 그리고 중간에 발생했던 문제를 어떻게 해결했는지 등 경험을 하고자 하여 글을 작성합니다.

 

1. 왜 세금계산서 자동화하게 됐는지? 📚


세금계산서는 정기 과금, B2B 상품 판매, 외주 거래, 월세, 광고/마케팅 비용 등 다양한 비즈니스 상황에서 필수적으로 발행됩니다. 초기에는 발행 건수가 많지 않기 때문에 수기로 처리해도 큰 문제가 없습니다.


하지만 발행 건수가 수십, 수백, 수천 건으로 늘어나면 상황이 완전히 달라집니다.
사람이 직접 처리하다 보면 금액 입력 실수, 중복 발행, 발행 대상 누락 등의 문제가 발생하기 쉽고, “누가 누구에게 발행했는지”를 일일이 추적하는 것도 점점 어려워집니다.

현재 수작업 프로세스



이러한 문제들은 단순한 불편함을 넘어, 회계 신뢰도와 운영 효율성에 직접적인 영향을 줍니다. 결국 일정 규모 이상에서는 수작업 방식으로는 한계가 명확해지고, 이를 해결하기 위해 자동화를 도입하게 됐습니다.


이번에 진행한 세금계산서 자동화는 부동산 관리 플랫폼에서 임차인이 납부하는 임대료, 관리비, 기타 비용에 대해 세금계산서를 자동으로 발행하도록 구현하여, 반복적인 수작업을 제거하고 정확성과 효율성을 동시에 확보하는 것을 목표로 잡았습니다. 구체적인 목표는 다음과 같았습니다.

1. 세금계산서 발행 소요 시간 100% 단축
2. 중복 발행 건수 0건 달성
3. 발행 이후 데이터 변경 이력 관리율 100%

 

 

 

 

2. 세금계산서 자동화 요구사항 📄


요구사항은 다음과 같습니다.

1. 중복 발행 방지
2. 발행 시 이메일을 추가하여 여러명한테 발송 가능
3. 임대료와 관리비와 기타비용을 임차인과 다른 사람한테 발행 가능
4. 청구서 과세/면세 여부에 따라 합산 발행 처리 (비용 절감)
5. 발행 이후 데이터 조회/변경/취소 관리 (어드민)

 

 

 

 

 

3. 자동화를 하기 위한 고민의 흔적들 🕐 


[ ERD 설계 ]

일단 구조는 세금계산서 발행 정보를 담고 있는 테이블(room_billing_tax)과 세금에 들어갈 청구서 정보들을 담고 있는 테이블(room_billing_tax_mapping)을 나누어 1:N 구조로 잡았다. 그리고 임차인과 다른 사람한테 세금계산서 발행할 수 있도록 별도의 테이블(contract_tax_buyer)을 만들었다. 


상세 필드들은 각각 나름의 이유가 있지만, 하나하나 모두 설명하기에는 다소 과한 부분이 있어서.. 전체적인 맥락에서 “이런 구조로 설계되었구나” 정도로 가볍게 봐주세요 😊

 



 

[ 우리 서버 → 국세청 API 만으로 자동화 가능한가? ]

국세청 전자세금계산서 시스템은 존재하지만, 일반 개발자가 바로 붙을 수 있는 오픈 API 구조가 아니다...🤣 보통 많이 사용하는 게 REST API 호출하는 게 아닌, XML 포맷 + 전송 규격을 맞춰야한다.


또한 API Key 넣어서 하는 인증서가 아닌 사업자용 공동인증서(구 공인인증서) 필요하다. 즉, "요청 → 전자서명 → 암호화 → 전송" 이 과정을 직접 구현해야 한다. 물론 구현할 수 있지만 배보다 배꼽이 더 큰 상황이 펼쳐지게 된다🫤


그래서 중개(미들웨어)를 사용해야 한다. 대표적으로 더존 / KG이니시스 / 비즈플레이 / KCP / 팝빌 / 바로빌 / 볼타 등 있다.
회사마다 성향이 다른데
- 더존 / KG이니시스 / 비즈플레이 / KCP = 업무 시스템(ERP/결제 중심) 이고
- 팝빌 / 바로빌 / 불타 = 개발자용 API 플랫폼 이다.


자동화를 해야 하기 때문에 팝빌 / 바로빌 / 불타 중에 골라야한다. 각 회사마다 장단점이 나뉘는데, 상세히 말하면 이야기 길어지기 때문에 크게 고려한 부분만 얘기하면
- 안전한가?
- 운영 금액이 적당한가?
- 문서가 잘되어있나?


3가지 기준으로 적합했던 곳은 팝빌 이었다!! SDK 기반으로 개발을 편리하게 할 수 있고 무엇보다 문서가 잘 되어 있어서 좋았다. 기술 문의 및 테스트 포인트 신청하는 경우 빠른 응답이 좋았다. 팝빌이 중개로 들어오면 우리 서버 + 팝빌 + 국세청 흐름은 아래와 같다.

┌──────────┐      API 호출       ┌──────────┐     자동 전송      ┌──────────┐
│          │  ───────────────→  │          │  ─────────────→  │          │
│  우리 서버  │                   │   팝빌    │                  │   국세청   │
│          │  ←───────────────  │          │  ←─────────────  │          │
└──────────┘   Webhook 콜백     └──────────┘   전송 결과 응답    └──────────┘
 

 



 

[ 대량 발행 시 어떻게 처리할 것인가? ]

운영 데이터를 기준으로 확인해보니, 말일 기준 하루 최대 약 5,000건 정도의 청구서가 생성되었다. 이때 한 번에 대량 발행을 진행하면 트래픽이 급증할 수 있다고 판단하여, 처리 방식을 분산하는 방향으로 설계했다.

초기에는 새벽 시간대에 일정 건수씩 나누어 배치 처리하는 방식도 고려했지만, 담당자 측에서 청구일 당일에 세금계산서를 바로 확인하고 싶다는 요구가 있었다.

그래서 최종적으로는 1시간 주기의 스케줄러를 통해 발행 대상을 분산 처리하는 방식을 선택했고, 이를 통해 트래픽 급증을 방지하면서도 실시간에 가까운 발행 요구사항을 만족시킬 수 있었다.



 

 

[ 발행 중복 검증 어떻게 할 것인가? ]

세금계산서의 중복 발행 여부는 데이터 기반으로 검증하도록 설계하였다. 

  • 계약 ID
  • 호실 ID
  • 청구월
  • 청구일자
  • 공급받는자 등록번호
  • 과세/면세 여부

위에 조합하여 동일한 조건의 데이터가 이미 존재하는지를 확인하는 방식이다. 동시 요청 케이스에 대해서는, 스케줄러는 1시간 주기로 동작하기 때문에 큰 문제가 없지만, 어드민에서 수동 발행 기능이 있어 동시 요청이 발생할 수 있었다.


이를 해결하기 위해 DB 테이블 스케줄러용 레코드를 별도로 두고, SELECT FOR UPDATE를 통해 행 수준의 배타 락을 적용하였다. 락을 획득한 프로세스만 발행 로직을 수행하고, 나머지는 락 해제 시까지 대기하도록 하여 동시 요청으로 인한 중복 발행을 방지했다.

 

 

 



[ 외부 API 실패 대응 어떻게 처리할 것인가?  ]

외부 API 실패에 대해서는 상태 관리와 재시도 전략을 중심으로 대응했다. 우선 상태를 아래처럼 PENDING, PROCESSING, SUCCESS, FAIL 관리하여 실패 건을 추적할 수 있도록 했다.

public enum TaxInvoiceStatus {
  // 팝빌 상태
  ISSUE_PENDING("발행대기", "발행 대기"),
  ISSUE_PROCESSING("발행진행중", "발행 진행중"),  
  ISSUE_SUCCESS("발행성공", "발행 성공"),
  ISSUE_FAIL("발행실패", "발행 실패"),

  // 국세청 상태
  SEND_BEFORE("전송전", "팝빌에서 국세청 전송을 준비중인 상태"),
  SEND_WAIT("전송대기", "팝빌에서 국세청 전송을 대기중인 상태"),
  SENDING("전송중", "팝빌에서 국세청 전송을 진행중인 상태"),
  SEND_SUCCESS("전송성공", "전자세금계산서 국세청 신고가 정상적으로 완료된 상태"),
  SEND_FAIL("전송실패", "국세청이 특정사유로 전자세금계산서 신고를 반려한 상태"),

  ISSUE_CANCEL("발행취소", "운영자가 취소한 상태"),

  CORRECTED("정정됨", "재계약으로 수정세금계산서로 정정된 상태");
  
  ..
}

네트워크 오류와 같은 일시적인 문제는 Redis Queue 넣어두고 재시도 로직을 통해 처리했다. 반복적인 실패가 발생할 경우에는 재시도 횟수를 제한하고, 최종 실패 건은 별도로 관리하여 운영자가 확인할 수 있도록 했다. 그리고 우리쪽에 데이터 문제나 코드 문제가 발생하는 경우 Teams로 알림을 보내서 개발자가 대응할 수 있도록 하였다.

실제 팀즈 화면

 


예외 상황으로, 외부에서 정상 처리되었는데 timeout이 발생하여 응답을 받지 못하는 케이스가 있다.

요청 보냄 → 실제로는 성공했음
근데 응답 못 받음 (timeout)
“실패했네?” → 다시 요청 보냄

 timeout이 발생하더라도 실제로는 외부에서 정상 처리되었을 가능성이 있기 때문에, 단순히 실패로 처리하지 않고 팝빌 상태 조회 API를 통해 실제 발행 여부를 확인하여 데이터 정합성을 맞췄다.

발행 요청
   ↓
세금계산서 발행 외부 API 호출
   ↓
[성공] → SUCCESS 
[실패] → 재시도
[timeout]
   ↓
상태 조회 외부 API 호출
   ↓
있음 → SUCCESS
없음 → 재시도

 

 

 

 

[ 공급받는자, 과세/면세 여부에 따라 합산 발행 어떻게 할것인가? ]

이번 건은 비용 절감과 유연성을 동시에 만족해야 해서 가장 복잡한 부분이었다🤮 청구 항목은 임대료, 관리비, 기타비용으로 구성되어 있으며, 각 항목은 과세/면세가 혼합될 수 있다. 또한 공급받는자가 다를 경우 별도로 구분하여 발행해야 한다. 즉, 공급받는자 + 과세/면세 여부를 기준으로 세금계산서를 합산 발행해야 하는 구조였다.

 

✔ 처리 기준

세금계산서는 다음 기준으로 그룹화하였다.
- 공급받는자 식별값: 등록번호(사업자번호/주민등록번호)
- 과세 / 면세 여부

👉 즉, "같은 등록번호 + 같은 과세/면세”끼리 묶어서 1건으로 발행하는 식이다.

 

✔ 예시 1 (공급받는자 동일)

홍길동 1명인 경우:
- 임대료: 과세
- 관리비: 과세 + 면세 혼합
- 기타비용: 면세

👉 결과
- 과세: 임대료 + 관리비
- 면세: 관리비 + 기타비용

→ 총 2건 발행

 

✔ 예시 2 (공급받는자 다른 경우)

홍길동 / 김철수로 나뉘는 경우:


홍길동
- 과세: 임대료
- 면세: 기타비용


김철수
- 과세: 관리비
- 면세: 관리비


👉
공급받는자별 + 과세/면세 기준으로
총 4건 발행

 


자 이렇게 문제 없이 공급받는자 변경해도 유연하게 처리하면서도, 과세/면세 합산 발행하여 불필요한 세금계산서 발행을 최소화하여 비용 절감을 할 수 있었다.

 

 

 

 

[ 트랜잭션 범위를 어떻게 할 것인가? ]

초기에는 여러 청구서를 하나의 트랜잭션으로 처리하려고 했지만, 일부 청구서에서 세금계산서 발행이 실패할 경우 전체가 롤백되어 나머지 청구서까지 처리되지 않는 문제가 있다고 판단했다. 이러한 문제를 방지하기 위해 트랜잭션 범위를 계약 단위로 분리하였다. 이를 통해 특정 계약에서 실패가 발생하더라도 다른 계약에는 영향을 주지 않도록 하여, 대량 발행 상황에서도 안정적으로 처리할 수 있도록 설계했다.

 

 


 

 

4. 전체 아키텍처 🧩


세금계산서 자동화 시스템은 크게 5개 레이어로 나뉩니다.

각 레이어의 역할

외부 스케줄러 - 매시간(1시간 주기) 내부 API를 호출하여 세금계산서 발행을 트리거하는 역할
내부 스케줄러 - 외부 스케줄러의 요청을 받아 정상 청구 및 재청구 발행 프로세스를 실행하는 역할
정상 청구 / 수정 발행 - 청구서를 세금계산서 발행 단위로 그룹화하고, 전체 발행 흐름을 orchestration 하는 역할
세금계산서 관리 서비스 - 청구서 데이터를 기반으로 과세/면세 매핑 처리
- 세금계산서 발행, 상태 관리, 수정분 처리, 발행 취소 등
핵심 비즈니스 로직을 담당하는 역할
팝빌 연동 - 팝빌 SDK를 래핑한 단일 인터페이스 계층
- 모든 외부 API 호출을 이 계층을 통해 수행
- 국세청 전송(발행) 관련 액션 처리
- Webhook을 통해 발행 결과 및 상태를 수신하는 역할

 

 



 

 

5. 세금계산서 자동 발행 프로세스 🗂


자동 발행에는 두 가지 케이스가 있습니다. 매일 정기적으로 실행되는 정상 발행과 청구 금액이 변경되거나 삭제되는 경우 기존 세금계산서를 정정해야 하는 수정 발행이 있습니다.

[ 정상 발행 ]

 

 

 

 

[ 수정 발행 ]

수정세금계산서는 다음과 같은 흐름으로 발행됩니다.

 

 

 

 



 

6. 중간 발생했던 문제 🚨


[ 수정 발행 시 외부 API 트랜잭션 불일치 문제 ]

수정 발행에서 금액이 변경되는 경우에는 기존 세금계산서를 취소하고, 다시 발행해야 하는 구조였다. 즉, 외부 API를 최소 2번 호출해야 하는 케이스였다.

이 과정에서 가장 큰 문제는 트랜잭션의 일관성이었다. 하나의 트랜잭션 안에서 처리한다고 해도, 외부 API는 롤백이 불가능하기 때문에 첫 번째 호출은 성공하고 두 번째 호출이 실패하는 경우 데이터 불일치가 발생할 수 있었다.

초기에는 세금계산서를 즉시 전송 방식으로 처리했는데, 이 방식은 국세청으로 바로 전송되기 때문에 발행 취소가 불가능했다. 따라서 중간에 문제가 발생하더라도 이미 발행된 세금계산서를 되돌릴 수 없는 구조적인 한계가 있었다.

즉시 전송은 발행 취소 X


이를 해결하기 위해 전송 방식을 익일 자동 전송으로 변경하였다. 이 방식은 국세청으로 전송되기 전까지 발행 취소가 가능하다는 점을 활용한 것이다. 또한 팝빌에서 제공하는 국세청 즉시 전송 API를 별도로 사용하여, 모든 발행 및 비즈니스 로직이 정상적으로 완료된 이후 마지막 단계에서만 전송이 이루어지도록 설계하였다.

국세청 전송 방식

이로 인해 정상 발행과 수정 발행 프로세스에도 변화가 생겼고, 전체 흐름의 마지막 단계에 국세청 전송 API가 추가되었다. 아래 다이어그램에서도 이러한 변경 사항을 확인할 수 있다.

결과적으로, 중간에 문제가 발생할 경우 발행 취소를 통해 롤백이 가능해졌고, 모든 과정이 정상적으로 완료된 경우에만 최종 전송이 이루어지도록 하여 외부 API 호출로 인한 트랜잭션 불일치 문제를 해결할 수 있었다.