본문 바로가기

Mobile/Android

[Android] 안드로이드 스프링부트 서버 통신

아. 날라가서 다시 쓴다.

 

스프링부트 서버 구축 진행 내용 요약

 

우선, AWS RDS 인스턴스를 생성하고, MySQL DB를 추가 구성으로 생성한 후 스프링부트 프로젝트에 연결했다.

 

MySQL Workbench에서도 호스트로 접속하여 워크벤치 내에서 user 테이블과 간단한 회원가입을 테스트할 용도로 

 

[identity(PK), name, id, password, passwordCheck] 와 같이 5개의 컬럼을 생성했다.

 

워크벤치에서 테이블을 생성한 후 스프링부트 프로젝트에서 실행한 결과 생성된 테이블이  반영된 것을 확인할 수 있었다.

 

스프링부트와 DB 연결을 완료하고, 테스트를 목적으로 간단한 컨트롤러와 연결되는 html 페이지를 생성하여 로컬 상에서 서버를 실행해 보았다.

 

그후, AWS EC2 서버에 만들어 놓은 스프링부트 프로젝트를 배포하는 과정을 거쳤다.

 

배포를 완료한 후! 외부에서 url 로 접속한 결과, 스프링부트 서버를 로컬에서 실행했을 때 연결되는 테스트 페이지와 같은 결과를 보이는 것을 확인할 수 있었다.

 

다음으로 해야할 일은, 궁극적인 목표인 안드로이드와 스프링부트 서버 통신이다.

 

스케줄러 앱에서는 백엔드를 맡아 안드로이드를 다룰 일이 없겠지만, 나름 안드로이드를 해온, 그리고 아직도 하는 사람으로서

 

안드로이드와 스프링부트 서버 통신까지 빨리 해보고 싶었다! 

 

 

Retrofit을 활용한 안드로이드 스프링부트 서버 통신

  1.  Retrofit 이란? - A type-safe HTTP client for Android and Java

 

    1)  Retrofit 은 서버와 클라이언트 간 http 통신을 위한 라이브러리

 

      -  통신을 편리하게 해주는 라이브러리인 OKhttp 를 보다 편하고 쉽게 사용하기 위해 만든 것

 

      -  Retrofit은 OKhttp를 기반으로 만들어져, Retrofit 라이브러리를 추가하면 Okhttp 메서드도 사용 가능  

 

    2)  기본적으로 백그라운드에서 실행되며, callback을 통해 Main Thread 에서 UI 업데이트를 간단히 할 수 있음

 

       -  그러나, 이 방법 보다는 유지 보수성과 예외 처리 부분에서 효율적인 RxJava, Coroutine 과 같이 구현하는 방식을 선호

 

    3)  Retrofit을 통해 JSON / XML 데이터를 쉽게 처리 가능 및 POJO(순수한 자바 객체)로 파싱

 

 

  2.  Retrofit 의 장점

 

    1)  속도가 기존 Okhttp 에 비해 빠름

 

    2)  HttpUrlConnection 보다 구현이 간단

 

    3)  동기/비동기 구현 편리

 

 

  3.  HTTP 통신 기본 개념

 

    1)  어떤 주소로 요청을 보내야 하는가?

 

       -  Request url 로 요청을 보내며, 이는 http의 header에 담기게 됨

 

    2)  어떤 형태로 응답을 받아야 하는가?

 

       -  json이나 html 형태

 

    3)  어떤 형태로 요청을 해야 하는가?

 

       -  GET, POST, PUT, DELETE 의 네 가지 메서드

 

    4)  어떤 파라미터를 가지고 요청해야 하는가?

 

      -  Request url에 포함되는 파라미터를 의미!

 

 

Retrofit 환경 세팅

 

  1.  build.gradle 추가

 

    implementation 'com.squareup.retrofit2:retrofit:2.6.0'
    implementation 'com.squareup.retrofit2:converter-gson:2.6.0'
    implementation 'com.google.code.gson:gson:2.8.6'
    implementation 'com.squareup.okhttp3:logging-interceptor:3.11.0'

 

  2.  Manifest 추가

 

    1)  통신을 위한 인터넷 사용 권한 추가

 

    2)  http로 시작하는 사이트에 접근하기 위해 usesCleartextTraffic="true" 설정

 

      -  안드로이드는 기본적으로 http 접근을 허용하지 않음

 

      -  만약 https를 지원하는 사이트와 통신할 거면 적지 않아도 됨

 

 

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

 

Retrofit 사용하기

 

  1.  API Interface 정의 - RetrofitService.class

 

    1)  GET 또는 POST 중 어떤 방식으로 주고받을지

 

      -  POST 방식으로 통신하기 위해 @POST 어노테이션 추가

 

    2)  Header와 Body에는 어떤 것이 들어가야 하는지

 

       -  userSignUp() 은 Request 통신 시 UserData 타입을 body로 가진다.

 

    3)  어떤 형식으로 Response를 받을지 작성

 

      -  response 타입은 Call<ResponseBody>

