[Android] Retofit2를 이용한 웹서버 통신

vector13·2021년 11월 21일
0

Android

목록 보기
7/12

* 배경

: 레트로핏(Retrofit)은 안드로이드랑 웹의 HTTP 통신 가능하게 하는 라이브러리이다.

-레트로핏 동작

: Call 객체를 이용해서 서버로 request를 보내고 response를 받아온다. 이 때 주고 받는 데이터는 json 형태이다.
즉, 인터페이스를 이용해서 Service 객체를 획득해 서버와 통신할 경우 Call 객체를 획득해 동작한다.

-그래서 무엇을 만드나? 만들어야 하는 것은 DB에 연결된 서버가 준비 되어야한다.

(글에서 사용하는 것은 장고 서버, mysql이다)
안드로이드에서 java를 이용해서 만들 것은 모델을 담는 파일과 Service객체를 가지는 API를 만든다.

기본적으로, Django의 모델이 생성되어있는 것을 기본으로 한다.
예시로 diary라는 모델에 models.py가 아래와 같이 작성되어있다.

(현재 user모델과 외래키 입력은 주석으로 처리해둔 상태이다. )

diary.models.py

class diary(models.Model):
    diary_id = models.AutoField(primary_key = True)
    diary_date = models.DateTimeField(auto_now_add = True)
    diary_weather = models.PositiveSmallIntegerField(validators = [MinValueValidator(1), MaxValueValidator(4)]) #1-맑음,2-흐림,3-비,4-눈
    diary_title = models.CharField(max_length = 20,)
    diary_content = models.TextField(max_length = 200)
    diary_todayme = models.CharField(max_length = 20, null = True, blank = True)
    diary_tomorrowme = models.CharField(max_length = 20, null = True, blank = True)
    diary_img = models.ImageField(upload_to = None, height_field = None, width_field = None, max_length = 100, null = True, blank = True)
#    user = models.ForeignKey(user, on_delete = models.CASCADE, null = True)
    def __str__(self):
        return self.diary_title

모델생성을 마치고 Django admin페이지에 들어가면 모델이 보이고
/api/diary/ 주소로 들어가면 모델에 데이터를 추가할 수 있다.

아래 사진은 /api/diary/ 주소로 들어갔을 때 POST가능한 필드가 보이는 화면이다.

이렇게 저장된 데이터들은 상단의 리스트에서 이렇게 보이고, Get버튼을 누르면 json으로 파싱된 데이터를 확인할 수 있다.

json 데이터 - 이 형태로 안드로이드에서 @GET 요청시 보낸다. (@POST도 동일)

안드로이드에서 해주어야 할 설정은 2부분이 있다.
첫 번째로 build.gradle(:app)에서 retrofit과 gson 컨버터를 dependencies에 implement해준다.

dependencies { 
..(생략).. 

//retrofit 설정추가
    implementation 'com.squareup.retrofit2:retrofit:2.8.0'
    implementation 'com.squareup.retrofit2:converter-gson:2.8.0'
    }

컨버터는 서버 연동시 주고받는 데이터가 json형태이기 때문에 파싱시켜서 객체로 변환해주는 기능을 말하고, Retrofit에는 컨버터 기능이 없기 때문에 외부 라이브러리를 연동해 따로 추가해준다.

두 번째 설정은 AndroidManifest.xml에서

<uses-permission android:name="android.permission.INTERNET"/>

이 코드를 <application 앞에 넣어준다.

이제 안드로이드에서 activity_main.xml을 이런 구조로 짠다.
GET버튼 누를 시 초록 부분에 웹서버에 저장된 데이터가 text필드에 출력된다. EditText에 입력시 title과 content로 들어간 데이터가 POST버튼 누르면 웹서버로 보내진다.

activity_main.xml

<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:orientation="vertical">

    <TextView
        android:id="@+id/result1"
        android:layout_width="match_parent"
        android:background="#D8528254"
        android:textSize="15dp"
        android:layout_height="wrap_content"/>

    <Button
        android:id="@+id/button1"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:text="get" />
    <!-- diary title -->
    <EditText
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:id="@+id/testContent1"
        />
    <!-- diary content -->
    <EditText
    android:layout_width="match_parent"
    android:layout_height="wrap_content"
    android:id="@+id/testContent2"
    />

    <Button
        android:id="@+id/button2"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:text="post" />

    <Button
        android:id="@+id/button3"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:text="patch" />

    <Button
        android:id="@+id/button4"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:text="delete" />


</LinearLayout>

이렇게 동작하기 위해서 해줘야 하는 것은
주고 받을 Model을 정의해주어야한다. 주의할 것은 항상 이름을 json데이터와 동일하게 해주어야한다.

Model을 정의한 파일명을 PostItem로 설정하고 diary_id를 제외한 필드를 모두 String으로 준다. (json으로 주고받으니까 dairy.models.py에서 정의한 것과는 다르게 String으로 준다.)

