티스토리 뷰

반응형

 작은 규모의 서비스를 개발한다면 서버와 클라이언트 간 호출이 대부분일 것이다. 하지만 더 큰 서비스가 될 수록 서버와 서버간 HTTP 호출이 필요해진다. 서버 아키택처가 MSA와 같은 형태라면 정말 많이 사용할 것이다.


 Spring boot에서는 다른 서버의 API endpoint를 호출할 때 RestTemplate을 많이 쓴다. 이 글에서는 RestTemplate을 활용하여 다른 서버를 호출하는 서비스 예제를 다뤄볼 것이다.





 시작하기 전에, RestTemplate을 잘 모른다면 여기를 참고하자. 이 예제는 JAVA 11로 작성되었으며 프로젝트의 전체 내용은 Github에 공유되어있다.



1. 설정하기

먼저 Spring boot를 사용하기 위해 build.gradle을 아래와 같이 설정한다.

plugins {
    id 'io.spring.dependency-management' version '1.0.8.RELEASE'
    id 'org.springframework.boot' version '2.2.2.RELEASE'
    id 'java'
}

group = 'com.resttemplate'
version = '0.0.1-SNAPSHOT'
sourceCompatibility = '11'

repositories {
    mavenCentral()
}

def springBootVersion = '2.2.2.RELEASE'


dependencies {
    implementation group: 'org.springframework.boot', name: 'spring-boot-starter-web', version: springBootVersion
    testCompile group: 'org.springframework.boot', name: 'spring-boot-starter-test', version: springBootVersion
}

매우 기본적인 설정이다. 이제 RestTemplate의 Bean 설정을 해준다. 매번 RestTemplate을 new 키워드로 생성하는 것은 번거롭고 안전하지 않다. Spring에서 제공하는 DI를 활용하는 것이 좋다.

@Configuration
public class RestTemplateConfig {

    @Bean
    public RestTemplate restTemplate(RestTemplateBuilder restTemplateBuilder) {
        return restTemplateBuilder
                .requestFactory(() -> new BufferingClientHttpRequestFactory(new SimpleClientHttpRequestFactory()))
                .setConnectTimeout(Duration.ofMillis(5000)) // connection-timeout
                .setReadTimeout(Duration.ofMillis(5000)) // read-timeout
                .additionalMessageConverters(new StringHttpMessageConverter(Charset.forName("UTF-8")))
                .build();
    }
}

RestTemplateBuilderRestTemplate Bean 설정을 해준 모습이다. 이 예제에서는 Timeout 설정과 MessageConverter 설정만 했는데, 그 외 Interceptor를 끼워넣는 등 여러가지 설정을 할 수 있다.



2. Wrapping

 이제 API를 호출하는 부분을 만들어보자. RestTemplate은 API 호출 기능을 하는 다양한 메소드를 제공하는데, 이번 예제에서는 exchange()를 쓸 것이다. exchange()의 장점은 HTTP Method에 상관없이 사용할 수 있다는 것이다. 그래서 나는 exchange()를 선호한다.


@Service
public class ApiService<T> {

    private RestTemplate restTemplate;

    @Autowired
    public ApiService(RestTemplate restTemplate) {
        this.restTemplate = restTemplate;
    }

    public ResponseEntity<T> get(String url, HttpHeaders httpHeaders) {
        return callApiEndpoint(url, HttpMethod.GET, httpHeaders, null, (Class<T>)Object.class);
    }

    public ResponseEntity<T> get(String url, HttpHeaders httpHeaders, Class<T> clazz) {
        return callApiEndpoint(url, HttpMethod.GET, httpHeaders, null, clazz);
    }

    public ResponseEntity<T> post(String url, HttpHeaders httpHeaders, Object body) {
        return callApiEndpoint(url, HttpMethod.POST, httpHeaders, body,(Class<T>)Object.class);
    }

    public ResponseEntity<T> post(String url, HttpHeaders httpHeaders, Object body, Class<T> clazz) {
        return callApiEndpoint(url, HttpMethod.POST, httpHeaders, body, clazz);
    }

