[Android] 라이브러리 버전 업데이트의 중요성

Daemon·2025년 11월 30일

Android

목록 보기
7/13
post-thumbnail

들어가며

소셜 로그인을 왜 사용하는 것일까? 카카오톡 혹은 네이버 버튼 하나로 가입이 끝나는 세상에서 이메일과 비밀번호를 입력하라고 하면 이탈률이 치솟는다. 그래서 대부분의 앱은 카카오, 네이버, 구글 로그인을 기본으로 탑재한다.

문제는 이 SDK들이 가만히 있지 않는다는 것이다.

버전 카탈로그에서 버전 숫자를 올려놓고 Gradle Sync를 돌리니 네이버 SDK에서 Deprecated 경고가 우수수 쏟아졌다. import에서 빨간 글씨로 바뀐 것을 보아하니 클래스 이름부터 콜백 인터페이스까지 포함해서 NaverIdLoginSDK가 NidOAuth로 바뀐 것 같았다.

이 글에서는 네이버 로그인 SDK 5.9.0에서 5.11.0으로 마이그레이션하면서 겪은 변경사항들을 정리한다. 단순히 "이렇게 바뀌었다"에서 끝나지 않고, 왜 네이버가 이런 방향으로 SDK를 개편했는지도 추측해본다.


1. 무엇이 바뀌었나

1.1 version catalog

안드로이드 프로젝트에서 보통은 Gradle Version Catalog 방식으로 의존성을 관리하고 있을 것이다. libs.versions.toml 파일 하나에 모든 버전이 모여있는 구조다.

# Before
kakao-user = { group = "com.kakao.sdk", name = "v2-user", version = "2.21.7" }
naver-oauth = { group = "com.navercorp.nid", name = "oauth", version = "5.9.0" }

# After
kakao-user = { group = "com.kakao.sdk", name = "v2-user", version = "2.22.0" }
naver-oauth = { group = "com.navercorp.nid", name = "oauth", version = "5.11.0" }

참고로 네이버 SDK는 JDK 버전에 따라 artifact가 다르다:

implementation("com.navercorp.nid:oauth:5.11.0")       // JDK 17
implementation("com.navercorp.nid:oauth-jdk8:5.11.0")  // JDK 8

1.2 패키지 구조 변화

빌드를 돌리면 빨간 줄이 가득했었던 이전 코드이다.

// Before - 더 이상 존재하지 않는 클래스들
import com.navercorp.nid.NaverIdLoginSDK
import com.navercorp.nid.oauth.NidOAuthLogin
import com.navercorp.nid.oauth.OAuthLoginCallback
import com.navercorp.nid.profile.NidProfileCallback
import com.navercorp.nid.profile.data.NidProfileResponse

// After - 새로운 구조
import com.navercorp.nid.NidOAuth
import com.navercorp.nid.oauth.util.NidOAuthCallback
import com.navercorp.nid.profile.domain.vo.NidProfile
import com.navercorp.nid.profile.util.NidProfileCallback

네이버 공식 문서에서는 이 변경에 대해 명확하게 언급하고 있다:

참고
SDK v5.11.0부터 NaverIdLoginSDK, NidOAuthLogin 클래스에서 제공하는 API를 통합해 제공합니다.

왜 이렇게 바꿨을까?

예전 SDK는 역할별로 클래스가 분리되어 있었다:

  • NaverIdLoginSDK: 초기화, 인증, 토큰 관리 담당
  • NidOAuthLogin: 프로필 조회 담당

추측 1: Facade 패턴

새 SDK는 NidOAuth 하나로 모든 기능을 제공한다. 공식 문서의 API 목록을 보면 이게 명확하다:

NidOAuth 클래스의 메서드:

  • getAccessToken()
  • getRefreshToken()
  • getState()
  • initialize()
  • requestLogin()
  • logout()
  • disconnect()
  • getUserProfile()
  • getUserProfileMap()

초기화부터 로그인, 프로필 조회, 로그아웃까지 전부 NidOAuth 하나에서 처리한다. 전형적인 Facade 패턴이다.

개발자 입장에서는 "네이버 로그인 관련 기능은 NidOAuth만 알면 된다"라는 명확한 Mental Model이 생긴다.

Mental Model은 대학교 사용자 인터페이스 수업에서 배웠던 용어인데, UX(사용자 경험) 혹은 DX(개발자 경험)를 높이고 싶으면 자연스레 알게 되는 개념이다.

