[BE:OUR] '공간 등록 폼' 데이터 저장 API - 주소를 이용해서 경위도값 구하기(KakaoMapAPI): RestTemplate과 JSON 라이브러리
기능 흐름: 호스트가 자신의 공간 등록 폼을 제출하면 DB에 저장
- 도로명 주소를 입력받고 해당 주소의 유효성을 확인
(프론트에서 행정안전부 주소 API 사용하여 구현 - https://jiyumi00.tistory.com/32)
- 호스트가 공간 등록 폼 제출 버튼을 누르면 주소를 바탕으로 경위도 값을 생성
(카카오맵 API 사용 - https://developers.kakao.com/docs/latest/ko/local/dev-guide#address-coord)
한 후 DB에 저장한다.
이번엔 카카오맵API를 활용하여 주소로 경위도 값을 구하는 KakaoMapService을 구현하는 것이 목표!
+추가로 hostId로 User 엔티티를 조회하는 UserService.getUserById(Long userId) 메소드를 구현할 것이다.
(space 객체에 담을 (host) user 객체를 조회하기 위해!)
1. KakaoMapService
application.yml 설정
kakao:
api:
key: ${MY_KAKAO_REST_API_KEY}
KakaoMapService
: getCoordinatesByAddress(String address) 메서드는 주소 문자열을 받아서, 위도(latitude)와 경도(longitude) 값을 담은 Coordinate를 반환하는 기능입니다.
import lombok.AllArgsConstructor;
import lombok.Getter;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Service;
import org.springframework.web.client.RestTemplate;
import org.springframework.http.*;
import org.json.JSONArray;
import org.json.JSONObject;
@Slf4j
@Service
@RequiredArgsConstructor
public class KakaoMapService {
private final RestTemplate restTemplate = new RestTemplate();
@Value("${kakao.api.key}")
private String kakaoApiKey;
public double[] getLatLng(String address) {
Coordinate coordinate = getCoordinatesByAddress(address);
return new double[]{coordinate.getLatitude(), coordinate.getLongitude()};
}
public Coordinate getCoordinatesByAddress(String address) {
String url = "https://dapi.kakao.com/v2/local/search/address.json?query=" + address;
HttpHeaders headers = new HttpHeaders();
headers.set("Authorization", "KakaoAK " + kakaoApiKey);
HttpEntity<String> entity = new HttpEntity<>(headers);
ResponseEntity<String> response = restTemplate.exchange(
url,
HttpMethod.GET,
entity,
String.class
);
if (response.getStatusCode() != HttpStatus.OK) {
throw new RuntimeException("카카오맵 API 호출 실패: " + response.getStatusCode());
}
JSONObject json = new JSONObject(response.getBody());
JSONArray documents = json.getJSONArray("documents");
if (documents.isEmpty()) {
throw new RuntimeException("주소로 좌표를 찾을 수 없습니다: " + address);
}
JSONObject location = documents.getJSONObject(0);
double latitude = location.getDouble("y");
double longitude = location.getDouble("x");
return new Coordinate(latitude, longitude);
}
@Getter
@AllArgsConstructor
public static class Coordinate {
private double latitude;
private double longitude;
}
}
*Lombok - Slf4j(Simple Logging Facade for Java): log라는 이름의 로거(logger) 객체가 자동으로 생성(아래와 같이 사용 가능)
log.info("좌표 응답 결과: {}", response);
log.error("에러 발생", e);
*Lombok - @RequiredArgsConstructor: final이나 @NonNull이 붙은 필드를 가진 생성자를 자동으로 생성.
아래의 필드에 대해
@RequiredArgsConstructor
public class KakaoMapService {
private final RestTemplate restTemplate;
private final String kakaoApiKey;
}
자동으로 아래 생성자를 생성함.
public KakaoMapService(RestTemplate restTemplate, String kakaoApiKey) {
this.restTemplate = restTemplate;
this.kakaoApiKey = kakaoApiKey;
}
1.1. 카카오맵 API 요청 URL 생성: 주소를 카카오맵 REST API에 넘기기 위한 URL 생성
String url = "<https://dapi.kakao.com/v2/local/search/address.json?query=>" + address;
1.2. HTTP 요청 헤더 설정
HttpHeaders headers = new HttpHeaders();
headers.set("Authorization", "KakaoAK " + kakaoApiKey);
HttpEntity<String> entity = new HttpEntity<>(headers);
- Authorization 해더에 카카오 API 키를 넣어야 함.
- HttpEntity는 요청 본문과 헤더 정보를 담는 객체.
1.3. API 호출 (GET 요청 보내기)
ResponseEntity<String> response = restTemplate.exchange(
url,
HttpMethod.GET,
entity,
String.class
);
- RestTemplate*을 사용해 HTTP GET 요청을 보냄.
- 응답은 문자열 형태(JSON 텍스트)로 받아옴.
- ResponseEntity*<String>은 HTTP 상태코드와 응답 본문을 함께 받는 객체.
1) HttpEntity: HttpHeaders와 body를 함께 묶어 전송하는 컨테이너 역할
- HTTP 요청(request)을 만들 때 사용되는 Spring 클래스
- 요청의 본문(body)이나 헤더(headers)를 담고 전송할 때 필요
vs ResponseEntity: 응답 전체(본문 + 상태 코드 + 헤더) 담은 객체.
2) RestTemplate
- Spring에서 제공하는 HTTP 요청을 보내는 클라이언트 도구.
- 외부 API 서버(예: 카카오 API)와 통신할 때 사용.
- GET, POST, PUT, DELETE 같은 요청을 쉽게 보낼 수 있게 함
: 내부적으로 HttpURLConnection 또는 HttpClient를 사용하지만, 그것보다 훨씬 간편하게 사용할 수 있게 도와주는 클래스이다.
1.4. 응답 코드 확인: 정상 응답(200)이 아니라면 예외 발생
if (response.getStatusCode() != HttpStatus.OK) {
throw new RuntimeException("카카오맵 API 호출 실패: " + response.getStatusCode());
}
1.5. JSON 응답 파싱: JSON 라이브러리 사용
JSONObject json = new JSONObject(response.getBody());
JSONArray documents = json.getJSONArray("documents");
- 응답 본문을 JSONObject로 파싱
- "documents" 배열을 추출
JSON 라이브러리를 써야하는 이유
(예시 JSON 응답)
{
"documents": [
{
"x": "127.123456",
"y": "37.123456",
"address_name": "서울시 강남구 ..."
}
]
}
If 직접 문자열 파싱한다면..
직접 문자열을 split()하거나 정규식으로 파싱하면 매우 복잡하고 취약하다..
String json = "{...}";
String lat = json.split("y\\":")[1].split(",")[0];
- 유지보수가 어렵고
- JSON 구조가 바뀌면 바로 오류 발생
- 숫자/문자열 구분도 어려움
따라서, org.json 라이브러리의 JSONObject, JSONArray를 사용하는 것이 좋음!
If JSON라이브러리를 사용한다면..
JSONObject json = new JSONObject(response.getBody());
JSONArray documents = json.getJSONArray("documents");
JSONObject location = documents.getJSONObject(0);
double latitude = location.getDouble("y");
double longitude = location.getDouble("x");
*JSONObject: JSON 객체(중괄호 {}로 감싸진 구조)를 자바에서 다루기 위한 클래스.
*JSONArray: JSON 배열(대괄호 []로 감싸진 리스트 구조)을 자바에서 다루기 위한 클래스.
1.6. 주소 결과가 비었는지 확인
if (documents.isEmpty()) {
throw new RuntimeException("주소로 좌표를 찾을 수 없습니다: " + address);
}
1.7. 첫 번째 주소 결과에서 위도/경도 추출: 첫 번째 결과를 꺼내서 y → 위도, x → 경도 값을 추출
JSONObject location = documents.getJSONObject(0);
double latitude = location.getDouble("y");
double longitude = location.getDouble("x");
1.8. Coordinate 객체로 반환: 좌표 값을 래핑해서 리턴
return new Coordinate(latitude, longitude);
*추후 필요에 따라 KakaoMapService에 대해 인터페이스 분리, 캐싱 등도 도입 예정.
2. UserService
: hostId로 User 엔티티를 조회하는 UserService.getUserById(Long userId) 메소드를 구현
package com.beour.user.service;
import com.beour.user.entity.User;
import com.beour.user.repository.UserRepository;
import lombok.RequiredArgsConstructor;
import org.springframework.stereotype.Service;
@Service
@RequiredArgsConstructor
public class UserService {
private final UserRepository userRepository;
public User getUserById(Long userId) {
return userRepository.findById(userId)
.orElseThrow(() -> new IllegalArgumentException("존재하지 않는 사용자입니다. ID = " + userId));
}
}
완성된 서비스 전체 흐름!
[사용자 입력 DTO]
↓
[User 조회] ← userService
↓
[주소 → 경위도 변환] ← kakaoMapService
↓
[Space 생성 및 저장]
↓
[Description 생성 및 저장]
[Tags 생성 및 저장]
[AvailableTimes 생성 및 저장]
[Images 생성 및 저장]
↓
[space.id 반환]