본문 바로가기

Build/MSA

자바, 스프링 내부망 통신 1편: 서버간 통신에 쓰일 라이브러리

1. 소개

기본적인 멀티스레드/블록킹 환경에서의 Http 요청에 대해 다루어 보려고 합니다. MSA구조를 배워가면서 스프링 환경에서의 서블릿을 이용한 통신 이외에 자연스럽게 서버간 내부 통신에 대한 니즈가 생겼고 다양한 Http Connection 방법들이 있어서 각자의 특징과 쓰임을 분석해보려고 작성하였습니다. 

 

기술들이 생겨나며 보여지는 특징은 각 기술들의 추상화 수준입니다. 특히 저수준에서 고수준으로 올라가면서 기술의 영역에서 비즈니스 영역으로 차츰 추상화 하는 방식으로 진화해왔습니다. 

 

2.  기술의 진화

A. URL Connection: <추상화 수준: URL Connection>

 

자바 1.0 초기부터 있었던 가장 기본적인 레벨의 URL Connection입니다. Http로 특정 되지 않았기에 URL의 주소를 FTP등으로 바꾸어 다른 프로토콜로의 통신도 가능합니다. 

 

HTTP통신을 하고 그 값을 사용하는 과정을 개인적으로 나열해 보았는데 아래 4가지 과정을 통해 이루어 진다고 생각해 이 과정을 비교하여 앞으로 4가지 단계별 차이점을 비교해 보겠습니다.

 

 1. URI 제작: 주소값을 바꿔서 다른 프로토콜로 요청할수도 있습니다.

 

 2. 연결 및 프로토콜 세부 내용 작성. (헤더, 바디) : 연결설정/ 데이터 전송 파트를 보면 알겠지만 수동으로 처리 해 주어야 됩니다. 심지어 POST, GET 같은 부분도 HTTP의 언어로 설정을 하는게 아닌 URL Connection의 설정을 통하여 하여 주어야 합니다. 

 

3. 응답읽기: 스트림이나 쿼리 스트링을 수동으로 파싱합니다.

 

4. 객체로 파싱하여 사용: 이부분은 코드로 없지만 수동으로 Object Mapper등을 통해 파싱해주어야 합니다. 

 

네이티브 언어 정도는 아니겠지만 평소에 사용하는 것보다 수동으로 설정해야 하는게 매우 많지만 덕분에 자유도가 높습니다. 다른 부분도 그렇지만 심지어 프로토콜 선택이 가능한 점과 HTTP언어가 아닌 URL Connection 설정을 통해 설정들을 해주어야 한다는 점이 눈에 띄어 볼드 처리하였습니다. 

 

import java.io.BufferedReader;
import java.io.InputStreamReader;
import java.io.OutputStream;
import java.net.URL;
import java.net.URLConnection;

