[Java] 안드로이드와 웹 서버 간 REST API 통신하기 – Retrofit 활용

2024. 10. 24. 03:29·Java/TIL
반응형

현재 진행하는 프로젝트에서 웹 서버를 구축한 상태로 해당 서버와 통신하는 앱을 만들어야하는 상황인데

추후에 웹 서버와 통신하는 앱을 구축할 일이 있다면 참고하기 위해서 블로그에 업로드 합니다.

해당 글은 GPT의 도움을 받아서 작성한 글이며, 순전히 제가 복기하기 위해서 투고하는 글임을 밝힙니다.

 

1. 서버 응답 준비 - 서버 측의 응답 형식 정의

서버에서는 ResponseDTO 객체를 통해 데이터를 API 클라이언트에 반환합니다.

 

이 ResponseDTO는 다음과 같은 형식을 가집니다.

public class ResponseDTO<T> {
    @SerializedName("api_version")
    private String apiVersion;
    private String status;
    @SerializedName("response_code")
    private Integer responseCode;
    private String message;
    private Integer count;
    private T data;

    // Getters and Setters

 

이 응답 객체는 서버측과 대응되는 형식으로 구성되어 있으며, 서버측에서는 응답의 성공 여부에 따라서 다음과 같은 메서드를 사용해 JSON 형식의 응답을 반환합니다.

    public static ResponseDTO success(String apiVersion, int responseCode, List<?> data) {
        return builder()
                .apiVersion(apiVersion)
                .status("success")
                .responseCode(responseCode)
                .data(data)
                .build();
    }

    public static ResponseDTO error(String apiVersion, int responseCode, String message) {
        return builder()
                .apiVersion(apiVersion)
                .status("error")
                .responseCode(responseCode)
                .message(message)
                .build();
    }
}
  • apiVersion: 서버의 API 버전을 명시
  • responseCode: HTTP 응답 코드(예: 200 OK, 404 Not Found, 500 Internal Server Error)
  • data: 서버가 클라이언트에 전달할 데이터 목록(예: List<ChargerDTO>)

서버의 JSON 응답 구조 ( List<ChargerDTO>)

{
    "apiVersion": "v1",
    "status": "success",
    "responseCode": 200,
    "message": null,
    "count": 2,
    "data": [
        {
            "id": 1,
            "name": "서면행정복지센터",
            "latitude": 36.1529162,
            "longitude": 126.5492647,
            "weekdayOpen": "09:00:00",
            "weekdayClose": "18:00:00",
            "createdAt": "2024-10-17T22:26:53.894333",
            "deletedYn": false}
    ]
}

 

서버는 클라이언트의 요청에 대해서 ResponseDTO<List<ChargerDTO>> 형태의 JSON 응답을 생성하고 반환해주고 있습니다.

 


 

2. Retrofit을 사용해 클라이언트 설정 (Android 앱)

 

 

앱에서는 서버와 통신을 위해 Retrofit 라이브러리를 사용합니다.

이 Retrofit 설정은 다음과 같은 흐름을 따릅니다.

 

APIClient 클래스

public class APIClient {
    private static final String BASE_URL = "<http://10.0.2.2:8080>"; // 로컬 서버 주소
    private static Retrofit retrofit;

    public static Retrofit getRetrofit() {
        if (retrofit == null) {
            // Gson 설정: LocalDate와 LocalDateTime 처리를 위해 TypeAdapter 등록
            Gson gson = new GsonBuilder()
                    .registerTypeAdapter(LocalDate.class, new LocalDateAdapter())
                    .registerTypeAdapter(LocalDateTime.class, new LocalDateTimeAdapter())
                    .serializeNulls()  // null 값도 직렬화
                    .create();

            retrofit = new Retrofit.Builder()
                    .baseUrl(BASE_URL)
                    .addConverterFactory(GsonConverterFactory.create(gson))
                    .build();
        }
        return retrofit;
    }
}
  • Gson: Retrofit은 JSON 데이터를 객체로 변환하기 위해 Gson을 사용합니다.