추측 2: Clean Architecture 영향

새로운 패키지 구조를 보면 흥미롭다:

com.navercorp.nid.profile.domain.vo.NidProfile    // domain 레이어
com.navercorp.nid.profile.util.NidProfileCallback // util 레이어
com.navercorp.nid.oauth.util.NidOAuthCallback     // util 레이어

domain, util 같은 레이어 구분이 보인다. Clean Architecture나 레이어드 아키텍처의 영향을 받은 것 같다.


2. SDK 초기화

Application 클래스에서 SDK를 초기화하는 부분이다.

// Before
NaverIdLoginSDK.initialize(context, clientId, clientSecret, "앱이름")

// After
NidOAuth.initialize(context, clientId, clientSecret, "앱이름", callback)

클래스 이름만 바뀐 게 아니다. 초기화 완료 콜백이 추가됐다.

공식 문서에 따르면:

SDK 초기화
NidOAuth.initialize(context, {OAUTH_CLIENT_ID}, {OAUTH_CLIENT_SECRET}, {OAUTH_CLIENT_NAME}, callback)

  • callback: SDK 초기화 완료 및 실패에 대한 콜백

NidOAuthInitializingCallback이라는 새로운 인터페이스가 생겼다:

val callback = object : NidOAuthInitializingCallback {
    override fun onSuccess() {
        Log.d(TAG, "SDK 초기화 성공")
    }

    override fun onFailure(e: Exception) {
        Log.e(TAG, "SDK 초기화 실패", e)
    }
}

NidOAuth.initialize(context, clientId, clientSecret, "앱이름", callback)

왜 초기화 콜백이 생겼을까?

추측: 비동기 초기화 지원

공식 문서의 NidOAuthLoginState를 보면 힌트가 있다:

  • NEED_INIT: 초기화가 필요한 상태. 해당 상태에서는 requestLogin / repromptPermissions 요청이 불가합니다.
  • OAUTH_DATA_INITIALIZING: NidOAuth 필수 데이터 초기화가 진행 중인 상태. 해당 상태에서는 requestLogin / repromptPermissions 요청이 불가합니다.

SDK가 내부적으로 DataStore나 네트워크 호출 같은 비동기 작업을 수행하는 것 같다. 예전에는 이걸 동기로 처리했다가, 앱 시작 시간에 영향을 줬을 수 있다.

Callback을 통해 초기화 완료 시점을 명확히 알 수 있게 되면서, 초기화가 끝나기 전에 로그인을 시도하는 Race Condition도 방지할 수 있다.


3. 로그인 요청

여기서부터 본격적인 변화가 시작된다.

3.1 Activity 강제 캐스팅 X

예전 SDK의 단점이라고 할 수 있는 부분은 Activity 컨텍스트를 강제했다는 거다.

// Before - Activity 캐스팅 필수
fun loginWithNaver(context: Context, onResult: (Result<String>) -> Unit) {
    val activity = context as? Activity
    if (activity == null) {
        onResult(Result.failure(IllegalStateException("Activity context required")))
        return
    }

    NaverIdLoginSDK.authenticate(activity, object : OAuthLoginCallback {
        override fun onSuccess() {
            val token = NaverIdLoginSDK.getAccessToken()
            onResult(Result.success(token ?: ""))
        }

        override fun onFailure(httpStatus: Int, message: String) {
            onResult(Result.failure(RuntimeException("Failed: $message")))
        }

        override fun onError(errorCode: Int, message: String) {
            onFailure(errorCode, message)
        }
    })
}

// After - 그냥 Context면 된다
fun loginWithNaver(context: Context, onResult: (Result<String>) -> Unit) {
    NidOAuth.requestLogin(context, object : NidOAuthCallback {
        override fun onSuccess() {
            val token = NidOAuth.getAccessToken()
            if (token.isNullOrEmpty()) {
                onResult(Result.failure(IllegalStateException("Token empty")))
            } else {
                onResult(Result.success(token))
            }
        }

        override fun onFailure(errorCode: String, errorDesc: String) {
            onResult(Result.failure(RuntimeException("Failed($errorCode): $errorDesc")))
        }
    })
}

Activity 캐스팅 코드가 통째로 사라졌다.

왜 Activity 대신 Context를 받게 됐을까?

추측 1: Jetpack Compose 대응

