Skip to content

Spring Security와 OAuth2 Client를 이용한 회원 가입 개발기

Zinzo edited this page Jun 26, 2025 · 3 revisions

회원 가입 개발하는 과정에서 어떠한 고민을 했고, 어떤 방식으로 풀어나갔는지를 간단한 글로 남겨 보았습니다.

1. 회원 가입으로 구글 간편 로그인 도입 계기

우리 서비스는 사내 맛집을 등록하고 공유하는 것을 주요 기능으로 삼고 있습니다.
서비스의 주 타겟은 직장인들이며, 대부분 사내에서 PC를 통한 접근을 할 것으로 예상하였습니다.

자체 회원 가입과 간편 로그인 중에서 고민했었는데, 많은 직장인들이 회사 이메일을 구글에 연동해서 쓰는 경우가 많고,
또 직접 아이디 비밀번호를 일일히 입력하는 것보단, 인증 및 관리를 OAuth를 통해 처리하는 것이 기간이 정해져 있는 프로젝트에 더욱 적합하다고 보았습니다.

1-1. 직접 회원가입과 OAuth 방식의 차이

구분 직접 회원가입 OAuth 방식
비밀번호 관리 직접 관리 필요 관리가 필요 없음
보안 자체적으로 검증 외부 서비스의 인증을 신뢰
구현 난이도 단순 입력 검증 처리로 단순 OAuth2 연동, 시큐리티 등
유지보수 자체 DB 관리 부담 외부 API 의존성이 높음

그중에서도 소수의 회사들은 카카오톡 같은 메신저 접근이 제한되는 경우가 있기에,
구글 간편 로그인을 구현하는 것을 목표로 두었습니다.

올해 초 시큐리티 교육도 들었고, 사내에서도 비회원이라는 새로운 회원 유형을 추가한 경험이 있어 시큐리티에 대한 자신감이 많이 생겼었습니다.
또, 간편 로그인을 연동해 본 경험이 없어 이번 기회를 통해 경험을 쌓아보고 싶었습니다.

2. 회원 가입 플로우

모여락의 간단 회원 가입 플로우는 아래와 같은 단계로 이루어져 있습니다. image

image

UML
@startuml
"OAuth2 로그인" --> "DB 조회" : 이메일 확인
"DB 조회" --> "기존 회원" : 계정 있음
"DB 조회" --> "신규 회원" : 계정 없음
"신규 회원" --> "온보딩" : 최소 정보 응답
"온보딩" --> "가입 완료" : 유효성 검증 및 DB Insert
"기존 회원" --> "완료" : 로그인
"가입 완료" --> "완료"
@enduml
  1. OAuth2 로그인 선택
  • 사용자는 로그인 화면에서 구글 간편 로그인(OAuth2)를 선택하고, 계정을 선택합니다.
  1. 내부 DB 계정 조회
  • 구글에서 제공 받은 이메일 정보를 기반으로,
    내부 DB에 해당 계정이 존재하는지 여부를 확인합니다.
  1. 신규 회원 가입 분기
  • 내부에 계정이 없다면 신규 회원으로 간주합니다.
    구글에서 제공 받은 정보를 바탕으로 가입을 위한 최소 정보의 응답을 내려줍니다.
    (예, 이름, 이메일)
  1. 온보딩 및 최종 확인
  • 고객은 온보딩 과정을 통해, 모여락 서비스에 필요한 최소 정보를 입력합니다.
    마지막 회원가입 버튼을 눌러 최종적으로 회원 가입을 요청합니다.
  1. 유효성 검증 및 가입 처리
  • 입력된 정보를 바탕으로 서버에서는 유효성 검증이 완료되었다면,
    회원 DB에 insert를 하고, 회원 가입 성공에 대한 응답을 내려줍니다.

3. 구현 방식

Spring Security, OAuth2 Client를 사용하여 구현하였습니다.
OAuth2 제공 업체는 Google을 이용하였습니다.