    private ResponseEntity<T> callApiEndpoint(String url, HttpMethod httpMethod, HttpHeaders httpHeaders, Object body, Class<T> clazz) {
        return restTemplate.exchange(url, httpMethod, new HttpEntity<>(body, httpHeaders), clazz);
    }
}

 @Autowired를 활용하여 RestTemplate을 주입받도록 처리했다. 굳이 이렇게 개발한 이유는 필요할 때마다 '다양한' RestTemplate을 주입받기 위함이다. RestTemplate은 Bean객체이므로 BeanFactory에서 생성되면 설정이 고정된다. 그래서 동적으로 RestTemplate의 설정을 변경할 수 없다.


 예를 들면, 외부 호출 시마다 Timeout을 다르게 설정하고 싶을 때는 RestTemplate의 빈 설정을 여러개 하는 수밖에 없다. 이럴 때 위와 같은 구조(RestTemplate을 주입받는 구조)에서는 서비스마다 내가 원하는 RestTemplate을 주입할 수 있다.


 한편, ApiService에 제네릭이 선언되어 있는 것이 보일 것이다. 이는 HTTP 요청에 대한 응답 Body의 타입을 의미한다. 이를 제네릭으로 선언함으로써, 다양한 타입으로 응답Body를 받아 처리 할 수 있다.



 다만 이부분은 조금 불완전하다. Spring에서 Default로 사용하는 (De)Serializer인 Jackson이 알 수 없는 타입은 LinkedHashMap으로 강제 전환해버리기 때문이다. 이는 제네릭이 컴파일 타임에는 별 효력이 없기 때문이기도 한데, 자세한 것은 여기를 참고하자. 



3. 사용 예제

 이제 위에서 정의한 서비스를 사용하는 예제를 만들어보자. 다른 서버를 호출하는 예제이므로, 호출할만한 서버가 필요하다. 직접 만들기는 조금 귀찮으니까 여러분께 숙련된 조교를 소개한다. 바로 Postman-echo이다.


 Postman은 API를 직접 호출해볼 수 있는 툴로 유명하다. 그리고 자체적으로 echo 서버를 제공하기도 한다. 자세한 것은 여기를 참고하자. 이 예제는 https://postman-echo.com/post 를 사용할 것이고 POST 요청을 거의 그대로 돌려주는 endpoint이다. 요청 및 응답 예제는 아래와 같다.


Request Body

{
    "name": "jake",
    "age": 10,
    "info": {
        "gender": "male"
    }
}

Response Body

{
    "args": {},
    "data": {
        "name": "jake",
        "age": 10,
        "info": {
            "gender": "male"
        }
    },
    // 후략
}

ResponseBody.data에 RequestBody를 그대로 돌려주는 것이 보일 것이다. 그럼 이제 이 endpoint를 호출하는 예제를 작성해보자. 먼저 요청 Body Class와 응답 Body Class를 소개한다.

public class Person {
    private String name;
    private int age;
    private Map<String, String> info = new HashMap<>();
    
    // Getter & Setter
}

 info를 Map으로 선언해서 자유롭게 데이터를 넣을 수 있게 처리했다.

@JsonIgnoreProperties(ignoreUnknown = true)
public class Response {
    private Person data;
    private Map<String, String> headers;

    public Person getData() {
        return data;
    }

    public void setData(Person data) {
        this.data = data;
    }
}
 맨 위의 어노테이션은 이 클래스에 정의되지 않은 필드가 있을 경우에 무시하겠다는 뜻이다. PostMan-echo의 응답은 꽤 복잡한데, 이를 다 정의하기가 좀 귀찮아서 저렇게 해봤다.


 이렇게 모델을 정의했으니, 이제 ApiService를 쓰기만 하면 된다!
@Service
public class RestTemplateTestService {

    private ApiService<Response> apiService;

    @Autowired
    public RestTemplateTestService(ApiService<Response> apiService) {
        this.apiService = apiService;
    }

    public Response callPostExternalServer() {
        Person person = new Person();
        person.setName("jake");
        person.setAge(10);
        person.addInfo("gender", "male");

        return apiService.post("https://postman-echo.com/post", HttpHeaders.EMPTY, person, Response.class).getBody();
    }
}

생성자를 잘 째려보자. ApiService를 주입받을 때 제네릭을 활용했다. 이렇게하면 외부 호출의 결과로 온 ResponstEntitygetBody()를 호출했을 때 Response.class의 형태로 응답을 받을 수 있다. 만약 응답 결과를 Map으로 처리하고 싶으면 ApiService<Map<String, Object>> 로 선언하면 된다.



4. 마무리

 지금까지 RestTemplate을 wrapping 하는 예제를 간단하게 둘러봤다. RestTemplate을 필요할 때마다 주입받아 그 때 그 때 exchange()로 호출해서 사용해도 동작에 지장은 없을 것이다. 하지만 이렇게 정리해서 쓰면 반복되는 소스를 줄일 수 있고, 여러명이 협업할 때 실수를 줄일 수 있을 것이다.


 그런데 아직 RestTemplate의 Error Handling처리를 하지 않았다. 외부 서버에서 내가 원치 않는 응답을 줄 수도 있다. 이것과 관련해서 다른 포스팅에서 다루도록 하겠다.




-끝-




아이콘 제작자 Freepik from www.flaticon.com



«   2021/10   »
          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            
글 보관함
Total
743,146
Today
48
Yesterday
236