Compose에서는 LocalContext.current로 Context를 가져오는데, 항상 Activity라는 보장이 없기 때문에 네이버 SDK 팀도 변화에 적응하기 위해서 Activity 종속성을 제거한 것 같다.

추측 2: Activity Result API 채택

공식 문서를 보면 ActivityResultLauncher를 사용한 로그인도 지원한다:

private val launcher = registerForActivityResult(
    ActivityResultContracts.StartActivityForResult()
) { result ->
    when (result.resultCode) {
        RESULT_OK -> {
            binding.tvAccessToken.text = NidOAuth.getAccessToken()
        }
        RESULT_CANCELED -> {
            val errorCode = NidOAuth.getLastErrorCode().code
            val errorDescription = NidOAuth.getLastErrorDescription()
        }
    }
}

NidOAuth.requestLogin(context, launcher)

이건 구식 startActivityForResult() 대신 최신 Activity Result API를 지원한다는 의미다. 이 API는 Activity 인스턴스 없이도 동작한다.

3.2 메서드 이름 변경

// Before
NaverIdLoginSDK.authenticate(activity, callback)

// After
NidOAuth.requestLogin(context, callback)

authenticate에서 requestLogin으로 바뀌었다.

왜 이름을 바꿨을까?

추측: 의미의 명확화

authenticate는 "인증하다"라는 의미인데, 사실 이 메서드는 인증만 하는 게 아니다. 공식 문서에 따르면:

requestLogin() 메서드 내부 동작:
1. 먼저 갱신 토큰이 있는지 확인합니다.
2. 갱신 토큰이 있으면 접근 토큰의 갱신을 시도합니다.
3. 갱신에 성공하면 callback.onSuccess()가 호출됩니다.
4. 갱신에 실패하면 로그인 창이 나타납니다.
5. 갱신 토큰이 없으면 로그인 창이 나타납니다.

즉, 이 메서드는 "로그인 요청"이지 단순한 "인증"이 아니다. 토큰이 있으면 갱신하고, 없으면 로그인 창을 띄운다. requestLogin이 더 정확한 이름이다.

작성하는 코드의 양이 많아질수록 네이밍을 어떻게 할지 고민하는 것에 소홀해지는 순간들이 있었는데, 혼자서 작업하는 것이 아니라 협업이나 이렇게 많은 사람들이 사용하는 SDK일수록 그 중요성은 높아지는 것 같다.

3.3 콜백 인터페이스 변경

onError가 사라졌다.

// Before - 콜백이 3개
interface OAuthLoginCallback {
    fun onSuccess()
    fun onFailure(httpStatus: Int, message: String)
    fun onError(errorCode: Int, message: String)
}

// After - 콜백이 2개
interface NidOAuthCallback {
    fun onSuccess()
    fun onFailure(errorCode: String, errorDesc: String)
}

왜 onError를 없앴을까?

추측: 의미 없는 구분의 제거

예전 SDK에서 onFailure와 onError의 차이가 뭐였을까? 대부분의 개발자는 이렇게 구현했다:

override fun onError(errorCode: Int, message: String) {
    onFailure(errorCode, message)  // 그냥 onFailure 호출
}

둘 다 "실패"인데 왜 나눠놨는지 명확하지 않았다. 새로운 SDK는 이런 불필요한 복잡성을 제거했다.

에러 코드 타입이 바뀌었다.

// Before
override fun onFailure(httpStatus: Int, message: String)

// After
override fun onFailure(errorCode: String, errorDesc: String)

httpStatus 파라미터 Int에서 errorCode 파라미터 String으로 변경되었다.

왜 에러 코드를 String 타입으로 바꿨을까?

추측: OAuth 스펙 준수와 확장성

공식 문서의 NidOAuthErrorCode를 보면:

NidOAuthErrorCode 속 Code는 크게 두 가지로 구분됩니다.
1. 'The OAuth 2.0 Authorization Framework' 문서의 '4.1.2.1. Error Response'에서 제시하는 오류 유형
2. SERVERERROR로 시작하는 서버 오류, CLIENTERROR로 시작하는 클라이언트 오류

OAuth 2.0 스펙의 에러 코드는 문자열이다:
invalid_grant, expired_token, consent_required

이걸 숫자로 매핑하면 의미가 사라진다.

또한 ACTIVITY_IS_SINGLE_TASK, WEB_VIEW_IS_DEPRECATED, NO_APP_FOR_AUTHENTICATION 같은 SDK 특유의 에러 코드도 문자열이 더 직관적이다.


