
여러 소셜 로그인(카카오, 네이버, 구글, 페이스북 등)을 지원하다 보면 컨트롤러에 유사한 로그인 코드가 반복되거나, if-else·switch 문이 길어져 유지보수가 어려워지기 쉽습니다.
이 글에서는 다음 내용을 다룹니다.
- 여러 소셜 로그인을 하나의 엔드포인트로 통합하는 방법
- 로그인 방식별로 전략 패턴(Strategy Pattern)을 적용해 확장 가능한 구조를 만드는 방법
- 새로운 소셜 로그인이 추가되더라도 컨트롤러를 수정하지 않고 확장하는 설계 방식
예제 코드는 Spring 기반의 REST API를 기준으로 작성했습니다.
요구사항 및 초기 상황

프로젝트에서 로그인 기능을 맡았고, 사용자 경험을 높이기 위해 다양한 소셜 로그인을 제공하기로 했다고 가정해 보겠습니다.
지원 대상: 카카오, 네이버, 페이스북, 구글
전체 로그인 흐름은 다음과 같습니다.
- 클라이언트에서 각 소셜 로그인 SDK를 통해 인증
- 인증 결과로 받은 토큰·사용자 정보를 서버로 전달
- 서버에서 유효성을 검증한 뒤 회원가입 또는 로그인 처리
- 최종적으로 세션 또는 액세스 토큰 발급
초기 구현 단계에서는 소셜 로그인마다 별도의 엔드포인트를 만들고, 컨트롤러에서 각 로그인 로직을 직접 처리했습니다.
@RestController
@RequiredArgsConstructor
public class LoginController {
private final LoginService loginService;
@PostMapping("/api/v1/login/kakao")
public ResponseEntity<LoginResponse> loginWithKakao() {
// 카카오 로그인 로직
}
@PostMapping("/api/v1/login/naver")
public ResponseEntity<LoginResponse> loginWithNaver() {
// 네이버 로그인 로직
}
@PostMapping("/api/v1/login/facebook")
public ResponseEntity<LoginResponse> loginWithFacebook() {
// 페이스북 로그인 로직
}
@PostMapping("/api/v1/login/google")
public ResponseEntity<LoginResponse> loginWithGoogle() {
// 구글 로그인 로직
}
}
기존 구현의 문제점

겉보기에는 단순해 보이지만, 위와 같은 방식은 시간이 지날수록 다음과 같은 문제가 발생합니다.
1. 중복 코드 증가
각 메서드는 요청 검증 → 회원 조회 또는 등록 → 세션 생성 및 응답이라는 동일한 흐름을 반복합니다. 이 과정에서 불필요한 중복 코드가 계속 늘어납니다.
2. 확장 시 생기는 부담
새로운 소셜 로그인을 지원할 때마다 컨트롤러에 메서드를 추가해야 합니다. 엔드포인트도 함께 늘어나며 구조가 점점 복잡해집니다.
3. 변경에 취약한 구조
로그인의 공통 처리 로직이 여러 메서드에 분산되어 있어 정책이 변경되면 여러 위치를 동시에 수정해야 합니다. 이로 인해 유지보수 비용이 커집니다.
이 문제를 해결하기 위해 먼저 엔드포인트를 하나로 통일하는 작업을 진행했습니다.
Path Variable로 엔드포인트 통일하기
우선 모든 소셜 로그인 요청을 하나의 엔드포인트로 받고, provider 값을 Path Variable로 전달해 로그인 방식을 구분하는 방법을 적용해 보겠습니다.
@RestController
@RequiredArgsConstructor
public class LoginController {
private final LoginService loginService;
@PostMapping("/api/v1/login/{provider}")
public ResponseEntity<LoginResponse> login(
@PathVariable("provider") final String provider
) {
LoginResponse response;
switch (provider) {
case "kakao":
response = loginService.loginWithKakao();
break;
case "naver":
response = loginService.loginWithNaver();
break;
case "facebook":
response = loginService.loginWithFacebook();
break;
case "google":
response = loginService.loginWithGoogle();
break;
default:
throw new RuntimeException("잘못된 로그인 요청");
}
return ResponseEntity.ok(response);
}
}
엔드포인트를 /api/v1/login/{provider}로 통합하면서 URL 구조가 훨씬 단순해졌습니다.
하지만 이 방식에도 여전히 다음과 같은 한계가 남아 있습니다.
switch (provider)블록이 로그인 방식이 늘어날수록 계속 길어집니다.- 새로운 소셜 로그인이 추가되면 컨트롤러 코드를 반드시 수정해야 합니다.
- 테스트 및 유지보수 측면에서 OCP(개방–폐쇄 원칙)을 위반하는 구조입니다.
이 문제를 해결하기 위해, 이제 이 분기 로직을 전략 패턴(Strategy Pattern)으로 분리해 보겠습니다.
전략 패턴(Strategy Pattern) 간단 소개

