FeignClient @FormProperty 트러블 슈팅
FeignClient 란?
Feign Client란 Netflix에서 개발한 Http Client다.
카카오 소셜로그인

카카오 토큰을 받는 API는 x-www-form-urlencoded 타입이 필수값이다.
x-www-form-urlencoded 타입은 Spring에서 @RequestParam, @ModelAttribute 어노테이션으로 주입받을 수 있다.
둘중에 하나를 선택해야하는데 @RequestParam은 사용하고싶지않았다.
@RequestParam 단일 파라메터
@FeignClient(url = "https://kauth.kakao.com", value = "kakaoAuthApi")
public interface KakaoAccessTokenClient {
@PostMapping(
value = "/oauth/token",
consumes = MediaType.APPLICATION_FORM_URLENCODED_VALUE,
produces = MediaType.APPLICATION_JSON_VALUE
)
KakaoAccessTokenResponse getKakaoToken(
@RequestParam("grant_type") String grantType,
@RequestParam("client_id") String clientId,
@RequestParam("redirect_uri") String redirectUri,
@RequestParam("code") String code
);
}
첫번째 방식은 파라메터가 많고 모두 타입이 동일해서 실수할 가능성이 높아서 사용하고싶지않았다. (Builder를 사용하고싶었다)
@RequestParam Map
@FeignClient(url = "https://kauth.kakao.com", value = "kakaoAuthApi")
public interface KakaoAccessTokenClient {
@PostMapping(
value = "/oauth/token",
consumes = MediaType.APPLICATION_FORM_URLENCODED_VALUE,
produces = MediaType.APPLICATION_JSON_VALUE
)
KakaoAccessTokenResponse getKakaoToken(
@RequestParam HashMap<String,String> paramMap
);
}
HashMap<Object, Object> request = new HashMap<>();
request.put("grant_type", grantType);
request.put("client_id", clientId);
request.put("redirect_uri", redirectUri);
request.put("code", code);
두번째 방식은 HashMap으로 HttpClient 이외의 Service 등의 layer에서 구체적인 Api 명세를 알아야해서 사용하고싶지않았다.
@ModelAttribute
@FeignClient(url = "https://kauth.kakao.com", value = "kakaoAuthApi")
public interface KakaoAccessTokenClient {
@PostMapping(
value = "/oauth/token",
consumes = MediaType.APPLICATION_FORM_URLENCODED_VALUE,
produces = MediaType.APPLICATION_JSON_VALUE
)
KakaoAccessTokenResponse getKakaoToken(
KakaoAccessTokenRequest request
);
}
@Builder
public record KakaoAccessTokenRequest(
@JsonProperty("grant_type")
String grantType,
@JsonProperty("client_id")
String clientId,
@JsonProperty("redirect_uri")
String redirectUri,
String code
) {
}
위와 같이 작성했지만 정삭적으로 값이 들어오지 않았다.
이유는 x-www-form-urlencoded 타입은 Json 타입이 아니기때문에 @JsonProperty를 사용할 수 없었다.
그렇다고 변수명을 스네이크 케이스로 사용하고싶지도 않았다.
@BindParam
spring-framwork 이슈를 찾아보니 나와 같은 고민을 하는 사람들이 있었고 @BindParam이 추가되었다.
https://github.com/spring-projects/spring-framework/issues/18012#issuecomment-1651169636
@Getter
public class KakaoAccessTokenRequest {
private String grantType;
private String clientId;
private String redirectUri;
private String code;
@Builder
public KakaoAccessTokenRequest(
@BindParam("grant_Type")
final String grantType,
@BindParam("client_id")
final String clientId,
@BindParam("redirect_uri")
final String redirectUri,
final String code
) {
this.grantType = grantType;
this.clientId = clientId;
this.redirectUri = redirectUri;
this.code = code;
}
}
public record KakaoAccessTokenRequest(
String grantType,
String clientId,
String redirectUri,
String code
) {
@Builder
public KakaoAccessTokenRequest(
@BindParam("grant_Type")
final String grantType,
@BindParam("client_id")
final String clientId,
@BindParam("redirect_uri")
final String redirectUri,
final String code
) {
this.grantType = grantType;
this.clientId = clientId;
this.redirectUri = redirectUri;
this.code = code;
}
}

하지만 @BindParam 방식 또한 정삭적을 동작하지 않았습니다.
아마 FeignClient에 BindParamNameResolver를 아직 지원하지 않는것같다.
@FormProperty
조금더 찾아보니 FeignClient에서는 @FormProperty 어노테이션을 제공하고 x-www-form-urlencoded 타입에서 사용이 가능할것같았다.
https://github.com/OpenFeign/feign-form
public record KakaoAccessTokenRequest(
@FormProperty("grantType")
String grantType,
@FormProperty("clientId")
String clientId,
@FormProperty("redirectUri")
String redirectUri,
String code
) {
}
하지만 @FormProperty 또한 정상적으로 동작하지않았다.
디버깅을 해보니 FeignClient @FormProperty를 바인딩 하기위해서 내부적으로 PojoUtil 클래스를 사용한다.

문제는 isFinal 메서드에 있었다.
프로젝트에서 Dto에서 record를 사용하고있었고 record를 사용하면 각 필드는 private final 필드로 정의되기 때문에 정상적으로 작동하지않는다.
접근 제어자를 강제하는것은 Pojo가 아니라 Java-Bean이다.
https://www.geeksforgeeks.org/pojo-vs-java-beans/
마무리
아직 FeignClient @FormProperty에서 record를 사용할 수 없다.
https://github.com/OpenFeign/feign-form/pull/120
위 코드를 제거해서 PR을 올렸는데 마지막 업데이트가 4년전인 하위 모듈이기때문에 언제 업데이트 될지는 모르겠다.