Retrofit 사용기

dongbin is free·2023년 1월 19일
0

Android

목록 보기
1/6

사용하게된 계기

필자는 2022년 학과 졸업작품을 제작하는 과정에서 교내 포탈사이트 API 정보를 가져와야하는 작업이 필요하였다. 당시 작품은 Java 기반의 Android Native App 프로젝트였기에 수업시간에 배운 Thread, AsyncTask, HttpURLConnection 을 이용하여 json 데이터를 받아왔었다.

통신 작업을 처리해주는 Connector 클래스를 아래와 같이 SingleTon 패턴으로 정의하여 작업을 진행했었다.

public class Connector {
    private HttpURLConnection connection;
    public Connector() {}
    public static Connector getInstance() {
        return LazyHolder.INSTANCE;
    }
    private static class LazyHolder {
        private static final Connector INSTANCE = new Connector();
    }
    public JSONObject getResponse(String link, String token, JSONObject request) {
        try {
            URL url = new URL(link);
            connection = (HttpURLConnection) url.openConnection();
            // Bearer
            connection.setRequestProperty("Authorization", "Bearer " + token);
            connection.setConnectTimeout(10000);// 연결 대기 시간 설정
            connection.setRequestMethod("POST");  // 전송 방식 POST
            connection.setDoInput(true);        // InputStream으로 서버로부터 응답받음
            connection.setDoOutput(true);       // OutputStream으로 POST데이터를 넘겨줌
            // 서버 Response를 JSON 형식의 타입으로 요청
            connection.setRequestProperty("Accept", "application/json");
            // 서버에게 Request Body 전달 시 application/json으로 서버에 전달
            connection.setRequestProperty("content-type", "application/json");

            OutputStream os = connection.getOutputStream();
            os.write(request.toString().getBytes(StandardCharsets.UTF_8));
            os.flush();

            // 연결 상태 확인
            if (connection.getResponseCode() != HttpURLConnection.HTTP_OK) {
                Log.d("Failed", "request 실패!");
                return null;
            }
            // --------------
            // 서버에서 전송받기
            // --------------
            BufferedReader reader = new BufferedReader(new InputStreamReader(connection.getInputStream(), "utf-8"));
            StringBuilder sb = new StringBuilder();
            String str;
            while ((str = reader.readLine()) != null) {
                sb.append(str);
            }
            reader.close();
            os.close();
            connection.disconnect();

            JSONObject response = new JSONObject(sb.toString());
            return response;
        } catch (IOException | JSONException exception) {
            exception.printStackTrace();
            return null;
        }
    }
}

해당 Network 작업은 Main Thread단에서 불가능하여 동기적으로 처리할 시 Thread 를 이용하고, 비동기적으로 처리할 시 AsyncTask 를 이용하였으나, 생각치 못한 변수에 맞닥뜨린다.


AsyncTask is Deprecated in API Level 30


프로젝트를 갈아엎은 원인이 되었으며, 해당 게시글Deprecated 된 이유가 잘 정리되어 있어서 나중에 또 참고하자는 마음으로 링크를 남긴다.

간단히 요약하자면

1. 단점

- 오직 한 번만 실행되어 재사용 불가
- 종료를 직접 해주지 않으면, 메모리 릭 발생
- 항상  UI 쓰레드상에서 호출

2. 시스템적 결함

- 메모리 릭
- 순차 실행으로 인한 낮은 퍼포먼스
- Fragment상에서  AsnycTask 실행 시 NPE
- 예외처리 메소드 부재
- AsyncTask 병렬 실행 시 doInBackground() 실행순서 보장되지 않음

이로 인해 대체할 수 있는 것들을 찾아 Coroutine, Volley, RxJava, Retrofit 에 대한 레퍼런스 자료들을 확인해보았다.

RxJava는 지금 당장 프로젝트를 완료해야하는 상황에서 러닝커브가 너무 높다는 점으로 추후 알아보기로 하고, Coroutine은 필자의 프로젝트가 Java 로 작성되어 사용할 수 없었다.

Volley vs Retrofit 을 결정해야 했는데, 참고 게시글을 통해 응답시간이 더 빠르고 직전에 진행했던 웹 프로젝트와 유사하게 작성할 수 있어보이는 Retrofit 을 사용하기로 정했다.


