티스토리 뷰

JAVA에서 HTTP 통신을 하기위한 대표적인 라이브러리가 HttpUrlConnection이 있는데 이걸로 API를 개발했을때는 소스코드도 굉장히 길어지고 가독성도 좋지않으며 Type Safety하지않아 JSON으로 받은데이터를 GSON으로 일일이 Convert 해줘야했다. 그런와중에 Retrofit이란걸 알게되었는데 Retrofit을 처음봤을때 코드가 간결하고 가독성이 있고 사용하기도 편해서 이번장에서 정리해보려고 한다. JAVA는 11버전으로 한다.

 

라이브러리 추가

JAVA에서 Retrofit API를 사용하기위해선 총 6가지의 라이브러리가 필요하다.

<!-- 1 -->
<dependency>
    <groupId>com.squareup.retrofit2</groupId>
    <artifactId>converter-gson</artifactId>
    <version>2.5.0</version>
</dependency>
<!-- 2 -->
<dependency>
    <groupId>com.google.code.gson</groupId>
    <artifactId>gson</artifactId>
    <version>2.8.8</version>
</dependency>
<!-- 3 -->
<dependency>
    <groupId>org.jetbrains.kotlin</groupId>
    <artifactId>kotlin-stdlib</artifactId>
    <version>1.5.31</version>
</dependency>
<!-- 4 -->
<dependency>
    <groupId>com.squareup.okhttp3</groupId>
    <artifactId>okhttp</artifactId>
    <version>4.9.1</version>
</dependency>
<!-- 5 -->
<dependency>
    <groupId>com.squareup.okio</groupId>
    <artifactId>okio</artifactId>
    <version>2.5.0</version>
</dependency>
<!-- 6 -->
<dependency>
    <groupId>com.squareup.retrofit2</groupId>
    <artifactId>retrofit</artifactId>
    <version>2.5.0</version>
</dependency>
  • 1 : Retrofit 객체를 생성할 때 GsonConverFactory를 등록하기위해 필요하다.
  • 2 : HTTP API를 요청해서 받아온 JSON 데이터를 Java객체로 역직렬화 해주기 위해 gson 라이브러리가 필요하다.
  • 3 : Retrofit API의 내부코드를 살펴보면 kotlin 코드로 이뤄진부분이 있는데 해당부분을 실행하기 위해서 필요하다.
  • 4 : Retrofit API내부에 okhttp3 라이브러리를 참조한다.
  • 5 : Retrofit API내부에 okio 라이브러리를 참조한다.
  • 6 : Retrofit API를 사용하기 위해서 필요하다.

6가지의 라이브러리를 추가한다. 그리고 이 예제에서 파일전송에 대한 예제코드도 나오는데 프로젝트 경로하위에 이미지파일 하나만 넣어두자. 필자는 java.JPG 이미지를 넣었다.

Member class 생성

public class Member {
    private Long id;
    private String name;
    private int age;

    public Long getId() {
        return id;
    }

    public String getName() {
        return name;
    }

    public int getAge() {
        return age;
    }

    @Override
    public String toString() {
        return "Member{" +
                "id=" + id +
                ", name='" + name + '\'' +
                ", age=" + age +
                '}';
    }
}

API를 통해 받아온 데이터를 해당클래스에 매핑한다. Gson으로 Convert할꺼기 때문에 Getter or Setter만 있으면 된다.

MemberDTO class 생성

package example.retrofit;

public class MemberDTO {
    private String name;
    private int age;

    public String getName() {
        return name;
    }

    public int getAge() {
        return age;
    }

    public MemberDTO(String name, int age) {
        this.name = name;
        this.age = age;
    }
}

Retrofit으로 HTTP API 호출할 때 해당 DTO를 넘긴다.

Retrofit 설정

import com.google.gson.Gson;
import retrofit2.Retrofit;
import retrofit2.converter.gson.GsonConverterFactory;

public class RetrofitClient {
    private final Retrofit retrofit;

    private RetrofitClient() {
        this.retrofit = new Retrofit.Builder().baseUrl("http://localhost")
                .addConverterFactory(GsonConverterFactory.create())
                .build();
    }

    private static class RetrofitClientLazyHolder {
        private static final RetrofitClient INSTANCE = new RetrofitClient();
    }

    public static RetrofitClient getInstance() {
        return RetrofitClientLazyHolder.INSTANCE;
    }

    public <T> T create(Class<T> service) {
        return this.retrofit.create(service);
    }
}

여기서 중요한부분은 baseUrl 메서드 인자에 endpoint baseUrl이 오면된다. 그리고 create 메서드엔 Class 타입을 받는데 Class가 아니라 interface를 넘겨줘야한다. interface를 넘기지 않으면 예외가 발생된다.

