본문 바로가기
카테고리 없음

Spring Boot 소셜 로그인 (이메일 인증, 카카오, DB 라우팅)

by ricepuppy9733 2026. 6. 12.

소셜 로그인 구현이 그냥 라이브러리 가져다 쓰면 끝나는 줄 알았습니다. 실제로 해보니 OAuth2 흐름 처리부터 JWT 연동, 이메일 인증, 심지어 DB 서버까지 이원화해야 한다는 걸 뒤늦게 깨달았습니다. 이 글은 Spring Boot 환경에서 이메일 인증과 카카오 소셜 로그인, 그리고 마스터/슬레이브 DB 라우팅까지 한 번에 구성하면서 제가 직접 부딪힌 것들을 정리한 내용입니다.

소셜 로그인

이메일 인증, 생각보다 막히는 구간이 많습니다

이메일 인증 기능을 처음 붙였을 때 가장 먼저 막힌 건 메일 서버 설정이었습니다. 직접 메일 서버를 구축하려 했는데, 스팸 차단 정책 때문에 대부분의 발신이 막혀버렸습니다. 결국 Gmail SMTP를 쓰는 방향으로 선회했고, 이 과정에서 앱 비밀번호 발급이 필수라는 걸 알게 됐습니다.

Gmail 설정에서 2단계 인증을 먼저 활성화한 뒤, 앱 비밀번호를 따로 발급받아야 합니다. 여기서 앱 비밀번호란 구글 계정의 실제 비밀번호 대신 특정 앱에서만 사용할 수 있도록 발급되는 16자리 전용 코드입니다. 이걸 application.yml의 password 항목에 넣어야 정상적으로 SMTP 인증이 통과됩니다.

build.gradle에는 spring-boot-starter-mail 의존성을 추가합니다. 이 스타터(Starter)란 Spring Boot가 특정 기능을 쓸 수 있도록 관련 라이브러리를 묶어 한 번에 제공하는 의존성 패키지입니다. 이걸 추가하면 JavaMailSender 인터페이스를 주입받아 바로 이메일 발송 코드를 작성할 수 있습니다.

회원가입 흐름은 이렇습니다.

  • 사용자가 회원가입 요청을 보내면, enabled 필드가 false인 상태로 DB에 저장됩니다.
  • UUID를 생성해서 인증 링크 URL에 포함시키고 이메일로 발송합니다.
  • 해당 UUID를 EmailVerify 테이블에 사용자 정보와 함께 저장합니다.
  • 사용자가 메일 내 링크를 클릭하면 서버로 UUID가 전달되고, 일치하는 레코드를 찾아 enabled를 true로 업데이트합니다.

MimeMessage와 MimeMessageHelper를 사용하면 HTML 형식의 이메일을 전송할 수 있습니다. 일반 텍스트 대신 버튼 스타일을 입힌 링크나 색상 있는 안내 문구를 넣을 수 있어서, 실제 서비스처럼 보이는 인증 메일을 만들 수 있습니다. 제가 직접 써봤는데, 이 부분은 사용자 경험 측면에서 생각보다 효과가 컸습니다.

카카오 OAuth2 연동, 흐름을 먼저 이해해야 합니다

카카오 로그인을 붙이기 전에 OAuth2 흐름부터 이해하는 게 순서입니다. OAuth2(Open Authorization 2.0)란 사용자가 자신의 비밀번호를 직접 제공하지 않고도 카카오, 구글 같은 외부 서비스가 보유한 계정 정보를 우리 서비스에서 인증 수단으로 활용할 수 있게 해주는 표준 프로토콜입니다.

spring-security-oauth2-client 의존성을 추가하면 인증 요청 URL 생성, 카카오 서버로의 리다이렉트 처리, 사용자 정보 요청까지 전체 흐름을 Spring Security가 자동으로 처리해줍니다. 제가 처음에 이 흐름을 몰랐을 때는 직접 HTTP 요청을 짜야 하나 싶었는데, 라이브러리가 상당 부분을 커버해줬습니다.

application.yml에는 카카오 개발자 콘솔에서 발급받은 REST API 키와 Client Secret, 그리고 리다이렉트 URI를 등록해야 합니다. 이 리다이렉트 URI는 카카오 개발자 사이트의 로그인 설정에도 동일하게 등록되어 있어야 합니다. 둘 중 하나라도 어긋나면 인증 요청이 거부되는데, 이 부분에서 시간을 꽤 썼습니다.

OAuth2UserService를 커스텀하는 이유는 카카오에서 받아온 사용자 정보를 우리 서비스의 User 엔티티와 연결하기 위해서입니다. DefaultOAuth2UserService를 상속받고 loadUser 메서드를 오버라이드하면, 카카오 응답의 properties 맵에서 nickname과 고유 id를 추출할 수 있습니다. 카카오 ID를 email 필드에 저장하는 방식으로 기존 회원 여부를 판별하고, 처음 로그인하는 경우 자동으로 회원가입 처리합니다. 이 로직은 트랜잭션으로 감싸는 것이 좋습니다. 트랜잭션이란 DB 작업 여러 개를 하나의 단위로 묶어 모두 성공하거나 모두 실패하도록 보장하는 메커니즘으로, 중간에 오류가 발생했을 때 데이터 불일치를 막아줍니다(출처: Spring 공식 문서).

JWT 쿠키 발급과 SecurityConfig 통합