구현

1. Gradle에 라이브러리 추가하기

Retrofit을 사용하기 위해 라이브러리를 앱 단위 Gradle에 추가하였다

implementation 'com.squareup.retrofit2:retrofit:2.9.0'
implementation 'com.squareup.retrofit2:converter-gson:2.9.0'

# 통신 결과를 gson 형태로 변환하기 위해 converter-gson을 추가하였으나
# 원하는 통신 결과 변환을 위해서 아래와 같은 라이브러리를 추가하여도 된다.

# xml converter
implementation 'com.tickaroo.tikxml:retrofit-converter:0.8.13'
implementation 'com.tickaroo.tikxml:annotation:0.8.13'
implementation 'com.tickaroo.tikxml:core:0.8.13'
annotationProcessor 'com.tickaroo.tikxml:processor:0.8.13'

# html converter
implementation 'com.github.slashrootv200:retrofit-html-converter:0.0.2'

2. Api interface 정의하기

이후 진행되는 과정은 Java, Kotlin 간 소스가 상이하기에 함께 작성하였다. 이 부분이 Spring 프레임워크를 사용할 때와 비슷해 Retrofit 을 사용하기로 결정한 이유이다.

// Java Language
public interface LoginApi {
	/* Login */
	@Headers({"Accept: application/json", "content-type: application/json"})
	@POST("auth2/login.sku")
	Call<ResponseLogin> getUserData(@Body RequestLoginData requestLoginData);
}
// Kotlin Language
interface LoginApi {
    @Headers("Accept: application/json", "content-type: application/json")
    @POST("auth2/login.sku")
    fun getUserData(@Body requestLoginData : RequestLoginData): Call<ResponseLogin>
}

3. Retrofit Client 구현하기

로그인 인증에 필요한 LoginClient( )을 구현한다 .baseUrl( )안에 BaseUrl을 넣어주고, .addConverterFactory( )안에 해당되는 컨버터 Factory를 넣어주며 여기서는 SSL 인증을 우회하여 통신하기 위해, .client( )안에 바로 아래 정의한 okHttp 클라이언트를 넣어주어 빌드한다.

// Java Language
public class LoginClient {
	private LoginApi loginApi;
	private Retrofit retrofit;

	private final static String BASE_URL = "https://sportal.skuniv.ac.kr/sportal/";

	public LoginClient() {
		retrofit = new Retrofit.Builder()
				.baseUrl(BASE_URL)
				.addConverterFactory(GsonConverterFactory.create())
				.client(getUnsafeOkHttpClient().build())
				.build();
		loginApi = retrofit.create(LoginApi.class);
	}
    
	// 인증서 없이(SSL 우회) 통신 - 실상 안전하진 않은 통신
	public OkHttpClient.Builder getUnsafeOkHttpClient() {
		try {
			final TrustManager[] trustAllCerts = new TrustManager[] {
					new X509TrustManager() {
						@Override
						public void checkClientTrusted(java.security.cert.X509Certificate[] chain, String authType) throws CertificateException {
						}

						@Override
						public void checkServerTrusted(java.security.cert.X509Certificate[] chain, String authType) throws CertificateException {
						}

						@Override
						public java.security.cert.X509Certificate[] getAcceptedIssuers() {
							return new java.security.cert.X509Certificate[]{};
						}
					}
			};

			final SSLContext sslContext = SSLContext.getInstance("SSL");
			sslContext.init(null, trustAllCerts, new java.security.SecureRandom());

			final SSLSocketFactory sslSocketFactory = sslContext.getSocketFactory();

			OkHttpClient.Builder builder = new OkHttpClient.Builder();
			builder.sslSocketFactory(sslSocketFactory, (X509TrustManager)trustAllCerts[0]);
			builder.hostnameVerifier(new HostnameVerifier() {
				@Override
				public boolean verify(String hostname, SSLSession session) {
					return true;
				}
			});
			return builder;
		} catch (Exception e) {
			throw new RuntimeException(e);
		}
	}

	public LoginApi getLoginApi() { return loginApi; }
}
// Kotlin Language
class RetrofitClient {
    private var loginApi : LoginApi
    private var retrofit : Retrofit

    // SeoKyeong Portal Login Url
    private val baseUrl : String = "https://sportal.skuniv.ac.kr/sportal/"