필드를 정의하고, 생성자를 생성하고 getter와 setter를 만들어준다.

PostItem.java

package com.example.testapp;

public class PostItem { //필드 선언 
    private String diary_weather;
    private String diary_img;
    private String diary_content;
    private String diary_date;
    private String diary_title;
    private String diary_todayme;
    private String diary_tomorrowme;
    private int diary_id;
	
    //여기서부턴 생성자 
    public PostItem(){}
    //매개변수 개수대로  
    public PostItem(
            int diary_id,String diary_title, String diary_date, String diary_weather, String diary_content,
            String diary_todayme, String diary_tomorrowme,String diary_img) {
        this.diary_weather = diary_weather;
        this.diary_img = diary_img;
        this.diary_content = diary_content;
        this.diary_date = diary_date;
        this.diary_title = diary_title;
        this.diary_todayme = diary_todayme;
        this.diary_tomorrowme = diary_tomorrowme;
        this.diary_id = diary_id;
    }
    
    //getter와 setter 설정 
    public int getDiary_id() {
        return diary_id;
    }

    public int setDiary_id(int diary_id) {
        this.diary_id = diary_id;
        return diary_id;
    }

    public String getWeather() {
        return diary_weather;
    }

    public String setWeather(String diary_weather) {
        this.diary_weather = diary_weather;
        return diary_weather;
    }

    public String getImage() {
        return diary_img;
    }

    public String setImage(String diary_img) {
        this.diary_img = diary_img;
        return diary_img;
    }

    public String getTitle() {
        return diary_title;
    }

    public String setTitle(String diary_title) {
        this.diary_title = diary_title;
        return diary_title;
    }
    public String getContent() {
        return diary_content;
    }

    public String setContent(String diary_content) {

        this.diary_content = diary_content;
        return diary_content;
    }

    public String getDate() {
        return diary_date;
    }

    public String setDate(String diary_date) {
        this.diary_date = diary_date;
        return diary_date;
    }
    public String getTodayme() {
        return diary_todayme;
    }

    public String setTodayme(String diary_todayme) {
        this.diary_todayme = diary_todayme;        
        return diary_todayme;
    }

    public String getTomorrowme() {
        return diary_tomorrowme;
    }

    public String setTomorrowme(String diary_tomorrowme) {
        this.diary_tomorrowme = diary_tomorrowme;        
        return diary_tomorrowme;
    }

}

이제 API를 만들어 준다.
인터페이스 이름은 MyAPI.java로 설정했다. 일단 GET과 POST만 동작하게 만들었다. 레트로핏에서 사용하는 어노테이션은 @GET @POST @PUT @DELETE @HEAD가 있지만 여기서는 일단 @GET @POST만 사용함.

GET과 POST의 차이점은 데이터를 받는 GET 요청 request시 오는 response응답은 List형태의 모델이고,
데이터를 보내는 POST 요청 request시 오는 response응답은 PostItem모델 타입이다.
그리고 get에는 메소드만 호출하지만, post에는 Body에 PostItem 타입의 diary를 보낸다.

MyAPI.java

package com.example.testapp;
import java.util.List;
import retrofit2.Call;
import retrofit2.http.Body;
import retrofit2.http.DELETE;
import retrofit2.http.GET;
import retrofit2.http.PATCH;
import retrofit2.http.POST;
import retrofit2.http.Path;

public interface MyAPI {
    //GET버튼 누르면 여기로 와서 동작한다. 
    @GET("diary/")
    Call<List<PostItem>> get_diary();
    
    //POST버튼 누르면 여기로 와서 동작한다. 
    @POST("diary/")    
    Call<PostItem> post_diary( @Body PostItem diary);
    
    @GET("/diary/{pk}/")
    Call<PostItem> get_diary_pk(@Path("pk") int pk);
}

이제 동작을 시키기 위해서 Activity작업을 해준다.
BASE_URL의 경우 서버를 동작시킨 곳의 ip주소를 입력해준다.
서버 동작시키는 곳의 ipv4주소는 명령 프롬프트에서 ipconfig를 입력하면 알 수 있고, 서버 동작시킬 때
python manage.py runserver 아이피주소:포트번호 로 시킨다.

package com.example.testapp;
import androidx.annotation.RequiresApi;
import androidx.appcompat.app.AppCompatActivity;
import android.os.Build;
import android.os.Bundle;
import android.util.Log;

import okhttp3.OkHttpClient;
import okhttp3.logging.HttpLoggingInterceptor;
import retrofit2.Retrofit;
import retrofit2.converter.gson.GsonConverterFactory;

import android.view.View;
import android.widget.Button;
import android.widget.EditText;
import android.widget.TextView;

import com.google.gson.Gson;

import java.util.List;
import retrofit2.Call;
import retrofit2.Callback;
import retrofit2.Response;

public class MainActivity extends AppCompatActivity implements View.OnClickListener{
	