전략 패턴은 다음과 같은 상황에서 유용한 디자인 패턴입니다.
- 동일한 행위(전략)가 있지만, 그 구체 구현이 여러 가지로 달라질 때
- 실행 시점 또는 설정에 따라 전략 구현체를 유연하게 바꿔서 사용하고 싶을 때
전략 패턴을 적용하는 기본 구조는 다음과 같습니다.
- 공통 행위를 정의하는 전략 인터페이스를 만든다.
- 각 경우에 대한 구체 전략 클래스를 구현한다.
- 클라이언트(여기서는 컨트롤러)는 어떤 전략을 사용할지만 선택하고, 내부 구현은 알지 않는다.
이 구조는 소셜 로그인 처리 흐름과 잘 맞습니다.
- 공통 행위: 로그인 요청 처리 → 회원 조회/등록 → 세션·응답 생성
- provider별로 달라지는 부분: 토큰 검증 방식, 사용자 정보 조회 방식
즉, 공통 로직은 유지하면서도 각 로그인 방식을 유연하게 확장할 수 있는 구조를 만들 수 있습니다.
전략 패턴 적용 설계
1. 로그인 전략 인터페이스 정의
먼저 로그인 과정에서 공통으로 필요한 행위를 정의하는 LoginStrategy 인터페이스를 만듭니다.
public interface LoginStrategy {
LoginResponse login(LoginRequest request, HttpSession session);
String getProviderName();
}
- login(...)
요청 검증, 회원 조회·등록, 세션 설정 등 실제 로그인 흐름을 처리합니다. - getProviderName()
이 전략이 담당하는 provider 이름(예:"kakao","naver")을 반환합니다.
이후 provider 값으로 적절한 전략을 찾을 때 사용합니다.
2. 카카오 로그인 전략 구현 예시
아래는 카카오 로그인을 처리하는 전략 구현체 예시입니다.
@Service
@RequiredArgsConstructor
@Slf4j
public class KakaoLoginStrategy implements LoginStrategy {
private static final String PROVIDER_NAME = "kakao";
private final MemberRepository memberRepository;
@Override
public LoginResponse login(LoginRequest request, HttpSession session) {
if (!isRequestValid(request)) {
throw new InvalidLoginRequestException(INVALID_LOGIN_REQUEST);
}
Member member = memberRepository.findMemberById(request.getId())
.orElseGet(() -> registerNewMember(request));
session.setAttribute("memberId", member.getId());
return new LoginResponse("login succeed");
}
@Override
public String getProviderName() {
return PROVIDER_NAME;
}
private boolean isRequestValid(LoginRequest request) {
// 액세스 토큰 검증, 필수 필드 체크 등 요청 유효성 검증 로직
}
private Member registerNewMember(LoginRequest request) {
// 신규 회원 등록 로직
}
}
다른 소셜 로그인도 동일한 방식으로 구현할 수 있습니다.
@Service
public class NaverLoginStrategy implements LoginStrategy {
// getProviderName() -> "naver"
// login(...)은 네이버 로그인 규약에 맞게 구현
}
@Service
public class GoogleLoginStrategy implements LoginStrategy {
// getProviderName() -> "google"
}
@Service
public class FacebookLoginStrategy implements LoginStrategy {
// getProviderName() -> "facebook"
}
각 provider는 처리 방식만 다를 뿐, 컨트롤러는 동일한 인터페이스를 바라보기 때문에 구조가 일관됩니다.
3. 전략 선택을 위한 LoginStrategyManager
이제 provider 문자열을 기반으로 적절한 전략을 찾아주는 역할이 필요합니다.
이를 위해 LoginStrategyManager를 구현합니다.
@Service
public class LoginStrategyManager {
private final Map<String, LoginStrategy> loginStrategyMap;
public LoginStrategyManager(List<LoginStrategy> strategies) {
this.loginStrategyMap = strategies.stream()
.collect(Collectors.toMap(LoginStrategy::getProviderName, Function.identity()));
}
public LoginStrategy getLoginStrategy(String provider) {
LoginStrategy strategy = loginStrategyMap.get(provider);
if (strategy == null) {
throw new IllegalArgumentException("지원하지 않는 로그인 제공자입니다: " + provider);
}
return strategy;
}
}
- Spring은
LoginStrategy인터페이스를 구현한 모든 빈을List<LoginStrategy>로 자동 주입합니다. - 각 전략의
getProviderName()을 key로Map<String, LoginStrategy>를 구성합니다. - 컨트롤러는 단순히 provider 문자열로 전략을 조회해 사용하면 됩니다.
4. 컨트롤러에서 전략 사용하기
컨트롤러는 더 이상 조건문을 사용하지 않고 전략 매니저에 작업을 위임합니다.
@RestController
@RequiredArgsConstructor
public class LoginController {
private final LoginStrategyManager loginStrategyManager;
@PostMapping("/api/v1/login/{provider}")
public ResponseEntity<LoginResponse> login(
@PathVariable("provider") final String provider,
@RequestBody LoginRequest loginRequest,
HttpSession session
) {
LoginStrategy strategy = loginStrategyManager.getLoginStrategy(provider);
LoginResponse response = strategy.login(loginRequest, session);
return ResponseEntity.ok(response);
}
}
컨트롤러의 책임은 다음 세 가지로 단순해졌습니다.
- provider 값과 요청 정보를 전달받고
LoginStrategyManager에서 적절한 전략을 조회한 뒤strategy.login(...)을 호출해 결과를 반환
이 구조에서는 로그인 방식이 추가되거나 변경되더라도 컨트롤러를 수정할 필요가 없습니다.
전략만 새로 구현하여 등록하면 되기 때문에 확장성 면에서도 매우 유리합니다.
확장 시나리오