import com.example.androidspringconnectiontest.data.SignUpData;
import com.example.androidspringconnectiontest.data.SignUpResponse;

import retrofit2.Call;
import retrofit2.http.Body;
import retrofit2.http.POST;

public interface RetrofitService {
    // 회원가입
    @POST("/user/signup")
    Call<SignUpResponse> userSignUp(@Body SignUpData data);

}

 

 

  2.  데이터 클래스 정의

 

    -  서버에 전송하거나, 응답으로 받아올 데이터를 클래스로 정의

 

    -  RequestParam과 ResponseParam을 정의하는 것

 

    1)  SignUpData - 회원가입 요청 시 보낼 데이터

 

import com.google.gson.annotations.SerializedName;

public class SignUpData {
    /**
     * SerializedName 으로 JSON 객체와 해당 변수를 매칭
     * @SerializedName 괄호 안에는 해당 JSON 객체의 변수 명 적기
     * 이때, POST 매핑으로 받아올 값은, 굳이 annotation 을 붙이지 않고, JSON 객체의 변수명과 일치하는 변수만 선언하면 됨
     */
    @SerializedName("name")
    private String name;
    @SerializedName("id")
    private String id;
    @SerializedName("password")
    private String password;
    @SerializedName("passwordCheck")
    private String passwordCheck;

    public SignUpData(String name, String id, String password, String passwordCheck) {
        this.name = name;
        this.id = id;
        this.password = password;
        this.passwordCheck = passwordCheck;
    }
}

 

    2)  SignUpResponse - 회원가입 요청에 대한 응답으로 돌아올 데이터

 

import com.google.gson.annotations.SerializedName;

public class SignUpResponse {

    @SerializedName("code")
    private int code;

    @SerializedName("message")
    private String message;

    public int getCode() {
        return code;
    }

    public String getMessage() {
        return message;
    }
}

 

 

  3.  Retrofit 객체 생성 - RetrofitClient.class

 

    (1)  MainActivity에서 Retrofit 선언하기

 

       -  MainActivity에서 Retrofit 객체 생성

 

    ->  그러나, 만약 서버 호출이 필요할 때마다 인터페이스를 구현해야 한다면, 매우 비효율적일 것

 

    ->  따라서 Client 파일은 싱글톤으로 따로 Retrofit 객체를 생성하는 것이 바람직하다.

 

      +  싱글톤(Singleton pattern)

 

     - 싱글톤 패턴을 따르는 클래스는 생성자가 여러 차례 호출되어도 실제로 생성되는 객체는 하나이고,

 

     - 최초 생성 이후 호출된 생성자는 최초의 생성자가 생성한 객체를 리턴

 

     - 주로, 공통된 객체를 여러 개 생성해서 사용하는 DBCP(DataBase Connection Pool)와 같은 상황에서 많이 사용

 

     - 하나의 객체로 앱이 종료될때까지 여러 번 생성되지 않고 자원을 아끼면서 레트로핏 통신 가능

 

 

    (2)  RetrofitService 인터페이스와 동일한 package인 network에 RetrofitClient.class 추가

 

      1)  API 호출 시 사용하게 되는 첫 번째 클래스

 

        -  Retrofit 객체를 생성하고, RetrofitService 클래스를 이후 MainActivity.class 에서 회원가입 버튼 클릭을 통한 API 호출 시 연결 

      2)  사용되는 메서드

 

        -  baseUrl() :  서비스에서 사용할 루트를 설정하는 메서드 -> 이전에 할당받은 public dns

 

        -  addConverterFactory() : JSON을 분석할 수 있는 객체를 추가하는 메서드

 

import com.google.gson.Gson;
import com.google.gson.GsonBuilder;

import retrofit2.Retrofit;
import retrofit2.converter.gson.GsonConverterFactory;

public class RetrofitClient {

    private final static String BASE_URL = "http://ec2-3-84-71-239.compute-1.amazonaws.com:8080/";

    public static RetrofitService getApiService() {
        return getClient().create(RetrofitService.class);
    }

    public static Retrofit getClient() {
        Gson gson = new GsonBuilder().setLenient().create();
        return new Retrofit.Builder()
                .baseUrl(BASE_URL)
                .addConverterFactory(GsonConverterFactory.create(gson))
                .build();
    }
}

 

 

  4.  MainActivity 에서 회원가입 구현 

 

    -  간단하게, [이름/아이디/비밀번호/비밀번호 확인]을 입력 한 후 회원가입 버튼을 클릭할 경우 회원가입 요청을 보내도록 화면 설계

 

간단한 회원가입 폼

 

    (1)  RetrofitService 객체 생성

 

      -  인터페이스에서 미리 정의해두었던 메서드(userSignUp)를 호출하면 서버에 요청을 보내게 된다.

 

      -  enqueue()에 파라미터로 넘긴 콜백에서는 통신이 성공/실패 했을 때 수행할 동작을 재정의

 