여기서 일반적이지 않게 느껴지는 부분은, LocalDate와 LocalDateTime의 처리를 위해서 TypeAdapter를 등록한 부분일 것인데, 

 

for (JsonElement recordElement : recordsArray) {
    JsonObject recordObject = recordElement.getAsJsonObject();

    ChargerDTO charger = new ChargerDTO();
    for (Field field : ChargerDTO.class.getDeclaredFields()) {
        field.setAccessible(true);  // private 필드 접근 허용
        String fieldName = fieldMapping.get(field.getName());  // JSON 필드명과 DTO 필드명 매핑
        if (fieldName != null) {
            try {
                String fieldValue = recordObject.get(fieldName).getAsString();  // JSON 데이터 추출
                
                // 데이터 타입별 매핑
                if (field.getType() == String.class) {
                    field.set(charger, fieldValue);
                } else if (field.getType() == Integer.TYPE || field.getType() == Integer.class) {
                    field.set(charger, Integer.parseInt(fieldValue));
                } else if (field.getType() == Double.TYPE || field.getType() == Double.class) {
                    field.set(charger, Double.parseDouble(fieldValue));
                } else if (field.getType() == LocalTime.class) {
                    field.set(charger, LocalTime.parse(fieldValue + ":00"));
                } else if (field.getType() == Time.class) {
                    field.set(charger, Time.valueOf(fieldValue + ":00"));
                } else if (field.getType() == LocalDate.class) {
                    field.set(charger, LocalDate.parse(fieldValue));
                } else if (field.getType() == Date.class) {
                    field.set(charger, Date.valueOf(fieldValue));
                } else if (field.getType() == Boolean.TYPE || field.getType() == Boolean.class) {
                    field.set(charger, fieldValue.equals("Y"));
                }
            } catch (IllegalAccessException | NumberFormatException e) {
                log.warn("타입별 데이터 파싱 중 오류 발생 - 필드명: {}\n{}", field.getName(), e.getStackTrace());
            }
        }
    }
    chargerDtoList.add(charger);
}

 

웹 서버의 구조가 JSON 데이터를 DTO 객체로 변환하면서 각 필드를 Reflection을 통해 매핑하고 있습니다.

여기서 날짜와 시간 데이터의 파싱에서 문제가 발생했었고 해당 문제를 간단하게 말하자면 다음과 같습니다.

 

  • 서버에서는 LocalDate 타입을 기대하지만, JSON에서는 "2023-07-03"처럼 단순 문자열로 전달됨.
  • 서버에서는 LocalDateTime 타입이 필요하지만, JSON에서는 "2024-10-17T22:26:53.894333" 형식으로 넘어옴.

 

이 문제를 해결하기 위해서 서버측에서 Gson의 커스텀 TypeAdapter를 도입했고,

TypeAdapter 를 사용해서 각 날짜/시간 타입에 대해 문자열을 Java의 날짜/시간 객체로 변환하는 로직을 정의했습니다.

 

결국 모바일에서도 해당 부분을 처리해주는 로직이 필요했고 다음과 같이 구현했습니다.

 

 

LocalDateAdapter

public class LocalDateAdapter extends TypeAdapter<LocalDate> {
    private static final DateTimeFormatter formatter = DateTimeFormatter.ISO_LOCAL_DATE;

    @Override
    public void write(JsonWriter jsonWriter, LocalDate localDate) throws IOException {
        if (localDate == null) {
            jsonWriter.nullValue();
        } else {
            jsonWriter.value(localDate.format(formatter));
        }
    }

    @Override
    public LocalDate read(JsonReader jsonReader) throws IOException {
        String date = jsonReader.nextString();  // 문자열에서 날짜 추출
        return LocalDate.parse(date, formatter);  // LocalDate 객체로 변환
    }
}

기능:

  • JSON에서 "2023-07-03" 같은 날짜 문자열을 읽어 LocalDate 객체로 변환
  • JSON으로 변환할 때는 LocalDate 객체를 ISO 형식 문자열로 변환

 

LocalDateTimeAdapter

