Home 개인 프로젝트 MSA 전환 - (4) OpenFeign을 활용한 서비스 간 통신 구현
Post
Cancel

개인 프로젝트 MSA 전환 - (4) OpenFeign을 활용한 서비스 간 통신 구현


MSA 구조에서는 서비스들이 독립적으로 운영되기 때문에, 서로 간에 데이터를 주고받을 일이 굉장히 많습니다.
이번 글에서는 서비스 간 통신을 훨씬 더 쉽게 만들어주는 OpenFeign을 활용해서, 실제로 어떻게 구현했는지 정리해보려 합니다.



✅ MSA 환경에서의 서비스 간 통신

MSA에서는 다음과 같은 다양한 방식으로 다른 서비스에 요청을 보낼 수 있습니다.

  • REST Template : 과거 가장 많이 쓰던 HTTP 클라이언트
  • WebClient : 비동기 통신을 지원하는 Spring WebFlux 기반 클라이언트
  • Feign Client : 인터페이스 기반 선언형 HTTP 클라이언트


📌 RestTemplate의 한계

RestTemplate은 오랫동안 표준처럼 사용됐지만, 직접 HTTP 요청 객체를 만들고, 예외처리하고, 실패 복구까지 모두 신경 써야 했습니다.

더불어 스프링 5 이후부터는 공식적으로 WebClient 사용이 권장되고 있습니다.

문제점

  • 코드가 장황하고 중복이 많아짐
  • 장애 복구 로직이 복잡함
  • 서비스 이름 기반 연결이 불편함 (Eureka 연동 불편)
  • 비동기 처리의 복잡함

그래서 더 간결하고 유연한 방식이 필요했습니다.


📌 Feign Client의 등장

Feign Client는 이런 문제를 아주 깔끔하게 해결할 수 있습니다.

  • HTTP 요청을 마치 메서드 호출하듯 선언가능
  • Eureka 연동을 통한 서비스 이름 기반 호출
  • 코드 간결화 및 유지보수성 향상
  • Ribbon, Resilience4j와 통합 가능




✅ OpenFeign 이란?

OpenFeign은 Netflix Feign 프로젝트를 기반으로 Spring Cloud에서 공식 지원하는 선언형 HTTP 클라이언트입니다.
Java 인터페이스와 어노테이션을 통해 쉽게 REST API 호출을 정의할 수 있으며,
복잡한 HTTP 통신 로직을 직접 작성하지 않고도 서비스 간 통신을 간결하게 구현할 수 있습니다.

📌 OpenFeign의 주요 특징

  • 선언형 API 호출
  • Eureka, Ribbon, Resilience4j와 통합
  • 자동 로드밸런싱 지원
  • Fallback으로 장애 복구 지원
  • Decoder, Retry, Timeout 같은 고급 커스터마이징 가능

📌 OpenFeign을 사용하는 이유

MSA에서는 서비스 IP나 포트가 자주 바뀌기 때문에, 직접 URL이나 포트를 지정하여 요청을 보내는건 적합하지 않습니다.

OpenFeign을 사용하면, IP 대신 서비스 이름으로 통신할 수 있고,
통신 실패 시 자연스럽게 대체 로직으로 복구하고 코드도 훨씬 깔끔하게 유지할 수 있습니다.

결론적으로, MSA 아키텍처에 가장 잘 어울리는 통신 방법이라 선택하게 됐습니다.




✅ OpenFeign 고급 기능

기능설명
Fallback서비스 실패 시 대체 로직 처리 (서킷브레이커)
Timeout 설정요청/응답 타임아웃 조정
Retry 설정실패 시 재시도 횟수 설정
Error Decoder커스텀 에러 핸들링 구현

📌 Fallback 설정 예시

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
@FeignClient(
        name = "user-service",
        path = "/api/v1/user/",
        fallback = UserFeignFallback.class
)
public interface UserFeignClient {
    @GetMapping("/user-info")
    UserApiResponse<UserFeignResponse> getUserInfo(@RequestParam String email);
}

