0. 들어가기
위치기반 장소 검색 Java 애플리케이션 개발이라는 부트캠프의 첫 Java과제가 나왔다.
나의 첫 과제를 해결한 과정을 소개해 보겠다.
0.1 문제 설명
우선 이 과제를 통해 필수적으로 익혀야 하는 것들의 목록을 정리해 보았다.
1. 입력받은 값 기반으로 출력값을 내놓아 보기(입출력)
2. 자바와 OpenAPI를 연결해 보기(HTTP method 활용)
3. Kakao의 지도 REST-API를 활용해 값을 받아와 보기(REST-API key를 통해 인증하고 카카오가 요구하는 방식으로 요청하여 원하는 데이터 받기)
4. 받아온 데이터 파싱하여 원하는 데이터로 변환하기(JSON-String 다루기)
5. 사용자가 원하는 링크를 콘솔에 붙여넣으면 해당 검색값의 카카오 지도를 브라우저로 띄워주기
6. 완성된 과제를 깃허브에서 나의 브랜치를 만들고 push한 뒤, main 브랜치를 대상으로 Pull Request 생성하기
처음엔 과제를 하며 한번에 완성도 있게 만들고 싶었지만..
REST-API를 이용해 보는 게 처음이었는데, 객체지향적으로 프로그래밍하는 것도, 요구사항을 구현하는 것도 둘 다 제대로 안되는데 한번에 하다가 자꾸 꼬여서 그냥 하나에 때려넣어 구현부터 하기로 했다.
이후 추가로 리펙토링, 테스트 코드 작성, 기능 추가를 한 것은 다음 글에서 정리하겠다.
1. 입출력
Java를 통해 만드는 콘솔 프로그램이기 때문에 기본적인 입출력을 할 수 있어야 한다.
Scanner의 객체 인스턴스를 scanner를 만들어 next시리즈 method를 통해 입력받았다.
System.out.print("위치 키워드를 입력하세요:");
query = scanner.nextLine();
System.out.print("검색 반경을 입력하세요(1000:1km):");
radius = scanner.nextInt();
scanner.nextLine(); // nextInt사용 후 엔터 제거
System.out.println();
System.out.println("입력한 위치 키워드: " + query);
System.out.println("검색 반경: " + String.format("%.1f", radius / 1000.0) + "km");
System.out.println();
System.out.println("**약국 검색 결과**");
여기서 주의할 점이 있었는데, scanner.nextInt() method를 통해 입력받은 후 엔터를 제거하지 않으면 다음 입력에 그 개행문자가 들어가 스킵되어 버린다는 것이다.
우리는 콘솔 창을 통해 입력하고, 엔터를 쳐서 입력을 완료한다. 여기서 엔터는 개행 문자 '\n'인데 이 개행 문자 역시 입력 스트림에 그대로 저장된다.
숫자를 입력하고 엔터를 치면, 3은 nextInt의 리턴값으로 나오지만 개행 문자는 여전히 스트림에 남아있어 다음 입력을 받아야 할 창에서 바로 개행문자를 입력으로 받아버린다는 것이다.
문제를 해결하는 간단한 방법은 nextLine()을 한 번 불러줘 개행문자를 입력받아 스트림 버퍼를 비워주는 것이다.
scanner.nextLine()의 경우는 다른데 이 함수는 마지막 개행문자 '\n'까지 모두 입력받고, '\n'을 버린 나머지를 String으로 리턴해 주는 함수이다.
일단 버리기 전에 개행문자까지 다 받아서 '\n'가 중간 스트림 버퍼에 남아있지 않으니 따로 버퍼 처리를 해줄 필요가 없다.
데이터를 받는 작업을 하기 전에, 먼저 사용자가 볼 화면을 띄워준다.
입력을 print method로 받거나, 중간중간 println을 통해 요구사항의 입출력 화면과 같게 만들었다.
2. OpenAPI 연결과 사용
카카오의 지도 REST-API를 사용해 검색값을 받아오려면 HTTP의 method를 사용해야 한다.
왜 HTTP인지, 어떤 구조로 통신하는지는 따로 작성하도록 하겠다.
실시간 강의를 통해 받은 선택지 중 HttpClient를 사용하기로 결정했다.
간단하게 이야하기하자면 HttpClient, HttpGet, HttpResponse 세 가지 객체가 필요하다.
1. HttpClient객체는 요청과 응답을 주고받아주는 역할을 하며
2. HttpGet으로 카카오로 GET요청을 하고
3. 받아온 응답은 HttpResponse에 담아 엔티티와 상태를 뽑아낸다.
먼저 연결한 코드부터 보자
// 키워드 검색으로 좌표 먼저 가져오기
try (CloseableHttpClient httpClient = HttpClients.createDefault()) {
String encodedQuery = URLEncoder.encode(query, "UTF-8"); // URL 인코딩
HttpGet httpGet = new HttpGet(baseUrl + restAPI + "?query=" + encodedQuery);
httpGet.addHeader("Authorization", "KakaoAK ${REST_API_KEY}");
CloseableHttpResponse httpResponse = httpClient.execute(httpGet);
HttpEntity entity = httpResponse.getEntity();
int statusCode = httpResponse.getStatusLine().getStatusCode();
if (statusCode == 200) {
String result = EntityUtils.toString(entity);
JSONObject jsonObject = new JSONObject(result);
JSONArray documentsArray = jsonObject.getJSONArray("documents");
JSONObject documentObject = documentsArray.getJSONObject(0);
x = documentObject.getString("x");
y = documentObject.getString("y");
} else {
System.out.println("Error: " + statusCode);
}
} catch (IOException e) {
e.printStackTrace();
}
2.1 입력받은 키워드의 X,Y좌표 획득하기(+HTTP 연결과정)
카카오가 제공하는 REST-API의 사용을 위해서는 먼저 HTTP 메서드를 사용해 요청보낼 객체를 생성해야 한다.
HTTP Get method를 보내고, 응답을 받아올 httpClient를 먼저 선언해야 한다.
근데 선언부가 try문 안에 들어가 있고, CloseableHttpClient라는 추상 클래스 타입으로 선언한다. 그 이유가 무엇일까?
찾아보면 자원을 사용 후 반드시 닫아줘야 한다고 한다. 자바는 GC가 알아서 닫아주는 것 아니었나? 하면 자바 책을 다시 꺼내들 필요가 있다.
GC의 개념은 오직 "메모리"에 한정된다. 그 중에서도 Heap영역을 관리하는데, 어쨌든 파일, 네트워크 연결, DB 연결들과 같은 시스템 리소스는 JVM의 관리범위 밖에 있고, 개발자가 명시적으로 해제해 주어야 한다.
그렇지 않으면 성능이 저하되거나, 프로그램이 정상적으로 작동하지 않을 수 있다.
예: 파일 입출력 스트림을 닫지 않아, 다른 스레드에서 해당 파일에 접근이 안 되는 경우, 소켓을 닫지 않아 계속해서 네트워크 자원을 차지하는 경우
하지만 try문의 괄호 안에 Closeable 인터페이스를 구현한 객체를 선언하면 개발자가 직접 명시적으로 해제하지 않고도 try블록 종료시 자동으로 close() method를 호출하여 리소스 해제를 안전하게 해 준다.
사용자 입력값, 도메인과 kakao developers에서 발급받은 REST-API key를 헤더에 넣어 HttpGet 객체를 만들고 그 객체를 httpClient의 excute method의 입력값으로 넣어 GET요청을 한다.
받아온 응답은 HttpResponse 객체에 전달해 getEntity()와 getStatusCode() method를 통해 엔티티와 상태를 받아온다.
먼저 HTTP 상태코드를 보고 200이면(정상 성공) 진행하고 아니면 예외처리한다.
HttpEntity를 선언해 getEntity()로 받아온 엔티티를 전달해 주고
이를 EntityUtils 클래스의 스태틱 method toString()을 통해 String으로 결과를 받아온다.
받아온 String에는 긴~ JSON형태의 문자가 들어있으므로 이를 편하게 다루기 위해 다시 JSONObject에 넣어준다.
"documents"라는 key 의 값으로 나머지 검색결과가 들어있어 꺼내주고, index0으로 첫번째 검색결과를 가져와 좌표값을 각각 x, y key로 뽑아준다.
여기서 인덱스0을 한 이유는 키워드 검색 시 카카오가 가장 정확한 결과를 상위 값으로 준다고 판단했기 때문이다.
만약 첫 결과가 부정확할 확률이 걸린다면 세개정도 받아와 사용자에게 뭐가 맞냐고 질문해서 정확도를 높여볼 수 있겠다.
2.2 x, y좌표와 입력받은 m내의 약국 카테고리 검색
try문을 닫았으므로 다시 열어주고 비슷한 과정을 반복하면 된다.
키워드 검색에서 카테고리 검색으로 바뀌었으므로 베이스가 되는 URL이 카테고리 REST-API의 사용법에 따라 조금 바뀔 것이고, 쿼리 파라미터에 원하는 입력값을 넣어주면 되겠다.
예를들어 요구사항에서 상위 10개의 결과를 출력하라 했으니 쿼리 파라미터에 10개만 받아오도록 넣는다.
원하는 특정 결과를 받아오기 위해 어떻게 하면 되는지에 대한 자세한 사항은 kakao developers 공식문서에 정말 쉽게 설명되어있어 생략한다.
2.3 입출력 화면
여기까지의 입출력 화면이다. 근처 약국의 상위 10개 검색결과를 받아와 출력한다.
3. 브라우저 결과 띄워주기
먼저 결과부터 보여주겠다.
출력받은 약국 목록에서 확인하고 싶은 URL을 콘솔에 입력하면 브라우저를 띄워준다. exit를 입력할 때까지 반복하며, 정상적인 URL이 아니라면 사용자에게 알려주고 exit을 입력했다면 프로그램을 종료한다.
while (true) {
System.out.print("kakaomap URL(장소 URL):");
String resultUrl = scanner.nextLine();
if(resultUrl.equals("exit")){
scanner.close();
System.out.println("프로그램 종료");
break;
}else{
try {
Desktop.getDesktop().browse(URI.create(resultUrl));
} catch (Exception e) {
System.out.println("올바르지 않은 url");
}
}
}
찾아보니 아주 간단하게 Desktop 클래스의 스태틱 method에 입력받은 주소를 넣어주면 해당 주소를 기본 브라우저로 열어줄 수 있었다. 예외사항에선 사용자에게 오류 메시지를 출력하고 다시 입력창으로 돌아간다.
exit을 입력하면 scanner를 닫고 종료메시지를 출력한 뒤 종료한다.
4. Github를 통한 Pull Request 생성
과제의 제출 방법은 나의 branch를 만들고 거기에 push한 뒤, main 브랜치에 Pull Request를 생성하여 제출하는 것이었다.
혹시 main브랜치를 건들지 않을까 무지 조심했다. 그래서 git init을 여러번 하고 주소연결을 여러개 하다가 꼬여 .git파일을 삭제하고 다시 연결해서 올렸다.
과정 동기 중 한명이 실수해 브랜치를 다 닫아버린 일이 있었는데, 복구 방법이 매우 쉬웠다. 얼마나 철렁했을지 내가 더 안타까웠다.
마무리
사실 내 진정한 목표에서는 1번만 완성했다.
이번 과제의 나의 목표들!
1. 절차지향적으로 마구 때려넣어 구현해보기(제출)
2. 객체지향적으로 리팩토링하기
2.1 객체를 먼저 생각해 보고 클래스 만들기
2.2 노출되는 REST API 인증값 숨기기
3. 기능 추가하기
3.1 주유소와 약국 중 선택하기
3.2 검색한 키워드 중 가장 처음 키워드로 x, y좌표를 추출하는데, 틀릴 경우를 대비해 키워드 검색시 3개의 선택지 출력 후 선택을 통한 확인절차
4. 심화
4.1 카카오 API에 결합도가 너무 높지 않나? 카카오API가 아닌 네이버API를 사용하면 API만 바꿀 수 있는가? 외부API를 다루는 클래스 추상화 해보기
4.2 카카오 API의 가능한 사용량은 하루 10만건인데 카카오를 다 쓰면 네이버API로 바뀌는 코드를 만들어보자
5. 해당 내용들을 통해 이래서 객체지향을 하는구나, 이래서 전략패턴을 쓰는것이 좋구나 이해한 것 정리
과제기간 안에 내 추가목표를 다 하려고 했는데 초반에 다른 일을 하다가 과정이 밀리고, 독감인지 감기몸살까지 겹쳐 아직 밀린 과정 따라가기 급하다.. 얼른 따라간 다음 2~5번 목표도 완성해 다른 글로 작성하도록 하겠다.
참고
https://developers.kakao.com/docs/latest/ko/local/dev-guide#address-coord
Kakao Developers
카카오 API를 활용하여 다양한 어플리케이션을 개발해보세요. 카카오 로그인, 메시지 보내기, 친구 API, 인공지능 API 등을 제공합니다.
developers.kakao.com