public class URLConnectionExample {
    public static void main(String[] args) {
        try {
           //출력 스트림을 사용하는 POST예제.
        
            // 1. URL 객체 생성
            URL url = new URL("http://example.com");

            // URLConnection 객체 생성
            URLConnection connection = url.openConnection();

            //2. 연결 설정
            connection.setDoOutput(true); // 출력 스트림 사용 설정 (POST)
            connection.setRequestProperty("Content-Type", "application/json");
            connection.setRequestProperty("Accept", "application/json");

            //2. 요청 데이터 전송 (POST 요청의 경우)
            String jsonInputString = "{\"key\":\"value\"}";
            try (OutputStream os = connection.getOutputStream()) {
                byte[] input = jsonInputString.getBytes("utf-8");
                os.write(input, 0, input.length);
            }

            //3. 응답 읽기
            try (BufferedReader br = new BufferedReader(
                    new InputStreamReader(connection.getInputStream(), "utf-8"))) {
                StringBuilder response = new StringBuilder();
                String responseLine;
                while ((responseLine = br.readLine()) != null) {
                    response.append(responseLine.trim());
                }
                System.out.println("Response: " + response.toString());
            }

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

 

B. HTTP URL Connection <추상화 수준: HTTP>

 

HTTP URL Connection은 URL Connection을 상속한 객체로 자바 1.1 부터 등장한 HTTP 관련 기능이 추가된 객체입니다. HTTP 특정 메서드들이 추가되며 설정이 편해졌으나 프로토콜 선택의 자유도는 낮아졌습니다. HTTP 프로토콜 사용에 특화 되면서 HTTP 관련 객체로 추상화 수준이 높아졌습니다. 추가된 대표적인 메서드들은 아래와 같습니다.

 

  • setRequestMethod(String method): 허용된 HTTP 메서드 설정
  • getReesponseCode() / getResponseMessage() : HTTP 응답코드와 메세지 받기
  • setFixedLengthStreamingMode(): 요청 본문 길이 설정
  • setInstanceFollowRedirects(boolean followRedirects) : 자동 리다이렉트 설정
  • getInstanceFollowRedirects(): 현재 자동 리다이렉트 설정 확인
  • setAuthenticator(Authenticator auth): HTTP인증 위한 Authenticator 추가
  • getHeaderField(int n)/ getHeaderFieldKey(int n): n번째 헤더 필드에 접근

이렇게 리다이렉트, HTTP 응답코드 얻기, 헤더 접근등 HTTP 설정이나 값을 얻는 HTTP 관련 메서드들이 생겼습니다. URL Connection에서 형변환 하여 사용하게 되고(아래 예시 처럼) HTTP에 특정하여 통신을 하게 되고 다른 프로토콜을 사용시 런타임에 IO Exception을 던집니다.

 

다시 4단계 과정을 통해 좀더 자세히 알아보자면 2단계에서 HTTP요청을 사용하기 더 편해졌습니다.

 

 1. URI 제작: HTTP 주소를 사용하여야 합니다.

 

 2. 연결 및 프로토콜 세부 내용 작성. (헤더, 바디) :  HTTP 관련 메서드가 생겼습니다.

 

3. 응답읽기: 아직 수동입니다.

 

4. 객체로 파싱하여 사용: 여전히 수동입니다. 

import java.io.BufferedReader;
import java.io.InputStreamReader;
import java.io.OutputStream;
import java.net.HttpURLConnection;
import java.net.URL;

public class URLConnectionExample {
    public static void main(String[] args) {
        String urlString = "http://example.com/api"; 
        String jsonInputString = "{\"key\":\"value\"}"; 

        try {
            // 1. URL 객체 생성
            URL url = new URL(urlString);

            // 1. URLConnection 객체 생성 및 형변환
            HttpURLConnection connection = (HttpURLConnection) url.openConnection();

            // 2. 요청 설정
            connection.setRequestMethod("POST"); // 요청 메서드 설정 (GET, POST, PUT, DELETE 등)
            connection.setRequestProperty("Content-Type", "application/json; utf-8");
            connection.setRequestProperty("Accept", "application/json");
            connection.setDoOutput(true); // 출력 스트림 사용 설정

            // 2. 요청 본문 작성
            try (OutputStream os = connection.getOutputStream()) {
                byte[] input = jsonInputString.getBytes("utf-8");
                os.write(input, 0, input.length);
            }

            // 3. 응답 코드 확인
            int responseCode = connection.getResponseCode();
            System.out.println("Response Code: " + responseCode);

            // 3. 응답 읽기
            try (BufferedReader br = new BufferedReader(
                    new InputStreamReader(connection.getInputStream(), "utf-8"))) {
                StringBuilder response = new StringBuilder();
                String responseLine;
                while ((responseLine = br.readLine()) != null) {
                    response.append(responseLine.trim());
                }
                System.out.println("Response: " + response.toString());
            }

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

 

 

C. Rest Template <추상화 수준: REST API>

 

스프링 3.0부터 생긴 기능으로 spring web 모듈에 있습니다. 자바에서 RestFul API를 사용하기 더 쉽게 각 과정들이 객체와 메서드들로 추상화 되었습니다. 

 

4가지 과정을 통해 좀 더 살펴 보자면

 

 1. URI 제작: HTTP 요청을 적습니다. *UriComponentBuilder는 이전 기술에서도 사용가능합니다.

 

 2. 연결 및 프로토콜 세부 내용 작성. (헤더, 바디) :  HTTP Entity를 통해 설정 후 getForEntity때 연결합니다. Rest Template객체에 설정 정보를 모으고 또한 HTTP 통신도 합니다

 

3. 응답읽기: getForEntity로 4번과 함께 수행됩니다. 내부적으로는 스트림을 쓰거나 쿼리 스트링을 파싱합니다.

 

4. 객체로 파싱하여 사용: getForEntity로 3번과 함께 수행됩니다. JSON과 XML의 파싱이 매우 쉬워졌습니다.

 

HTTP 설정은 HTTP Entity 객체를 통해 헤더와 본문으로 나누어 편하게 수정가능하고 Rest Template을 통해 2번 연결 과정과 응답을 읽는 과정과 REST API에서 많이 쓰이는 JSON/XML 파싱 작업이 getForEntity라는 단일 메서드로 묶이면서 매우 간편하게 요청을 주고 받을 수 있습니다.

 

    	//1. URI 제작 
    	URI uri = UriComponentsBuilder
                .fromUriString("http://127.0.0.1")
                .path("/test/server")
                .queryParam("name", "tester")
                .queryParam("age", 20)
                .encode()
                .build()
                .toUri();

        // 2. 설정
        HttpHeaders headers = new HttpHeaders();
        headers.setContentType(MediaType.APPLICATION_JSON);

        // 2. HttpEntity 객체 생성
        HttpEntity<String> request = new HttpEntity<>(requestBody, headers);

        RestTemplate restTemplete = new RestTemplate();

        //3. 응답읽기 4. 객체로 파싱
        ResponseEntity<User> result = restTemplete.getForEntity(uri, request, User.class);
        //getForObject을 통해 String이나 Integer로 값을 받을 수도 있습니다.
        System.out.println(result.getStatusCode());
        System.out.println(result.getBody());

 

D. Feign Client <추상화 수준: 비즈니스 로직> 


Feign Client는 Netflix에서 처음에 만들어진 선언적 웹 서비스 클라이언트입니다. 이후 스프링 클라우드 패키지로 들어가서 관리되고 있습니다. 

 

사용성이 너무나 높아진 Rest Template이지만 비동기 불가, 넌블록킹 불가등의 기술적인 한계외에도 비즈니스 로직에서 바라본다면 HTTP 통신 관련 코드가 주요 비즈니스 로직은 아닙니다. 그렇기에 Rest Template의 코드는 여전히 반복적인 면이 많고 비즈니스적인 면을 다루지 않는 코드가 늘어나는 단점이 있습니다. 

 

Feign은 여기서 한단계 더 나아가 비즈니스 로직을 담고 있는 인터페이스 레벨의 메서드만으로 HTTP 통신을 사용할수 있게 해주는 라이브러리가 Feign Client입니다. 또한, DI를 통해 주입 하게 되어서 테스트 하게도 쉬워집니다. 거기다가 Feign은 스프링 클라우드 Eureka와 사용시 마이크로 서비스의 각 위치(IP)도 제공 받을 수 있기에 서버간 통신을 통합 테스트 하기도 쉽습니다.

 

4가지 과정으로 본다면

 1. URI 제작: 인터페이스에서 어노테이션으로 제작합니다.

 

 2. 연결 및 프로토콜 세부 내용 작성. (헤더, 바디) :  필요시 Config나 application.yml으로 설정합니다.

 

3. 응답읽기, 4. 객체로 파싱하여 사용: 인터페이스 내용을 바탕으로 런타임에 프록시를 DI 합니다. 

 

작성해야되는 코드들을 없애 버리고 인터페이스와 설정 파일로 대체하고 스프링 DI를 이용해 런타임에 프록시 클래스를 동적 로딩하여 주입하여 줍니다. (Spring Data JPA와 같은 원리 입니다.)

 

-1. Build Gradle 스프링 클라우드 관련 부분 추가

//이미 없다면 스프링 클라우드 의존성 부분 추가 
dependencyManagement {

    imports {
        mavenBom "org.springframework.cloud:spring-cloud-dependencies:2023.0.0"
    }

}

//dependencies 부분에 추가
implementation 'org.springframework.cloud:spring-cloud-starter-openfeign'

 

-2. 필요시 Config나 application.yml으로 추가 설정

@Configuration
public class FeignConfig {
    @Bean
    public Request.Options requestOptions() {
        return new Request.Options(5000, 5000);
    }

    @Bean
    public Logger.Level feignLoggerLevel() {
        return Logger.Level.FULL;
    }

    @Bean
    public ErrorDecoder errorDecoder() {
        return new CustomErrorDecoder();
    }
}

 

-3. 인터페이스에서 메서드 만들기

package com.example.accountapi.application.tools.client;

import org.springframework.cloud.openfeign.FeignClient;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.PostMapping;

@FeignClient(name = "order-api")
public interface OrderServiceClient {
    @PostMapping("/orderFeignTesting")
    ResponseEntity<String> orderTesting();
}

 

-4. @EnableFeignClients 추가

*basepackages 추가는 필수 사항은 아닙니다.

@SpringBootApplication
@EnableFeignClients(basePackages = "com.example.accountapi.application.tools.client")
public class AccountApiApplication {

    public static void main(String[] args) {
        SpringApplication.run(AccountApiApplication.class, args);
    }


}

 

-5. 서비스에서 사용

사용할 곳에 Client를 스프링 DI 해주면 사용할 수 있습니다. 

@Service
public class UserService {
    private final ExampleClient exampleClient;

    @Autowired
    public UserService(ExampleClient exampleClient) {
        this.exampleClient = exampleClient;
    }

    public User getUser(Long id) {
        return exampleClient.getUser(id);
    }
}

 

이렇게 장점이 많은 Feign Client 이지만 HTTP에러의 로깅과 응답에 불편한 점도 있다는 것을 알게 되었습니다. 자세한 설명은 jifrozen님의 블로그를 참고 바랍니다.(jiFrozen님)

 

3. 결론

Feign Client를 이용하면 스프링 DI와 Annotation을 통해 편리하게 HTTP 통신이 가능하나 스프링 클라우드의 사용이 익숙하지 않다면 처음에 러닝 커브가 있을 수 있습니다. MSA구조에서 사용시 다른 서버의 IP를 얻을 수 있는 등의 장점이 있지만 Rest API의 요청 정도라면 Rest Template을 통해서 구현하는 것도 괜찮을 것 같습니다. 쉬운 통합 테스팅은 힘들 수 있지만 HTTP통신은 외부 의존성이기에 유닛 테스트로 진행하면 모킹을 할 수도 있고 꼭 테스트를 해봐야 한다면 내부 통신이나 API사용하는 부분을 모아 클래스를 만들어 메서드를 가져와 테스트를 할 수 있습니다. 코드 레벨에서 비즈니스 로직과 섞이는 것만 조심한다면 간단한 API요청에서는 Rest Template이 좋은 선택일듯 합니다. 그래서 간단한 API 요청은 Rest Template을 MSA간 통신에는 Feign Client를 사용해 보려 합니다. 

 

 

출처: 

YUNHO님: MSA 서비스간 통신시 RestTemplate vs. FeignClient

Should I use HttpURLConnection or RestTemplate?

Contributor9님: [Java] Spring Boot Web 활요이 RestTemplate 이해하기

jiFrozen님:[Dining-together] Microservice간 통신(resttemplate vs. feign client)

jjang-a님: SpringBoot OpenFeign(FeignClient)사용하기

IT이야기님: [요약] Java URLConnection과 HttpURLConnection 사용 방법

Spring Doc: RestTemplate

Spring Cloud Doc: Feign