개요
첫 번째 과제에 대한 리팩토링을 하기도 전에 두 번째 과제가 올라왔다. 이번에는 카카오 검색 API를 이용해 책을 검색하고, 해당 데이터를 DB에 저장하는 것이다. 첫 번째 과제에서 시도했던 로직들 중 JSON 형태로 값을 받아오는 건 동일하게 진행하면 될 듯싶었다. 지난 과제에서 아쉬웠던 부분들을 보완해서 조금 더 객체지향적인 코드로 만들기 위해 노력하기로 다짐했다.
과제 요구사항
크게 아래와 같이 5가지의 요구사항이 있다.
1. 사용자로부터 책 검색어를 입력받는다.
2. 카카오 책 검색 API를 이용해 입력받은 검색어를 토대로 최대 10건의 결과를 받는다.
3. 받은 결과를 콘솔창에 정해진 포멧으로 출력한다.
4. DB에 저장할 것인지 여부를 Y/N 로 입력받는다.
5. 사용자의 선택에 따라 DB에 저장 여부를 결정하고, 현재 저장된 전체 DB 튜플들을 title 오름차순으로 정렬해 출력한다.
사용한 API
- Kakao Book 검색 API : Kakao에게 검색 요청을 하기 위한 API
- JSON 외부 API : 카카오에서 결괏값을 JSON 형태로 받기 위한 API
- HTTPURLConnection : Kakao 서버에 접속하기 위한 API
- MySQL Connector : MySQL DBMS와 연결하기 위한 API
진행사항
1. 먼저 책을 검색하는 유저 인터페이스를 클래스를 만들자!
지난번 과제에선 Application 객체에서 로직의 흐름이 이어졌다. 생각해 보면 Application 클래스에 입력받는 것, 출력하는 것, 주요 흐름의 로직이 있는 것이 좀 어색하게 느껴졌다. 보다 더 적합한 클래스가 없을까 생각한 게 BookListSearchUserInterface 클래스다. 클래스 이름에서 나타나 듯 '유저가 책 검색을 위해 사용되는 최전방'이랄까나? Application의 main 메소드에서 해당 클래스의 객체를 만들고, start()를 호출해 비로소 프로그램이 실행된다.
2. 외부 API를 이용할 땐 '인터페이스'란 완충지대를 만들자!
첫 번째 과제에서 카카오 지도 맵 API를 사용했을 때, "만약 카카오가 아닌 구글 또는 네이버 맵 API로 바꾼다면 내 코드는 얼마나 확장성이 좋을까?"란 자문을 했었다. 당시 내 대답은 "No"였다. 왜냐하면 카카오 맵 API와 연결하는 클래스와 직접 연결이 되어 있어 수정에 매우 취약했기 때문이다.

private void searchMyLocations(Scanner scan) {
myLocation = kakaoSearchAPI.searchMyLocation(scan);
}
이러한 문제를 해결하고자 인터페이스를 구현했다. 해당 인터페이스에 선언된 메소드만 이용하면, 구현 클래스에서 실제 어떻게 오버라이딩했는지 알 필요 없이 사용할 수 있다. 따라서 내가 선언한 인터페이스만 구현한다면 구글이든지, 네이버든지, 카카오든지 코드에서 변경될 사항이 없을 것이다.
이를 위해 두 가지 인터페이스를 만들었다. 하나는 DBMS의 변경을 위해 BookRepository 인터페이스를, 나머지 하나는 검색 API를 위한 BookSearchApiClient 인터페이스이다.
public interface BookRepository {
void getConnection();
Books getAllBooks();
void saveBooks(Books books);
void closesResource();
}
public interface BookSearchApiClient {
Books getBooks(String keyword);
}
두 인터페이스는 자신들이 필요한 추상 메소드를 선언해 놨다. 따라서 호출하는 입장에선 어떤 구현 객체인지 상관없이 해당 메소드만 이용하면 원하는 기능을 사용할 수 있다. 나는 MySQL과 연결하는 MySQLBookRepository 클래스와 카카오 서비스를 이용하는 KakaobookSearchApiClient 클래스를 만들어 두 인터페이스를 구현하도록 했다.
3. MySQL에서 한글이 입력 안된다..?
나는 내 로컬 PC에 MySQL Server를 설치하고 싶지 않았다. 따라서 MySQL 도커 이미지를 받아 컨테이너를 만들어 실행했다. 하지만 문제가 발생했다. 테이블을 만들고 데이터를 입력하면 '?' 이렇게 출력되는 것이다. 구글링을 해보니 MySQL의 일정 버전 이전에는 한글 인코딩 설정이 안 돼있어 깨진다고 한다. 이 문제를 해결하느라 시간이 꽤 오래 걸렸다. my.cnf 파일을 만들어 도커의 etc/mysql/conf.d 디렉터리에 넣으라는데, 현재 상황은 MySQL이 도커 위에 있기 때문에 vi 또는 vim과 같은 문서 편집기를 이용할 수 없었다.
그래서 내 컴퓨터에 my.cnf 파일을 생성한 다음, 명령어로 도커에 해당 파일을 복사해 넣었다. 고민 끝에 한글이 깔끔하게 잘 나와서 기뻤다.

