Developing Myself Everyday
article thumbnail

Retrofit Interface 작성


 이제부터는 API를 사용하기 위한 Retrofit Interface를 작성하겠다. Retrofit에 대한 이해가 없다면 Retrofit에 대해 공부를 하고 다시 돌아오는 것이 좋을 수 있다. 당장 따라서 구현하는 것은 할 수 있겠지만 앞으로 수도없이 사용할 것이기에 Retrofit에 대해 완벽하게 이해를 하는 것이 좋다.

 

 

SMS API

 

api.ncloud-docs.com

 API 통신을 하기 위해서는 위의 문서를 반드시 참고하여야 한다. Retrofit와 Rest API에 대한 지식이 충분하다면 위의 문서만으로도 충분히 구현할 수 있다.

 

 Retrofit을 사용하기 위해서는 가장 먼저 인터페이스를 통해 API 요청을 정의해야 한다. HTTP 메서드(GET, POST, PUT, DELETE)를 지정하고 요청 URL과 요청 매개변수 등을 설정해야 하는 과정이다. 위의 게시글을 가게 되면 어떤 방식으로 요청 URL을 작성해야 하는지 나와있다.

 

 위의 요청 URL을 보게 되면 어떤 방식으로 Interface를 작성해야 하는지 잘 나와있다. 

POST https://sens.apigw.ntruss.com/sms/v2/services/{serviceId}/messages

 위의 줄에서 알 수 있는 것은 이 API 요청은 POST 방식으로 이뤄지며

BaseUrl이 'https://sens.apigw.ntruss.com' 이고 '/sms/v2/services/{serviceId}/messages' 가 경로라는 것을 알 수 있다. 

 

interface NaverSMSApiService {
    @POST("/sms/v2/services/{serviceId}/messages")
    @Headers(
        "accept: application/json",
        "content-type: application/json; charset=utf-8"
    )
    fun sendSms(
        @Path("serviceId") serviceId: String,
        @Body body: SMSRequest
    ): Call<SMSResponse>
}

 위의 인터페이스를 보게 되면 문서에서 알 수 있었던 POST 방식, 경로, Headers, 그리고 요청 바디가 나와 있다. sendSms 함수에서는 serviceid와 요청 바디인 SMSRequest를 넣어 요청을 보내면 응답으로 SMSResponse 형식의 데이터를 받는다는 것을 알 수 있다. (이때 SMSRequest와 SMSResponse는 우리가 위에서 만들었던 데이터클래스이다.) 

 

 이때 위에서 우리가 봤던 URL이랑 인터페이스가 다르다고 생각할 수 있다. 그건 지극히 정상이다. 그 이유는 Header에는 우리가 데이터를 바로 집어넣을 수가 없다. 나중에 API 요청을 할 때 추가해줘야 한다. 

 

@Path("serviceId") serviceId: String 를 넣어주면  @POST("/sms/v2/services/{serviceId}/messages") 에 데이터를 넣어주게 된다고 생각하면 된다.

 

 

 

Retrofit Manager 작성


 사실 꼭 이렇게 Manager를 따로 만들어야 하는 것은 아니다. 아래의 코드를 그대로 복사해서 나중에 API 요청을 할 때 바로 집어넣어도 상관 없다. 다만 이렇게 작성하는 것이 나중에 코드의 재사용을 할 수 있는 좋은 방법이 된다. 각자 어떤 기능을 가지고 있는지 하나 하나 설명해주도록 하겠다.

private val okHttpClient = OkHttpClient.Builder()
        .addInterceptor { chain ->
            val original = chain.request()

            val timestamp = System.currentTimeMillis()
            val signature = getSignature(timestamp)

            // Retrofit 요청 객체에 필요한 헤더를 추가합니다.
            val request = original.newBuilder()
                .header("x-ncp-apigw-timestamp", timestamp.toString())
                .header("x-ncp-iam-access-key", Constants.ACCESS_KEY_ID)
                .header("x-ncp-apigw-signature-v2", signature!!)
                .method(original.method, original.body)
                .build()

            // 요청을 실행하고 결과를 반환합니다.
            chain.proceed(request)
        }
        .addInterceptor(HttpLoggingInterceptor().setLevel(HttpLoggingInterceptor.Level.BODY))
        .connectTimeout(100, TimeUnit.SECONDS)
        .readTimeout(100, TimeUnit.SECONDS)
        .writeTimeout(100, TimeUnit.SECONDS)
        .build()

 위의 코드는 OkHttpClient 객체를 생성해서 요청에 필요한 값을 설정하는 코드이다. 

 2개의 addInterceptor가 존재하는데 첫 번째 인터셉터에서는 전에 Retrofit 인터페이스를 만들었을 때 추가하지 않았던 Header을 추가해주고 있다. 

 두 번째 인터셉터는 네트워크에 로그를 찍기 위해 추가한 것이다. 이 과정을 거치면 네트워크 상에서 일어나는 일도 로그에서 확인할 수 있어서 편리하다.

 

 - val timestamp = System.currentTimeMillis()

 - header("x-ncp-apigw-timestamp", timestamp.toString()) 

