개요
FeignClient를 사용한 토큰 및 회원 정보 조회 기능을 구현한 경험을 공유하고자 합니다.
FeignClientConfig
FeignClient는 Spring에서 HTTP 요청을 처리하기 위해 사용되는 도구이며 REST API를 호출을 간단하게 할 수 있도록 합니다.
Kakao와 Google과 같은 소셜 로그인 기능을 구현하기 위해 FeignClient를 활용했으며, 각 소셜 플랫폼의 API를 호출하고, 사용자의 인증 정보를 받아오는 기능을 구현했습니다.
build.gradle
dependencies {
implementation 'org.springframework.cloud:spring-cloud-starter-openfeign:4.1.2'
}
FeignClientConfig
@Configuration
@EnableFeignClients(basePackages = {"com.juu.juulabel.api"})
public class FeignClientConfig {
}
@EnableFeignClients : 루트 패키지에서 사용되지 않을 경우, basePackages 또는 basePackageClasses를 지정해야합니다.
소셜 로그인 구현
@RestController
@RequestMapping(value = {"/v1/api/members"})
@RequiredArgsConstructor
public class MemberController {
private final MemberService memberService;
@Operation(summary = "카카오 로그인")
@PostMapping("/login/kakao")
public ResponseEntity<CommonResponse<LoginResponse>> kakaoLogin(@Valid @RequestBody OAuthLoginRequest oAuthLoginRequest) {
return CommonResponse.success(SuccessCode.SUCCESS, memberService.login(oAuthLoginRequest));
}
@Operation(summary = "구글 로그인")
@PostMapping("/login/google")
public ResponseEntity<CommonResponse<LoginResponse>> googleLogin(@Valid @RequestBody OAuthLoginRequest oAuthLoginRequest) {
return CommonResponse.success(SuccessCode.SUCCESS, memberService.login(oAuthLoginRequest));
}
// 생략 ...
}
카카오와 구글 로그인을 처리하는 두 개의 메소드는 동일한 방식으로 로그인 요청을 처리합니다. 그리고 OAuth 제공자는 OAuthLoginRequest 객체의 Provider 값을 통해 구분합니다.
@Slf4j
@Service
@RequiredArgsConstructor
public class MemberService {
private final OAuthProviderFactory providerFactory;
private final JwtTokenProvider jwtTokenProvider;
private final MemberReader memberReader;
private final MemberWriter memberWriter;
// ...
@Transactional
public LoginResponse login(OAuthLoginRequest oAuthLoginRequest) {
OAuthLoginInfo authLoginInfo = oAuthLoginRequest.toDto();
Provider provider = authLoginInfo.provider();
String accessToken = providerFactory.getAccessToken(
provider,
authLoginInfo.propertyMap().get(AuthConstants.REDIRECT_URI),
authLoginInfo.propertyMap().get(AuthConstants.CODE)
);
OAuthUser oAuthUser = providerFactory.getOAuthUser(provider, accessToken);
String email = oAuthUser.email();
boolean isNewMember = !memberReader.existsByEmailAndProvider(email, provider);
Token token = createTokenForMember(isNewMember, email);
return new LoginResponse(
token,
isNewMember,
new OAuthUserInfo(email, oAuthUser.id(), provider)
);
}
// ... 생략
}
OAuthProviderFactory에서 OAuth 제공자인 Provider를 받아 각 Provider에 따른 토큰 정보와 인증 정보를 내려줍니다.
OAuthProviderFactory
@Component
public class OAuthProviderFactory {
private final Map<Provider, OAuthProvider> providerMap;
private final KakaoProvider kakaoProvider;
private final GoogleProvider googleProvider;
public OAuthProviderFactory(
KakaoProvider kakaoProvider,
GoogleProvider googleProvider
) {
providerMap = new EnumMap<>(Provider.class);
this.kakaoProvider = kakaoProvider;
this.googleProvider = googleProvider;
initialize();
}
private void initialize() {
providerMap.put(Provider.KAKAO, kakaoProvider);
providerMap.put(Provider.GOOGLE, googleProvider);
}
private OAuthProvider getOAuthProvider(Provider provider) {
OAuthProvider oAuthProvider = providerMap.get(provider);
if (Objects.isNull(oAuthProvider)) {
throw new InvalidParamException(ErrorCode.NOT_FOUND_OAUTH_PROVIDER);
}
return oAuthProvider;
}
public String getAccessToken(Provider provider, String redirectUri, String code) {
return getOAuthToken(provider, redirectUri, code).accessToken();
}
private OAuthToken getOAuthToken(Provider provider, String redirectUri, String code) {
return getOAuthProvider(provider).getOAuthToken(redirectUri, code);
}
public String getProviderId(OAuthUser oAuthUser) {
return oAuthUser.id();
}
public String getEmail(OAuthUser oAuthUser) {
return oAuthUser.email();
}
public OAuthUser getOAuthUser(Provider provider, String accessToken) {
return getOAuthProvider(provider).getOAuthUser(accessToken);
}
}
OAuthProviderFactory는 유연성과 확장성을 고려하여 설계했습니다. 필요에 따라 새로운 OAuth 제공자(Provider)를 손쉽게 추가할 수 있도록 설계된 팩토리 클래스입니다.
각 소셜 플랫폼에 특화된 OAuthProvider의 구현체(GoogleProvider와 KakaoProvider)가 존재합니다. 이 인터페이스를 통해 공통된 메소드(getOAuthToken(), getOAuthUser())를 사용하여 각 플랫폼별 로직의 일관성을 유지합니다.
서비스 로직에서 getAccessToken()를 호출했을 때, getOAuthToken()를 호출해서 Provider에 맞는 OAuthToken을 가져와 accessToken 정보를 가져옵니다. 만약 Provider가 KAKAO라고 한다면 OAuthProvider의 구현체인 KakaoProvider의 getOAuthToken() 메소드를 통해 토큰 정보를 불러오게 됩니다.
OAuthProvider (interface)
public interface OAuthProvider {
OAuthToken getOAuthToken(String redirectUri, String code);
OAuthUser getOAuthUser(String accessToken);
}
OAuthProvider 인터페이스는 소셜 로그인 기능을 공통으로 관리하기 위해 설계했습니다. 이 인터페이스는 getOAuthToken(), getOAuthUser() 메소드를 정의하여, 각 소셜 플랫폼에서 토큰 또는 인증 정보를 받아오는 역할을 합니다.
public interface OAuthToken {
String tokenType();
String accessToken();
int expiresIn();
String refreshToken();
String scope();
int refreshTokenExpiresIn();
}
public interface OAuthUser {
String id();
String email();
}
OAuthToken과 OAuthUser를 인터페이스로 작성해서 다른 소셜 로그인의 OAuth 제공자들이 동일한 방식으로 데이터를 처리하도록 설계했습니다.
KakaoProvider (implements)
@Component
@RequiredArgsConstructor
public class KakaoProvider implements OAuthProvider {
private final KakaoApiClient kakaoApiClient;
private final KakaoAuthClient kakaoAuthClient;
@Value("${spring.security.oauth2.client.registration.kakao.authorization-grant-type}")
private String grantType;
@Value("${spring.security.oauth2.client.registration.kakao.client-id}")
private String clientId;
@Value("${spring.security.oauth2.client.registration.kakao.client-secret}")
private String clientSecret;
@Override
public OAuthToken getOAuthToken(String redirectUri, String code) {
return kakaoAuthClient.generateOAuthToken(
grantType,
clientId,
redirectUri,
code,
clientSecret
);
}
@Override
public OAuthUser getOAuthUser(String accessToken) {
return kakaoApiClient.getUserInfo(getBearerToken(accessToken));
}
private String getBearerToken(String accessToken) {
return AuthConstants.TOKEN_PREFIX + accessToken;
}
}
KakaoProvider는 OAuthProvider의 구현체로, 카카오 로그인 인증 처리 로직을 포함하며, KakaoApiClient, KakaoAuthClient를 사용하여 카카오의 OAuth 서버와 통신을 하게 됩니다. 여기서 KakaoApiClient는 redirectUri와 code를 가지고 Token 정보를 요청하는 Feign입니다.
FeignClient를 사용해 카카오 API와 통신하며, 사용자 정보를 조회합니다. 카카오 API로부터 받은 사용자 정보는 OAuthUser 구현체인 KakaoUser 클래스로 매핑되며, 통일된 방식으로 사용자 정보를 관리할 수 있도록 합니다.
FeignClient를 사용해 카카오 API와 통신하며, 토큰 정보를 정보를 조회합니다. 카카오 API로부터 받은 토큰 정보는 OAuthToken 구현체인 KakaoToken 클래스로 매핑되며, 액세스 토큰, 토큰의 유효 기간, 리프레시 토큰 등의 정보를 관리할 수 있도록 합니다.
다음과 같은 방식으로 OAuth 제공자를 추가해도 유연하게 대처할 수 있도록 설계 및 개발을 진행했습니다. 유연하고 확장성 있는 개발의 핵심은 인터페이스를 잘 활용하는 것이라는 말을 들었었는데 실제로 고려해서 작업을 진행하니 어렵군요.. ^0^
글이 도움이 되었으면 합니다. 감사합니다!
참고 :
'PROJECT > 주라벨' 카테고리의 다른 글
[주라벨] Map을 활용한 Sensory 정보 그룹화 및 성능 개선 (0) | 2024.08.16 |
---|