Kakao map API 역지오코딩, AJAX, View 에서 테스트

JungWooLee·2022년 8월 20일
0

SpringBoot ToyProject

목록 보기
7/14

이어서 진행하기

역지오코딩을 이용한 현재 위치 얻어내기

이전에 지오코딩을 통하여 현재 주소값을 이용하여 현재 위도, 경도를 알아보는 법을 테스트, 적용해보았습니다

이번 과제는 브라우저에 따라 다르겠지만 위치정보를 자바스크립트를 통하여 현재 주소가 맞는지 정확한 주소 정보를 노출시키는 것을 적용시켜보려 합니다


역지오코딩 API

Reverse Geocoding 을 지원하는 API는 크게 구글, 네이버, 카카오 정도를 찾을 수 있었습니다
구글과 네이버의 경우 완전한 무료는 아니며 적용시키고자 하였을 때 불필요하게 설정사항들이 많아 결국 기존 지오코딩으로 활용하던 카카오 지오코딩을 통하여 진행합니다

지오코딩 관련 포스팅
https://velog.io/@wjddn3711/%EB%82%98%EC%9D%98-%EC%A3%BC%EB%B3%80-%EB%A7%9B%EC%A7%91%EB%93%A4%EC%9D%84-%EA%B0%84%ED%8E%B8%ED%95%98%EA%B2%8C-%EC%95%8C%EC%95%84%EB%B3%B4%EC%9E%903

추가적으로 카카오 지도 또한 띄워 현재 위치가 맞는지 다시 한번 확인 절차를 거칠 수 있도록 합니다
https://apis.map.kakao.com/web/sample/basicMarker/

그외 지오코딩 관련 개발자 가이드
https://developers.kakao.com/docs/latest/ko/local/dev-guide

위를 참고하여 만들게 되었습니다


테스트 해보기

1. 모델 만들기

본래 진행하던 테스트에서는 가능성만을 보고 했기에 따로 모델을 매핑하는 과정을 생략하였지만 스크래핑이나 API 를 사용할 때에 결과값을 매핑하는 클래스는 필수적입니다.

왜 모델링을 해야하는가 ??

  • 가장 큰 이유는 리턴해주는 결과값에 변화가 있을때에 원인 분석을 하기 쉽기 때문입니다. 수시로 바뀌는 기술들과 업데이트들에 발마추어 스크래핑이나 API 의 구조나 기술의 변경을 감지하고 발빠르게 변경하기 위함입니다
  • 원치않은 데이터가 담길 수 있는 위험이 있습니다. 예를들어 Json 형태로 받았을때에 .get(something) 등을 하게 될 시 참조 데이터의 구조 변경이 이루어졌다면 의도하지 않은 값이 들어갈 것이며 이러한 오류를 찾아내기도 힘들기 때문입니다
  • 반복적으로 호출되는 결과값을 파싱할때에 같은 모델로 파싱하여 생산적인 코드를 유지할 수 있습니다

POSTMan 테스트
기존 카카오 Authorization 을 그대로 사용하여 x, y 값을 쿼리로 POST 하였을때의 결과값입니다

이를 POJO Java 모델로 변경하게 되면 다음과 같습니다

@Getter
@Setter
public class KaKaoMapResponse {
    public Meta meta;
    public ArrayList<Document> documents;

    public class Document{
        public String region_type;
        public String code;
        public String address_name;
        public String region_1depth_name;
        public String region_2depth_name;
        public String region_3depth_name;
        public String region_4depth_name;
        public double x;
        public double y;
    }

    public class Meta{
        public int total_count;
    }
}

대게는 필요한 정보만 모델로서 매핑하는 경우가 있지만 정확한 분석을 위하여 모든 필드값들을 넣어주었습니다

2. 테스트

	@Test
    @DisplayName("xy를받아주소값반환받는다")
    public void  xy를받아주소값반환() throws Exception {
        // given
        double x = 126.9863309;
        double y = 37.563398;
        String url = "https://dapi.kakao.com/v2/local/geo/coord2regioncode.json";

        UriComponents uri = UriComponentsBuilder.newInstance()
                .fromHttpUrl(url)
                .queryParam("x",x)
                .queryParam("y",y)
                .build();

        HttpHeaders httpHeaders = utility.getDefaultHeader();
        httpHeaders.add("Authorization", String.format("KakaoAK %s",authorization_key));

        // when
        HttpEntity requestMessage = new HttpEntity(httpHeaders);
        ResponseEntity response = restTemplate.exchange(
                uri.toUriString(),
                HttpMethod.GET,
                requestMessage,
                String.class);
        // then
        // 해당 JObject와 Response 객체간의 매핑
        Gson gson = new Gson();
        KaKaoMapResponse mapped_data = gson.fromJson(response.getBody().toString(),KaKaoMapResponse.class);
        String target = mapped_data.documents.get(0).address_name;
        assertThat(response.getStatusCode(), equalTo(HttpStatus.OK));
    }
  • 이전 포스팅에서 진행하였던 것과 같이 UriComponents 를 사용하여 사용으로 인코딩 되도록 적용
  • gson 의 json 모델 매핑을 활용하여 response body와 모델을 매핑
  • 가장 유망한 target 인 0번째 결과값을 볼 수 있도록 디버깅