위의 코드는 현재 시간을 가져와서 헤더에 추가해주고 있다. 


 - header("x-ncp-iam-access-key", Constants.ACCESS_KEY_ID)

 위의 헤더에는 우리가 이전에 메모했던 인증키의 ACCESS_KEY를 넣어주면 된다.


 - header("x-ncp-apigw-signature-v2", signature!!)

 이제부터는 조금 복잡해 지는 부분이다. 네이버에서는 시그니쳐 키라는 방식을 사용한다. 이 키는 안드로이드 스튜디오에서 따로 설정해서 생성해야만 한다. 아래는 시그니쳐 키를 생성하는 방법이다. 

 이때 주의해줘야 할 것이 있는데 val url = "/sms/v2/services/$SERVICE_ID_NAVER/messages" 의 값과 우리가 Retrofit 인터페이스를 설정했을 때의  @POST("/sms/v2/services/{serviceId}/messages") url이 일치해야 한다는 것이다. 인터페이스의 serviceid값은 API를 호출할 때 넣어주지만 시그니쳐 키를 생성할 때에는 직접적으로 넣어줘야 시그니쳐 키를 정상적으로 생성할 수 있다. 이때 serviceid에 우리가 이전에 메모했었던 서비스 ID를 넣어주면 된다.

 

fun getSignature(timestamp: Long): String? {
        val space = " " // one space
        val newLine = "\n" // new line
        val method = "POST" // method
        val url = "/sms/v2/services/$SERVICE_ID_NAVER/messages"

        val buffer = StringBuffer()
        buffer.append(method)
        buffer.append(space)
        buffer.append(url)
        buffer.append(newLine)
        buffer.append(timestamp)
        buffer.append(newLine)
        buffer.append(ACCESS_KEY_ID)
        println(buffer.toString())

        val key = SECRET_KEY.toByteArray()
        val signingKey = SecretKeySpec(key, "HmacSHA256")

        val bytes = buffer.toString().toByteArray(Charsets.UTF_8)
        val mac = Mac.getInstance("HmacSHA256")
        mac.init(signingKey)
        val digest = mac.doFinal(bytes)
        return android.util.Base64.encodeToString(digest, android.util.Base64.NO_WRAP)
    }

 

 

 

 

private val retrofit = Retrofit.Builder()
        .baseUrl("https://sens.apigw.ntruss.com")
        .addConverterFactory(GsonConverterFactory.create())
        .client(okHttpClient)
        .build()

    private val naverSMSMApiService = retrofit.create(NaverSMSApiService::class.java)

    fun getNaverSMSMApiService(): NaverSMSApiService {
        return naverSMSMApiService
    }

 

 위의 코드는 이제 Retrofit 객체를 생성하기 위한 코드이다. 다음의 과정을 거치게 된다

  • Retrofit.Builder()로 Retrofit 객체를 생성한다.
  • baseUrl 메소드로 API 서버의 기본 URL을 설정한다.
  • addConverterFactory 메소드로 JSON 데이터를 처리하는 GsonConverterFactory 객체를 생성하여 추가한다.
  • client 메소드로 OkHttpClient 객체를 추가한다.
  • API 서비스 인터페이스 생성
  • Retrofit 객체의 create 메소드로 API 서비스 인터페이스를 생성한다.
  • create 메소드는 인터페이스와 해당 인터페이스를 구현하는 객체를 연결한다.
  • NaverSMSApiService::class.java로 NaverSMSApiService 인터페이스를 연결한다.
  • 이렇게 생성된 인터페이스를 이용하여 API 호출 메소드를 호출할 수 있다.
  • API 서비스 인터페이스 반환
  • getNaverSMSMApiService 메소드를 호출하여 생성한 API 서비스 인터페이스를 반환한다.

 

전체 코드