    private static String BASE_URL = "http://아이피주소:8000/api/";
    private MyAPI mMyAPI;
    private TextView mListTv;
    private EditText testContent1;
    private EditText testContent2;
    private Button mGetButton;
    private Button mPostButton;
    private Button mPatchButton;
    private Button mDeleteButton;

    String diary_weather = "2";
    String diary_img;	//이미지 필드에는 NULL만 차게 할것! 
    String diary_date = "";
    String diary_todayme = "";
    String diary_tomorrowme = "";
    String diary_content ="default content";
    String diary_title="default title";
    int diary_id=13;

	//레트로핏 생성하는 부분이다. create 된 레트로핏 이름은 mMyAPI
    private void initMyAPI(String baseUrl){
        Log.d(TAG,"initMyAPI : " + baseUrl);

        Retrofit retrofit = new Retrofit.Builder()
                .baseUrl(baseUrl)
                .addConverterFactory(GsonConverterFactory.create())
                .build();

        mMyAPI = retrofit.create(MyAPI.class);
    }

    private final  String TAG = getClass().getSimpleName();

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);

        testContent1 = findViewById(R.id.testContent1);
        testContent2 = findViewById(R.id.testContent2);

        mListTv = findViewById(R.id.result1);

        mGetButton = findViewById(R.id.button1);
        mGetButton.setOnClickListener(this);
        mPostButton = findViewById(R.id.button2);
        mPostButton.setOnClickListener(this);
        mPatchButton = findViewById(R.id.button3);
        mPatchButton.setOnClickListener(this);
        mDeleteButton = findViewById(R.id.button4);
        mDeleteButton.setOnClickListener(this);
        
		//BASE_URL기준으로 레트로핏 Call 객체 획득 
        initMyAPI(BASE_URL);
    }

    @RequiresApi(api = Build.VERSION_CODES.O)
    @Override
    public void onClick(View v) {
        //GET버튼 클릭시 
        if( v == mGetButton){
            Log.d(TAG,"GET");
            //Service이용해서 CALL 보낸다 
            Call<List<PostItem>> getCall = mMyAPI.get_diary();
		//Call 객체 네트워킹 시킴 
            getCall.enqueue(new Callback<List<PostItem>>() {
                @Override
                //반응 오면 onResponse
                public void onResponse(Call<List<PostItem>> call, Response<List<PostItem>> response) {
                //반응이 성공이면 
                    if( response.isSuccessful()){
                        List<PostItem> mList = response.body();
                        String result ="";
                        for( PostItem item : mList){
                            result += "title : " + item.getTitle() + " text: " + item.getContent() + "\n";
                        }

                        mListTv.setText(result);
                    }else {
                        Log.d(TAG,"Status Code : " + response.code());
                    }
                }

                @Override
                public void onFailure(Call<List<PostItem>> call, Throwable t) {
                    Log.d(TAG,"Fail msg : " + t.getMessage());
                }
            });
        }
        
        //EditText의 입력 내용을 받아서 데이터 필드에 저장 
        diary_title = testContent1.getText().toString();
        diary_content = testContent2.getText().toString();
        //POST버튼 클릭시 
        if(v == mPostButton){
            Log.d(TAG,"POST");
            //모델 객체 item생성 
            PostItem item = new PostItem(diary_id,diary_title, diary_date, diary_weather,
                    diary_content, diary_todayme, diary_tomorrowme,diary_img);
            
            //Service이용해서 CALL 보낸다 
            Call<PostItem> postCall = mMyAPI.post_diary(item);
	//Call 객체 네트워킹 시킴
            postCall.enqueue(new Callback<PostItem>() {
                @Override
                //반응 오면 onResponse
                public void onResponse(Call<PostItem> call, Response<PostItem> response) {
                //반응이 성공이면 
                    if(response.isSuccessful()){
                        Log.d(TAG,"등록 완료");
                    }else {
                        Log.d(TAG, item.getDiary_id()+item.getTitle());
                        //안될 경우 item에 저장값 보기위한 로그출력 
                        Log.d(TAG,new Gson().toJson(item));
                        //상태코드 로그 출력 
                        Log.d(TAG,"Status Code : " + response.code());                       

                    }
                }

                @Override
                public void onFailure(Call<PostItem> call, Throwable t) {
                    Log.d(TAG,"Fail msg : " + t.getMessage());
                }
            });
          
        }        
    }
}

이 예제의 경우 Post 에러를 많이 만났던 이유는 diary.models.py에서 선언한 diary_img 필드가 ImageField로 지정되어있는데 안드로이드에서 Call 객체로 보낼 때 ""가 저장된 String으로 보내려 해서 그랬던 것이었다.
초기값을 ""로 지정한 것을 그냥 선언만 시켜 null저장 했을 땐 오류없이 동작했다.

그럼 안녕히..

profile
HelloWorld! 같은 실수를 반복하지 말기위해 적어두자..

0개의 댓글