public class LocalDateTimeAdapter extends TypeAdapter<LocalDateTime> {
    private static final DateTimeFormatter formatter = DateTimeFormatter.ISO_LOCAL_DATE_TIME;

    @Override
    public void write(JsonWriter out, LocalDateTime value) throws IOException {
        if (value != null) {
            out.value(value.format(formatter));
        } else {
            out.nullValue();
        }
    }

    @Override
    public LocalDateTime read(JsonReader in) throws IOException {
        if (in.peek() == JsonToken.NULL) {
            in.nextNull();  // null 값 처리
            return null;
        }
        return LocalDateTime.parse(in.nextString(), formatter);  // 문자열을 LocalDateTime으로 변환
    }
}

기능:

  • JSON의 "2024-10-17T22:26:53.894333" 문자열을 LocalDateTime 객체로 변환
  • null 값 처리: deletedAt 같은 필드가 null인 경우 예외가 발생하지 않도록 처리

 

이후 Gson 설정에 커스텀 Adapter를 등록해주었습니다.

public class APIClient {
    private static final String BASE_URL = "http://10.0.2.2:8080"; // 로컬 개발 서버
    private static Retrofit retrofit;

    public static Retrofit getRetrofit() {
        if (retrofit == null) {
            Gson gson = new GsonBuilder()
                    .registerTypeAdapter(LocalDate.class, new LocalDateAdapter())
                    .registerTypeAdapter(LocalDateTime.class, new LocalDateTimeAdapter())
                    .serializeNulls()  // null 값 직렬화 허용
                    .create();

            retrofit = new Retrofit.Builder()
                    .baseUrl(BASE_URL)
                    .addConverterFactory(GsonConverterFactory.create(gson))
                    .build();
        }
        return retrofit;
    }
}

 

조금은 특수한 상황이었기에 상황에 대한 설명을 추가했지만, 기본적으로 Retrofit 라이브러리를 사용해 웹 서버와 통신하는 방식은 매우 간단하고 직관적입니다.

 

 

 

우선, API 요청을 정의하는 인터페이스에서 HTTP 메서드와 엔드포인트를 선언합니다.

// 서버 API 호출 규칙을 정의하는 인터페이스
public interface ApiService {
    // 서버에서 "/api/v1/items" 엔드포인트로 GET 요청을 보내 데이터를 받아옵니다.
    @GET("/api/v1/items") 
    Call<List<ItemDTO>> getItems();  // 서버가 List<ItemDTO> 형태로 데이터를 응답합니다.
}

 

 

또 Retrofit은 서버와 통신할 준비를 하기 위해 Retrofit 인스턴스를 만들어야 합니다. 이 작업을 APIClient 클래스에서 처리합니다.

  • BASE_URL: 서버와의 통신이 시작되는 기본 URL입니다.
    • 예를 들어, https://example.com/api/v1/items로 요청을 보내려면 BASE_URL이 https://example.com/ 여야 합니다.
  • GsonConverterFactory: 서버에서 주고받는 JSON 데이터를 Java 객체로 변환해 주는 도구입니다.
public class APIClient {
    // 서버의 기본 URL
    private static final String BASE_URL = "https://example.com/";

    // Retrofit 인스턴스를 저장할 정적 변수
    private static Retrofit retrofit;

    // Retrofit 인스턴스를 반환하는 메서드 (싱글톤 패턴 사용)
    public static Retrofit getRetrofit() {
        if (retrofit == null) {  // 아직 인스턴스가 없으면 생성
            retrofit = new Retrofit.Builder()
                    .baseUrl(BASE_URL)  // 서버의 기본 URL 설정
                    .addConverterFactory(GsonConverterFactory.create())  // JSON 직렬화/역직렬화 설정
                    .build();
        }
        return retrofit;  // 이미 생성된 인스턴스를 반환
    }
}

 

 

이제 실제로 서버에 API 요청을 보내고, 응답을 처리하는 코드를 작성해 볼 차례입니다.

// 1. ApiService 인터페이스의 구현체를 만듭니다.
ApiService apiService = APIClient.getRetrofit().create(ApiService.class);