4. DTO의 멤버 변수를 객체로 만들자
첫 번째 과제에서 받았던 피드백 중 하나가, DTO 객체의 멤버 변수를 원시 타입이 아닌 참조 타입으로 하라는 것이었다. 예를 들어 Book 클래스의 멤버 변수론 title, price, Author 등이 있을 수 있다. 일반적으론 int형 또는 String 타입(원시 타입은 아니지만)으로 선언하는데, 그러지 말고 객체화시키라는 말이다. 처음에는 그렇게까지 해야 하나 싶었지만, 여러 장점을 발견할 수 있었다. 일단 메소드의 매개 변수와 반환 타입에 클래스명이 들어가니 어떤 값들이 오고 가는지 한눈에 파악하기 쉽다. 또 각 멤버 변수에 값이 할당될 때 객체화 되어 있으면 생성자를 이용해서 들어온 값을 검증할 수 있다. 그렇지 않고 마냥 int 또는 String 타입이었다면, if 문을 덕지덕지 붙여야 할 것이다.

5. 사용자로부터 입력 값을 받는 것도 객체화 해보자!
지금까지 사용자로부터 입력 값을 받을 때 단순히 메소드 내부에 구현했다. 하지만 위에서 언급했던 것처럼 멤버 변수 타입들 하나하나 클래스로 만드는 데, 사용자에게 입력 값을 받는 것도 따로 분리하면 어떨까 생각이 들었다. 잘 분리만 한다면 역할과 책임을 나누라는 객체지향의 원칙에도 잘 맞고, 무엇보다 해당 클래스 내부에서 입력된 값에 대한 검증 또는 처리를 할 수 있게 되기 때문이다. 특히 나는 이 점이 너무 마음에 들었다. 첫 과제에서 사용자 입력 값을 받는 데 본능적으로 분리가 필요하다고 느꼈기 때문이다. 하지만 구체적인 방법이 떠오르지 않았는데, 이번에 아이디어가 생겨 시도해 봤다.
SearchKeyword 클래스는 사용자가 검색할 키워드를 입력받는 책임을 가진다.
@Getter
public class SearchKeyword {
private String keyword;
public SearchKeyword(String input) {
if (Objects.equals("", input)) {
throw new InputMismatchException("공백은 입력할 수 없습니다. 다시 입력하시오.");
}
keyword = input;
}
}
멤버 변수론 String 타입의 keyword가 있으며, 생성자에서 공백에 대한 예외처리를 한다.
다음으로 WantSaving 클래스가 있다. 이것은 사용자에게 검색된 결과를 DB에 저장할 것인지 묻는 책임을 가진다.
@Getter
public class WantSaving {
private boolean answer;
public WantSaving(String input) throws InputMismatchException {
if (!("Y".equalsIgnoreCase(input.trim()) || "N".equalsIgnoreCase(input.trim()))) {
throw new InputMismatchException("Y 또는 N 입력하시오.");
}
if ("Y".equalsIgnoreCase(input.trim())) {
answer = true;
}
if ("N".equalsIgnoreCase(input.trim())) {
answer = false;
}
}
}
멤버 변수로 boolean 타입의 answer을 가지고 있으며, 생성자에서 Y 또는 N에 따라 다른 값이 할당된다.
두 클래스 모두 잘못된 입력값이 들어오면 예외를 발생시킨다. 그리고 해당 메소드를 호출한 메소드에게 예외를 떠넘긴다. 호출한 외부 메소드는 예외가 발생하면 미리 지정한 메시지를 출력시키고, continue 키워드를 통해 조건을 만족시키는 입력값을 받을 때까지 반복문을 돌게 만든다.
내 생각에는 역할과 책임을 분리시켰기 때문에 유지보수에 더 좋은 코드가 되지 않았나 생각해 본다.

6. 느낀 점
재미있었다..! 당연히 부족한 점이 많지만, 그래도 지난 과제를 하며 느꼈던 답답함이 줄어든 것 같다. 역할과 책임을 분리함으로써 유지 보수에 좋은 코드가 무엇인지 고민할 수 있었던 것도 의미 있었던 시간이었다. 과거 인터페이스를 사이에 두어서 확장성을 향상한다는 이야기를 많이 들었었는데, 확 와닿지 않았다. 하지만 이번 과제를 하면서 그 말의 진의를 깨달을 수 있었던 것도 큰 소득이다.
JDBC가 어떤 식으로 동작하는지 공부한 것도 좋았다. 현재 현업에선 사용하고 있지 않지만, MyBatis나 JPA나 모두 JDBC를 기반으로 삼고 있다는 말이 기억에 남는다. JDBC의 불편함을 직접 느끼면서 앞으로 사용할 기술들이 왜 등장했는지 알아보는 것도 기대가 된다.
자바 콘솔 프로그램을 만들다 보면, 점차 욕심이 생긴다. 더 좋은 UI(콘솔이지만..), 더 좋은 성능, 유지 보수에 더 좋은 코드가 무엇인지 고민하게 된다. 그런 의미에서 나는 이번 과제를 즐겼던 것 같다. 그리고 확실히 과거에 비해 성장한 내 모습을 볼 수 있어서 뿌듯했던 시간이었다.
'후기 > 프로젝트' 카테고리의 다른 글
| 패스트캠퍼스X야놀자 부트캠프: 토이 프로젝트 1 (여행 여정을 기록과 관리하는 SNS 서비스 1단계) 후기 (0) | 2023.09.14 |
|---|---|
| 패스트캠퍼스X야놀자 부트캠프: Java 심화과제 1 (위치기반 장소 검색 Java 애플리케이션 개발) 후기 (0) | 2023.08.26 |