일 | 월 | 화 | 수 | 목 | 금 | 토 |
---|---|---|---|---|---|---|
1 | 2 | |||||
3 | 4 | 5 | 6 | 7 | 8 | 9 |
10 | 11 | 12 | 13 | 14 | 15 | 16 |
17 | 18 | 19 | 20 | 21 | 22 | 23 |
24 | 25 | 26 | 27 | 28 | 29 | 30 |
- android ar 개발
- 안드로이드 유닛 테스트 예시
- 2022 플러터 설치
- 스택 큐 차이
- 2022 플러터 안드로이드 스튜디오
- rxjava disposable
- 멤버변수
- 안드로이드 유닛 테스트
- jvm이란
- 서비스 vs 쓰레드
- 안드로이드 유닛테스트란
- 스택 자바 코드
- rxjava cold observable
- rxjava hot observable
- jvm 작동 원리
- 안드로이드 라이선스 종류
- Rxjava Observable
- 안드로이드 레트로핏 crud
- 안드로이드 라이선스
- 자바 다형성
- 큐 자바 코드
- 객체
- 서비스 쓰레드 차이
- 플러터 설치 2022
- android retrofit login
- 안드로이드 레트로핏 사용법
- ar vr 차이
- ANR이란
- 안드로이드 os 구조
- 클래스
- Today
- Total
나만을 위한 블로그
[Android] Retrofit 사용 시 4xx 응답을 파싱하는 방법 본문
레트로핏을 사용하다 보면 서버 응답을 받아오는 부분을 아래와 같이 작성할 수 있다.
repository.method(sendDataMap).enqueue(new Callback<String>() {
@Override
public void onResponse(Call<String> call, Response<String> response) {
if (response.isSuccessful()) {
data.setValue(response.body());
} else {
data.setValue(response.body());
}
}
@Override
public void onFailure(Call<String> call, Throwable t) {
data.setValue(null);
}
});
레트로핏의 onResponse()는 서버 통신이 성공했을 경우 호출되는 콜백 메서드다. 그리고 이 안에서 if로 조건을 걸어 isSuccessful()이 호출됐고 body()가 null이 아닐 경우에만 클라이언트에서 파싱해 사용할 JSON 값을 담는다.
그러나 이 방식을 사용할 경우 받을 수 없는 4xx로 시작하는 HTTP 에러 코드와 같이 넘어오는 메시지를 파싱할 수 없다. null이 들어가서 내 의도와 다른 값을 받게 되는 경우가 발생할 수도 있다.
왜 그런지 설명하기 전에 코드부터 보고 시작하자. 그나마 쓸 수 있는 백엔드 언어가 PHP기 때문에 테스트 API 코드는 PHP로 구성했다.
아래 PHP 파일들은 DB 커넥션과 API 파일이다. 내 경우 컴퓨터에 xampp가 설치돼 있어 로컬 서버 환경이 구축돼 있기 때문에 이를 바탕으로 진행했다. Node.js를 쓰더라도 흐름만 안다면 코드로 작성할 수 있을 것이다.
각 파일명은 파일 내 최상단의 주석에 써놨다.
<?php
// connect.php
$username = "root";
$password = "";
$db = "DB 이름";
$hostname = "localhost";
$con = mysqli_connect($hostname, $username, $password, $db);
if ($con)
{
// echo "register DB 접속 성공";
}
else
{
echo "register DB 접속 실패";
}
<?php
// testapi.php
require 'connect.php';
$user_id = $_POST['user_id'];
if ($con) {
$sql = "SELECT * FROM user WHERE user_id = '$user_id'";
$result = mysqli_query($con, $sql) or die(mysqli_error($con));
if (mysqli_num_rows($result) > 0) {
$status = "성공";
$errorCode = 200;
echo json_encode(array(
'status' => $status,
'errorCode' => $errorCode
), JSON_UNESCAPED_UNICODE);
} else {
$status = "실패";
$errorCode = 400;
http_response_code($errorCode);
echo json_encode(array(
'status' => $status,
'errorCode' => http_response_code($errorCode)
), JSON_UNESCAPED_UNICODE);
}
} else {
$status = "에러";
$resultCode = 500;
echo json_encode(array(
'status' => $status,
'errorCode' => $errorCode
), JSON_UNESCAPED_UNICODE);
}
mysqli_close($con);
API라고 부르기도 부끄러운 코드지만 난 백엔드 개발자가 아니고 안드로이드 개발자기 때문에 이게 최선이라고 변을 늘어놓겠다. 쥐구멍 하나 주세요
아무튼 이렇게 설정하고 안드로이드에서 필요한 설정들을 해주고 레트로핏을 사용하기 위한 클래스, 인터페이스를 만들어준다.
의존성 문구가 없다면 앱 수준 gradle에 아래의 의존성 문구부터 넣고 시작한다.
implementation 'com.squareup.retrofit2:retrofit:2.9.0'
implementation 'com.squareup.retrofit2:converter-gson:2.9.0'
implementation 'com.squareup.retrofit2:converter-scalars:2.9.0'
implementation 'com.squareup.okhttp3:okhttp:4.7.2'
implementation 'com.squareup.okhttp3:logging-interceptor:4.7.2'
인터페이스는 아래와 같이 구성했다.
import retrofit2.Call;
import retrofit2.http.Field;
import retrofit2.http.FormUrlEncoded;
import retrofit2.http.POST;
public interface TestInterface {
@FormUrlEncoded
@POST("testapi.php")
Call<String> testApiCall(
@Field("user_id") String userId
);
}
레트로핏 객체를 설정하는 ApiClient 파일 내용은 아래와 같다.
import android.util.Log;
import com.google.gson.Gson;
import com.google.gson.GsonBuilder;
import okhttp3.OkHttpClient;
import okhttp3.logging.HttpLoggingInterceptor;
import retrofit2.Retrofit;
import retrofit2.converter.gson.GsonConverterFactory;
import retrofit2.converter.scalars.ScalarsConverterFactory;
public class TestApiClient {
private static Retrofit retrofit;
private static final String BASE_URL = "http://10.0.2.2/";
public static Retrofit getRetrofit() {
Gson gson = new GsonBuilder()
.setLenient()
.create();
OkHttpClient client = new OkHttpClient.Builder()
.addInterceptor(setHttpLoggingInterceptor())
// .connectTimeout(AppDefine.CONNECT_TIMEOUT, TimeUnit.SECONDS) // 60 * 1000
// .writeTimeout(AppDefine.WRITE_TIMEOUT, TimeUnit.SECONDS) // 60 * 1000
// .readTimeout(AppDefine.READ_TIMEOUT, TimeUnit.SECONDS) // 60 * 1000
.build();
if (retrofit == null) {
retrofit = new Retrofit.Builder()
.baseUrl(BASE_URL)
.client(client)
.addConverterFactory(ScalarsConverterFactory.create())
.addConverterFactory(GsonConverterFactory.create(gson))
.build();
}
return retrofit;
}
public static HttpLoggingInterceptor setHttpLoggingInterceptor() {
HttpLoggingInterceptor interceptor = new HttpLoggingInterceptor(message -> Log.e("HttpLoggingInterceptor", "message : " + message));
return interceptor.setLevel(HttpLoggingInterceptor.Level.BODY);
}
}
참고로 xampp를 쓸 경우 localhost를 써도 작동하지 않을 수가 있다. 그럴 때는 10.0.2.2를 넣어주면 작동한다.
이제 XML과 자바 파일을 코딩해준다. 결과 확인을 위해 editText, 버튼, 텍스트뷰만 대충 갖다놨다.
<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
tools:context=".MainActivity">
<EditText
android:id="@+id/edittext"
android:layout_width="match_parent"
android:layout_height="wrap_content"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintTop_toTopOf="parent"
app:layout_constraintBottom_toBottomOf="parent"
android:layout_marginStart="20dp"
android:layout_marginEnd="20dp"/>
<Button
android:id="@+id/button"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="40dp"
android:text="API 호출"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintTop_toBottomOf="@+id/edittext"/>
<TextView
android:id="@+id/result_textview"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="30dp"
android:gravity="center"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintTop_toBottomOf="@+id/button"/>
</androidx.constraintlayout.widget.ConstraintLayout>
import android.os.Bundle;
import android.util.Log;
import android.widget.Button;
import android.widget.EditText;
import android.widget.TextView;
import androidx.appcompat.app.AppCompatActivity;
import java.io.IOException;
import retrofit2.Call;
import retrofit2.Callback;
import retrofit2.Response;
public class MainActivity extends AppCompatActivity {
private final String TAG = this.getClass().getSimpleName();
private EditText editText;
private Button button;
private TextView textView;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
editText = findViewById(R.id.edittext);
button = findViewById(R.id.button);
textView = findViewById(R.id.result_textview);
button.setOnClickListener(v -> testMethod(editText.getText().toString()));
}
private void testMethod(String userId) {
TestInterface testInterface = TestApiClient.getRetrofit().create(TestInterface.class);
Call<String> call = testInterface.testApiCall(userId);
call.enqueue(new Callback<String>() {
@Override
public void onResponse(Call<String> call, Response<String> response) {
if (response.isSuccessful()) {
Log.e(TAG, "isSuccessful() : " + response.body());
} else {
Log.e(TAG, "isSuccessful()이 아님 : " + response.body());
}
textView.setText(response.body());
}
@Override
public void onFailure(Call<String> call, Throwable t) {
Log.e(TAG, "에러 : " + t.getMessage());
}
});
}
}
이 상태로 실행하면 아래와 같이 로그가 출력된다.
실패 케이스를 입력할 경우 null이 출력되고 텍스트뷰에도 결과가 set되지 않는다. 4xx 에러를 갖고 오지 못하는 것이다.
그런데 요구사항 중 401 에러를 받을 경우 어떤 문장을 토스트로 출력하라는 요구사항이 있다면 어떻게 해아 할까?
방법은 onResponse() 안의 else 내부 코드를 아래와 같이 바꾸는 것이다.
import android.os.Bundle;
import android.util.Log;
import android.widget.Button;
import android.widget.EditText;
import android.widget.TextView;
import androidx.appcompat.app.AppCompatActivity;
import java.io.IOException;
import retrofit2.Call;
import retrofit2.Callback;
import retrofit2.Response;
public class MainActivity extends AppCompatActivity {
private final String TAG = this.getClass().getSimpleName();
private EditText editText;
private Button button;
private TextView textView;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
editText = findViewById(R.id.edittext);
button = findViewById(R.id.button);
textView = findViewById(R.id.result_textview);
button.setOnClickListener(v -> testMethod(editText.getText().toString()));
}
private void testMethod(String userId) {
TestInterface testInterface = TestApiClient.getRetrofit().create(TestInterface.class);
Call<String> call = testInterface.testApiCall(userId);
call.enqueue(new Callback<String>() {
@Override
public void onResponse(Call<String> call, Response<String> response) {
if (response.isSuccessful()) {
Log.e(TAG, "isSuccessful() : " + response.body());
textView.setText(response.body());
} else {
try {
String body = response.errorBody().string();
Log.e(TAG, "error - body : " + body);
} catch (IOException e) {
e.printStackTrace();
}
}
}
@Override
public void onFailure(Call<String> call, Throwable t) {
Log.e(TAG, "에러 : " + t.getMessage());
}
});
}
}
그럼 이렇게 로그가 찍힌다.
이제 null이 아니라 4xx 에러를 받아서 파싱할 수 있게 됐다.
gif가 아니라 스크린샷으로도 확인해 보자. 먼저 성공 케이스인 aaa를 입력 후 API를 호출하면 로그는 이렇게 나온다.
isSuccessful()이 호출되고 서버에서 받은 응답을 보여주고 있다. 여기서 실패 케이스인 abc를 입력 후 api를 호출하면 로그 내용이 조금 바뀐다.
isSuccessful()이 아니라 try-catch 안의 error - body 부분이 출력된다. 왜 이 부분은 if에 설정한 isSuccessful()에 해당하지 않고 else로 빠져서 출력되는 걸까? 왜 null이 나오지 않을까? 해답은 아래의 공식문서에 있다.
https://square.github.io/retrofit/2.x/retrofit/retrofit2/Response.html#isSuccessful--
isSuccessful() : code()가 200~300 범위에 있으면 true를 반환한다
그렇다. isSuccessful()은 서버 응답이 2~300에 속할 때만 호출되는 메서드다. 그래서 400대의 HTTP 코드를 갖는 서버 응답을 받지 못하고 else로 빠져나가는 것이다.
그래서 4xx 에러 코드와 같이 딸려오는 JSON 값이 로그에 찍히고, 난 이걸 받아서 파싱할 수 있게 된 것이다.
참고로 errorBody()의 설명은 아래와 같다.
errorBody() : 실패한 응답의 원시 응답 본문이다
협업 시 에러코드 별로 어떤 문구를 사용자에게 보여줄지 백엔드 개발자와 약속하고 문서로 남기는 경우가 많다. 이를 잘 정의해 놓고 클라이언트에서 4xx 에러 코드를 갖는 응답을 파싱하지 못한다면 문제가 될 수 있으니, 이 글을 참고해서 4xx 에러 코드를 파싱해 보자.
'Android' 카테고리의 다른 글
[Android] 단위 테스트 시 쉐어드 프리퍼런스를 사용하는 방법 (0) | 2022.02.05 |
---|---|
[Android] 웹뷰에 표시된 Node.js GET 요청을 레트로핏으로 받아오기 (0) | 2022.02.03 |
[Android] Material CalendarView 커스텀 사용법 정리 (0) | 2022.01.23 |
[Android] TextWatcher란? (0) | 2022.01.20 |
[Android] 스피너 커스텀하는 방법 (0) | 2021.12.22 |