4. 사용자 프로필 조회

처음에 언급했던 소셜 로그인의 이점 중 하나인 소셜 계정으로부터의 사용자 정보를 추출해서 굳이 사용자가 자신의 정보를 입력하지 않아도 기본적으로 우리가 제공해줄 수 있다는 점이다.
로그인 후 사용자 정보를 가져오는 부분인데 여기가 가장 많이 바뀌었다.

// Before
fun fetchNaverUserInfo(onResult: (Result<SocialUserInfo>) -> Unit) {
    NidOAuthLogin().callProfileApi(object : NidProfileCallback<NidProfileResponse> {
        override fun onSuccess(result: NidProfileResponse) {
            val profile = result.profile
            if (profile != null) {
                val userInfo = SocialUserInfo(
                    name = profile.name,
                    gender = profile.gender?.lowercase(),
                    birthYear = profile.birthYear,
                    email = profile.email
                )
                onResult(Result.success(userInfo))
            } else {
                onResult(Result.failure(IllegalStateException("Profile is null")))
            }
        }

        override fun onFailure(httpStatus: Int, message: String) { ... }
        override fun onError(errorCode: Int, message: String) { ... }
    })
}

// After
fun fetchNaverUserInfo(onResult: (Result<SocialUserInfo>) -> Unit) {
    NidOAuth.getUserProfile(object : NidProfileCallback<NidProfile> {
        override fun onSuccess(result: NidProfile) {
            val profile = result.profile
            val userInfo = SocialUserInfo(
                name = profile.name.ifEmpty { null },
                gender = profile.gender.lowercase().ifEmpty { null },
                birthYear = profile.birthYear.ifEmpty { null },
                email = profile.email.ifEmpty { null }
            )
            onResult(Result.success(userInfo))
        }

        override fun onFailure(errorCode: String, errorDesc: String) { ... }
    })
}

4.1 인스턴스 생성 → 정적 메서드

NidOAuthLogin().callProfileApi()에서 NidOAuth.getUserProfile()로 바뀌었다.

왜 인스턴스 생성을 없앴을까?

추측: 불필요한 객체 생성 제거, API 일관성

예전에는 프로필을 가져올 때마다 NidOAuthLogin() 인스턴스를 새로 만들었다. 이 객체가 상태를 갖고 있었을까? 아마 아닐 것이다.

새 SDK는 모든 API가 NidOAuth.xxx() 형태로 통일되었다:

NidOAuth.initialize()
NidOAuth.requestLogin()
NidOAuth.getAccessToken()
NidOAuth.getUserProfile()
NidOAuth.logout()
NidOAuth.disconnect()

4.2 getUserProfile vs getUserProfileMap

새 SDK는 프로필 조회 메서드가 두 개다:

// 타입 안전한 방식
NidOAuth.getUserProfile(callback: NidProfileCallback<NidProfile>)

// Map 형태로 받는 방식
NidOAuth.getUserProfileMap(callback: NidProfileCallback<NidProfileMap>)

공식 문서에 따르면:

getUserProfileMap(callback)
API 명세에 정의된 값 외의 다른 값도 함께 map 형태로 받을 수 있습니다.

이렇게 되면 네이버 측에서 프로필 API에 새 필드 그러니까 사용자 정보를 더 추가하고 싶을 때, SDK 업데이트 없이도 그 값을 받을 수 있게 하려는 의도다. 사용자의 이름, 성별 이외에도 다른 정보가 추가된다는 것은 어떻게 보면 비즈니스 로직이라고 볼 수 있기 때문에 확장성을 고려했다는 것을 추측해볼 수 있었다.

4.3 빈 문자열 처리

여기서 함정이 있다.

예전 SDK는 값이 없으면 null을 줬다. 새 SDK는 빈 문자열 ""을 준다. 그래서 .ifEmpty { null } 처리를 해줘야 한다.

name = profile.name.ifEmpty { null }

이건 문서에 명시되어 있지 않아서 직접 로그 찍어보고 알았다.


6. 버전 관리의 중요성

프로젝트에 합류했을 때 CTO가 하신 말씀이 있다.

"버전 업데이트는 미리미리 해두는 게 좋아요. 나중에 한꺼번에 하면 위험해요."

솔직히 처음엔 잘 와닿지 않았다. 잘 돌아가는 코드를 왜 굳이 건드려? 괜히 업데이트했다가 버그 생기면 어쩌려고? 그런 생각이었다.

