Retrofit은 OkHttp 기반으로 동작한다. 따라서 OkHttp를 제외한 다른 HTTP 모듈들에 비해 빠르다. 사용하기도 편리하다. 그래서 통신 모듈을 Retrofit으로 결정했다. 자세한 내용은 벤치 마크를 참고하자
build.gradle(Module: app)에 아래 의존성 추가 (2020.08.08 기준)
// dependencies for retrofit
implementation 'com.google.code.gson:gson:2.8.6'
implementation 'com.squareup.retrofit2:retrofit:2.9.0'
implementation 'com.squareup.retrofit2:converter-gson:2.9.0'
implementation 'com.squareup.okhttp3:okhttp:4.8.1'
// dependencies for lombok
compileOnly 'org.projectlombok:lombok:1.18.12'
annotationProcessor 'org.projectlombok:lombok:1.18.12'
통신을 위해 manifest 밑에 다음 코드 추가
<uses-permission android:name="android.permission.INTERNET"/>
StoreService Interface는 우리가 요청을 보낼 URL들을 정의해놓는다. 코드를 보면 바로 이해할 수 있다.
import java.util.List;
import retrofit2.Call;
import retrofit2.http.GET;
public interface StoreService {
@GET("/api/store")
Call<List<StoreDto>> getStoreListOrderByGrade();
}
항상 Call<>
안에 서버의 응답에 해당하는 Dto를 넣어야한다. 그리고 GET
, POST
, PATCH
, PUT
, DELETE
처럼 서버에 등록된 라우터에 맞는 HTTP METHOD와 URL을 작성하면 된다. getStoreListOrderByGrade()
의 응답 JSON은 다음과 같다.
[
{
"store_number": 8,
"store_name": ".S.",
"vote_grade_average": 4.8,
"vote_grade_count": 0,
"store_id": 2,
"starred": 0
},
...
...
...
{
"store_number": 8,
"store_name": "나",
"vote_grade_average": 0,
"vote_grade_count": 0,
"store_id": 5,
"starred": 0
}
]
동일한 column들의 반복이므로 List<StoreDto>
를 통해 응답을 받고자 한다. 그러면 다음으로 StoreDto
를 정의해보자
Dto는 Data Tranfer Object의 약자이다. 말 그대로 데이터를 옮겨주는 객체이다. 그래서 아무런 기능이 없다. 그리고 등록만 해놓으면 Retrofit에 등록해놓은 GsonFactory
가 응답 받은 JSON을 Dto로 변환해준다. 개꿀! 코드를 살펴보자
import com.google.gson.annotations.Expose;
import com.google.gson.annotations.SerializedName;
import lombok.Getter;
import lombok.Setter;
@Getter
@Setter
public class StoreDto {
@SerializedName("store_number")
@Expose
private Integer storeNumber;
@SerializedName("store_name")
@Expose
private String storeName;
@SerializedName("vote_grade_average")
@Expose
private float voteGradeAverage;
@SerializedName("vote_grade_count")
@Expose
private Integer voteGradeCount;
@SerializedName("store_id")
@Expose
private Integer storeId;
@SerializedName("starred")
@Expose
private Integer starred;
}
getter()
와 setter()
가 꼭 필요하다. 이런 코드를 작성하기가 너무 귀찮기 때문에 찾아보니 http://www.jsonschema2pojo.org/ Json을 입력하면 자바 코드로 변환해주는 녀석이 있었다. 우리 프로젝트의 경우에는 다음과 같은 설정으로 변환을 진행했다.
주의사항!!
소수형을 Integer로 생성해주더라 직접 float로 바꿔주자
우리가 위에서 작성한 Service Interface를 실행해주는 객체를 생성해주는 녀석이다. 이 녀석을 통해 통신원을 생성해보자 일단 코드를 보자
import android.text.TextUtils;
import okhttp3.OkHttpClient;
import retrofit2.Retrofit;
import retrofit2.converter.gson.GsonConverterFactory;
public class ServiceGenerator {
public static final String BASE_URL = "YOUR_URL";
private static OkHttpClient.Builder httpClient = new OkHttpClient.Builder();
private static Retrofit.Builder builder =
new Retrofit.Builder()
.baseUrl(API_BASE_URL)
.addConverterFactory(GsonConverterFactory.create());
private static Retrofit retrofit = builder.build();
public static <S> S createService(Class<S> serviceClass) {
return createService(serviceClass, null);
}
public static <S> S createService(
Class<S> serviceClass, final String authToken) {
if (!TextUtils.isEmpty(authToken)) {
AuthenticationInterceptor interceptor =
new AuthenticationInterceptor("Bearer " + authToken);
if (!httpClient.interceptors().contains(interceptor)) {
httpClient.addInterceptor(interceptor);
builder.client(httpClient.build());
retrofit = builder.build();
}
}
return retrofit.create(serviceClass);
}
}
method 들은 public static
으로 정의가 되어있고 멤버 변수들은 private static
으로 정의가 되어있는걸보니 아마도 통신원을 여러개를 무분별하게 생성해서 프로그램의 성능 저하가 발생 하지는 않을 것 같다. 코드의 출처는 여기를 참고하자 굉장히 설명도 잘해놨다.
주의사항!!!
- JWT를 사용하기 위해서는 TOKEN 문자열에 꼭 "Bearer "를 더해주자.
- BASE_URL은 https 통신이어야 한다.
- BASE_URL은 마지막에 꼭 "/"로 끝나야한다.
우리 프로젝트에서 사용하는 인증방식인 JWT는 TOKEN을 기반으로 인증을 진행한다. 따라서 통신을 할 때마다 Authorization 헤더에 TOKEN을 추가해서 전송해야하는데 Retrofit에서는 OkHttp3 Interceptor를 통해 Retrofit Instance를 생성할 때 Retrofit이 기반하고 있는 OkHttp3의 코어에 헤더를 수정하는 것 같다(뇌피셜;; 아마 Retrofit과 OkHttp는 Keras와 Tensorflow의 관계 같다)
import java.io.IOException;
import okhttp3.Interceptor;
import okhttp3.Request;
import okhttp3.Response;
public class AuthenticationInterceptor implements Interceptor {
private String authToken;
public AuthenticationInterceptor(String token) {
this.authToken = token;
}
@Override
public Response intercept(Chain chain) throws IOException {
Request original = chain.request();
Request.Builder builder = original.newBuilder()
.header("Authorization", authToken);
Request request = builder.build();
return chain.proceed(request);
}
}
이 녀석은 HTTP 통신에 사용하는 Request
객체를 생성해준다. 새로운 생성한 Request
의 헤더에는 TOKEN을 담은 Authorization 옵션이 추가되어 있다.
어쨌든 통신이 필요한 곳에서 ServiceGenerator.createService(ServiceInterface.class, TOKEN)
을 호출하면 되는데 아래에서 호출하는 코드를 살펴보자
통신이 필요한 액티비의 onCreate
함수에서 호출을하자
protected void onCreate(Bundle savedInstanceState) {
...
...
String TOKEN = getToken(); // access token을 가져오는 함수를 직접 정의하셔야합니다.
storeService = ServiceGenerator.createService(StoreService.class, TOKEN);
...
...
...
loadStores();
}
prviate void loadStores() {
storeService.getStoreListOrderByGrade().enqueue(new Callback<List<StoreDto>>() {
@Override
public void onResponse(Call<List<StoreDto>> call,
Response<List<StoreDto>> response) {
if (response.isSuccessful()) {
// response.body()
// response.body()에서 넘어오는 데이터로 Adapter에 뿌려주기
} else {
Log.d("REST FAILED MESSAGE", response.message());
}
}
@Override
public void onFailure(Call<List<StoreDto>> call, Throwable t) {
Log.d("REST ERROR!", t.getMessage());
}
}
이런 식으로 작성하면 된다. 기억할 것은 Service Interface
를 생성한 후 (ServiceGenerator
를 통해 생성해야한다) Service Interface
에 정의되어 있는 함수를 호출하고 CallBack
함수를 정의해주면 된다.
https://uareuni.tistory.com/30
https://chuumong.github.io/android/2017/01/13/Get-Started-With-Retrofit-2-HTTP-Client
https://jungwoon.github.io/android/2019/07/11/Retrofit/
https://futurestud.io/tutorials/android-basic-authentication-with-retrofit
https://stackoverflow.com/questions/41078866/retrofit2-authorization-global-interceptor-for-access-token
안녕하세요 덕분에 헤더에 JWT를 넣고 서버에 보내는 방법에 대해 차근차근 이해할 수 있었습니다. 감사합니다. 이것을 읽으면서 궁금한 점이 생겨 이렇게 댓글을 달게 되었습니다. 서버에서 로그인시에 JWT 값을 받아와서 이를 사용자 인증이 필요할 때마다 헤더에 실어 보내는 경우 storeService = ServiceGenerator.createService(StoreService.class, TOKEN); 이 부분을 불러올 때, TOKEN에 에러메세지가 뜨는데 어떻게 해결해야하는건지 궁금합니다! 블로그에서 많이 배우고 있습니다. 감사합니다! (에러:CANNOT RESLOVE SYMBOL "TOKEN")