// 2. GET 요청을 보낼 준비를 합니다.
Call<List<ItemDTO>> call = apiService.getItems();

// 3. 서버에 요청을 보내고 응답을 처리합니다.
call.enqueue(new Callback<List<ItemDTO>>() {
    @Override
    public void onResponse(Call<List<ItemDTO>> call, Response<List<ItemDTO>> response) {
        // 서버 응답이 성공적일 경우 실행됩니다.
        if (response.isSuccessful() && response.body() != null) {
            List<ItemDTO> items = response.body();  // 응답 데이터를 받아옵니다.
            Log.d("API Response", "Items: " + items.toString());
        } else {
            // 서버가 오류 메시지를 보낸 경우
            Log.e("API Error", "Response Error: " + response.message());
        }
    }

    @Override
    public void onFailure(Call<List<ItemDTO>> call, Throwable t) {
        // 네트워크 오류가 발생했을 때 실행됩니다.
        Log.e("API Error", "Network Error: " + t.getMessage());
    }
});

 

 

전체적인 통신 과정을 정리해보자면 다음과 같습니다.

Retrofit 통신 과정

1. Retrofit 객체 생성
    - Retrofit.Builder를 사용해 서버의 기본 URL과 Gson과 같은 직렬화 라이브러리를 설정합니다.

2. API 인터페이스 구현
    - create() 메서드를 통해 인터페이스의 인스턴스를 생성하고, 해당 인스턴스를 사용해 API 호출을 준비합니다.

3. API 요청 전송
    - enqueue() 메서드를 통해 비동기로 서버에 요청을 보냅니다.

4. 응답 처리
    - 성공 시: 서버에서 반환된 데이터가 response.body()에 담깁니다.
    - 실패 시: 네트워크 오류나 서버 오류에 대한 메시지가 onFailure()에서 처리됩니다.

3. API 호출 인터페이스 정의

다시 원래 이야기로 돌아와서 저는 서버의 API와의 통신을 위한 인터페이스를 다음과 같이 정의했습니다.

 

ChargerService

public interface ChargerService {
    @GET("/api/v1/chargers")
    Call<ResponseDTO<List<ChargerDTO>>> getChargerData();
}
  • @GET: HTTP GET 요청을 사용해 /api/v1/chargers 경로에서 데이터를 가져옴
  • Call 객체: Retrofit은 서버로부터 응답을 비동기로 가져오며, 응답 데이터는 ResponseDTO 형태로 감싸서 전달됨

4. Android 앱에서 서버로 요청 보내기 및 응답 처리

이제 MainActivity에서 서버로부터 데이터를 가져오고 이를 처리하는 코드를 구현할 차례입니다.

 

MainActivity

public class MainActivity extends AppCompatActivity {
    private ChargerService chargerService;

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

        chargerService = APIClient.getRetrofit().create(ChargerService.class);
        fetchChargerData();
    }

    private void fetchChargerData() {
        // 서버로 GET 요청 보내기
        Call<ResponseDTO<List<ChargerDTO>>> call = chargerService.getChargerData();
        call.enqueue(new Callback<ResponseDTO<List<ChargerDTO>>>() {
            @Override
            public void onResponse(Call<ResponseDTO<List<ChargerDTO>>> call, Response<ResponseDTO<List<ChargerDTO>>> response) {
                if (response.isSuccessful() && response.body() != null) {
                    List<ChargerDTO> chargers = response.body().getData();

                    // JSON 변환 및 로그 출력
                    Gson gson = new GsonBuilder().setPrettyPrinting().create();
                    String jsonResult = gson.toJson(chargers);
                    Log.d("API Response", "Chargers JSON: " + jsonResult);
                } else {
                    Log.e("API Error", "Response Error: " + response.message());
                }
            }

            @Override
            public void onFailure(Call<ResponseDTO<List<ChargerDTO>>> call, Throwable t) {
                Log.e("API Error", "Network Error: " + t.getMessage());
            }
        });
    }
}

 