그런데 이번에 깨달았다.

6.1 버전을 방치하면 벌어지는 일

네이버 SDK를 5.9.0에서 5.11.0으로 올리는 건 마이너 버전 2단계 차이다. 별거 아닌 것 같지만 그 사이에 Breaking Change가 숨어있었다. 만약 5.9.0에서 6.x.x로 올려야 했다면? 상상만 해도 아찔하다.

라이브러리 버전을 오래 방치하면 이런 일이 생긴다:

  1. 변경사항이 누적된다: 한 버전씩 올리면 변경 로그 읽고 대응하면 되는데, 5개 버전을 한 번에 올리면 그 사이의 모든 Breaking Change를 한꺼번에 맞닥뜨린다.
  2. 마이그레이션 가이드가 사라진다: SDK 공식 문서는 보통 직전 버전에서의 마이그레이션만 친절하게 설명한다. 3년 전 버전에서 올라오는 사람을 위한 가이드는 없다.
  3. 의존성 지옥에 빠진다: A 라이브러리를 올리려면 B도 올려야 하고, B를 올리면 C가 깨지고... 도미노처럼 무너진다.

6.2 Version Catalog의 진짜 가치

우리 프로젝트는 libs.versions.toml로 버전을 관리하고 있다.

[versions]
agp = "8.13.1"
firebaseBom = "34.6.0"
composeBom = "2025.11.01"
kakao = "2.22.0"
naver = "5.11.0"

버전 정보를 한 곳에서 편하게 관리하려고 쓰는 것이라고만 생각하고 해당 방식을 채택했는데, 이번 사례를 통해서 중요성을 더 느꼈다.

Version Catalog의 진짜 장점은 버전 업데이트의 심리적 장벽을 낮춰준다는 거다.

파일 하나만 열면 프로젝트의 모든 의존성 버전이 한눈에 보인다. "아, 네이버 SDK 새 버전 나왔네" 하고 숫자 하나 바꾸면 된다. 여러 모듈에 흩어진 build.gradle을 뒤질 필요가 없다.

덕분에 "귀찮으니까 나중에 하지 뭐"가 "어차피 한 줄인데 지금 하자"로 바뀐다.

6.3 작은 고통을 자주 vs 큰 고통을 한 번에

버전 업데이트는 운동과 비슷하다.

매일 30분씩 가볍게 운동하면 건강을 유지할 수 있다. 하지만 1년 동안 운동 안 하다가 갑자기 마라톤 뛰려고 하면 몸이 버티질 못한다.

5.9.0 → 5.10.0 → 5.11.0 이렇게 한 단계씩 올렸다면 각 단계에서 변경사항을 차분히 확인하고 대응할 수 있었을 거다. 하지만 방치했다가 한 번에 올리니까 온갖 에러가 한꺼번에 터졌다.


정리하자면

네이버 SDK v5 마이그레이션은 생각보다 손이 많이 갔다. 하지만 결과적으로 코드가 깔끔해졌다:

  • 단일 진입점: NaverIdLoginSDK + NidOAuthLogin → NidOAuth 통합
  • Activity 종속성 제거: Compose 환경에서 훨씬 편해졌다
  • 콜백 단순화: onError와 onFailure 중복이 사라졌다
  • Activity Result API 지원: 최신 안드로이드 권장 방식 채택

변경의 이유를 추측해보면:

  • Facade 패턴 도입: 복잡한 API를 단순하게
  • Compose/최신 안드로이드 대응: Activity 강제를 Context로 완화
  • OAuth 스펙 준수: 에러 코드를 String으로
  • 비동기 초기화: 앱 시작 성능과 안정성 개선

그리고 이번 경험을 통해 배운 것:

  • 버전 업데이트는 미루지 말자: 쌓이면 쌓일수록 고통이 커진다
  • Version Catalog를 적극 활용하자: 버전 관리의 심리적 장벽을 낮춰준다

SDK 버전 업데이트는 귀찮지만 결국 더 나은 방향으로 가고 있다는 증거다. 이번 네이버 SDK 변경도 불편했지만, 막상 적용하고 나니 코드가 더 깔끔해졌다. 네이버 SDK 팀도 나름의 고민 끝에 이런 결정을 내렸을 것이라고 생각한다.

앞으로는 새 버전 나오면 바로바로 체크해봐야겠다. Deprecated 경고가 빨간 줄로 가득 차서 당황하고 싶지는 않다..

0개의 댓글