@Component
public class UserFeignFallback implements UserFeignClient {
    @Override
    public UserApiResponse<UserFeignResponse> getUserInfo(String email) {
        return new UserApiResponse<>(500, "Fallback: 사용자 정보 조회 실패", null);
    }
}

📌 Timeout 설정 예시

1
2
3
4
5
6
7
@Configuration
public class UserFeignConfig {
    @Bean
    public Request.Options requestOptions() {
        return new Request.Options(5000, 10000); // 연결 타임아웃 5초, 응답 타임아웃 10초
    }
}

📌 Retry 설정 예시

1
2
3
4
5
6
7
@Configuration
public class UserFeignConfig {
    @Bean
    public Retryer retryer() {
        return new Retryer.Default(1000, 5000, 3); // 초기 대기 시간 1초, 최대 대기 시간 5초, 최대 재시도 횟수 3회
    }
}

📌 Error Decoder 설정 예시

1
2
3
4
5
6
7
8
9
10
11
12
13
14
@Configuration
public class UserFeignConfig {
    @Bean
    public ErrorDecoder errorDecoder() {
        return new CustomErrorDecoder();
    }
}
public class CustomErrorDecoder implements ErrorDecoder {
    @Override
    public Exception decode(String methodKey, Response response) {
        // 커스텀 에러 처리 로직
        return new BaseException("Feign Error: " + response.status());
    }
}

📌 Decoder 설정 예시

1
2
3
4
5
6
7
8
9
10
@RequiredArgsConstructor
public class UserApiResponseDecoder implements Decoder {

    private final ObjectMapper objectMapper;

    @Override
    public Object decode(Response response, Type type) throws IOException {
        return objectMapper.readValue(response.body().asInputStream(), objectMapper.getTypeFactory().constructType(type));
    }
}




✅ OpenFeign 적용하기

UserService ↔ AccountBookService 간 통신을 OpenFeign으로 구현합니다.


📌 1. 의존성 추가

1
2
3
dependencies {
    implementation 'org.springframework.cloud:spring-cloud-starter-openfeign'
}

📌 2. @EnableFeignClients 추가

1
2
3
4
5
6
7
@EnableFeignClients
@SpringBootApplication
public class AccountBookApplication {
  public static void main(String[] args) {
    SpringApplication.run(AccountBookApplication.class, args);
  }
}

📌 3. Feign Client 인터페이스 작성

1
2
3
4
5
6
7
8
9
10
11
@FeignClient(
        name = "user-service",
        path = "/api/v1/user/",
        configuration = UserFeignConfig.class
)
public interface UserFeignClient {

    @GetMapping("/user-info")
    UserApiResponse<UserFeignResponse> getUserInfo(@RequestParam String email);
}

4. 📌 서비스를 호출하는 곳에서 선언 및 사용

1
2
3
4
5
6
7
8
9
10
priavte final UserFeignClient userFeignClient;

public UserFeignResponse testUserInfo(String email) {
  UserApiResponse<UserFeignResponse> userInfo = userFeignClient.getUserInfo(email);

  if (userInfo.getCode() != 200) {
    throw new BaseException(userInfo.getMessage());
  }
        return userInfo.getData();
    }



✅ OpenFeign 통신 흐름

img_1.png




✅ OpenFeign 응답 커스텀 마이징

기본적으로 OpenFeign은 서버로부터 받은 HTTP 응답을 자동으로 디코딩하여 객체로 변환해줍니다.
하지만, 제 프로젝트에서는 다음과 같은 특수한 형태의 응답 포맷을 사용하고 있습니다.

1
2
3
4
5
6
7
8
{
  "code": 200,
  "message": "성공",
  "data": {
    "name": "홍길동",
    "email": "hong@example.com"
  }
}

code : 응답 코드 (성공/실패 판단)
message : 응답 메시지
data: 실제 응답 데이터

📌 문제점