세션 방식으로 소셜 로그인을 구현하면 일단 동작은 합니다. 하지만 서버를 수평 확장하거나 REST API 방식으로 프론트엔드와 분리할 경우 세션 관리가 복잡해집니다. 그래서 JWT(JSON Web Token) 방식으로 전환했습니다. JWT란 사용자 인증 정보를 JSON 형태로 담아 서버가 서명한 토큰으로, 서버가 별도로 상태를 저장하지 않아도 토큰만으로 사용자를 식별할 수 있습니다.

일반 이메일 로그인은 LoginFilter에서 성공 시 JWT를 발급해 쿠키에 담고, 소셜 로그인은 OAuth2AuthenticationSuccessHandler에서 동일한 방식으로 처리합니다. 두 경로가 모두 같은 JwtUtil.generateToken() 메서드를 호출하도록 구성했습니다. 덕분에 토큰 생성 로직이 한 곳에만 존재하고 일관성이 유지됩니다.

SecurityConfig에서 주의할 부분은 필터 순서입니다. JwtAuthFilter를 UsernamePasswordAuthenticationFilter 앞에 배치해서 쿠키에 담긴 토큰을 먼저 검사하고, 유효하면 SecurityContextHolder에 인증 정보를 등록합니다. SecurityContextHolder란 현재 요청을 처리하는 스레드에 인증된 사용자 정보를 저장하는 컨테이너로, 이후 컨트롤러나 서비스에서 현재 로그인 사용자를 꺼내 쓸 때 이 컨테이너를 참조합니다.

CORS 설정도 빠뜨리면 안 됩니다. 프론트엔드가 다른 포트에서 실행되는 경우, 브라우저가 쿠키를 포함한 요청을 차단합니다. setAllowCredentials(true)와 함께 허용할 Origin을 명시해야 쿠키 기반 인증이 정상 동작합니다.

DB 라우팅, 읽기와 쓰기를 분리하는 이유

마스터/슬레이브 구성은 처음 접했을 때 왜 이게 필요한지 직관적으로 와닿지 않았습니다. 그런데 트래픽이 늘어나면서 조회 쿼리가 쓰기 쿼리와 같은 DB 서버를 공유할 때 생기는 병목을 실제로 경험하고 나서야 이 구조의 의미가 명확해졌습니다.

스프링에서 이를 구현하는 핵심은 AbstractRoutingDataSource입니다. 이 클래스란 여러 DataSource 중 하나를 런타임에 동적으로 선택할 수 있게 해주는 스프링 제공 추상 클래스로, determineCurrentLookupKey 메서드를 오버라이드하여 어떤 DB를 쓸지 결정하는 로직을 직접 작성할 수 있습니다.

구현 방식은 이렇습니다. 현재 트랜잭션이 readOnly=true로 설정되어 있으면 SLAVE DataSource를, 그렇지 않으면 MASTER DataSource를 반환합니다. 이 라우팅 로직을 LazyConnectionDataSourceProxy로 한 번 더 감싸는 이유도 있습니다. LazyConnectionDataSourceProxy란 실제 DB 커넥션을 SQL이 처음 실행되는 시점까지 지연시키는 프록시 객체입니다. 트랜잭션이 시작됐지만 실제 쿼리가 없는 경우 불필요한 커넥션을 미리 점유하지 않아도 된다는 장점이 있습니다.

HikariCP는 각 DataSource에 적용된 커넥션 풀 라이브러리입니다. HikariCP란 미리 DB 연결을 일정 수만큼 만들어두고 재사용하는 방식으로, 매 요청마다 새 연결을 맺는 오버헤드를 줄여줍니다. Spring Boot 2.x 이후부터는 기본 커넥션 풀로 채택되어 있어 별도 설정 없이 DataSourceBuilder에 타입만 지정하면 사용할 수 있습니다(출처: Baeldung - HikariCP).

세 가지 DataSource 빈이 등록되는 순서도 중요합니다. @DependsOn을 통해 masterDataSource와 slaveDataSource가 먼저 생성된 후 routingDataSource가 만들어지고, 최종적으로 LazyConnectionDataSourceProxy가 이를 감싸도록 의존 관계를 명시해야 합니다.

이 세 가지를 한 프로젝트 안에서 동시에 구성하고 나니, 각 컴포넌트가 왜 존재하는지 훨씬 선명하게 보였습니다. 이메일 인증은 보안의 첫 관문이고, OAuth2 소셜 로그인은 사용자 진입 장벽을 낮추고, DB 라우팅은 서비스가 커졌을 때 버틸 수 있는 구조를 만들어줍니다. 세 가지가 따로 노는 기능이 아니라 서로 맞물려 돌아간다는 걸 직접 붙여보기 전까지는 몰랐습니다. 비슷한 구성을 고민하고 있다면, 라우팅부터 시작하지 말고 이메일 인증과 JWT 흐름을 먼저 완성한 뒤 소셜 로그인을 얹는 순서를 권합니다. 그게 디버깅할 때 훨씬 수월합니다.


참고: https://dltldnr2563.tistory.com/entry/%EC%BD%94%EB%94%A9%EA%B3%B5%EB%B6%8020250812-%EC%9D%B4%EB%A9%94%EC%9D%BC%EC%9D%B8%EC%A6%9D-%EC%B9%B4%EC%B9%B4%EC%98%A4-DB%EB%9D%BC%EC%9A%B0%ED%8C%85


소개 및 문의 · 개인정보처리방침 · 면책조항

© 2026 자동식단생성 연관 블로그