View 에서 확인하기

앞서 해보았던 과정들을 종합하여 View 단에서 뿌려주고 확인해보는 과정을 가집니다

1. 인덱스 페이지

<!DOCTYPE html>
<html lang="ko" xmlns:th="http://www.thymeleaf.org">
<head>
    <meta charset="UTF-8">
    <!--    <meta id="_csrf" name="_csrf" th:content="${_csrf.token}"/>-->
    <!--    <meta id="_csrf_header" name="_csrf_header" th:content="${_csrf.headerName}"/>-->
    <title>Title</title>
    <script src="https://ajax.googleapis.com/ajax/libs/jquery/3.5.1/jquery.min.js"></script>
</head>
<body>

<h1>OAuth Test 중</h1>
<div class="col-md-12">
    <!--    로그인 기능 영역 -->
    <div class="row">
        <div class="col-md-6">
            <th:block th:if="${userName != null}">
                Logged in as: <span id="user" th:text="${userName}"></span>
                <a href="/logout" class="btn btn-info active" role="button">Logout</a>
            </th:block>
            <th:block th:unless="${userName != null}">
                <a href="/oauth2/authorization/google" class="btn btn-success active"
                   role="button">Google Login</a>
                <a href="/oauth2/authorization/naver" class="btn btn-secondary active"
                   role="button">Naver Login</a>
            </th:block>
        </div>
    </div>
</div>
<div id="myLocationInfo"></div>
<div id="fullAddress"></div>

<script>

    /* 비동기적으로 현재 위치를 알아내어 지정된 요소에 출력한다. */
    function whereami(elt) {
        // 이 객체를 getCurrentPosition() 메서드의 세번째 인자로 전달한다.
        var options = {
            // 가능한 경우, 높은 정확도의 위치(예를 들어, GPS 등) 를 읽어오려면 true로 설정
            // 그러나 이 기능은 배터리 지속 시간에 영향을 미친다.
            enableHighAccuracy: false, // 대략적인 값이라도 상관 없음: 기본값

            // 위치 정보가 충분히 캐시되었으면, 이 프로퍼티를 설정하자,
            // 위치 정보를 강제로 재확인하기 위해 사용하기도 하는 이 값의 기본 값은 0이다.
            maximumAge: 30000,     // 5분이 지나기 전까지는 수정되지 않아도 됨

            // 위치 정보를 받기 위해 얼마나 오랫동안 대기할 것인가?
            // 기본값은 Infinity이므로 getCurrentPosition()은 무한정 대기한다.
            timeout: 15000    // 15초 이상 기다리지 않는다.
        }

        if (navigator.geolocation) // geolocation 을 지원한다면 위치를 요청한다.
            navigator.geolocation.getCurrentPosition(success, error, options);
        else
            elt.innerHTML = "이 브라우저에서는 Geolocation이 지원되지 않습니다.";


        // geolocation 요청이 실패하면 이 함수를 호출한다.
        function error(e) {
            // 오류 객체에는 수치 코드와 텍스트 메시지가 존재한다.
            // 코드 값은 다음과 같다.
            // 1: 사용자가 위치 정보를 공유 권한을 제공하지 않음.
            // 2: 브라우저가 위치를 가져올 수 없음.
            // 3: 타임아웃이 발생됨.
            elt.innerHTML = "Geolocation 오류 " + e.code + ": " + e.message;
        }


        // geolocation 요청이 성공하면 이 함수가 호출된다.
        function success(pos) {

            console.log(pos); // [디버깅] Position 객체 내용 확인

            // 항상 가져올 수 있는 필드들이다. timestamp는 coords 객체 내부에 있지 않고,
            // 외부에서 가져오는 필드라는 점에 주의하다.
            var msg = "당신의 " +
                " 위도 : " + pos.coords.latitude +
                " 경도 : " + pos.coords.longitude

            elt.innerHTML = msg;     // 모든 위치 정보를 출력한다.
            console.log(pos.coords.longitude)
            getFullAddress(pos.coords.longitude, pos.coords.latitude); // 위도, 경도를 통하여 주소값을 띄워준다
        }
    }

    function getFullAddress(longtitude, latitude) {

        var AddressRequest={
            x : longtitude,
            y : latitude
        };

        $.ajax({
            url: "/user/api/getFullAddress",
            data: AddressRequest,
            type:"POST",
        }).done(function (fragment) {
            console.log('done');
            $("#fullAddress").replaceWith(fragment);
        });
    }

    // 나의 위치정보를 출력할 객체 구하기
    var elt = document.getElementById("myLocationInfo");

    // 나의 위치정보 출력하기
    whereami(elt);
</script>
</body>
</html>

좌표값을 받을 모델을 따로 하나 생성해주었습니다

  • 로깅을 위해 Tostring 은 임시로 달아주었구요
@Getter
@Setter
@ToString
public class AddressRequest {
    double x;
    double y;
}

