테니스 매칭 서비스인 ‘Tennis GG’의 백엔드 초기 구성을 잡으면서 고민했던 내용들을 정리해본다. 특히 회원 관리 부분에서 일반 이메일 가입과 소셜 로그인을 어떻게 깔끔하게 합칠지가 주된 고민이었다.
왜 이 글을 쓰게 되었는지
이번 프로젝트에서는 Java 21과 Spring Boot 최신 버전을 도입하면서, 인증 구조를 어떻게 가져갈지 결정해야 했다. 혼자 개발하다 보니 놓치기 쉬운 보안 이슈(JWT 관리 등)도 챙겨야 했기에 기록으로 남겨둔다.
1. 개발 환경 (Environment Setup)
혼자 개발하더라도 스펙은 확실하게 잡고 가고 싶었다. 특히 Java 21의 신기능들을 적극적으로 활용해보고 싶었다.
- Language: Java 21
- Framework: Spring Boot 4.0.1 (실험적인 선택이지만 도전!)
- DBMS: MySQL (User, Match 등 정형 데이터), Redis (캐시, 토큰, 인증코드)
- Auth: Spring Security + OAuth2 Client + JWT
2. 회원 관리 프로세스 설계 (User Management)
회원 정보는 users 테이블 하나에서 통합 관리하기로 했다. provider 필드로 EMAIL, KAKAO, APPLE을 구분하는 방식이다.
3. (v1) 이메일 인증을 통한 회원가입 설계
사실 초기(v1)에는 이메일 인증 기반의 회원가입을 완벽하게 설계하고 구현까지 했었다. 결과적으로는 소셜 로그인만 남기기로 결정했지만, 이메일 인증을 구현하려는 다른 분들에게 도움이 될까 싶어 당시 설계 내용을 공유한다.
이메일 가입은 무조건 인증이 선행되어야 한다. 인증 코드를 저장할 공간으로 TTL(Time To Live) 설정이 편한 Redis를 선택했다.
3.1 이메일 인증 흐름 (Email Verification Flow)
과정은 크게 3단계로 나뉜다: 코드 발송 -> 코드 검증 -> 회원가입
1) 인증 코드 발송 (POST /auth/send-verification)
사용자가 이메일을 입력하면 6자리 랜덤 코드를 생성해서 메일로 쏜다. 이때 Redis에 email_verification:{email} 키로 코드를 저장하고, 유효기간을 3분으로 설정했다. (너무 길면 보안상 별로다.)
// Request Body
{
"email": "user@example.com"
}
2) 인증 코드 검증 (POST /auth/verify-email)
사용자가 메일함에서 코드를 확인하고 입력하면, Redis에 저장된 코드와 대조한다.
- 일치하면: Redis에
email_verified:{email}키로 “인증 완료됨” 상태를 저장한다. (유효기간 10분) - 불일치/만료: 당연히 에러(
400 Bad Request)를 뱉는다.
// Request Body
{
"email": "user@example.com",
"code": "123456"
}
3) 회원가입 완료 (POST /auth/signup)
최종 가입 요청이 들어오면 DB에 넣기 전에 한 번 더 체크한다. isEmailVerified(email) 함수를 통해 Redis에 “인증 완료” 상태가 남아있는지 확인하는 것이다. 이게 없으면 “인증 먼저 하고 오세요”라고 튕겨낸다.
// Request Body
{
"username": "tennis_lover",
"password": "strongPassword123!", // BCrypt 암호화 필수
"email": "user@example.com",
"nickname": "테니스왕",
// ... 기타 정보
}
4. 과감한 결정: 결국 소셜 로그인으로 (v2)
…하지만 위 기능을 다 만들어놓고 써보니 “너무 번거로웠다”. 모바일 앱에서 이메일 치고, 앱 나가서 메일 확인하고, 다시 와서 코드 입력하고… 이탈률이 눈에 보였다.
그래서 과감하게 이메일 가입 로직을 걷어내고 소셜 로그인(카카오, 애플) 만 남기기로 결정했다. 덕분에 User 엔티티에서 비밀번호 관련 필드를 제거할 수 있었고, 보안 로직도 훨씬 심플해졌다.
변경된 User 엔티티 (v2)
@Entity
@Getter
@NoArgsConstructor(access = AccessLevel.PROTECTED)
@Table(name = "users")
public class User {
@Id
@Column(columnDefinition = "BINARY(16)")
private UUID id;
@Column(unique = true, nullable = false)
private String email;
// password 삭제!
@Column(length = 20)
private String nickname;
@Enumerated(EnumType.STRING)
@Column(nullable = false)
private Provider provider; // KAKAO, APPLE
@Column(nullable = false)
private String socialId;
@Enumerated(EnumType.STRING)
private Role role; // USER, ADMIN
// ...
}
5. 로그인 및 토큰 관리 (JWT + Redis)
세션 대신 JWT(Stateless) 방식을 채택했다.
- AccessToken: 12시간 (API 요청용)
- RefreshToken: 14일 (재발급용, Redis 저장)
마무리
이메일 인증을 열심히 구현했지만 버리는 과정이 아깝지는 않았다. 덕분에 Redis 활용법도 익혔고, UX에 대한 고민도 해볼 수 있었으니까. 결국 남은 건 소셜 로그인이지만, 혹시 이메일 인증이 필요한 분들은 위 설계를 참고하면 좋을 것 같다.
다음 글에서는 카카오/애플 로그인 구현 실전을 다뤄보겠다.