저희 곽두철 프로젝트에는 카카오 소셜 로그인만을 이용하기로 결정했었습니다.
하지만 추후에 네이버, 구글 등등 다양한 플랫폼이 추가될 것을 고려해 추상화를 하도록 결정이 났습니다.
이번 포스트에서는 카카오를 중심으로 글을 써보겠습니다.
1. 왜 전략 패턴을 사용할까?
소셜 로그인을 구현할 때, 카카오, 네이버, 구글 등 여러 제공자의 로그인 방식이 각각 다릅니다. 각기 다른 로그인 로직을 하나의 서비스에 담아두면 코드가 복잡해지고 유지보수가 어렵습니다. 전략 패턴을 사용하면 각 소셜 로그인 로직을 별도의 클래스로 분리하여 코드의 확장성과 유지보수성을 높일 수 있습니다.
1-1. 전략 패턴의 장점?
전략 패턴을 사용하면 런타임에 다른 소셜 로그인 로직으로 쉽게 교체할 수 있어 유연한 구조를 가질 수 있습니다.
예를 들어, 원하는 플랫폼을 찾아 그때마다 가져다 쓸 수 있는 구조가 가능합니다.
2. 프로젝트 구조
전략 패턴을 적용하기 위해 다음과 같은 구조를 사용합니다.
- application
- OAuthService
- LoginClients
- application.client
- LoginClient (인터페이스)
- KakaoLoginClient (카카오 로그인 구현체)
- domain.oauth
- SocialType
- ui
- OAuthController
- dto
- LoginRequest
- LoginResponse
- KakaoUserInfoResponse
3. 각 플랫폼의 공통된 메소드 추출하기
package org.doochul.application.client;
import org.doochul.domain.oauth.SocialType;
import org.doochul.ui.dto.KakaoUserInfoResponse;
public interface LoginClient {
String requestToken(final String authCode);
KakaoUserInfoResponse findUserInfo(final String accessToken);
SocialType getSocialType();
}
각 소셜 로그인 클라이언트에서 구현해야 하는 메소드를 추상화하여 LoginClient 인터페이스로 정의했습니다.
- 클라이언트에서 받은 authCode를 이용하여 토큰을 받는 메소드
- 1에서 받은 토큰을 활용하여 유저의 정보를 받아오는 메소드
- 어떤 플랫폼인지 알려주는 메소드
4. 이제 구현체를 만들 시간
package org.doochul.infra.oauth.kakao;
import java.util.Objects;
import org.doochul.ui.dto.KakaoProfileResponse;
import org.doochul.ui.dto.KakaoTokenResponse;
import org.doochul.ui.dto.UserInfo;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.http.MediaType;
import org.springframework.stereotype.Component;
import org.springframework.web.reactive.function.client.WebClient;
import org.springframework.web.util.UriComponentsBuilder;
import reactor.core.publisher.Flux;
@Component
public class KakaoClient {
private static final String USER_INFO_URI = "<https://kapi.kakao.com/v2/user/me>";
private final WebClient webClient;
@Value("${token.uri}")
private String TOKEN_URI;
@Value("${redirect.uri}")
private String REDIRECT_URI;
@Value("${grant.type}")
private String GRANT_TYPE;
@Value("${client.id}")
private String CLIENT_ID;
public KakaoClient() {
this.webClient = WebClient.create(USER_INFO_URI);
}
public String request(final String authCode) {
final String uri = UriComponentsBuilder.fromUriString(TOKEN_URI)
.queryParam("grant_type", GRANT_TYPE)
.queryParam("client_id", CLIENT_ID)
.queryParam("redirect_uri", REDIRECT_URI)
.queryParam("code", authCode)
.toUriString();
Flux response = webClient.post()
.uri(uri)
.contentType(MediaType.APPLICATION_FORM_URLENCODED)
.retrieve()
.bodyToFlux(KakaoTokenResponse.class);
return Objects.requireNonNull(response.blockFirst()).access_token();
}
public UserInfo getUserInfo(final String token) {
Flux response = webClient.get()
.header("Authorization", "Bearer " + token)
.retrieve()
.bodyToFlux(KakaoProfileResponse.class);
return Objects.requireNonNull(response.blockFirst()).toUserInfo();
}
}
카카오 로그인 API 호출을 위한 클라이언트 클래스입니다.
- Token을 발행받는 request 메소드
- 유저 정보를 받는 getUserInfo 메소드를 구현했습니다.
package org.doochul.infra.oauth.kakao;
import lombok.RequiredArgsConstructor;
import org.doochul.application.client.LoginClient;
import org.doochul.domain.oauth.SocialType;
import org.doochul.ui.dto.UserInfo;
import org.springframework.stereotype.Component;
@Component
@RequiredArgsConstructor
public class KakaoLoginClient implements LoginClient {
private final KakaoClient kakaoClient;
@Override
public String requestToken(final String authCode) {
return kakaoClient.request(authCode);
}
@Override
public UserInfo findUserInfo(final String accessToken) {
return kakaoClient.getUserInfo(accessToken);
}
@Override
public SocialType getSocialType() {
return SocialType.KAKAO;
}
}
LoginClient 인터페이스의 카카오 로그인 구현체를 만들어 줬습니다.
5. 일급 컬렉션 만들기
지금까지 카카오를 중심으로 블로그를 작성했습니다.
하지만 저의 목표는 네이버, 구글 등 다양한 소셜 로그인을 구현할 때 중복을 없애고, 확장성이라는 이점을 활용하는 것입니다. 이를 위해 각 소셜 로그인 클라이언트를 관리하는 LoginClients 클래스를 만들어보겠습니다.
package org.doochul.application;
import java.util.EnumMap;
import java.util.Map;
import java.util.Optional;
import java.util.Set;
import org.doochul.application.client.LoginClient;
import org.doochul.domain.oauth.SocialType;
import org.doochul.ui.dto.UserInfo;
public class LoginClients {
private final Map<SocialType, LoginClient> clients;
public LoginClients(final Set<LoginClient> clients) {
final EnumMap<SocialType, LoginClient> mapping = new EnumMap<>(SocialType.class);
clients.forEach(client -> mapping.put(client.getSocialType(), client));
this.clients = mapping;
}
public UserInfo findUserInfo(final SocialType socialType, final String code) {
final LoginClient client = getClient(socialType);
final String accessToken = client.requestToken(code);
return client.findUserInfo(accessToken);
}
private LoginClient getClient(final SocialType socialType) {
return Optional.ofNullable(clients.get(socialType))
.orElseThrow(() -> new IllegalArgumentException("해당 OAuth2 제공자는 지원되지 않습니다."));
}
}
LoginClients 클래스는 다양한 소셜 로그인 로직의 진입점입니다. getClient 메소드를 통해 적절한 플랫폼에 맞는 LoginClient를 선택하고, 토큰 발급과 유저 정보를 조회하는 과정을 한 번에 처리합니다.
6. 빈으로 등록하기
package org.doochul.config;
import java.util.Set;
import org.doochul.application.LoginClients;
import org.doochul.application.client.LoginClient;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
@Configuration
public class VendorConfiguration {
@Bean
public LoginClients loginClients(final Set<LoginClient> clients) {
return new LoginClients(clients);
}
}
LoginClients를 빈으로 등록하여 LoginClient의 구현체들이 LoginClients 생성자 안으로 DI가 일어나도록 설정합니다.
이번 글에서는 카카오 로그인을 중심으로 전략 패턴을 활용한 소셜 로그인 구현 방법을 알아봤습니다. 이 구조를 활용하면 다른 플랫폼의 로그인 로직도 쉽게 확장할 수 있을 것입니다.
'WEB' 카테고리의 다른 글
Java Thread란 무엇일까? (1) | 2024.08.30 |
---|---|
SubModule이란? (0) | 2024.08.28 |
배포 서버없이 프론트단과 백단 통신하기 (3) | 2024.08.27 |
HTML이 웹 브라우저에서 어떻게 작동할까? (1) | 2024.08.26 |
@NoArgsConstructor 액세스 레벨을 PROTECTED로 하는 이유 (1) | 2024.06.09 |
Redis 내부동작 파헤치기 (0) | 2024.05.05 |
Redis Lock 동시성 해결하기 (0) | 2024.05.01 |
인터셉터와 리졸버 (0) | 2024.04.12 |