본문 바로가기
Experience

특수 사례 패턴 (외부 API를 대하는 방법)

by I move forward every day. 2022. 12. 19.

출처: Pixabay로부터 입수된 Pexels님의 이미지 입니다.

클린코드라는 책에서 외부 라이브러리를 대하는 방법에 대해 알려주었다. 마침 진행하는 프로젝트에서 검색엔진이라는 외부 API를 사용하였고 해당 API를 사용할 때 특수 사례 패턴을 사용하여 코드를 작성하였다. 분명 부족한 점들이 많겠지만 이번 글을 통해 어떤 방식으로 적용하였고 어떤 이점들을 얻었는지 정리해보려 한다.

특수 사례 패턴 간단 설명
클래스를 만들거나 객체를 조작해 특수 사례를 처리하는 방식이다. 클래스나 객체가 예외적인 상황을 캡슐화해서 처리하므로 클라이언트 코드가 예외적인 상황을 처리할 필요가 없어진다.

 

특수 사례 패턴 적용 전 문제점들


@GetMapping("/list")
public ModelAndView ExampleController(
    HttpServletRequest request,
    SearchVO searchVo
){
        SearchEnginRequestDto searchEnginRequestDto = new SearchEnginRequestDto();
        searchEnginRequestDto.setSearchSort(searchVo.getSearchSort());
        searchEnginRequestDto.setPage(searchVo.getPageNum());
        searchEnginRequestDto.setNget(searchVo.getNget());
        
        // 검색엔진 호출의 결과값을 받는다.
        // 호출하는 곳에서 매번 예외처리를 해줘야한다.
        Map<String, Object> map = new HashMap<>();
        try{
            map = new SearchEngin().sendRequest(searchEnginRequestDto);
        }catch(IRException e){
        // Exception 처리
            ........
        }
        
        // 받은 결과값들을 꺼내준다. 
        // 매번 타입캐스팅을 해야한다.
        HashMap resultSet = (HashMap) map.get("resultSet");
        HashMap result = new HashMap();
        List<Map> resultList = new ArrayList<>();

        if(!((ArrayList) resultSet.get("result")).isEmpty()){
            result = (HashMap) ((ArrayList) resultSet.get("result")).get(0);
        }
        if(result.get("resultDocuments") != null){
        	resultList  = (List)result.get("resultDocuments");
        }
        
        // 결과물을 model에 담아 프론트로 보내준다.
        ModelAndView model = new ModelAndView();
        // .............. 생략
        return model;
}
  • Exception 처리를 호출하는 곳에서 항상 처리해 줘야 한다.
  • 사용자가 올바른 유형으로 타입 캐스팅 할 책임을 져야 한다
  • 호출하는 곳에서 책임지고 매번 null값을 체크하고 처리해야 한다.
  • 위의 모든 문제들은 호출하는 곳에서 매번 반복되는 문제를 해결해야 한다. 또한 예외처리, 키값 입력, 타입 케스팅, null값 체크는 코드의 길이는 늘리고 가독성은 낮춰 한번에 읽어 내려가기 어렵게 한다.

분명 생각지 못한 많은 문제들이 더 존재하리라 생각된다. 발견된 문제점들을 특수사례 패턴 및 리펙토링을 통해 하나씩 해결해 나가 보자.

 

문제 해결 과정


첫 번째: 예외처리를 한 클래스에 모아서 처리할 수 있다.

// 감싸기 클래스 내부 코드의 일부(api 호출 메서드 생략)
@Slf4j
public class SearchEngin{

    public SearchEngin(){};
    
    // Exception을 한곳에서 공통적으로 처리
    public SearchResponseDto sendRequest(searchEnginRequestDto searchEnginRequestDto){
        try{
            return this.callSearchEngin(searchEnginRequestDto);
        }catch(IRException e){
            log.info("SearchEnginException: " + e);
            return new SearchResponseDto();
        }catch(.... e){
            ................
        }
    }
}

특수 사례 패턴 이전에는 검색엔진을 호출하는 곳마다 예외처리를 위해 try, catch 문을 반복 작성해야 했다. 그러나 패턴 적용 이후에는 모든 예외처리를 한 곳에서 처리할 수 있게 되었으며 오류 발생시 한곳에서 처리할 수 있게 되었다.

예외 발생 시에 new SearchResponseDto() 객체를 리턴해준 이유는 SerchResponseDto에 getter(조회함수)를 구현해 두었고 null값을 처리할 수 있도록  해두었기 때문이다.

 


두 번째: 타입 캐스팅 DTO를 이용해 처리한다.

// DTO 적용 전
Map<String, Object> map = new SearchEngin().sendRequest(searchEnginRequestDto);
HashMap resultSet = (HashMap) map.get("resultSet");

HashMap result = new HashMap();
List<Map> resultList = new ArrayList<>();

if(!((ArrayList) resultSet.get("result")).isEmpty()){
    result = (HashMap) ((ArrayList) resultSet.get("result")).get(0);
    resultList  = (List)result.get("resultDocuments");
}
// DTO 적용 후
SearchResultResponseDto item = new SearchEngin()
            .sendRequest(searchEnginRequestDto)
            .getFirstResult();