Retrofit interface 생성

import retrofit2.Call;
import retrofit2.http.GET;

import java.util.List;

public interface MemberService {
    @GET("/members")
    Call<List<Member>> findByAll();
}

설명을 안해도 될 정도로 간단하다. findByAll 메서드를 호출하면 GET /members로 요청이 전달된다. baseUrl은 위에 정의한 값이고 그 뒤에는 path만 정의해주면 된다.

중요한건 리턴타입을 Call 제네릭타입으로 해줘야한다.

Retrofit 사용법

import java.io.IOException;
import java.util.List;

public class Main {
    public static void main(String[] args) {
        try {
            RetrofitClient retrofitClient = RetrofitClient.getInstance();
            MemberService memberService = retrofitClient.create(MemberService.class); // 1

            List<Member> members = memberService.findByAll().execute().body(); // 2
            System.out.println(members.toString());

        } catch (IOException e) {
            e.printStackTrace();
        }
    }
}

create 메서드 인자로 넘어간 MemberService는 위에 작성된 interface이며 2번의 execute 메서드가 실행되면 API를 호출하여 body 메서드를 통해 결과값을 받아온다.



public static void main(String[] args) {
    RetrofitClient retrofitClient = RetrofitClient.getInstance();
    MemberService memberService = retrofitClient.create(MemberService.class);

    Call<List<Member>> members = memberService.findByAll();
    members.enqueue(new Callback<List<Member>>() {
        @Override
        public void onResponse(Call<List<Member>> call, Response<List<Member>> response) {
            List<Member> members = response.body();
        }
        @Override
        public void onFailure(Call<List<Member>> call, Throwable throwable) {

        }
    });
}

성공 및 실패여부에 따라 예외처리를 하고싶으면 해당코드처럼 Call< T > 타입으로 받아와서 처리해주면 된다.

Retrofit 지원기능 살펴보기

@GET("/member/{id}")
Call<Member> findByPathId(@Path("id") Long id);

@GET("/member")
Call<Member> findByQueryId(@Query("id") Long id);

위에는 @Path("key")로 url path에다 세팅하고 밑에는@Query("key")로 쿼리스트링에 세팅한다.

 

@POST("/member")
Call<Member> createMember(@Body MemberDTO memberDTO);

body에 데이터를 담아전송할 땐 @Body 어노테이션을 사용한다. Spring에서는 @RequestBody로 받으면된다.

 

지금까지 작성한 코드로 테스트를 돌려보자

API 서버 세팅

import com.example.retrofit.dto.Member;
import org.springframework.web.bind.annotation.*;
import org.springframework.web.multipart.MultipartFile;

import java.util.ArrayList;
import java.util.List;

@RestController
public class MemberController {

    public static List<Member> members = new ArrayList<>();

    static {
        members.add(Member.builder().id(1L).name("kim-jong-hyun").age(28).build());
        members.add(Member.builder().id(2L).name("kim-jong-hyun").age(28).build());
    }

    @GetMapping("/members")
    public List<Member> members() {
        return members;
    }

    @GetMapping("/member/{id}")
    public Member member(@PathVariable Long id) {
        return members
                .stream()
                .filter(member -> member.getId() == id)
                .findFirst().get();
    }

    @GetMapping("/member")
    public Member member2(@RequestParam Long id) {
        return members
                .stream()
                .filter(member -> member.getId() == id)
                .findFirst().get();
    }

    @PostMapping("/member")
    public Member member3(@RequestBody Member member) {
        Member createMember = Member
                .builder()
                .id((long)members.size()+1)
                .name(member.getName())
                .age(member.getAge())
                .build();
        members.add(createMember);
        return createMember;
    }
 }

여기서 한가지 팁을 알려드리자면 위에 @Query 어노테이션으로 /member를 호출할 때 "id" 라는 key로 요청을 보냈으면 받는쪽에서는 @RequestParam("key")로 받거나 @RequestParam으로 받는 변수명이 key와 일치하면 자동으로 매핑된다.



정상적으로 처리되었다. JSON데이터를 주고받았지만 파일을 전송해야할 때 도 있는데 파일전송도 알아보자



@Multipart
@POST("/file")
Call<String> createFile(@Part MultipartBody.Part uploadFile);

파일전송은 contentType이 application/json이아니라 multipart/form-data로 전송되기때문에 @Multipart 어노테이션을 메서드 위에 붙혀주고 매개변수로 MultipartBody.Part 타입을 넘겨주고 @Part 어노테이션을 붙혀준다.



