0. 들어가기
절차지향적으로 한번에 만들었던 코드들을 객체지향적으로 리펙터링 해보자.
우선 기본적인 사항을 피드백 받았는데, 이를 5개로 정리해 보았다.
1. 코드의 가독성이 너무 떨어졌다. 주석 없이 메서드 시그니처와 변수명, 클래스명만으로 충분히 이해야 되도록 작성해야 한다. 하지만 너무 주석으로 넘기며 만들었던 것이 문제였다. 이름을 가독성 있게 선정하자.
2. 키를 하드코딩하면 인증 키였다는 것을 확인하고 수정하기 어렵다. 이를 1차적으로는 상수로 관리해야 하고, 그렇다고 해도 키가 변경되거나 재발급을 할 경우 번거롭게 코드를 열어서 수정해야 한다. 외부로부터 가져와 사용하는 방법을 고민해야 한다.
3. HTTP 응답코드가 200일때 ok이라고 했는데, 이는 매직넘버라고 부르는 용어라고 한다. 코드가 길어진다면 유지보수를 하기 매우 힘들어지니 어떤 의미인지를 상수나 열거형으로 관리하자. 또, 응답을 확인하는 과정 역시 if문이 아닌 리턴으로 요청 성공을 보여주는 메서드로 분리시키는 것이 읽는 사람이 확인하기 좋다.
4. resultUrl.equals("exit")으로 문자열을 비교했는데, 이는 resultUrl이 null일 경우 NullPointerException이 발생한다. 절대 null일 수 없는 문자열을 앞에 두어 참조하게 하면 이를 예방할 수 있다.
5. URI를 만들다 예외가 날 수도 있지만, browse를 하는 도중에도 예외가 생길 수 있다. 한번에 예외처리하고 알려주면 무엇이 문제였는지 모르고, 문제가 되는 URL도 무엇이었는지 알려줘야 사용자 입장에서 편하다.
0.1 github에 내 repository 생성
원본 repository에 내 이름으로 브랜치를 만들고 push했었는데, 내 계정에 따로 repository를 만들고 다시 업로드하였다. 실수해도 파장이 내 계정을 넘어가지 않고 편하게 관리하기 위함이다.
이후 브랜치 하나를 만들어 수정본을 push하고 메인 브랜치에 pull request를 보내 merge하는 방식으로 수정할 것이다. 이는 수정내역을 편하게 확인할 수 있고, git과 github에 익숙해지기 위함이다.
1. 받은 피드백 반영
받은 피드백 중 빠르게 반영 가능한 것을 먼저 하고 절차지향적 코드로 바꿔보자.
1.1 가독성 있는 이름 사용
주석으로 떼우던 코드 가독성을, 이름만 보고도 알 수 있도록 변경하였다. 인텔리제이의 shift + f6을 사용하면 한번에 바꿀 수 있다.
1.2 고정값의 경우 상수, 열거형으로 정의
우선 하드코딩된 숫자나 문자열을 의미를 알 수 있고, 중복작성시 편리하도록 상수로 선언해 준다. 이 때 클래스 내에서 선언하면 네이밍이 겹칠 수 이쏙, 불필요하게 상수가 많아지는 단점이 있다.
이를 해결하기 위해 상수를 선언하는 클래스를 분리하고, 서로 관련 있는 상수값들은 모아서 enum으로 구현한다.
우아한형제들 기술블로그에 이동욱 개발자님이 업로드하신 Enum클래스를 보고 그것을 사용하기로 했다. 이를 이용하면, 싱글톤 형태로 어플리케이션 전체에 사용할 수 있다.
그래서 필요한 Http statusCode를 열거형으로 묶으려 했는데, 직접 만들 필요 없이 Enum으로 선언된 http 응답코드 클래스가 있었다.
자바 표준 라이브러리에서는 http 응답코드를 위한 Enum클래스를 제공하지는 않지만, Apache HttpClient라이브러리의 HttpStatus 클래스에서 HTTP 응답 코드에 대한 상수들을 정의할 수 있다.
REST_API_KEY의 경우, 우선 상수로 그냥 선언했다.
리펙토링이 끝나면 다음 할일에 아래 목록을 적어놓았다.
API 키와 같은 구성 정보는 외부 설정 파일에 저장하여 관리할 수도 있다는 것을 보았다. properties파일을 사용해 API키를 저장하고 어플 실행시 해당 파일을 읽어와 가져오는 것으로 먼저 해 보겠다.
나중에 스프링을 사용한다면 의존성 주입 컨테이너를 활용해 값을 주입할 수 있다고 한다.
REST_API_KEY가 노출되지 않도록 보안조치가 필요할 수 있는데, 이 또한 우선 리펙토링을 마치면 고려해보도록 하겠다.
1.3 상세한 예외처리
예외처리 역시 절차지향적 코드로 변경 후 다루도록 하겠다.
추후 수정시 글을 수정하도록 하겠다.
2. 절차지향적 코드로의 변경
위 5개의 피드백을 반영하며, 코드 역시 절차지향적으로 바꾸어 보자.
여기서도 중요한 핵심은 메서드나 클래스를 구분하는 단위가 코드 덩어리의 정리가 아니라, 행위와 책임을 기준으로 분리해야 한다는 것이다.
2.1 클래스 나누기
1. main 메서드가 실행되는 클라이언트 Client
2. 키워드와 검색반경 입력받아주는 클래스 InputManager
3, 4. 카카오 요청보내는 KakaoRequester 인터페이스와 그를 구현한 KakaoRequesterImpl 클래스
5. 카카오 요청보낼 세부내용 중 상수값들이 모인 클래스 KakaoAPIConfig
6. 검색결과를 출력해주고 브라우저 띄우는 클래스 OutputManager
일단 역할을 기준으로 나누어 보았다.
여기서 외부 API를 다루는 클래스는 추상화해서 결합도를 낮춰야 카카오가 아닌 네이버 지도 API를 사용할 때도 그대로 갈 수 있다고 한다. 인터페이스를 Request 이런 이름으로 선언하고 구현내용마다 네이버, 카카오를 각각 구현하면 된다. 아직 네이버를 생각하지 않아 인터페이스명도 카카오 그대로 두었다.
인터페이스를 만든 이유는 클라이언트 입장에서 안의 구현함수가 길어 함수 확인이 힘들기 때문이다. 클라이언트의 입장에서 나누어 보려고 노력하였다.
public class Client {
public static void main(String[] args) {
InputManager inputManager = new InputManager();
KakaoRequester kakaoRequester = new KakaoRequesterImpl();
OutputManager outputManager = new OutputManager();
HashMap<String, String> coordinate = new HashMap<>();
String keyword = inputManager.getUserKeyword();
int radius = inputManager.getRadius();
JSONArray resultJsonArray = kakaoRequester.KeywordSearch(keyword);
// 받아온 JSONArray객체에서 첫 값의 x, y좌표 얻어서 HashMap으로 저장
JSONObject firstJSONResult = resultJsonArray.getJSONObject(0);
coordinate.put("x", firstJSONResult.getString("x"));
coordinate.put("y", firstJSONResult.getString("y"));
resultJsonArray = kakaoRequester.categorySearch(coordinate, radius, MAX_SIZE);
outputManager.printResult(resultJsonArray, keyword, radius);
outputManager.openBrowser();
}
}
다 때려넣었던 main method의 코드가 많이 짧아졌다.
kakaoRequester의 serch method가 키워드검색, 카테고리검색 종류에 상관없이 결과값만을 뽑아 JSONArray로 반환하는 동일한 동작을 하게 하고 싶었다.
x, y를 뽑기 위해 처리하는 세 줄을(주석 있는 부분) 클래스로 만들어야 하나 고민하다 그냥 클라이언트에 적었다. 어디 구조에 들어가야할지 고민이다.
outputManager에서도 브라우저 출력하는 부분은 또 독립적으로 나눠야 했는데, 이에대한 기준도 잘 세우지 못해 그냥 outputManager에 넣었다. 공부하고 다시 고쳐야겠다.
2.2 Scanner.close()로 인한 NoSuchElementException
// 오류 코드
public class InputManager {
public String getUserKeyword(){
System.out.print("위치 키워드를 입력하세요:");
Scanner scanner = new Scanner(System.in);
String userResponse = scanner.nextLine();
scanner.close();
return userResponse;
}
public int getRadius(){
System.out.print("검색 반경을 입력하세요(1000:1km):");
Scanner scanner = new Scanner(System.in);
int radius = scanner.nextInt();
scanner.nextLine();
System.out.println();
scanner.close();
return radius;
}
}
위치 키워드를 입력받고, getRadius로 반경을 입력받으려는 순간 NoSuchElementException이 뜨면서 오류가 났다. 무엇이 문제지? 하고 메시지를 보니 scanner.close() 부분에서 예외가 생겼다.
찾아보니 scanner.close()를 해줘서 오류가 난 것이다! 아니 입출력같은 시스템은 직접 닫아줘야 하는 게 아니었나? 하고 찾아보니 파일이나 네트워크 소켓 등 다른 스트림과는 달리 System.in과 같은 표준 입력 스트림을 사용하는 scanner를 close하면 하위 스트림인 System.in까지 닫아버리고 JVM이 표준 입출력 스트림을 관리하는 방식 때문이다. JVM은 이 스트림들(System.in, System.oup, System.err)을 시작부터 끝까지 유지하고 JVM 종료시에만 이 스트림들을 닫는다.
왜 JVM은 표준 입력 스트림을 라이프사이클 내내 가져가고, 그러면 이를 사용하는 scanner는 왜 객체를 닫을때 스트림까지 닫아버리는가? 뒤에 못 열게? 또 파일이나 네트워크 소켓 등 다른 종류의 스트림에서는 왜 표준 입력 스트림과 다른가?
너무 궁금했지만 시간이 부족해 이는 다른 글에서 작성하도록 하겠다..
chat GPT의 말로는 JVM이 알아서 하니까 닫지 않아도 문제없다고 한다. 이는 나중에 다른 글에서 작성할 때 검증하도록 하자.
chat GPT의 답변
따라서 프로그램에서 여러 번 데이터를 읽어야 하는 경우, 모든 작업이 끝난 후에만 한 번 Scanner.close()를 호출해야 합니다.
그런데 실제로는 System.in과 같은 표준 입출력 스트림은 JVM이 종료될 때까지 자동으로 관리되므로 일반적으로 명시적으로 닫아주지 않아도 됩니다. 따라서 대부분의 경우 Scanner.close() 메서드 호출을 생략하고, 필요한 곳에서 새로운 Scanner 객체를 생성해서 사용해도 문제가 없습니다.
하지만 파일 등의 외부 리소스와 연결된 스트림의 경우는 반드시 사용 후에 닫아주어야 합니다. 이렇게 하지 않으면 리소스 누수가 발생할 수 있습니다.
싱거운 해결법은 scanner.close()는 다 쓰고 마지막에 한 번만 해주면 된다.
마무리
이미 다 만든걸 나누는 작업인데도 생각보다 시간이 꽤 걸렸다. 멘토님이 왜 이미 있는걸 고치는게 더 힘든 작업이고, 배우는게 많다고 하셨는지 알게 되었다.
결과물
https://github.com/pabu-lim/FC_Java_Assignment1_findPharmacy
GitHub - pabu-lim/FC_Java_Assignment1_findPharmacy
Contribute to pabu-lim/FC_Java_Assignment1_findPharmacy development by creating an account on GitHub.
github.com
남은 목표
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. 해당 내용들을 통해 이래서 객체지향을 하는구나, 이래서 전략패턴을 스는것이 좋구나 이해한 것 정리
참고
https://techblog.woowahan.com/2527/
Java Enum 활용기 | 우아한형제들 기술블로그
{{item.name}} 안녕하세요? 우아한 형제들에서 결제/정산 시스템을 개발하고 있는 이동욱입니다. 이번 사내 블로그 포스팅 주제로 저는 Java Enum 활용 경험을 선택하였습니다. 이전에 개인 블로그에 E
techblog.woowahan.com