RetrofitService service = RetrofitClient.getClient().create(RetrofitService.class);

 

    (2)  통신 성공여부에 따른 호출 메서드

 

      1)  onResponse() : 통신에 성공할 경우 호출되는 메서드

 

        -  response 객체에 응답으로 돌려받은 데이터가 들어있음   

 

      2)  onFailure() : 통신에 실패할 경우 호출되는 메서드

 

import androidx.appcompat.app.AppCompatActivity;

import android.os.Bundle;
import android.util.Log;
import android.view.View;
import android.widget.Button;
import android.widget.EditText;
import android.widget.TextView;
import android.widget.Toast;

import com.example.androidspringconnectiontest.R;
import com.example.androidspringconnectiontest.data.SignUpData;
import com.example.androidspringconnectiontest.data.SignUpResponse;
import com.example.androidspringconnectiontest.network.RetrofitClient;
import com.example.androidspringconnectiontest.network.RetrofitService;

import retrofit2.Call;
import retrofit2.Callback;
import retrofit2.Response;

public class MainActivity extends AppCompatActivity {

    private EditText name;
    private EditText id;
    private EditText password;
    private EditText passwordCheck;
    private Button signUpButton;
    private RetrofitService service;

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

        TextView signUpText = (TextView) findViewById(R.id.signUpText);
        name = (EditText) findViewById(R.id.name);
        id = (EditText) findViewById(R.id.id);   // email 형태 : AutoCompleteTextView
        password = (EditText) findViewById(R.id.password);
        passwordCheck = (EditText) findViewById(R.id.passwordCheck);
        signUpButton = (Button) findViewById(R.id.signUpButton);

        service = RetrofitClient.getClient().create(RetrofitService.class);

        signUpButton.setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View view) {
                attemptSignUp();
            }
        });

    }

    // 유효성 검사 및 회원가입 메서드 호출
    private void attemptSignUp() {
        name.setError(null);
        id.setError(null);
        password.setError(null);
        passwordCheck.setError(null);

        boolean cancel = false;
        View focusView = null;

        String nameText = name.getText().toString();
        String idText = id.getText().toString();
        String passwordText = password.getText().toString();
        String passwordCheckText = passwordCheck.getText().toString();

        // 이름 유효성 검사사
        if (nameText.isEmpty()) {
            name.setError("이름을 입력해주세요.");
            focusView = name;
            cancel = true;
        }

        // 아이디 유효성 검사
        if (idText.isEmpty()) {
            id.setError("아이디 입력해주세요.");
            focusView = id;
            cancel = true;
        }

        // 비밀번호 유효성 검사
        if (passwordText.isEmpty()) {
            password.setError("비밀번호를 입력해주세요.");
            focusView = password;
            cancel = true;
        } else if (!isPasswordValid(passwordText)) {
            password.setError("6자 이상의 비밀번호를 입력해주세요.");
            focusView = password;
            cancel = true;
        }

        if (cancel) {
            focusView.requestFocus();
        } else {
            startSignUp(new SignUpData(nameText, idText, passwordText, passwordCheckText));
        }

    }

    // 회원가입 처리
    private void startSignUp(SignUpData data) {
        // enqueue()에 파라미터로 넘긴 콜백 - 통신이 성공/실패 했을 때 수행할 동작을 재정의
        service.userSignUp(data).enqueue(new Callback<SignUpResponse>() {
            @Override
            public void onResponse(Call<SignUpResponse> call, Response<SignUpResponse> response) {
                SignUpResponse result = response.body();
                Toast.makeText(MainActivity.this, result.getMessage(), Toast.LENGTH_SHORT).show();

                if (result.getCode() == 200) {
                    finish();
                }
            }

            @Override
            public void onFailure(Call<SignUpResponse> call, Throwable t) {
                Toast.makeText(MainActivity.this, "회원가입 에러 발생", Toast.LENGTH_SHORT).show();
                Log.e("회원가입 에러 발생", t.getMessage());

            }
        });
    }

    private boolean isPasswordValid(String passwordText) {
        return passwordText.length() >= 8;
    }

}

 

 

참고

  -  https://choi-dev.tistory.com/53

  -  https://todaycode.tistory.com/41

  -  https://velog.io/@eeheaven/AndroidStudio-SpringBoot-KnockKnock-%EA%B0%9C%EB%B0%9C%EC%9D%BC%EC%A7%80-0118-%EC%95%88%EB%93%9C%EB%A1%9C%EC%9D%B4%EB%93%9C-%EC%8A%A4%ED%8A%9C%EB%94%94%EC%98%A4%EC%8A%A4%ED%94%84%EB%A7%81%EB%B6%80%ED%8A%B8-%EC%97%B0%EA%B2%B0

  -  https://m.blog.naver.com/zion830/221661486117