해당 과정을 보고 두 개의 핸들러로 나누어 진다고 생각했다.
이를 컨트롤러로 작성하면 아래와 같다.
@Controller
@RequestMapping("/api/v1/oauth")
@RequiredArgsConstructor
public class SocialLoginController {
private final KakaoSocialLoginService kakaoSocialLoginSercice;
private final GoogleSocialLoginService googleSocialLoginService;
@GetMapping("/kakao")
public String kakaoLogin() {
String redirectUrl = kakaoSocialLoginSercice.login();
return "redirect:" + redirectUrl;
}
@GetMapping("/google")
public String googleLogin() {
String redirectUrl = googleSocialLoginService.login();
return "redirect:" + redirectUrl;
}
}
@RestController
@RequiredArgsConstructor
public class CallbackController {
private final KakaoSocialLoginService kakaoSocialLoginSercice;
private final GoogleSocialLoginService googleSocialLoginService;
@GetMapping("/oauth/kakao/callback")
public ApiResponse<JwtInfoDto> kakaoCallback(String authCode) {
return ApiResponse.onSuccess(kakaoLoginService.callback(authCode));
}
@GetMapping("/oauth/google/callback")
public ApiResponse<JwtInfoDto> googleCallback(String authCode) {
return ApiResponse.onSuccess(googleLoginService.callback(authCode));
}
}
SocialLoginController
와 콜백을 맡는 CallbackController
두 개를 생성하였다.SocialLoginController
에서 호출하는 핸들러는 특정 url로 리다이렉트되며, provider서버에서 지정한 콜백 url로 CallbackController
의 핸들러를 호출한다.SocialLoginController
의 핸들러를 호출하면 자동으로 CallbackController
의 핸들러가 호출됨을 확인할 수 있다.kakaoLogin(), googleLogin()
: 사용자로 하여금 로그인 진행 + 로그인 성공하면 지정한 콜백 주소로 인증 코드와 함께 리다이렉트kakaoCallback(String authCode), googleCallback(String authCode)
: 리다이렉트되어 호출되는 핸들러 + 인증 코드로 액세스토큰을 받음 + 다시 액세스토큰으로 provider로부터 사용자 정보(이메일)을 받음SocialLoginService, Callback
의 메서드만 구현하면 소셜 로그인은 구현 가능하다.kakaoCallback(String authCode)
에서 JwtInfoDto를 반환하는 이유는 provider에서 받아온 이메일로 자체 JWT 액세스토큰을 생성하여 반환하기 때문이다.카카오와 구글 모두 거의 동일한 프로세스로 소셜로그인을 통해 사용자 정보를 가져오기 때문에 Service 계층에서 똑같은 메서드를 사용할텐데 이를 묶어서 하나의 인터페이스를 만들 수 있을까?
후에 네이버와 같은 다른 provider를 추가하거나 삭제할 가능성도 존재하기에 명세로 만든다면 이득이 있을거라고 생각했다.
SocialLoginService
라는 인터페이스를 생성하였다.
public interface SocialLoginService {
String login();
JwtInfoDto callback(String authCode);
}
@Controller
@RequestMapping("/api/v1/oauth")
@RequiredArgsConstructor
public class SocialLoginController {
private final SocialLoginService kakaoSocialLoginService;
private final SocialLoginService googleSocialLoginService;
@GetMapping("/kakao")
public String kakaoLogin() {
String redirectUrl = kakaoLoginService.login();
return "redirect:" + redirectUrl;
}
@GetMapping("/google")
public String googleLogin() {
String redirectUrl = googleLoginService.login();
return "redirect:" + redirectUrl;
}
}
NoUniqueBeanDefinitionException
예외가 발생한다.KakaoSocialLoginService
, GoogleSocialLoginService
모두 SocialLoginService
를 상속했기 때문에 SocialLoginService
라는 타입의 빈이 2개 존재하게 된다.Qualifier
라는 애노테이션을 통해 주입할 빈을 명시적으로 지정하였다.@Controller
@RequestMapping("/api/v1/oauth")
@RequiredArgsConstructor
public class SocialLoginController {
@Qualifier("kakaoLoginService")
private final SocialLoginService kakaoSocialLoginService;
@Qualifier("googleLoginService")
private final SocialLoginService googleSocialLoginService;
@GetMapping("/kakao")
public String kakaoLogin() {
String redirectUrl = kakaoLoginService.login();
return "redirect:" + redirectUrl;
}
@GetMapping("/google")
public String googleLogin() {
String redirectUrl = googleLoginService.login();
return "redirect:" + redirectUrl;
}
}
@Service("googleLoginService")
@RequiredArgsConstructor
public class GoogleLoginService implements SocialLoginService{
......
}
@Service("kakaoLoginService")
@RequiredArgsConstructor
public class KakaoLoginService implements SocialLoginService {
......
}
각 필드에 대해 구현체를 지정함으로써 주입할 빈이 모호해지는 문제를 해결하였다.
하지만 해당 코드에도 문제는 있었다.
위 컨트롤러는 카카오 로그인을 호출하던 구글 로그인을 호출하던 항상 두개의 빈을 주입받는 상황이다.
어떤 provider의 로그인을 호출하는지에 대해 주입되는 의존관계를 동적으로 깔끔하게 정리하고 싶다는 생각이 들었다.
그러면 카카오, 구글 나눠서 핸들러를 작성하지 않고 하나의 핸들러만 존재하는 좀 더 객체지향에 가까워지리라는 생각이었다.
Map<String, SocialLoginService>
타입을 의존관계로 설정하면 모든 SocialLoginService
의 구현체를 자동으로 put
하게 됨을 이용하였다. 이때 key
는 구현체의 빈 이름, value
는 해당 구현체의 인스턴스이다.SocialLoginService
의 구현체를 자동주입받고, 메소드를 실행할 때 provider를 받아와 해당 provider에 해당하는 구현체를 인스턴스로 가져오는 식으로 구현을 진행하였다.@RestController
@RequiredArgsConstructor
public class CallbackController {
private final Map<String, SocialLoginService> socialLoginServices;
@GetMapping("/oauth/{provider}/callback")
public ApiResponse<JwtInfoDto> callback(@PathVariable String provider,
@RequestParam(name = "code") String authCode) {
SocialLoginService socialLoginService = socialLoginServices.get(provider);
if(socialLoginService == null) throw new IllegalArgumentException("지원하지 않는 로그인 제공자입니다.");
return ApiResponse.onSuccess(socialLoginService.callback(authCode));
}
}
@Service("google")
@RequiredArgsConstructor
public class GoogleLoginService implements SocialLoginService{
......
}
@Service("kakao")
@RequiredArgsConstructor
public class KakaoLoginService implements SocialLoginService {
......
}
provider
를 @PathVariable을 사용해 바인딩하여 값을 받고 꼭 @RequestParam으로 파라미터를 받아야한다. 그렇지 않으면 파라미터를 인식하지 못한다. (아마 @PathVariable을 사용하기 때문일 것이다)socialLoginServices.get(provider)
로 호출한 provider
에 해당하는 구현체를 가져와 해당 구현체의 메서드를 호출함으로써 구현을 완료하였다.JwtInfoDto
가 잘 나오는 것을 확인할 수 있었다.provider
를 추가하기 쉽게 객체지향적으로 설계를 해놓았으니 추후에 네이버, 깃허브같은 provider
를 추가하는것도 고려해볼 예정이다.