🌐 개요
청약플래닛은 종합적인 부동산 정보를 제공하기 위해 여러 한국 정부 및 상업 API와 통합됩니다. 이 문서는 모든 외부 API 통합, 목적 및 구현 세부 정보를 설명합니다.
외부 API 연동 문서
청약플래닛 백엔드 관련 문서입니다.
청약플래닛은 종합적인 부동산 정보를 제공하기 위해 여러 한국 정부 및 상업 API와 통합됩니다. 이 문서는 모든 외부 API 통합, 목적 및 구현 세부 정보를 설명합니다.
정부 주택 공사로부터 한국 공공 주택 청약 정보를 직접 검색합니다.
https://api.odcloud.kr/api/ApplyhomeInfoDetailSvc/v1GET /getAPTLttotPblancDetail
serviceKey: API 인증 키page: 페이지 번호 (기본값: 1)perPage: 페이지당 레코드 수 (기본값: 50, 최대: 100)HOUSE_MANAGE_NO: 주택 관리 번호 (선택 사항)PBLANC_NO: 공고 번호 (선택 사항){
"currentCount": 1,
"data": [
{
"HOUSE_MANAGE_NO": "2024000001",
"PBLANC_NO": "2024강남01",
"HOUSE_NM": "래미안 강남포레스트",
"HSSPLY_ADRES": "서울특별시 강남구 대치동",
"TOT_SUPLY_HSHLDCO": "100",
"RCRIT_PBLANC_DE": "2024-06-19",
"RCEPT_BGNDE": "2024-07-01",
"RCEPT_ENDDE": "2024-07-03",
"SPSPLY_RCEPT_BGNDE": "2024-06-25",
"SPSPLY_RCEPT_ENDDE": "2024-06-27"
}
],
"matchCount": 1,
"page": 1,
"perPage": 50,
"totalCount": 1
}
서비스 통합:
@Service
public class SubscriptionService {
@Value("${sub.apt.api.url}")
private String subAptApiUrl;
@Value("${sub.apt.api.key}")
private String apiKey;
public String updateSubAPT() {
String requestUrl = subAptApiUrl + "?page=1&perPage=50&serviceKey=" + apiKey;
try {
ResponseEntity response = restTemplate.exchange(
requestUrl, HttpMethod.GET, null, String.class);
processSubscriptionData(response.getBody());
} catch (Exception e) {
log.error("LH API 호출 실패: {}", e.getMessage());
return "API 호출 실패: " + e.getMessage();
}
return "Success";
}
}
시장 분석 및 가격 비교를 위한 부동산 거래 가격 데이터 검색
https://apis.data.go.kr/1613000/RTMSDataSvcAptTradeGET /getRTMSDataSvcAptTrade
serviceKey: API 인증 키LAWD_CD: 행정 구역 코드 (시군구 코드)DEAL_YMD: 거래 연월 (YYYYMM)numOfRows: 레코드 수 (기본값: 50)pageNo: 페이지 번호
-
<거래금액>120,000거래금액>
<건축년도>2020건축년도>
<년>2024년>
<법정동>대치동법정동>
<아파트>래미안아파트>
<월>06월>
<일>15일>
<전용면적>84.93전용면적>
<지번>123-1지번>
<층>15층>
비동기 처리:
@Async
@Retryable(value = Exception.class, maxAttempts = 3)
public CompletableFuture collectRealPriceData(String dealYMD) {
List codes = sggCodeRepository.findAll();
List> futures = codes.stream()
.map(code -> CompletableFuture.runAsync(() ->
processCodeData(code, dealYMD), taskExecutor))
.collect(Collectors.toList());
return CompletableFuture.allOf(futures.toArray(new CompletableFuture[0]));
}
한국 주소를 지리적 좌표로 변환하고 위치 기반 서비스를 제공합니다.
https://dapi.kakao.comGET /v2/local/search/address.json
Authorization: KakaoAK {REST_API_KEY}
query: 검색할 주소 (한국어)analyze_type: 분석 유형 (similar, exact)page: 페이지 번호size: 페이지당 결과 수 (1-30){
"meta": {
"total_count": 1,
"pageable_count": 1,
"is_end": true
},
"documents": [
{
"address_name": "서울 강남구 대치동 944-32",
"y": "37.494870",
"x": "127.062583",
"address_type": "REGION_ADDR",
"address": {
"region_1depth_name": "서울",
"region_2depth_name": "강남구",
"region_3depth_name": "대치동",
"mountain_yn": "N",
"main_address_no": "944",
"sub_address_no": "32"
}
}
]
}
WebClient 설정:
@Configuration
public class WebClientConfig {
@Value("${kakao.api.key}")
private String kakaoApiKey;
@Bean
public WebClient kakaoWebClient() {
return WebClient.builder()
.baseUrl("https://dapi.kakao.com")
.defaultHeader("Authorization", "KakaoAK " + kakaoApiKey)
.codecs(configurer -> configurer.defaultCodecs().maxInMemorySize(1024 * 1024))
.build();
}
}
주소 지오코딩 서비스:
@Service
public class GeocodingService {
private final WebClient webClient;
public GeocodingService(WebClient kakaoWebClient) {
this.webClient = kakaoWebClient;
}
public Mono getCoordinates(String address) {
return webClient.get()
.uri(uriBuilder -> uriBuilder
.path("/v2/local/search/address.json")
.queryParam("query", address)
.build())
.retrieve()
.bodyToMono(String.class)
.map(this::parseCoordinates)
.onErrorReturn(new CoordinateResponseDTO(null, null));
}
private CoordinateResponseDTO parseCoordinates(String jsonResponse) {
try {
JsonNode root = new ObjectMapper().readTree(jsonResponse);
JsonNode document = root.path("documents").get(0);
if (document != null) {
double latitude = document.path("y").asDouble();
double longitude = document.path("x").asDouble();
return new CoordinateResponseDTO(latitude, longitude);
}
} catch (Exception e) {
log.error("좌표 파싱 실패: {}", e.getMessage());
}
return new CoordinateResponseDTO(null, null);
}
}
@Retryable(
value = {RestClientException.class, SocketTimeoutException.class},
maxAttempts = 3,
backoff = @Backoff(delay = 1000, multiplier = 2)
)
public String callExternalAPI(String url) {
return restTemplate.getForObject(url, String.class);
}
@Component
public class ExternalAPICircuitBreaker {
private final RestTemplate restTemplate;
private final CircuitBreaker circuitBreaker = CircuitBreaker.ofDefaults("externalAPI");
public String callWithCircuitBreaker(String url) {
return circuitBreaker.executeSupplier(() -> restTemplate.getForObject(url, String.class));
}
}
@Component
public class RateLimitedAPIClient {
private final RestTemplate restTemplate;
private final RateLimiter rateLimiter = RateLimiter.create(1.0);
public String callAPI(String url) {
rateLimiter.acquire();
return restTemplate.getForObject(url, String.class);
}
}
@Cacheable(value = "externalAPICache", key = "#url")
public String getCachedAPIResponse(String url) {
return callExternalAPI(url);
}
@CacheEvict(value = "externalAPICache", allEntries = true)
@Scheduled(fixedRate = 3600000)
public void clearCache() {
log.info("외부 API 캐시 지우는 중");
}
@Component
public class APIHealthIndicator implements HealthIndicator {
private final SubscriptionService subscriptionService;
private final GeocodingService geocodingService;
private final NaverNewsAPI naverNewsAPI;
@Override
public Health health() {
try {
testLHAPI();
testKakaoAPI();
testNaverAPI();
return Health.up().withDetail("apis", "모든 외부 API가 정상입니다.").build();
} catch (Exception e) {
return Health.down().withDetail("error", e.getMessage()).build();
}
}
private void testLHAPI() throws Exception { subscriptionService.updateSubAPT(); }
private void testKakaoAPI() throws Exception { geocodingService.getCoordinates("서울시 강남구").block(); }
private void testNaverAPI() throws Exception { naverNewsAPI.fetchNews("부동산", 1, 1); }
}
외부 API 문서 버전: 1.0
최종 업데이트: 2025-06-26
총 통합: 5개 API