Feign의 기본 디코더는 data 필드까지 자동으로 파싱해주지 않습니다.
제가 원하는 형태는 data필드의 값을 객체로 변환하여 응답받는 것이기 때문에 이에 맞게 코드를 추가하도록 하겠습니다.

📌 해결 방법

  1. UserApiResponse → 공통 응답 포맷을 담기 위한 Generic 클래스
1
2
3
4
5
6
7
8
9
10
11
12
13
14
@Getter
@Setter
@NoArgsConstructor
public class UserApiResponse<T> {
    private int code;
    private String message;
    private T data;

    public boolean isSuccess() {
        return code == 200;
    }
}


  1. UserApiResponseDecoder → Feign 응답을 읽어와 ObjectMapper를 사용해 UserApiResponse로 변환
1
2
3
4
5
6
7
8
9
10
@RequiredArgsConstructor
public class UserApiResponseDecoder implements Decoder {

    private final ObjectMapper objectMapper;

    @Override
    public Object decode(Response response, Type type) throws IOException {
        return objectMapper.readValue(response.body().asInputStream(), objectMapper.getTypeFactory().constructType(type));
    }
}
  1. UserFeignConfig → Feign Client에 이 디코더를 적용
1
2
3
4
5
6
7
8
9
@Configuration
public class UserFeignConfig {

    @Bean
    public Decoder feignDecoder(ObjectMapper objectMapper) {
        return new UserApiResponseDecoder(objectMapper);
    }
}

  1. UserFeignClient -> configuration에 UserFeignConfig를 추가
1
2
3
4
5
6
7
8
9
10
@FeignClient(
    name = "user-service",
    path = "/api/v1/user/",
    configuration = UserFeignConfig.class
)
public interface UserFeignClient {

    @GetMapping("/user-info")
    UserApiResponse<UserFeignResponse> getUserInfo(@RequestParam String email);
}
  1. UserFeignResponse→ 실제 응답 데이터의 형태를 담기 위한 클래스
1
2
3
4
5
public record UserFeignResponse(
    String name,
    String email
) {}

위와같이 data 필드의 값만 객체로 변환하여 응답받을 수 있도록 구현했습니다.
서버별로 응답값이 다를 수 있기 때문에, 위와 같이 각 서버에 맞는 decoder를 구현하여 사용하면 됩니다.

📌 정리

역할설명
UserApiResponse<T>서버로부터 오는 공통 응답 형태를 표현
UserApiResponseDecoderJSON 응답을 UserApiResponse<T>로 변환
UserFeignConfigOpenFeign에 커스텀 디코더 등록
UserFeignClientUserService와 통신할 인터페이스
UserFeignResponse실제 필요한 사용자 정보만 추출하는 DTO

이렇게 설계함으로써 얻을 수 이점은 다음과 같습니다.

  • 비즈니스 코드에서는 응답 객체를 직접 파싱하거나 변환하는 불필요한 로직을 제거
  • isSuccess() 메서드로 간편하게 응답 성공 여부를 체크할 수 있음
  • 추후 다양한 서비스들과 통신할 때 일관된 응답 포맷 관리가 가능




✅ 마치며

이번 포스팅에서는 OpenFeign을 이용해 서비스 간 통신을 선언형으로 쉽게 구현하는 방법을 다뤘습니다.

MSA 환경에서는 각 서비스가 독립적이기 때문에, 서비스 간 통신을 얼마나 깔끔하게 설계하느냐가 유지보수성과 안정성에 큰 영향을 미칩니다.

OpenFeign을 통해 서비스 간 연결을 최소한의 코드로 구성하면서, 서비스 확장성까지 고려할 수 있게 되었습니다.

다음 포스팅에서는 config 서버를 활용한 설정 파일 관리에 대해 정리하겠습니다.

This post is written by PRO.

개인 프로젝트 MSA 전환 - (3) Service Discovery + Api Gateway 적용

개인 프로젝트 MSA 전환 - (5) Config Server를 활용한 설정 관리