전략 패턴을 적용한 구조에서는 새로운 로그인 방식 추가나 정책 변경에 어떻게 대응할 수 있을까요?
각 시나리오를 통해 확장성을 확인해 보겠습니다.
1. 새로운 소셜 로그인 추가
예를 들어 GitHub 로그인을 추가한다고 가정해 보겠습니다.
GithubLoginStrategy클래스를 하나 구현하고getProviderName()에서"github"을 반환하도록 설정하면 됩니다.
이때 컨트롤러, 매니저, 기존 전략 클래스는 전혀 수정할 필요가 없습니다.
전략만 추가하면 되는 구조이므로 OCP(개방–폐쇄 원칙)를 자연스럽게 만족합니다.
2. 정책 변경 / 비즈니스 로직 변경
특정 소셜 로그인만 정책이 변경되는 경우에도 해당 전략만 수정하면 됩니다.
예시:
- 카카오 로그인에서 특정 도메인의 이메일만 허용해야 한다면 →
KakaoLoginStrategy만 수정 - 네이버 로그인만 별도 로그를 남겨야 한다면 →
NaverLoginStrategy만 수정
반대로 모든 로그인 방식에 적용되는 공통 정책(세션 생성 방식, 응답 포맷 등)이 있다면
공통 추상 클래스로 분리하거나 별도 서비스로 묶어 재사용할 수도 있습니다.
마무리
이 글에서는 아래와 같은 과정을 통해 확장성 있는 소셜 로그인 구조를 만들어 보았습니다.
- 초기 구현: 소셜 로그인별로 개별 엔드포인트 구성
- 1차 리팩토링: PathVariable을 활용한 단일 엔드포인트 + switch 분기
- 최종 구조(전략 패턴 적용):
- 로그인 방식별 전략 분리
- LoginStrategyManager를 통한 전략 선택
- 컨트롤러는 전략 호출만 담당하는 단순 구조
이 방식이 제공하는 장점은 다음과 같습니다.
- 확장성: 새 소셜 로그인을 추가할 때 LoginStrategy 구현체만 만들면 됩니다.
- 유지보수성: 각 로그인 방식의 변경이 다른 기능에 영향을 거의 주지 않습니다.
- 가독성: 컨트롤러에서 복잡한 분기문이 사라져 코드가 훨씬 명확해집니다.
추후에는 이 구조 위에서 다음과 같은 확장도 자연스럽게 적용할 수 있습니다.
- Form 로그인, Apple 로그인 등 다양한 로그인 전략 추가
- JWT 기반 토큰 발급·검증 전략 도입
- 테스트 코드에서 Mock 전략을 주입해 개별 전략 단위 테스트 구현
전략 패턴을 적용하면 로그인 기능을 단순히 추가하는 코드가 아니라, 유연하게 성장하는 구조로 만들 수 있다는 점을 경험하실 수 있을 것입니다.
'Backend > Spring 🌱' 카테고리의 다른 글
| 스프링 컨테이너 이해하기: BeanFactory와 ApplicationContext (0) | 2024.03.12 |
|---|---|
| Spring MVC에서 HandlerMapping과 HandlerAdapter를 나눈 이유 (0) | 2024.02.07 |
| 이벤트 기반으로 파일 업로드 기능 구현하기 (0) | 2024.02.07 |
| Spring Boot 프로젝트에 PostGIS 적용하기: 위치 정보 관리를 위한 DB (0) | 2024.02.07 |
| 스프링부트 프로젝트 시작하는 법 (0) | 2024.02.02 |
안녕하세요, 저는 주니어 개발자 박석희 입니다. 언제든 하단 연락처로 연락주세요 😆