다른 브라우저는 아닌 것으로 알고 있지만 크롬의 경우 위치 동의를 얻어야 x,y 좌표를 얻을 수 있기 때문에 좌표값을 얻고 난뒤 AJAX 를 실시 할 수 있도록 success 이후에 요청하도록 하였습니다

아직 타임리프에 익숙치 않아 View 단에 적용하는 것에 시간이 과투자 되고 있긴 하지만 좋은 경험으로 생각하면서 공부중에 있습니다. (가끔씩은 asp 가 익숙해서 asp: 를 습관적으로 치곤 하네요)

그리고 앞서 진행했던 서비스 테스트를 임플리로 옮겨보았습니다

	@Override
    public String getAddress(double x, double y) {
        String url = "https://dapi.kakao.com/v2/local/geo/coord2regioncode.json";

        UriComponents uri = UriComponentsBuilder.newInstance()
                .fromHttpUrl(url)
                .queryParam("x",x)
                .queryParam("y",y)
                .build();

        HttpHeaders httpHeaders = utility.getDefaultHeader();
        httpHeaders.add("Authorization", String.format("KakaoAK %s",authorization_key));

        // when
        HttpEntity requestMessage = new HttpEntity(httpHeaders);
        ResponseEntity response = restTemplate.exchange(
                uri.toUriString(),
                HttpMethod.GET,
                requestMessage,
                String.class);
        // then
        KaKaoMapResponse mapped_data = gson.fromJson(response.getBody().toString(),KaKaoMapResponse.class);
        String target = mapped_data.documents.get(0).address_name;
        return target;
    }

그리고 RestApi를 받아줄 컨트롤러를 추가적으로 생성합니다

@RestController()
@RequestMapping("/user/api")
@Slf4j
public class UserController {

   @Autowired
   private UserService userService;

   @PostMapping("/getFullAddress")
   public String getFullAddress(AddressRequest request){
       String result = userService.getAddress(request.getX(), request.getY());
       log.info(request.toString());
       return result;
   }
}

Trouble Shooting

여기까지 진행한뒤 테스트

분명히 done 이라는 로그가 떴는데 주소값이 뜨질 않습니다?
통신이 되었다면 컨트롤러에서 남긴 로그가 뜰것이니 확인해보았습니다

로그에 AJAX request 모델 로그가 안남은것으로 보아 문제가 발생하였음을 알게되었습니다
원인을 알 수 없이 방황하던 찰나 OAuth Config 가 기존 프로젝트에서 따온것을 기억해넸죠..

public class SecurityConfig{
    @Autowired
    private final CustomOAuth2UserService customOAuth2UserService;
    @Bean
    public SecurityFilterChain filterChain(HttpSecurity http) throws Exception{
        http
                .csrf().disable()
                .headers().frameOptions().disable()
                .and()
                .authorizeRequests()
                .antMatchers("/","/css/**","/images/**",
                        "/js/**","h2-console/**").permitAll()
                .antMatchers("/api/v1/**").hasRole(Role.USER.name())
                .anyRequest().authenticated()
                .and()
                .logout()
                .logoutSuccessUrl("/")
                .and()
                .oauth2Login()
                .userInfoEndpoint()
                .userService(customOAuth2UserService);
        return http.build();
    }
}

원인 : antMatchers 는 login 없이 접근 허용 하는 url에 대한 설정사항인데 여기에 기존 api 가 아닌 이전 restAPI 프로젝트에 쓰였던 url 이 그대로 있었던 것입니다..

.antMatchers("/","/css/**","/images/**", "/js/**","h2-console/**","/user/api/**").permitAll() 로 수정하면 해결!

View Test

해당 api의 경우 User 권한이 없더라도 실행될 수 있도록 권한 설정을 할 예정입니다


앞으로..

프로젝트를 진행하면서 기존 회사에서 틈틈히 해보려 하였으나 부쩍 업무량이 늘어나서 너무 늘어지는게 아닌가 싶었습니다. 적어도 주말에는 바짝 한다는 생각으로 틈틈히 하여 이번달 말까지는 어느정도 웹으로서 동작이 가능하도록 목표를 가져볼까 합니다.

그리고 최근 개인 공부를 하다가 https://wildeveloperetrain.tistory.com/49 님의 포스팅을 보고 과연 구현체와 추상화를 분리하여 사용하는것이 맞는것인가 의구심이 들었습니다. 관습적으로 하는 Service, ServiceImpl 는 위 포스팅대로 1:1의 관계를 가지는 것이 대부분이며 오히려 비생산적인 코드 습관을 기르고 있는것이 아닌가 하는 고민이 생기더라구요

협업과 코드의 변경에 민감하게 반응할 수 있다는 점에서 채택한 추상화였으나 이제는 사실 Domain 등으로 분리하여 새로운 코딩 형태를 갖게 되는 것 같아 해당 문제에 대하여 어느정도 서칭하여 구조를 바꿔볼까 하는 생각이 있습니다.

다만 현재 프로젝트 속도로 보아 차후 리팩토링, 고도화 과정에서 이러한 점이 추가될 것 같긴하네요

0개의 댓글