Google OAuth를 사용하기 위해서는, 구글 클라우드에서 프로젝트 및 설정이 사전에 필요합니다.

3-1. OAuth2 회원가입 인증 흐름

모여락 서비스에서 구글 간편 로그인을 이용한 회원가입 과정에서, OAuth 인증 흐름을 간단하게 표현하였습니다. image

UML
@startuml
actor 회원
participant 모여락
participant "Google OAuth" as GoogleOAuth

회원 -> 모여락 : /oauth2/authorization/google 접속
모여락 -> GoogleOAuth : 인증 요청 (client_id, redirect_uri 등 포함)
GoogleOAuth -> 회원 : Google 로그인 화면 (계정 선택)
회원 -> GoogleOAuth : 계정 선택 및 로그인 승인
GoogleOAuth -> 모여락 : 인가 코드 전달 (redirect_uri로)
모여락 -> GoogleOAuth : 인가 코드로 토큰 요청
GoogleOAuth -> 모여락 : 액세스 토큰 + 사용자 정보 반환
@enduml

인가 코드 요청부터, 액세스 토큰 발급, 사용자 정보 반환까지 흐름입니다.
이 과정은 Spring SecurityOAuth Client 기능을 통해 비교적 단순하게 구현할 수 있었습니다.

3-1-1. OAuth Client Dependency 추가

# build.gradle.kts
dependencies {
   ...
   implementation("org.springframework.boot:spring-boot-starter-oauth2-client")
   ...
}

3-1-2. OAuth Client 설정 추가

# application.yml

spring:
  security:  
    oauth2:  
      client:  
        registration:  
          google:  
            client-id: ${GOOGLE_CLIENT_ID}  
            client-secret: ${GOOGLE_CLIENT_SECRET_ID}
            scope:  
              - profile  
              - email  
            redirect-uri: "{baseUrl}/login/oauth2/code/{registrationId}"

해당 설정을 추가하게 되면, 내부적으로 작동되게 됩니다.

  • client-id : 구글에서 발급받은 클라이언트 ID
    • 모여락 서비스에서 구글 OAuth에 요청 보낼 떄, 자신을 식별하는 값입니다.
  • client-secret : 구글에서 발급받은 클라이언트 시크릿 ID
    • 절대 노출이 되어서는 안 됩니다.
    • 인가 코드를 액세스 토큰으로 바꿀 때 보내는 비밀번호 같은 값입니다.
  • scope : 구글로부터 요청할 정보를 지정합니다.
  • redirect-uri : 구글에서 인증이 끝나면, 인가 코드를 보낼 주소입니다.
    • {baseUrl} : 현재 서비스의 루트 URL로 스프링이 자동으로 채워줍니다.
    • {registrationId} : OAuth 클라이언트 이름이 들어가게 됩니다. (예: google)

3-2. OAuth2 인증 후, 내부 처리

image

UML
@startuml
actor 회원
participant "Google OAuth" as oauth
participant 모여락
participant OAuth2UserService
participant AuthenticationSuccessHandler
participant AuthenticationFailureHandler
participant DB

oauth -> 모여락 : 액세스 토큰 + \n 사용자 정보 반환
모여락 -> OAuth2UserService : OAuth2UserRequest 전달
OAuth2UserService -> DB : 이메일 기반 회원 조회
DB --> OAuth2UserService : 회원 정보 또는 없음

alt 신규 회원
    OAuth2UserService -> AuthenticationSuccessHandler : 신규 회원 플래그 전달
    AuthenticationSuccessHandler -> DB : 신규 회원 DB 저장
else 기존 회원
    OAuth2UserService -> AuthenticationSuccessHandler : 기존 회원 플래그 전달
end

AuthenticationSuccessHandler -> 회원 : 인증 성공 응답

== 인증 실패시 ==
oauth -> 모여락 : OAuth2 인증 실패
모여락 -> AuthenticationFailureHandler : 401 응답 생성
AuthenticationFailureHandler -> 회원 : 401 Unauthorized 응답