응답 처리 흐름

  1. 서버 요청: fetchChargerData()에서 서버에 GET 요청을 보냄
  2. 비동기 응답 처리:
    • 성공 (onResponse): 응답이 성공적이고 유효한 경우, response.body().getData()로 데이터 리스트를 가져옴.
    • 실패 (onFailure): 네트워크 오류나 기타 이유로 실패할 경우 오류 메시지를 로그로 출력함.
  3. JSON 변환 및 로그: 서버에서 받은 데이터 리스트를 Gson을 이용해 JSON 문자열로 변환하고 로그로 출력함.

5. 최종 정리

1. 앱에서 서버로 요청 보내기

  • Retrofit을 사용해 서버에 GET 요청을 보냄.
  • 요청을 비동기적으로 처리하므로 UI가 멈추지 않음.

2. 서버에서 응답 반환

  • 서버는 JSON 형식으로 데이터를 반환.
  • 이 응답 데이터는 우리가 정의한 DTO 클래스와 매핑됨.

3. 클라이언트에서 응답 처리

  • onResponse(): 서버 응답이 성공하면 데이터를 받아서 원하는 형식으로 처리가 가능함.
  • onFailure(): 네트워크 오류나 서버 문제가 발생하면 오류를 처리할 수 있음.

4. Gson을 이용한 JSON 변환 (선택)

  • 데이터를 Gson 라이브러리를 이용해 JSON 형식으로 변환하거나 파싱할 수도 있음.

5. 데이터 직렬화/역직렬화 처리 (선택)

  • LocalDate와 LocalDateTime 같은 시간 데이터는 Retrofit이 기본적으로 처리하지 못함.
  • 이런 경우, TypeAdapter를 사용해 직렬화(서버로 보낼 때)와 역직렬화(서버 응답을 객체로 변환할 때)를 수행할 수 있음.

 

반응형
저작자표시 (새창열림)

'Java > TIL' 카테고리의 다른 글

[JAVA-TIL] JWT, JJWT 구현  (0) 2025.10.30
[Java] 일급 컬렉션(First-Class-Collection)이란?  (1) 2024.11.02
'Java/TIL' 카테고리의 다른 글
  • [JAVA-TIL] JWT, JJWT 구현
  • [Java] 일급 컬렉션(First-Class-Collection)이란?
Dongni
Dongni
배우고 느낀 것들, 작은 코드 한 줄부터 일상의 순간까지, 성장의 흔적들을 기록하고자 합니다.
    반응형
  • Dongni
    BitBard
    Dongni
  • 전체
    오늘
    어제
  • 공지사항

    • #include <HelloWorld!.h>
    • 분류 전체보기 (26)
      • 회고 (0)
      • Algorithm (6)
      • BOJ (2)
      • Glitch Guide (2)
      • Database (2)
        • SQL (1)
        • TIL (1)
      • Balloon Map (0)
      • HTTP (1)
        • TIL (1)
      • Java (4)
        • TIL (3)
        • Guide (1)
      • Spring (2)
        • TIL (2)
      • 우아한 테크코스 (6)
        • 일기장 (1)
        • 회고록 (3)
        • 품앗이 (2)
      • Docker (1)
        • TIL (1)
  • 블로그 메뉴

    • 홈
    • 태그
    • 방명록
  • 링크

    • Github
    • Youtube
    • BOJ
  • 인기 글

  • 태그

    springboot
    sql
    java
    스프링
    윈도우 11 와이파이 사라짐
    c++
    코드 56
    스프링부트
    Maps API
    파일컨벤션
    Flyway컨벤션
    윈도우 11 와이파이 연결
    네이버 지도 api
    노트북 와이파이 아이콘 사라짐
    spring
    윈도우 11 와이파이 아이콘
    코딩테스트
    version 23h2
    오물풍선
    windows가 이 디바이스의 클래스 구성을 설치 중입니다. (코드 56)
  • 최근 댓글

  • 최근 글

  • hELLO· Designed By정상우.v4.10.0
Dongni
[Java] 안드로이드와 웹 서버 간 REST API 통신하기 – Retrofit 활용
상단으로

티스토리툴바