object NaverSMSRetrofitManager {
    private val okHttpClient = OkHttpClient.Builder()
        .addInterceptor { chain ->
            val original = chain.request()

            val timestamp = System.currentTimeMillis()
            val signature = getSignature(timestamp)

            // Retrofit 요청 객체에 필요한 헤더를 추가합니다.
            val request = original.newBuilder()
                .header("x-ncp-apigw-timestamp", timestamp.toString())
                .header("x-ncp-iam-access-key", Constants.ACCESS_KEY_ID)
                .header("x-ncp-apigw-signature-v2", signature!!)
                .method(original.method, original.body)
                .build()

            // 요청을 실행하고 결과를 반환합니다.
            chain.proceed(request)
        }
        .addInterceptor(HttpLoggingInterceptor().setLevel(HttpLoggingInterceptor.Level.BODY))
        .connectTimeout(100, TimeUnit.SECONDS)
        .readTimeout(100, TimeUnit.SECONDS)
        .writeTimeout(100, TimeUnit.SECONDS)
        .build()

    private val retrofit = Retrofit.Builder()
        .baseUrl("https://sens.apigw.ntruss.com")
        .addConverterFactory(GsonConverterFactory.create())
        .client(okHttpClient)
        .build()

    private val naverSMSMApiService = retrofit.create(NaverSMSApiService::class.java)

    fun getNaverSMSMApiService(): NaverSMSApiService {
        return naverSMSMApiService
    }
}

 

 이제 Retrofit 인터페이스와 RetrofitManager 설정이 완료 되었으니 API를 호출하기만 하면 된다. 이제 그 과정을 진행해 보겠다.

 

 

API 호출


 API 호출하는 과정은 우리가 휴대폰 인증을 구현하려고 하는 액티비티나 프래그먼트에서 진행하면 된다. 

 첫 번째로 설정해야 하는 것은 우리가 생성한 NaverSMSRetrofitManager.getNaverSMSMApiService()를 불러와서 Retrofit 객체를 생성하는 것이다.

 그 다음엔 우리가 요청을 할 Body를 채워야 한다. 아래의 함수는 'to' 라는 사용자의 번호에 SMS를 보내는 기능을 구현한다. 그에 맞는 정보들을 Body에 채워주면 된다. 'from'에는 본인이 발신번호로 등록했던 번호를 넣으면 된다. 나는 메시지로 보낼 인증번호를 6자리의 랜덤번호로 설정하기 위해 다음과 같이 설정하였고 content로 전송하게 해 주었다.

 이젠 Call을 설정해서 API를 호출해야 한다. call에는 우리가 이전에 만들었던 Retrofit 인터페이스의 sendSms 메소드가 들어가게 된다.sendSms 메소드에는 서비스 ID와 body가 들어가게 된다.

 다음은 비동기적으로 API 호출 결과를 받아오는데 성공했을 때 호출되는 경우이다.

private fun sendSMS(to: String) {
        val smsApi = NaverSMSRetrofitManager.getNaverSMSMApiService()
        val authenticationNum = (100000..999999).random()
        val smsRequest = SMSRequest(
            type = "SMS",
            contentType = "COMM",
            countryCode = "82",
            from = "yournumber",
            content = "message",
            messages = listOf(
                Message(
                    content = "[MyStorage] 인증번호: $authenticationNum 인증번호를 입력해 주세요.",
                    to = to
                )
            )
        )

        val call = smsApi.sendSms(
            Constants.SERVICE_ID_NAVER,
            smsRequest
        )
        call.enqueue(object : Callback<SMSResponse> {
            override fun onResponse(call: Call<SMSResponse>, response: Response<SMSResponse>) {
                if (response.isSuccessful && response.body()!!.statusName == "success") {
                    try {
                        toast("$to 로 메세지를 성공적으로 발송했습니다.")
                        // 메시지 전송에 성공했을때 작업
                        } catch (e: JSONException) {
                        // 메시지 전송에 실패했을때 작업
                    }
                }
            }

            override fun onFailure(call: Call<SMSResponse>, t: Throwable) {
                // 통신에 실패했을때 작업
                call.cancel()
            }

        })
    }

 

 나는 아래와 같은 형태로 layout을 구성하였다. 전화번호를 입력하는 EditText의 값으로 메시지를 전송하고 랜덤으로 설정한 값과 인증번호 EditText의 값이 일치한다면 인증이 완료되도록 하였다.

 

 

 이렇게 네이버 SENS를 사용해서 간단하게 SMS 인증 기능을 구현해 보았다. 오류가 있거나 궁금한 점이 있다면 남겨주기 바란다.

profile

Developing Myself Everyday

@배준형

포스팅이 좋았다면 "좋아요❤️" 또는 "구독👍🏻" 해주세요!