같은 공공데이터포털(data.go.kr)에서 API를 신청하는데, 어떤 건 apis.data.go.kr로 호출하고, 어떤 건 api.odcloud.kr로 호출해야 한다. 같은 사이트에서 발급받은 같은 키인데 왜 도메인이 다를까? 여기에 GET과 POST 방식 차이까지 합쳐지면 혼란이 배가 된다.
이번 편에서는 금융위원회 기업개요 API와 국세청 사업자등록 상태조회 API, 이 두 서비스를 나란히 비교하면서 만든다. 같은 공공데이터인데 설계 철학이 다른 두 시스템의 차이를 짚는다.
두 가지 API 체계 비교
먼저 큰 그림부터 보자.
| 구분 | 기업개요 (금융위원회) | 사업자등록 상태 (국세청) |
|---|---|---|
| 도메인 | apis.data.go.kr | api.odcloud.kr |
| HTTP 방식 | GET | POST |
| 응답 구조 | response > body > items > item | data (최상위 배열) |
| 인증 방식 | serviceKey (쿼리 파라미터) | serviceKey (쿼리 파라미터) |
| 데이터 형식 | JSON (resultType=json) | JSON (기본) |
| 별도 신청 | 필요 (API 활용 신청) | 필요 (API 활용 신청) |
같은 공공데이터포털에서 발급받은 키로 두 API 모두 호출할 수 있다. 하지만 호출 체계가 달라서 코드 구조도 달라진다.
기업개요 API (GET 방식)
금융위원회의 기업개요 API는 전형적인 data.go.kr 스타일이다. GET 요청에 쿼리 파라미터로 검색어를 넘긴다.
define( 'NALKKUL_COMPANY_API_URL',
'https://apis.data.go.kr/1160100/service/GetCorpBasicInfoService_V2/getCorpOutline_V2' );
$url = NALKKUL_COMPANY_API_URL
. '?serviceKey=' . rawurlencode( NALKKUL_DATA_GO_KR_KEY )
. '&pageNo=1'
. '&numOfRows=20'
. '&resultType=json'
. '&corpNm=' . rawurlencode( $corp_nm );
$resp = wp_remote_get( $url, array(
'timeout' => 15,
'headers' => array( 'Accept' => 'application/json' ),
) );
코드에 http_build_query()를 쓴 흔적이 주석으로 남아 있다. 처음에 그렇게 짰다가 이중 인코딩 문제로 수동 빌드로 바꿨다. 이 삽질은 시리즈를 읽어온 사람이라면 이제 익숙할 것이다.
사업자등록 상태조회 API (POST 방식)
국세청 API는 완전히 다른 패턴이다. POST 방식에 JSON body를 보낸다.
define( 'NALKKUL_BIZ_API_STATUS_URL',
'https://api.odcloud.kr/api/nts-businessman/v1/status' );
$url = NALKKUL_BIZ_API_STATUS_URL . '?serviceKey=' . rawurlencode( NALKKUL_DATA_GO_KR_KEY );
$resp = wp_remote_post( $url, array(
'timeout' => 15,
'headers' => array( 'Content-Type' => 'application/json' ),
'body' => wp_json_encode( array( 'b_no' => $b_nos ) ),
) );
wp_remote_post를 쓰고, body에 JSON 문자열을 넘긴다. serviceKey는 여전히 쿼리 파라미터로 붙인다. 사업자번호 배열을 b_no 키로 보내면 된다. 한 번에 최대 100개까지 조회 가능하다 (우리는 5개로 제한).
기업 검색의 중복 문제
“삼성전자”를 검색하면 삼성전자, 삼성전자서비스, 삼성전자판매, 삼성전자로지텍… 줄줄이 나온다. 사용자가 원하는 건 삼성전자(주) 하나인데 비슷한 이름의 법인이 20개 넘게 뜬다.
중복 제거 로직을 단계별로 구현했다:
// 1단계: 같은 법인명이면 시장 우선순위가 높은 것만 남김
$market = $item['corpRegMrktDcdNm'] ?? '';
$priority = 0;
if ( strpos( $market, '유가' ) !== false ) $priority = 4; // 유가증권
elseif ( strpos( $market, '코스닥' ) !== false ) $priority = 3; // 코스닥
elseif ( strpos( $market, '코넥스' ) !== false ) $priority = 2; // 코넥스
elseif ( ! empty( $market ) ) $priority = 1; // 기타
// 같은 이름이면: 시장 우선순위 높은 것, 같으면 종업원수 많은 것
if ( $priority > $prev['priority'] ||
( $priority === $prev['priority'] && $emp > $prev['emp'] ) ) {
$unique[ $name ] = array( 'item' => $item, 'priority' => $priority, 'emp' => $emp );
}
// 2단계: 정확히 일치하는 것을 맨 위로
$search_lower = mb_strtolower( trim( $corp_nm ) );
foreach ( $unique as $name => $data ) {
if ( mb_strtolower( $name ) === $search_lower ) {
$exact[] = $data['item'];
} else {
$partial[] = $data['item'];
}
}
$filtered = array_merge( $exact, $partial );
유가증권 > 코스닥 > 코넥스 > 비상장 순으로 우선순위를 매긴다. “삼성전자”라는 이름이 3개 있으면, 유가증권(코스피 상장)인 삼성전자(주)만 남기고 나머지는 버린다. 그리고 검색어와 정확히 일치하는 법인을 목록 최상단에 올린다.
사업자번호 자동 포맷팅
사업자등록번호는 10자리 숫자인데, 사람이 읽기 쉽게 XXX-XX-XXXXX로 표시한다.
function nalkkul_biz_sanitize_bno( $input ) {
return preg_replace( '/[^0-9]/', '', $input );
}
function nalkkul_biz_format_bno( $digits ) {
if ( strlen( $digits ) !== 10 ) {
return $digits;
}
return substr( $digits, 0, 3 ) . '-' . substr( $digits, 3, 2 ) . '-' . substr( $digits, 5, 5 );
}
입력 시에는 하이픈을 포함하든 안 하든 상관없이 숫자만 추출한다. 출력 시에는 항상 하이픈 포함 형식으로 보여준다. 사용자가 “123-45-67890″이든 “1234567890”이든 둘 다 동일하게 처리된다.
상태 판정과 색상 코딩
국세청 API는 사업자 상태를 코드로 반환한다: 01(계속사업자), 02(휴업), 03(폐업). 각 상태에 색상, 아이콘, 설명을 매핑했다.
function nalkkul_biz_stt_map() {
return array(
'01' => array(
'label' => '계속사업자', 'desc' => '정상 영업 중',
'color' => '#16a34a', 'bg' => '#dcfce7', 'icon' => '✓'
),
'02' => array(
'label' => '휴업자', 'desc' => '휴업 상태',
'color' => '#ea580c', 'bg' => '#fff7ed', 'icon' => '⚠'
),
'03' => array(
'label' => '폐업자', 'desc' => '폐업 상태',
'color' => '#dc2626', 'bg' => '#fef2f2', 'icon' => '✗'
),
);
}
녹색(계속사업자), 주황(휴업), 빨강(폐업). 직관적이다. 과세 유형도 코드로 오는데, “01”~”07″까지 7종이다:
function nalkkul_biz_tax_type_map() {
return array(
'01' => '부가가치세 일반과세자',
'02' => '부가가치세 간이과세자',
'03' => '부가가치세 과세특례자',
'04' => '부가가치세 면세사업자',
'05' => '수익사업을 영위하지 않는 비영리법인',
'06' => '고유번호가 부여된 단체',
'07' => '부가가치세 간이과세자(세금계산서 발급사업자)',
);
}
빈 필드의 다양한 형태
기업개요 API에서 데이터가 없는 필드가 보내는 값이 일관성 없이 다양하다:
null""(빈 문자열)"0""-"- 아예 키 자체가 없는 경우
하나의 함수로 전부 처리한다:
function nalkkul_company_is_empty( $val ) {
if ( is_null( $val ) ) {
return true;
}
$val = trim( (string) $val );
return $val === '' || $val === '0' || $val === '-';
}
UI에서 빈 필드는 아예 표시하지 않는다. “대표자: ” 뒤에 아무것도 없으면 그 행 자체를 숨긴다.
날짜/금액 포맷팅
기업개요 API는 날짜를 "20210315" 형태 8자리 숫자로 반환한다. 급여도 원 단위 숫자다.
function nalkkul_company_format_date( $raw ) {
$raw = trim( $raw );
if ( empty( $raw ) || strlen( $raw ) < 8 ) {
return '';
}
$y = substr( $raw, 0, 4 );
$m = substr( $raw, 4, 2 );
$d = substr( $raw, 6, 2 );
return $y . '년 ' . $m . '월 ' . $d . '일';
}
function nalkkul_company_format_salary( $raw ) {
$raw = trim( $raw );
if ( empty( $raw ) || ! is_numeric( $raw ) || (int) $raw === 0 ) {
return '';
}
$val = (int) $raw;
$man = round( $val / 10000 );
return number_format( $man ) . '만원';
}
“20210315” → “2021년 03월 15일”, “85000000” → “8,500만원”. 한국어 사용자에게 친숙한 형태로 바꿔주는 것만으로도 서비스 품질이 크게 올라간다.
Rate Limiting 구현
누군가가 (또는 봇이) 검색 버튼을 반복 클릭하면 API 호출 한도가 금방 소진된다. IP 기반 rate limiting을 넣었다.
define( 'NALKKUL_COMPANY_RATE_LIMIT', 30 ); // 시간당 30회
function nalkkul_company_check_rate_limit() {
$ip = sanitize_text_field( $_SERVER['REMOTE_ADDR'] ?? '0.0.0.0' );
$key = 'nalkkul_co_rl_' . md5( $ip );
$data = get_transient( $key );
if ( false === $data ) {
set_transient( $key, array( 'count' => 1, 'start' => time() ), HOUR_IN_SECONDS );
return true;
}
if ( $data['count'] >= NALKKUL_COMPANY_RATE_LIMIT ) {
return false;
}
$data['count']++;
set_transient( $key, $data, HOUR_IN_SECONDS );
return true;
}
Transient API로 구현하니 Redis나 별도 테이블 없이도 동작한다. 한 시간에 30번 제한이면 일반 사용자에게는 넉넉하고, 남용은 막을 수 있는 수준이다. 사업자등록 조회에도 같은 패턴을 적용했다.
진위확인 기능 (사업자등록)
사업자등록 조회에는 “상태조회”와 “진위확인” 두 기능이 있다. 상태조회는 사업자번호만 넣으면 되지만, 진위확인은 사업자번호 + 개업일자 + 대표자 성명을 모두 넣어야 한다.
define( 'NALKKUL_BIZ_API_VALIDATE_URL',
'https://api.odcloud.kr/api/nts-businessman/v1/validate' );
$businesses = array(
array(
'b_no' => $b_no, // 사업자번호
'start_dt' => $start_dt, // 개업일자 (YYYYMMDD)
'p_nm' => $p_nm, // 대표자 성명
'p_nm2' => '',
'b_nm' => '',
'corp_no' => '',
'b_sector' => '',
'b_type' => '',
),
);
$resp = wp_remote_post( $url, array(
'timeout' => 15,
'headers' => array( 'Content-Type' => 'application/json' ),
'body' => wp_json_encode( array( 'businesses' => $businesses ) ),
) );
빈 필드도 보내야 한다. 누락하면 API가 에러를 반환한다. 응답에서 valid가 "01"이면 정보 일치, 아니면 불일치다.
실제 문제: “등록되지 않은 인증키” 에러
공공데이터포털에서 키를 발급받으면 끝인 줄 알았는데, 국세청 사업자등록 API는 별도로 활용 신청을 해야 한다. 기존 키로 호출하면 “등록되지 않은 인증키입니다” 에러가 뜬다.
// 이 에러를 명확하게 사용자에게 알려줘야 한다
if ( strpos( $raw, '등록되지 않은 인증키' ) !== false || $code === 401 || $code === 403 ) {
return new WP_Error( 'auth_error',
'API 인증키가 유효하지 않습니다. data.go.kr에서 해당 API 활용 신청을 확인해주세요.'
);
}
data.go.kr의 “활용 신청” 메뉴에서 국세청 사업자등록정보 진위확인 서비스를 검색해서 신청해야 한다. 자동 승인이니 시간은 안 걸리지만, 이걸 모르면 한참 헤맨다.
복수 사업자번호 동시 조회
상태조회는 여러 번호를 동시에 조회할 수 있다. 쉼표나 줄바꿈으로 구분해서 입력하면 파싱한다.
$parts = preg_split( '/[s,]+/', $raw );
$b_nos = array();
foreach ( $parts as $part ) {
$digits = nalkkul_biz_sanitize_bno( $part );
if ( strlen( $digits ) === 10 ) {
$b_nos[] = $digits;
}
}
if ( count( $b_nos ) > 5 ) {
wp_send_json_error( '한 번에 최대 5개까지 조회할 수 있습니다.' );
}
최대 5개로 제한한 건 UX와 API 부하 양쪽을 고려한 것이다. API 자체는 100개까지 지원하지만, 한 번에 100개를 조회하면 응답 시간이 길어지고 UI도 복잡해진다.
개별 결과 캐싱
복수 조회 결과는 사업자번호별로 개별 캐싱한다. 나중에 같은 번호를 단건 조회하면 캐시에서 바로 가져온다.
foreach ( $data as $item ) {
$bno = $item['b_no'] ?? '';
if ( strlen( $bno ) === 10 ) {
$ttl = defined('NALKKUL_CACHE_BIZ') ? NALKKUL_CACHE_BIZ : 600;
set_transient( 'nalkkul_biz_st_' . $bno, array( $item ), $ttl );
}
}
탭 UI로 상태조회/진위확인 분리
두 기능을 하나의 페이지에서 탭으로 분리했다. 탭 전환은 순수 JS/CSS로 처리한다.
<div class="tgbiz-tabs">
<button class="tgbiz-tab active" data-tab="status">상태조회</button>
<button class="tgbiz-tab" data-tab="validate">진위확인</button>
</div>
<div class="tgbiz-panel active" id="tgbiz-panel-status">...</div>
<div class="tgbiz-panel" id="tgbiz-panel-validate">...</div>
CSS에서 .tgbiz-panel { display: none; }과 .tgbiz-panel.active { display: block; }으로 보이기/숨기기를 제어한다. 별도 라이브러리 없이 깔끔하게 구현된다.
마무리
이번 편에서 다룬 것들:
apis.data.go.kr(GET)와api.odcloud.kr(POST) 두 API 체계의 차이- 기업 검색 시 중복 법인명 처리: 시장 우선순위 + 정확 일치 정렬
- 사업자번호 자동 포맷팅과 다양한 입력 형태 수용
- 상태 판정의 색상 코딩과 과세 유형 매핑
- 빈 값의 5가지 형태와 통합 처리
- 날짜/금액 한국어 포맷팅
- IP 기반 rate limiting (Transient 활용)
- “등록되지 않은 인증키” 에러 — 별도 신청 필요
기술적인 내용은 여기까지. 다음 편, 시리즈 마지막 글에서는 이렇게 만든 공공데이터 서비스들을 어떻게 수익화하는지, 실제 운영하면서 얻은 노하우를 공유한다.
시리즈 목차
- 1편: 미세먼지 실시간 조회 서비스
- 2편: 약국/병원 찾기 서비스
- [현재 글] 3편: 기업정보 조회와 사업자등록 확인
- 4편: 수익화 전략과 운영 노하우
데이터 출처: 금융위원회 기업개요정보(fss.or.kr), 국세청 사업자등록정보(nts.go.kr), 공공데이터포털(data.go.kr)