    init {
        // 클라이언트 초기화
        retrofit = Retrofit.Builder()
            .baseUrl(baseUrl)
            .addConverterFactory(GsonConverterFactory.create())
            .client(getUnsafeOkHttpClient().build())
            .build()
        loginApi = retrofit.create(LoginApi::class.java)
    }

    // SSL Auth Avoid
    private fun getUnsafeOkHttpClient() : OkHttpClient.Builder {
        val trustAllCerts = arrayOf<TrustManager>(object : X509TrustManager {
            override fun checkClientTrusted(p0: Array<out X509Certificate>?, p1: String?) {

            }

            override fun checkServerTrusted(p0: Array<out X509Certificate>?, p1: String?) {

            }

            override fun getAcceptedIssuers(): Array<X509Certificate> {
                return arrayOf()
            }
        })
        val sslContext = SSLContext.getInstance("SSL")
        sslContext.init(null, trustAllCerts, java.security.SecureRandom())

        val sslSocketFactory = sslContext.socketFactory

        var builder = OkHttpClient.Builder()
        builder.sslSocketFactory(sslSocketFactory, trustAllCerts[0] as X509TrustManager)
        builder.hostnameVerifier { _, _ -> true}

        return builder
    }

    fun getLoginApi() : LoginApi {
        return loginApi
    }
}

4. 통신 요청 및 응답 콜백 구현하기

비동기 처리 시 enqueue( ), 동기 처리 시 excute( ) 라는 차이점만 기억해두고 alt+enter를 통해 override method를 작성해주면 된다.

// Java Language
LoginClient loginClient = new LoginClient();
		loginApi = loginClient.getLoginApi();
		loginApi.getUserData(new RequestUserData(loginId, loginPassword, "password", "sku")).enqueue(new Callback<ResponseLogin>() {
			@Override
			public void onResponse(Call<ResponseLogin> call, Response<ResponseLogin> response) {
				if (response.isSuccessful()) {
					if (response.body().getRtnStatus().equals("S")) {
						loginData.setValue(response.body());
						loginState.setValue("success");
					} else {
						loginState.setValue("fail");
					}
				} else {
					loginState.setValue("fail");
				}
			}

			@Override
			public void onFailure(Call<ResponseLogin> call, Throwable t) {
				System.out.println(t);
				loginState.setValue("network");
			}
		});
// Kotlin Language
var loginClient = LoginClient()
        loginApi = loginClient.getLoginApi()
        loginApi.getUserData(RequestLoginData(id, pw, "password", "sku")).enqueue(object : Callback<ResponseLogin> {
            override fun onResponse(call: Call<ResponseLogin>, response: Response<ResponseLogin>) {
                // 응답 수신 성공
                if(response.isSuccessful) {
                    // 로그인 성공
                    if(response.body()?.rtnStatus.equals("S")) {
                        authData.value = response.body()
                        authState.value = "S"
                    }
                    // 로그인 실패
                    else {
                        authState.value = "F"
                    }
                } else {
                    // 응답 수신 실패
                    println(response)
                    authState.value = "R"
                }
            }

            override fun onFailure(call: Call<ResponseLogin>, t: Throwable) {
                // 통신 실패
                authState.value = "C"
            }
        })

해당 작업에서 사용된 RequestLoginData, ResponseLogin 객체는 필자가 필요한 json 데이터를 보내고 받기위해 정의한 클래스이다. 이 부분을 필요한 데이터를 주고 받을 수 있도록 converter에 맞게 변경해주면 된다.


졸업작품 프로젝트를 진행하는 중간에 학부 연구실 웹 프로젝트를 진행하여 API에 대한 기본적인 지식과 Spring Boot을 이용하여 Spring 프레임워크를 미약하게나마 다뤄보았던 경험이 필자에게 Retrofit 사용을 익히는데 꽤 도움이 되었다고 생각한다.

혹시 필자와 같이 어떻게 사용하는지 궁금해 미치겠는데, 마땅히 나에게 맞는 레퍼런스 자료를 찾기 힘든 사람들을 위해 SKUP 프로젝트 깃허브를 남기며 글을 마친다.

profile
배운 것을 적어나가는 그런 공간.. 적다 보면 또 까먹는 그런 사람..

0개의 댓글