@enduml

3-2-1. OAuth2UserService 구현체

구글 OAuth 클라이언트를 통해 인증이 된다면, 액세스 토큰과 사용자 정보를 전달받게 됩니다.
이때 Spring Security는, 받은 정보를 처리하기 위하여 OAuth2UserService를 구현한 클래스에서 처리할 수 있도록 넘겨주게 됩니다.

image

전달받은 인증 완료 정보가 OAuth2UserRequest 객체에 담겨 전달됩니다.
해당 객체를 기반으로 OAuth2User객체를 생성하게 되며,
여기에는 인증 완료된 계정의 이름, 이메일 등의 정보가 담기게 됩니다.

final Optional<User> user = userRepository.findByEmailAndUse(email, true);  
  
Map<String, Object> attributes = new HashMap<>(oauth2User.getAttributes());  
  
if (user.isPresent()) {  
    attributes.put("isNew", false);  
  
    return UserPrincipal.generate(user.get().getId(), email, name, attributes);  
}  
  
attributes.put("isNew", true);  
  
return UserPrincipal.newUserGenerate(email, name, attributes);

추출된 이메일을 이용해 회원 테이블에 조회하여 회원 가입인지 로그인인지 판단합니다.

  • 데이터가 없다면 신규 회원으로 간주
  • 있다면, 기존 회원이 재로그인하는 것으로 간주

3-2-2. AuthenticationSuccessHandler

AuthenticationSuccessHandler 구현체는 OAuth2 로그인 성공 후 추가 처리를 하는 역할을 하게 됩니다.

@Component  
@RequiredArgsConstructor  
class CustomOAuth2SuccessHandler implements AuthenticationSuccessHandler {  
  
