사이드프로젝트/(240808)이거왜오름?

이거왜오름?api호출 예외 작성

rkrkrr0101 2024. 8. 18. 12:48

내가 생각하는 api호출실패의 경우의수는

 

  • 에러코드가 0이 아닐때 처리
    • 컨텐츠타입이 xml이면 에러
      response.headers.contentType== MediaType.TEXT_XML
      이후 에러메시지와 에러코드 담아서 로깅
    • 이떄 에러코드를 보고 이쪽문제인지 저쪽문제인지 확인해서 나눠서 로깅
  • 결과값의 갯수가 0일때 처리(이쪽이 값을 잘못보냈거나)
  • http코드 예외발생시 처리
    • 400일경우

    • 500일경우

이렇게 크게 3개가 있음

아무래도 에러코드관련은 좀 지엽적인,api하나하나마다 다 다를 가능성이 크니,이것도 최하위 구현클래스쪽에서 구현하고,상위에서는 템플릿메서드로 가져다 쓰는게 좋을거같음

 

ApiFetcher의 apiCall의 예외처리부분을 추상메서드로 뽑아내고

private fun apiCall(
    restTemplate: RestTemplate,
    url: URI,
    isinCode: String,
): Map<String, String> {
    val log = LoggerFactory.getLogger(this.javaClass)
    try {
        val response = fetchApiResponse(restTemplate, url)

        responseErrorCheck(response)
        log.info("성공 법인코드={}", isinCode)

        return extractResponseAsMap(response)
    } catch (e: Exception) {
        log.warn("${javaClass.name}통신에 문제발생 메시지={} 스택트레이스={}", e.message, e.stackTrace)
        return emptyMap()
    }
}
protected abstract fun responseErrorCheck(response: ResponseEntity<String>)

BasicFetch에서는 이걸 구현해주면됨

override fun responseErrorCheck(response: ResponseEntity<String>) {
    checkHttpStatus(response)
    checkBodyType(response)
}
private fun checkBodyType(response: ResponseEntity<String>) {
    if (response.headers.contentType.toString() == "text/xml;charset=UTF-8") {
        val responseBody = response.body ?: ""
        val responseInputStream = responseBody.byteInputStream(Charsets.UTF_8)
        val documentBuilder = DocumentBuilderFactory.newInstance().newDocumentBuilder()
        val document = documentBuilder.parse(responseInputStream)
        document.documentElement.normalize()

        val errorCode = document.getElementsByTagName("returnReasonCode").item(0).textContent
        val errorMsg = document.getElementsByTagName("returnAuthMsg").item(0).textContent

        throw RuntimeException("""${javaClass.name} api 통신예외발생 코드:$errorCode 메세지:$errorMsg""")
    }
}

private fun checkHttpStatus(response: ResponseEntity<String>) {
    if (response.statusCode.value() != 200) {
        throw RuntimeException("${javaClass.name} http상태코드가 200이 아님")
    }
}

checkBodyType는,해당 api가 결과값이 xml로 리턴되면 에러페이지라서 이런식으로 했고,나중에 변경이 일어나도 이 api만 영향을 받고,그중에서도 저 부분만 영향을 받으니 저메서드만 수정하면되게 만듬

 

결과값의 갯수체크는 이쪽보다는 결과값추출메서드쪽에서 하는게 맞는거같아서 그쪽으로 넘겼음

override fun extractResponseAsMap(response: ResponseEntity<String>): Map<String, String> {
    val resMap = HashMap<String, String>()
    val itemNode = getItemNode(response)

    resMap["assetName"] = extractNodeValue(itemNode, "itmsNm")

    return resMap
}

private fun extractNodeValue(
    node: JsonNode,
    key: String,
): String {
    try {
        return node.get(key).asText()
    } catch (e: Exception) {
        throw NoSuchElementException("${javaClass.name}에서 해당하는 노드의 키값이 없음 키:$key ")
    }
}
private fun getItemNode(response: ResponseEntity<String>): JsonNode {
    val om = jacksonObjectMapper()
    val readTree = om.readTree(response.body)
    try {
        return readTree
            .get("response")
            .get("body")
            .get("items")
            .get("item")
            .toList()[0]
    } catch (e: IndexOutOfBoundsException) {
        throw NoSuchElementException("${javaClass.name}가 조회에 실패함 ${response.headers.eTag}")
    }
}

이런식으로 최하단 구현클래스에서 예외가 발생하면,상단의 추상클래스에서

private fun apiCall(
    restTemplate: RestTemplate,
    url: URI,
    isinCode: String,
): Map<String, String> {
    val log = LoggerFactory.getLogger(this.javaClass)
    try {
        val response = fetchApiResponse(restTemplate, url)

        responseErrorCheck(response)//예외체크하는 추상메서드
        log.info("성공 법인코드={}", isinCode)

        return extractResponseAsMap(response)
    } catch (e: Exception) { //예외발생시 처리되는곳
        log.warn("${javaClass.name}통신에 문제발생 메시지={} 스택트레이스={}", e.message, e.stackTrace)
        return emptyMap()
    }
}

catch에서 받아서 로그를 띄우고,빈맵을 던짐

 

이렇게 로그만 띄우고 패스하는 이유는,이 클래스 자체가 외부api통신이라 항상불안정할수 있다는걸 전제로 깔아야하고,추가로 외부api가 고장난다고 동작의 정확성이 좀 떨어질순 있어도 동작이 불가능해 지는게 아닌데,이거때문에

동작 자체가 뻗으면 안되기때문에 이렇게 처리했음

 

리스트로 ApiFetcher를 여러개 받아와서 한번에 돌릴껀데 그중에 하나 터질확률이 좀 되기도 하니까..

 

이렇게 처리하면

AssetFetcher(최상위인터페이스)-ApiFetcher(상위 추상클래스)-BasicFetcher(하위 구현클래스)

최상위 인터페이스는 클라이언트가 이쪽 클래스에게 시킬 일의 메시지(책임)만 표현하고

상위 추상클래스는 이 클래스의 고정적인 처리방식만 표현하고

하위 구현클래스는 이 클래스의 지엽적인 세부사항들을 표현하게되는

이런 구조가 아직도 깨지지 않게됨