여담이지만 나는 이미 HTTP 통신 과정을 어느정도는 알고 있었고 retrofit을 처음 써보지만 대충 따라해보면 될까 싶어서 무작정 대충 공부하고 시작하려니까
막상 개발할 때 조금 가다 막히고 조금 가다 막히고.. 원활하게 개발이 이루어지지 않았다.
뭐든 개념과 기초가 잘 잡힌게 중요한 것 같다! 공부는 지루하기도 하지만 파이팅🥰
1. [안드로이드] Retrofit2 기본 사용법2 -'GET/POST/PUT/DELETE'
2. [안드로이드] Retrofit2 '레트로핏' - 기본 사용법
Retrofit은 Android를 위한 HTTP Client이다.
따라서 Retrofit을 이용하면 HTTP API를 JAVA Interface로 변환할 수 있다!
안드로이드 스튜디오 프로젝트에는 build.gradle 파일이 두 개가 있는데 반드시 Module버전의 파일에서 설정해주어야 한다.
implementation 'com.android.volley:volley:1.1.1' // 서버 통신과 관련된 라이브러리
implementation 'com.squareup.retrofit2:retrofit:2.9.0'
implementation 'com.squareup.retrofit2:converter-gson:2.9.0'
implementation 'com.google.code.gson:gson:2.8.6'
<uses-permission android:name="android.permission.INTERNET" /> //인터넷 연결 허락
<application
...
android:usesCleartextTraffic="true">
이 클래스는 HTTP 통신하고자 하는 서버주소와 연결된 retrofit 클래스를 빌드하는 역할을 한다. 프로젝트마다 retrofit을 사용할 때 해당 클래스를 한 번 설정해주면 중복적으로 활용 가능하다.
import com.google.gson.Gson;
import com.google.gson.GsonBuilder;
import retrofit2.Retrofit;
import retrofit2.converter.gson.GsonConverterFactory;
public class RetrofitClient {
private static final String BASE_URL = "http://{통신 서버 http 주소}:{서버 port넘버}/";
public static Retrofit getInstance(){
Gson gson = new GsonBuilder().setLenient().create();
return new Retrofit.Builder()
.baseUrl(BASE_URL)
.addConverterFactory(GsonConverterFactory.create())
.build();
}
}
통신 서버 http 주소에는 주로 localhost가 들어가는데 이 때 오류가 난다면 cmd 창에서 ipconfig
검색을 통해 Ipv4주소를 찾아 넣으면 오류가 해결된다. 정확한 원인은 모르겠으나 구글링 결과 가끔 tomcat 서버가 쓰는 localhost 주소가 충돌이 일어나는 상황이 발생하기 때문이라고 한다. 어쨌든 Ipv4 주소를 쓰면 금방 해결되는 문제이다.
RetrofitClient class의 getInstance 함수는 말그대로 내가 설정한 url의 DB와 연결된 retrofit 객체를 build한 값을 리턴하는 함수이다. 이에 대한 활용은 추후에 언급하도록 하겠다.
받을/줄 Json 형태의 데이터를 객체화하기 위한 Class 를 만드는 과정이다.
모든 클래스에는 Getter 와 Setter 가 설정되어 있어야 한다.
만약 Server에서 데이터를 받는 형식의 통신이라면 server에서 날아 올 Json 형식을 고려해가며 해당 class를 작성해야 한다. 예를 들어 server에서 다음과 같은 데이터가 날아온다고 생각해보자.
{"data":[{"writerId":"testmember1","title":"테스트입니다","content":"성공인가요?","postedTime":"2022-01-22T23:14:56.314697"}]}
받은 데이터를 분석해보면 data라는 변수 안에 배열 형식으로 각각 writerId, title, content, postTime 변수의 데이터가 들어오는 것을 알 수 있다.
따라서 하나의 데이터에 데이터 배열이 또 들어와 있는 2중구조의 Json이다.
이를 받아서 객체화 할 class 역시 이 형식과 변수명을 고려해서 설정되어야 한다.
위의 예시 데이터에 대한 class 설정은 다음과 같다.
import com.google.gson.annotations.SerializedName;
import java.time.LocalDateTime;
public class PostData {
@SerializedName("writerId")
String writerId;
@SerializedName("title")
String title;
@SerializedName("content")
String content;
@SerializedName("postedTime")
String date;
public String getWriterId() {
return writerId;
}
public String getTitle() {
return title;
}
public String getContent() {
return content;
}
public String getDate() {
return date;
}
public void setWriterId(String writerId) {
this.writerId = writerId;
}
public void setTitle(String title) {
this.title = title;
}
public void setContent(String content) {
this.content = content;
}
public void setDate(String date) {
this.date = date;
}
}
Json 형식이 2중구조이기 때문에 class 계층도 2중구조로 class안에 class가 들어가는 형식으로 짜야한다.
먼저 json 데이터 배열 안에 들어있던 writerId
,title
,content
,그리고 postedTime
변수를 받아올 PostData class를 선언한다.
이 때 중요한 점은 @SerializedName("Json데이터에서의 각 변수명") 을 설정해줘서 Json 데이터에서의 각각 데이터값과 Class에서 설정된 변수가 매칭될 수 있도록 설정해주어야 한다는 것이다.
자 이제 Json 데이터 안에서 배열 부분이 처리가 되었으니 배열을 갖고 있는 data
변수 부분을 따로 처리해주면 된다.
import com.google.gson.annotations.SerializedName;
import java.util.List;
public class PostListData {
@SerializedName("data")
public List<PostData> data;
public List<PostData> getData() {
return data;
}
public void setData(List<PostData> data) {
this.data = data;
}
@Override
public String toString(){
return "Data{"+"data = "+data+"}";
}
}
마찬가지로 배열을 가져오는 변수의 이름이 data
이므로 @SerializedName으로 해당 변수명과 PostListData 클래스의 멤버변수를 매칭시켜주었다.
그리고 해당 변수 안에 배열이 들어있는 구조이기 때문에 똑같이 List형식으로 위에서 정의한 PostData 클래스를 멤버변수로 갖고있게 했다.
toString()함수를 오버라이드 해서 설정해준 까닭은 로그를 찍기 위함이며 보통 이렇게 2중구조의 data class는 로그 찍기가 힘들어서 따로 로그용 toString 함수를 정의해주는 것이 좋다.
만약 Server에 데이터를 주는 형식의 통신이라면 해당하는 server의 api에서 어떤 변수들을 필요로 하는지 고려하며 class를 작성해야 한다.
이 때 주는 class는 굳이 @SerializedName 어노테이션을 붙이지 않아도 된다.
다음은 회원가입을 할 때 nickname
,id
,password
변수를 사용자로부터 받아서 server에 넘겨 회원의 정보를 새로 저장시키기 위한 데이터 class이다.
public class SignUpInfo {
private String nickname;
private String memberId;
private String memberPassword;
public String getNickname() {
return nickname;
}
public String getMemberId() {
return memberId;
}
public String getMemberPassword() {
return memberPassword;
}
public void setNickname(String nickname) {
this.nickname = nickname;
}
public void setMemberId(String memberId) {
this.memberId = memberId;
}
public void setMemberPassword(String memberPassword) {
this.memberPassword = memberPassword;
}
}
자 이제 거의 다 왔따!!! 고지가 눈앞이다.
이제 모든 설정은 끝났고, 활용할 API Interface를 구현하기만 하면 된다.
이 과정은 Server에서 api controller 구현 과정과 매우 유사하기도 하고, 당연히 해당하는 server의 api의 주소와도 일치해야 한다.
예를 들어 서버에서
@GetMapping("/api/member/{userId}")
public MemberResponse getUserInfo(@PathVariable("userId" String userId){
Member member = memberService.findMember(userId);
MemberResponse response = member.stream().map(m->new MemberResponse(member.getnickname(),member.getId(),member.getPw());
return response;
}
다음과 같이 api가 구현되어 있다면
이에 해당하는 안드로이드에서의 API interface도
당연히 @Get("api/member/{userId})
로 시작해야 한다.
거두절미하고 이에 대한 API Interface는 다음과 같이 구현할 수 있다.
@GET("api/member/{id}")
Call<LoginResponseInfo> getMemberInfo(@Path("id") Long memberId);
Call<Response클래스>부분은 해당 API 통신 이후 server에서 받아 올 결과를 나타낸다. get으로 요청했던 결과 값을 Response 클래스에서 받아온다고 생각하면 된다. 이 Response 클래스는 위의 4. 데이터를 받을/줄 Class 양식 만들기 단계에서 이미 구현되었을 것이다!
다음으로 해당 api를 사용할 메소드명을 작성하고 그에 대한 변수로 만약 요청할 서버에 따로 주어야 할 정보가 있다면 변수에 다음과 같이 그 값을 넣어주면 된다.
그 값을 api 통신 path로 넣어주어야 하면 예시처럼 @Path("") 어노테이션을 넣고 매개변수를 설정하면 된다.
만약 회원가입과 같이 POST 통신에서 딱히 api 통신 path로 넣어줄 필요는 없지만 단순히 server에 특정 양식의 데이터를 보내야 하는 상황에서는
매개변수에 @Body 어노테이션을 붙인 뒤 해당 보내야하는 클래스 객체를 매개변수로 주면 된다.
Retrofit의 활용은 매우 단순하다.
Acitivity에서 HTTP 통신을 하고 싶은 시점에서
// memberAPI 연결 활성화하기
MemberAPI memberAPI = RetrofitClient.getInstance().create(MemberAPI.class);
Call<LoginResponseInfo> loginResponseInfoCall = memberAPI.login(id,pw);
loginResponseInfoCall.enqueue(new Callback<LoginResponseInfo>() {
@Override
public void onResponse(Call<LoginResponseInfo> call, Response<LoginResponseInfo> response) {
if(response.isSuccessful()){
// 통신 성공, 원하는 정보도 제대로 받아옴 or 보냄
}
else{
// 통신 성공, 그러나 원하는 정보를 받아오거나 보내지 못함
}
}
@Override
public void onFailure(Call<LoginResponseInfo> call, Throwable t) {// 통신 실패
Log.d("HTTP","로그인 연결 실패 ");
Log.e("연결실패", t.getMessage());
}
});
다음과 같이 코드를 짜주면 된다.
위의 코드 세 줄을 보다 구체적으로 보면
1. 먼저 활용할 API 클래스 객체를 선언, RetrofitClient에서 getInstance로 빌드된 retrofit 객체를 가져와 활용할 API클래스를 create하라고 명령을 준다.
2. 위에서 정의한 API 클래스에서 활용할 api 메소드의 리턴값을 Call<> 객체로 설정하고 해당 API클래스에서 활용할 api메소드와 그것이 필요로 하는 매개변수를 넣어준다.
3. 위에서 설정한 Call<>객체에서 enqueue 함수를 써서 callback으로 http 통신 성공시 받아올 클래스 CallBack<>객체를 생성한다.
아래 override부분 부터는 자동완성으로 생성되며 각 통신 성공 여부에 따른 상황 정의는 위의 주석과 같다!
통신은 성공했지만 뭔가 데이터를 예상한대로 받아오지 못했을때, 혹은 주지 못했을 때의 상황에서 무엇이 잘못되었는지 에러메세지를 출력할 수 있다.
아래와 같은 코드를 구현해보자 💃
ErrorBody error = new Gson().fromJson(response.errorBody().charStream(),ErrorBody.class);
해당 내용은 여기서 자세하게 확인해보자!
그래서 callBack<받을 클래스> 로 받아온 값은 어떻게 안드로이드에서 활용하냐면
받을 클래스 객체를 선언하고 = response.body();
로 받아와주면 끝이다!
그 이후에 위에서 선언한 객체에서 접근할 변수를 getter로 get해서 자유롭게 사용하면 된다.
예를들어 코드는 다음과 같이 진행된다.
LoginInfoResponse responseInfo = response.body();
String userId = responseInfo.getId();
@Body
, @Field
, @FieldMap
의 차이@Body
: 데이터를 @BODY에 담아서 요청하는 방식@Field
: key- value 형태의 데이터 전송, 객체를 따로 만들어서 전송할 필요 없이 바로 key-value 형태로 전송@FormUrlEncoded
@POST("posts")
Call<PostResult> setPostField(
@Field("userId") String userId,
@Field("title") String title,
@Field("body") String body
);
-------
사용할 때
setPostField("120", "test title", "test body")
@FieldMap
: @QueryMap처럼 @Field도 Map으로 한번에 전송이 가능Map<String, String> fields = new HashMap<>();
fields.put("userId", "120");
fields.put("title", "test title");
fields.put("body", "test body");
setPostFieldMap(fields);
ㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡ
@FormUrlEncoded
@POST("posts")
Call<PostResult> setPostFieldMap(
@FieldMap Map<String, String> fieldMap
);
짠!! 여기까지 하면 기본적인 Retrofit의 설정부터 활용까지 모든 것을 해결할 수 있다!
안드로이드 개발이 익숙하지 않지만 그래도 결과가 바로바로 보이니 개발이 즐거운 것 같다
파이팅💪