본문 바로가기
작디 작은 나만의 라이브러리/BridgeApi

[신입 개발자의 '작디 작고 작디 작고 자그만' 첫 라이브러리 제작기] 0. Bridge-Api를 구상하게 된 계기.

by 시니성 2024. 7. 22.

안녕하세요.
아주아주아주 오랜만에 생성형 AI가 만든 글이 아닌 직접 쓰는 글을 올리게 됐습니다.
이번 주제는 제가 처음으로 만들어본 작은 라이브러리, Bridge-Api의 제작기입니다.
여태까지 시리즈 글을 두 어번 시도해서 늘, 중간에 글쓰기를 멈추었는데, 이번 시리즈 글은 끝까지 적어보려 합니다.

이번 프롤로그에서는

  • 레거시 코드의 문제점 파악,
  • 새로운 라이브러리 구상,
  • 그리고 완성된 라이브러리가 적용된 코드

를 간략히 살펴보겠습니다.

레거시 코드와 그에 따르는 문제점

백엔드 측면:

레거시 코드

@JavascriptInterface
fun paymentCardRequest(cardRequest: String): String = try {
    LogHelper.info("paymentCardRequest : $cardRequest")
    val jsonData: CreditPaymentRequestDto = SerializeUtil.Json.deserialize(cardRequest)
    // 직렬화/역직렬화 코드가 매 함수마다 반복됨

    var retryCount = 0
    Log.e(TAG, "[encodeRequestCardData] $jsonData")

    // 호출부
    val paymentFuture: Future<CreditPaymentResponseDto> = Payment.instance.runPayment(jsonData)

    // 비즈니스 로직 구현부 생략...
} catch (e: PaymentException) {
    PreferenceUtil.setString(context, Constant.PREFERENCE_CARD_TERMINAL_YN, "P")
    LogHelper.info("paymentCardRequest PaymentException error: $e")
    e.printStackTrace()
    ObjectBoxUtil.responseError(ApiResponseCode.from(-1), e.message)
} catch (e: Exception) {
    LogHelper.error("paymentCardRequest error:$e")
    ObjectBoxUtil.responseError(ApiResponseCode.from(-1), "unknown error")
}

백엔드 코드의 주요 문제점은 다음과 같습니다:

  1. @JavascriptInterface 어노테이션으로 인해 모든 메서드가 외부에 노출됨
  2. 직렬화/역직렬화 로직이 비즈니스 로직과 뒤섞여 있음
  3. 공통된 에러 핸들러가 없어, 에러 처리 로직이 복잡하고 반복적임
  4. 라우팅 로직이 없어 각 함수가 독립적으로 존재하며, URL 기반의 접근이 불가능함
    (때문에, 프론트 엔드측에서는 함수명을 일일히 알고 있어야 함.)
  5. 컨트롤러 개념이 없어 관련 기능들을 그룹화하기 어려움

프론트엔드 측면:

레거시 코드

const FUNCTION_NAME = {
  setScreenOff: "setScreenOff",
  setScreenOn: "setScreenOn",
  powerOff: "powerOff",
  reboot: "reboot",
  setReactVersion: "setReactVersion",
  viewSettingDialog: "viewSettingDialog",
  screenLock: "screenLock",
  screenUnLock: "screenUnLock",
  powerSavingMode: "powerSavingMode",
  //... 위와 같이 네이티브 코드의 모든 @JavascriptInterface 함수명을 정확히 알고 있어야 함.
  1. 모든 함수 호출에 대해 인터페이스 이름과 메서드 이름을 문자열로 지정해야 함.
  2. RESTful API 스타일의 직관적인 인터페이스가 아니라 함수 호출 방식을 사용함.

BridgeApi의 구상 및 목적

위와 같은 문제점들을 해결하기 위해 Bridge API 라이브러리를 구상하게 되었습니다. 이 라이브러리의 주요 목표는 다음과 같습니다:

  1. 안드로이드 웹 뷰에서 JSBridge를 통해 네이티브 코틀린 코드를 호출 시 웹 개발자에게 익숙한 REST API 스타일의 인터페이스 제공.
  2. 자동화된 직렬화와 역직렬화.
  3. 이를 위한 라우팅 시스템 제공
  4. 컨트롤러 기반의 구조화된 코드 작성 지원

리팩터링된 코드 미리 보기

백엔드 측 코드:

// 컨트롤러 정의
class UserController {
    @Get("/:id")
    fun getUserById(@PathVariable("id") id: Long): ApiCommonResDto<UserResDto> {
        return ApiCommonResDto(
            status = 0,
            message = "success",
            data = UserResDto(
                id = id,
                name = "John",
                age = 20,
                type = UserType.SELLER,
            ),
        )
    }

    @Post("")
    fun createUser(@JsonBody userReq: UserReqDto): ApiCommonResDto<UserResDto> {
        return ApiCommonResDto(
            status = 0,
            message = "success",
            data = userReq.toDomain(1).toResDto(),
        )
    }
}
//router 생성
    val objectMapper = ObjectMapper().findAndRegisterModules()
        .registerModules(JacksonCustomSerializeModule())
        .configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false)

    val router = BridgeRouter.builder()
        .setSerializer(objectMapper)
        .registerController("api/v1/users", userController)
        .registerController("api/v1/products", productController)
        .build()

//router 사용. 프론트엔드는 하나의 @JavascriptInterface 함수만 알면 됨.
class BridgeApi(private val webView: WebView) {
    @JavascriptInterface
    fun bridgeRequest(promiseId: String, apiCommonRequestJsonString: String) {
        CoroutineScope(Dispatchers.Default).launch {
            try {
                Log.d("BridgeApi", "bridgeRequest: $apiCommonRequestJsonString")
                val result = router.bridgeRequest(apiCommonRequestJsonString)
                Log.d("BridgeApi", "bridgeRequest result: $result")
                webView.resolveAsyncPromise(promiseId, result)
            } catch (e: Exception) {
                Log.e("BridgeApi", "bridgeRequest error: ${e.message}", e)
                webView.rejectAsyncPromise(promiseId, e.serializeToJson())
            }
        }
    }
}

프론트 엔드 측 코드:

customBridgeApi.get<ApiCommonResponse<ErrorData>>('/api/v1/users/test/exception/general')
            .then(res => setResult(JSON.stringify(res, null, 2)))

마치며

다음 글에서는 Bridge-Api 라이브러리의 실제 구현 과정을 자세히 살펴보도록 하겠습니다. 많은 관심 부탁드립니다!

+ 라이브러리 레포지토리 : https://github.com/shiniseong/bridge-api