    @Override  
    public void onAuthenticationSuccess(  
            HttpServletRequest request, HttpServletResponse response, Authentication authentication)  
            throws IOException {  
  
        final UserPrincipal userPrincipal = (UserPrincipal) authentication.getPrincipal();  
  
        final Boolean isNew = userPrincipal.getAttribute("isNew"); // < 신규 회원

이전 OAuth2UserService 구현체에서, 신규 회원의 경우 isNew 항목이 true로 설정된 상태로 해당 구현체로 넘어오게 됩니다.

모여락 서비스처럼 별도의 온보딩 과정이 필요한 경우, 해당 구현체를 통해 최소한의 값을 응답으로 담아 클라이언트에 내져줄 수 있습니다.

  • 예: 이름, 이메일 등

반대로 기존 회원 고객이라면,

  • 토큰을 발급한다거나,
  • 로그인 API로 리다이렉트를 하는 등의 부가 처리를 해줄 수 있습니다.

3-2-3. CustomOAuth2FailureHandler

해당 구현체는, OAuth2 인증 과정에서 실패가 발생했을 때 후처리를 담당하는 구현체입니다.

인증이 실패했기 때문에, 클라이언트에게 401 Unauthorized 응답을 내려주게 됩니다.

추가로 필요에 따라,
실패 관련 로그를 작성한다거나
로그인 실패 횟수 카운팅,
인증 도중에 발생한 세션이나 쿠키 등을 제거해 주는 로직도 담당할 수 있습니다.

4. 고민 포인트

4-1. 회원 정보는 언제 insert 하는 것이 좋을까?

처음 구글 OAuth 클라이언트에서 정보를 받은 뒤, 즉시 회원 여부를 판단하고 계정이 없으면 회원 테이블에 insert 하는 방식을 고려하였습니다.
이 방식의 장점은, 고객이 온보딩 과정에서 이탈 후 다시 접속하더라도 온보딩 진행 상태를 기억할 수 있다는 점이였습니다.

다만 이 방식에서의 고민되는 부분은,
만약 해당 고객이 다시 돌아오지 않는다면 우리 서비스에는 불필요한 이메일 정보가 계속 테이블 내에 존재하게 된다는 점이었습니다.

만약 이 방식을 채택한다면,
특정 기간까지 온보딩 과정을 처리하지 않은 데이터를 주기적으로 삭제해 주는 스케줄 작업이 필요할 것이라고 판단하였습니다.

최대한 관리 포인트를 줄이고, 데이터 정합성을 높이기 위해 온보딩이 완료된 시점에 회원 정보를 insert하는 방식을 최종적으로 선택하였습니다.

4-2. 로그인 API로 리다이렉트를 해준다면?

AuthenticationSuccessHandler 이후 토큰 발급을 위한 로그인 API로 리다이렉트를 한다고 했을 경우,
HTTP MethodGETAPI만 가능하다는 제약이 있기 때문에 Query Parameter를 이용한 최소 정보를 전달하여 처리해야 합니다.

이를 처리해주기 위한 방식으로는 글로벌 캐시에 로그인 가능한 정보를 남겨 임시로 관리하는 방법을 고려 해 보았습니다.

구체적으로, Key는 UUID 같은 임의의 고유값으로 생성하고,
Value에는 회원 ID를 저장하여, 클라이언트가 리다이렉트될 때, 해당 UUID를 쿼리 파라미터로 전달받아 최종적으로 로그인 과정에서 확인하는 방식입니다.

모여락 서비스는 단일 애플리케이션이기 때문에,
ConcurrentHashMap을 이용해 처리한다면
Redis같이 TTL이 자동 관리가 되지 않기에 일정 시간 이후 데이터 정리해야 된다는 것을 신경 쓴다면 보다 안정적인 로그인 처리가 될 수 있을 것 같습니다.

5. 회고 및 기술 선택 만족도

간편 로그인 기능을 처음으로 이해하고 구현을 해 보았는데, Spring Security가 생각보다 많은 기능을 제공해 주고 있다는 것이 놀라웠습니다.
덕분에 어려움 없이 회원 가입을 구현할 수 있었던 것 같습니다.

만약 직접 회원 가입 기능을 개발했더라면,

  • 이메일 인증
  • 비밀번호 암호화 및 관리
  • 비밀번호 관리 정책 (찾기, 변경 등) 과 같은 부분을 모두 직접 구현하고, 유지보수 해야 했을 것입니다.

`Spring Security`와 `OAuth2 Client`를 활용한 덕분에,
민감 정보를 직접 관리하지 않으면서도 보안과 유지보수 측면을 챙길 수 있었고,
개발 기간을 크게 단축할 수 있었던 것 같습니다.

6. 효과

사용자 측면에서는 모여락 서비스에 종속적인 비밀번호 입력 등의 절차가 필요 없다는 점이 가장 큰 장점이라고 생각합니다.
이를 통해 가입 진입 장벽을 낮출 수 있을 것으로 기대됩니다.
또한 별도의 아이디나 비밀번호를 생성하고, 관리하지 않아도 된다는 점 역시 사용자에게 편리함을 제공할 수 있게 됩니다.

서버 측면에서는,
저장된 비밀번호를 직접 관리하지 않아도 되기 때문에 보안 부담과 유지보수 포인트가 줄어든다는 점이 장점입니다.

또한, 구글과 같은 OAuth 제공자는 자체적으로 2단계 인증이나,
이상 로그인 탐지 등 추가 보안 기능을 자체적으로 제공하기 때문에 이러한 이점을 함께 활용할 수 있다는 점 역시 효과가 될 수 있습니다.


마무리

모여락의 경우 OAuth 제공 업체를 Google로만 사용하고 있어서, 구글에 한정된 형태로 로직이 작성되어 있습니다.
지금 되돌아보면, 다른 제공자가 추가되더라도 수정 없이 확장할 수 있는 유연한 구조로 구성했더라면 좋았을 것이라는 아쉬움이 남습니다.

시간이 날 때, 다른 OAuth 제공자도 쉽게 추가할 수 있도록 구조를 개선해 보고자 합니다.

Clone this wiki locally