728x90
안녕하세요.
아주아주아주 오랜만에 생성형 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")
}
백엔드 코드의 주요 문제점은 다음과 같습니다:
- @JavascriptInterface 어노테이션으로 인해 모든 메서드가 외부에 노출됨
- 직렬화/역직렬화 로직이 비즈니스 로직과 뒤섞여 있음
- 공통된 에러 핸들러가 없어, 에러 처리 로직이 복잡하고 반복적임
- 라우팅 로직이 없어 각 함수가 독립적으로 존재하며, URL 기반의 접근이 불가능함
(때문에, 프론트 엔드측에서는 함수명을 일일히 알고 있어야 함.) - 컨트롤러 개념이 없어 관련 기능들을 그룹화하기 어려움
프론트엔드 측면:
레거시 코드
const FUNCTION_NAME = {
setScreenOff: "setScreenOff",
setScreenOn: "setScreenOn",
powerOff: "powerOff",
reboot: "reboot",
setReactVersion: "setReactVersion",
viewSettingDialog: "viewSettingDialog",
screenLock: "screenLock",
screenUnLock: "screenUnLock",
powerSavingMode: "powerSavingMode",
//... 위와 같이 네이티브 코드의 모든 @JavascriptInterface 함수명을 정확히 알고 있어야 함.
- 모든 함수 호출에 대해 인터페이스 이름과 메서드 이름을 문자열로 지정해야 함.
- RESTful API 스타일의 직관적인 인터페이스가 아니라 함수 호출 방식을 사용함.
BridgeApi의 구상 및 목적
위와 같은 문제점들을 해결하기 위해 Bridge API 라이브러리를 구상하게 되었습니다. 이 라이브러리의 주요 목표는 다음과 같습니다:
- 안드로이드 웹 뷰에서 JSBridge를 통해 네이티브 코틀린 코드를 호출 시 웹 개발자에게 익숙한 REST API 스타일의 인터페이스 제공.
- 자동화된 직렬화와 역직렬화.
- 이를 위한 라우팅 시스템 제공
- 컨트롤러 기반의 구조화된 코드 작성 지원
리팩터링된 코드 미리 보기
백엔드 측 코드:
// 컨트롤러 정의
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
728x90