public static void main(String[] args) {
    try {
        RetrofitClient retrofitClient = RetrofitClient.getInstance();
        MemberService memberService = retrofitClient.create(MemberService.class);

        File file = new File(System.getProperty("user.dir") + "\\java.JPG");
        RequestBody requestBody = RequestBody.create(file, MediaType.parse("multipart/form-data"));
        MultipartBody.Part uploadFile = MultipartBody.Part.createFormData("file", "java.JPG", requestBody);

        String fileName = memberService.createFile(uploadFile).execute().body();
        System.out.println("fileName => " + fileName);
    } catch (IOException e) {
        e.printStackTrace();
    }
}

여기서 중요한건 createFormData 메서드인데 첫번째인자엔 파라미터명을 정의한다. 두번째는 파일명을 적고 세번째는 전송할 파일이 담긴 RequestBody객체를 넣어준다. 파일객체를 생성해서 RequestBody에담고 FormData를 생성해서 FormData로 전송한다. 파일을 전송하고 파일명을 받아보자.

 

@PostMapping("/file")
public String createFile(@RequestPart("file") MultipartFile multipartFile) {
    return multipartFile.getOriginalFilename();
}

API 서버에 endpoint를 추가하고 테스트를 해보자.



아래와 같은 예외가 발생했을 것이다. JSON으로 파싱하다가 예외가 발생된건데 String은 단순 text이므로 JSON으로 파싱할 수 없다. RetrofitClient생성자 내부코드를 다음처럼 변경하자.

 

private RetrofitClient() {
    Gson gson = new Gson().newBuilder().setLenient().create();
    this.retrofit = new Retrofit.Builder().baseUrl("http://localhost")
            .addConverterFactory(GsonConverterFactory.create(gson))
            .build();
}

 

정상적으로 파일이 전송된 걸 확인할 수 있다. 근데 파일을 전송할 때 파일명을 원본파일명이 아닌 다른파일명으로도 전송할 수 있다.


근데 API를 개발하다보면 문자열 데이터와 파일을 한번에 받아야할 때가 있는데 Retrofit에서도 해당기능을 제공해준다.

@Multipart
@POST("/member")
Call<Member> createMember(@Part MultipartBody.Part uploadFile, @PartMap Map<String, RequestBody> body);

아까 만들었던 멤버생성 API인데 매개변수를 다음과 같이 수정하자. 두번째 인자에 Map 타입의 매개변수가 있는데 key에는 파라미터명 value에는 RequestBody객체가 있는데 RequestBody안에는 각각 key에 해당하는 value를 세팅을 하면되고 @PartMap 어노테이션을 선언해주면된다.

 

@PostMapping("/member")
public Member member3(Member member, @RequestPart("file") MultipartFile multipartFile) {
    Member createMember = Member
            .builder()
            .id((long)members.size()+1)
            .name(member.getName())
            .age(member.getAge())
            .image(multipartFile.getOriginalFilename())
            .build();
    members.add(createMember);
    return createMember;
}

////
public static void main(String[] args) {
    try {
        RetrofitClient retrofitClient = RetrofitClient.getInstance();
        MemberService memberService = retrofitClient.create(MemberService.class);

        // File
        File file = new File(System.getProperty("user.dir") + "\\java.JPG");
        RequestBody requestBody = RequestBody.create(file, MediaType.parse("multipart/form-data"));
        MultipartBody.Part uploadFile = MultipartBody.Part.createFormData("file", UUID.randomUUID() + ".JPG", requestBody);

        // Text
        MemberDTO memberDTO = new MemberDTO("kim-jong-hyun", 28);
        Map<String, RequestBody> body = new HashMap<>();
        body.put("name", RequestBody.create(memberDTO.getName(), MediaType.parse("text/plain")));
        body.put("age", RequestBody.create(String.valueOf(memberDTO.getAge()), MediaType.parse("text/plain")));
        Member member3 = memberService.createMember(uploadFile, body).execute().body();

        System.out.println(member3.toString());
    } catch (IOException e) {
        e.printStackTrace();
    }
}

Client쪽과 API 소스를 위처럼 수정하고 양쪽 Memeber 객체에 image필드를 추가해서 업로드된 파일이름을 추가하고 해당값이 잘 받아와지는지 Client쪽 Member클래스에 toString 메서드에 image필드도 추가하자.



정상적으로 받아와졌다. 데이터를 파일과 텍스트를 한번에 요청할 때는 요청값들을 하나씩 처리해줘야하는 단점이 있지만 지금은 main 한곳에서 샘플데이터 코드가 있다보니 해당부분을 빼고 실제로 클라이언트로부터 값을 받아서 처리하게되면HttpUrlConnection으로 작업했을때보다는 코드가 더 간결해지고 가독성이 좋아질것 같다.

728x90
댓글
공지사항
최근에 올라온 글
최근에 달린 댓글
Total
Today
Yesterday
링크
TAG
more
«   2024/05   »
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 31
글 보관함