List<HashMap> itemList = item.getResultDocuments();

검색엔진을 통해 가져온 데이터를 HashMap이 아닌 직접 만든 DTO에 초기화하여 사용 시 클라이언트가 호출한 데이터를 매번 올바른 유형으로 타입 캐스팅 할 책임을 지지 않아도 된다.

 


세 번째: 호출하는 곳에서 null 값을 체크해야 한다.

// SearchEnginResponse 적용 전
Map<String, Object> map = new SearchEngin().sendRequest(searchEnginRequestDto);
HashMap resultSet = (HashMap) map.get("resultSet");

HashMap result = new HashMap();
List<Map> resultList = new ArrayList<>();

if(!((ArrayList) resultSet.get("result")).isEmpty()){
    result = (HashMap) ((ArrayList) resultSet.get("result")).get(0);
    resultList  = (List)result.get("resultDocuments");
}
// SearchEnginResponse 적용 후
SearchResultResponseDto item = new SearchEngin()
            .sendRequest(searchEnginRequestDto)
            .getFirstResult();
List<HashMap> itemList = item.getResultDocuments();

검색엔진은 외부 API다. API 명세서대로 값이 날아오지 않을 가능성, 아무런 값이 들어있지 않을 가능성, 예외를 발생시킬 가능성이 존재한다. 그렇기에 API를 호출해 사용하는 곳에서는 매번 발생될 수 있는 예외에 대한 대비책을 마련해두어야 한다.

 

프로젝트에서 사용한 검색엔진은 데이터가 5 depth까지 들어간다. 그렇기에 응답값으로 받은 데이터중 어느 곳에 무슨 데이터가 없을지 전혀 예측할 수 없다. 위에서 말했듯이 API 명세서대로 값이 날아오지 않을 가능성이 있기 때문이다. 그래서 해당 데이터들을 안전하게 꺼내 사용할 수 있게 몇 가지 조회 함수들을 만들어 해당 함수들로만 리턴 받은 데이터를 조회할 수 있게 만들었고, 만약 값이 존재하지 않을 경우 null값이 아닌 default값을 만들어 리턴해주도록 만들었다.

 

덕분에 호출하는 곳에서 반복적으로 null 값을 체크하지 않아도 된다.

 


네 번째: 특수 사례 처리를 위해 만든 클래스를 사용해 테스트를 편리하게 한다.

API가 개발 또는 수정단계에 있어 사용할 수 없거나 API 명세서대로 응답값을 내려주기 전이라면 해당 API를 이용해 테스트를 하기가 곤란할 것이다. 이때에는 특수 사례 처리를 위해 만든 클래스에 가짜 데이터를 주입하여 API가 개발단계인 실제 API를 호출하는 것처럼 우리 프로그램을 개발하고 테스트할 수 있다.

 


다섯 번째: 중복 제거 및 유지보수력 향상

위의 모든 작업들이 특수 사례 패턴 적용 전에는 일일이 호출하는 곳에서 처리해야 하는 작업들이었다. 그러나 특수 사례 패턴 이용 시 한 두 곳에서 모든 과정을 처리할 수 있기 때문에 중복되는 작업들이 줄어들었다.

 

또한 한 두곳에서 모든 일을 처리할 수 있기에 수정사항 또는 에러 발생 시 쉽게 처리할 수 있다.

 

특수 사례 패턴을 통해 얻은 이점들


  • 예외(Exception) 처리를 한 클래스에 모아서 처리할 수 있다.
  • 타입 캐스팅이 필요할 시 DTO를 만들어 사용하는 방식으로 dto 클래스에서 처리할 수 있다.
  • 호출하는 곳에서 매번 null값을 체크하지 않아도 된다.
  • 특수 사례를 처리하기 위해 만들어둔 클래스에 가짜 데이터를 주입하여 실제 API를 호출한 것처럼 테스트를 할 수 있다.
  • 위의 작업들이 패턴을 적용하기 전에는 호출하는 곳에서 모두 처리해야 하는 반복작업이었지만 호출하는 곳에서 한 번에 처리할 수 있다. 코드량을 낮추고 가독성을 향상시킨다.

 

마무리


특수 사례 패턴을 활용함과 약간의 리펙토링을 적용해 보며 배운 것을 적용할 수 있어 기뻤다. 그리고 동시에 스스로 많은 부족함을 느꼈다. 뭐가 더 좋은 구조인지, 현재 짠 코드가 잘 만든 것이 맞는지 의문이다. 그러나 지금의 지식으로는 해결되지 않음을 알기에 기본 지식을 깊이 갈고닦아야겠다는 생각을 했다. 기본기가 많이 부족하여 더 좋은 방법과 무슨 문제가 더 있는지 알지 못하고 있다는 생각이 든다.

 

기본을 아주 깊게 공부해야 할 필요성을 알게 만들어준 고마운 경험이었다.