[Spring Boot] 카카오, 네이버, 구글 SNS 로그인
요즘 대부분의 웹 서비스는 별도의 회원가입 없이 카카오, 네이버, 구글 같은 SNS 계정으로 로그인할 수 있게 되어 있죠.
사용자 입장에서는 편리하고, 서비스 입장에서도 이탈률을 줄일 수 있는 중요한 기능입니다.
저는 지금까지 여러 프로젝트에 참여했지만, 로그인 기능을 직접 구현해 본 경험은 없었습니다. 그래서 언젠가는 꼭 한 번 제대로 구현해 봐야겠다는 생각을 갖고 있었고, 이번 개인 프로젝트를 계기로 SNS 로그인 기능을 직접 구현해보게 되었습니다.
이 글에서는 Spring Boot를 기반으로
- 카카오, 네이버, 구글의 로그인 기능을 각각 연동하고,
- 로그인한 사용자의 정보를 세션에 저장,
- 회원 여부에 따라 자동 로그인 처리 또는 약관 페이지로 이동 및 회원가입 까지 처리하는 기능을 구현하는 과정을 담았습니다.
1. 회원 엔티티 클래스
SNS 로그인 사용자도 저장할 수 있는 회원 테이블 구성.
package com.demo.domain;
import org.hibernate.annotations.ColumnDefault;
import org.hibernate.annotations.DynamicInsert;
import org.hibernate.annotations.DynamicUpdate;
import jakarta.persistence.Column;
import jakarta.persistence.Entity;
import jakarta.persistence.GeneratedValue;
import jakarta.persistence.GenerationType;
import jakarta.persistence.Id;
import jakarta.persistence.PrePersist;
import jakarta.persistence.SequenceGenerator;
import jakarta.persistence.Temporal;
import jakarta.persistence.TemporalType;
import java.util.Date;
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Getter;
import lombok.NoArgsConstructor;
import lombok.Setter;
import lombok.ToString;
@Getter
@Setter
@ToString
@Entity
@NoArgsConstructor
@AllArgsConstructor
@Builder
@DynamicInsert
@DynamicUpdate
public class Member {
@Id
@GeneratedValue(strategy = GenerationType.SEQUENCE, generator = "member_seq")
@SequenceGenerator(name="member_seq", sequenceName = "member_seq", allocationSize = 1)
private long member_seq;
@Column(unique = true)
private String id; // 아이디
private String pwd; // 비밀번호
private String name; // 이름
private String email; // 이메일
private String telephone;
private String provider;
private String address; // 기본 주소
private String addressDetail; // 상세 주소
@ColumnDefault("0")
private int membercode; // 회원 코드
@Temporal(value = TemporalType.TIMESTAMP)
@ColumnDefault(value = "CURRENT_TIMESTAMP")
private Date signupDate; // 가입 날짜
@ColumnDefault("0") // 탈퇴 요청 기본값을 0으로 설정 (대기)
private int withdrawalRequest; // 회원 탈퇴 요청 상태
@PrePersist
protected void onCreate() {
if (this.signupDate == null) {
// 메서드를 사용하여 엔티티가 처음 저장될 때 signupDate 필드가 'null'이면 현재 날짜 및 시간을 설정
this.signupDate = new Date();
}
}
}
- private String provider 를 통해 SNS 로그인인지 일반 회원가입인지 구분.
2. SNS 사용자 정보 DTO
SNS 로그인 후 각 플랫폼(카카오, 네이버, 구글)에서 반환하는 사용자 정보가 다르기 때문에
공통 포맷으로 받아 처리할 수 있도록 DTO 클래스를 따로 구성.
package com.demo.dto;
import com.fasterxml.jackson.annotation.JsonIgnoreProperties;
import com.fasterxml.jackson.annotation.JsonProperty;
import lombok.Getter;
import lombok.Setter;
@JsonIgnoreProperties(ignoreUnknown = true)
@Getter
@Setter
public class User {
@JsonProperty("id")
private String id;
@JsonProperty("email")
private String email;
@JsonProperty("verified_email")
private boolean verifiedEmail;
@JsonProperty("provider")
private String provider;
@JsonProperty("name")
private String name;
@JsonProperty("given_name")
private String givenName;
@JsonProperty("family_name")
private String familyName;
@JsonProperty("picture")
private String picture;
@JsonProperty("locale")
private String locale;
}
3. Repository / Service 계층
(SNS 계정 로그인에서 사용하는 부분만 작성했습니다.)
package com.demo.persistence;
import java.util.List;
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.data.jpa.repository.Modifying;
import org.springframework.data.jpa.repository.Query;
import org.springframework.data.repository.query.Param;
import org.springframework.transaction.annotation.Transactional;
import com.demo.domain.Member;
public interface MemberRepository extends JpaRepository<Member, Long> {
@Query(value="SELECT * FROM member WHERE id =:id", nativeQuery=true)
Member findByLoginId(String id);
}
package com.demo.service;
import java.util.List;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.stereotype.Service;
import com.demo.domain.Member;
import com.demo.persistence.MemberRepository;
@Service
public class MemberServiceImpl implements MemberService {
@Autowired
private MemberRepository memberRepo;
@Override
public Member getMember(String id) {
return memberRepo.findByLoginId(id);
}
}
4. HTML + JS : SNS 로그인 버튼 및 redirect
<!DOCTYPE html>
<html xmlns:th="http://www.thymeleaf.org">
<head>
<meta charset="utf-8">
<title>login</title>
<!-- 카카오 로그인 SDK -->
<script src="https://developers.kakao.com/sdk/js/kakao.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/17.0.2/umd/react.production.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react-dom/17.0.2/umd/react-dom.production.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/babel-standalone/6.26.0/babel.min.js"></script>
<!-- 네이버 로그인 SDK -->
<script type="text/javascript" src="https://static.nid.naver.com/js/naveridlogin_js_sdk_2.0.2.js"></script>
<!-- 네이버 로그인 라이브러리 -->
<script type="text/javascript" src="https://static.nid.naver.com/js/naverLogin_implicit-1.0.3.js" charset="utf-8"></script>
<!-- 구글 로그인 -->
<meta name="google-signin-client_id" content="발급받은 CLIENT ID">
<script src="https://accounts.google.com/gsi/client" async defer></script>
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/@fortawesome/fontawesome-free@6.5.2/css/all.min.css">
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/sweetalert2@11.4.10/dist/sweetalert2.min.css">
<script src="https://cdn.jsdelivr.net/npm/sweetalert2@11.4.10/dist/sweetalert2.min.js"></script>
<script src="https://code.jquery.com/jquery-3.6.0.min.js"></script>
<link rel="stylesheet" th:href="@{/css/login.css}">
<script th:src="@{js/login.js}"></script>
</head>
<body>
<div class="login-container">
<div class="social-login">
<h1>소셜 로그인</h1>
<div id="naver_id_login" class="login-button" style="display:block;">
</div>
<div id="kakao_id_login" style="cursor:pointer; background-color:#FEE500; border-radius:5px;">
<img src="images/kakao_login.png" alt="카카오 로그인" onclick="kakaoLogin()">
</div>
<div id="google_id_login" data-client_id="발급받은 CLIENT ID"
data-callback="onSignIn" data-auto_prompt="false">
</div>
<div class="g_id_signin" data-type="standard" data-shape="rect"
data-theme="outline" data-text="signin_with" data-size="large">
</div>
</div>
</div>
<script>
// 1. 카카오 SDK 초기화
Kakao.init('발급받은 API KEY');
console.log('Kakao 초기화 여부:', Kakao.isInitialized());
// 2. 로그인 함수 정의
function kakaoLogin() {
Kakao.Auth.login({
scope: 'profile_nickname, account_email',
success: function (authObj) {
console.log('인증 성공', authObj);
Kakao.API.request({
url: '/v2/user/me',
success: function (res) {
console.log('사용자 정보', res);
const kakaoAccount = res.kakao_account;
const userInfo = {
id: res.id.toString(),
email: kakaoAccount.email,
name: kakaoAccount.profile.nickname,
provider: 'kakao'
};
fetch('/oauth/kakao/callback', {
method: 'POST',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify(userInfo)
})
.then(response => response.json())
.then(result => {
if (result.status == 'login_success') {
window.location.href = '/main';
} else if (result.status == 'signup_required') {
sessionStorage.setItem('kakao_user', JSON.stringify(userInfo));
window.location.href = '/oauth/contract'
}
})
.catch(error => {
console.error('오류 발생', error);
});
},
fail: function (error) {
console.error('사용자 정보 요청 실패', error);
}
});
},
fail: function (err) {
console.error('로그인 실패', err);
}
});
}
</script>
<script>
// 네이버 로그인 초기화
var naver_id_login = new naver_id_login("발급받은 CLIENT ID", "http://localhost:9010/oauth/naver/callback");
var state = naver_id_login.getUniqState();
naver_id_login.setButton("green", 3, 50);
naver_id_login.setDomain("localhost:9010");
naver_id_login.setState(state);
naver_id_login.setPopup(true); // 팝업 모드 활성화
naver_id_login.init_naver_id_login();
</script>
<script>
window.onload = function() {
google.accounts.id.initialize({
client_id: "발급받은 CLIENT ID",
callback: onSignIn
});
google.accounts.id.renderButton(
document.getElementById("google_id_login"), {
theme: "outline",
size: "large",
text: "signin_with"
}
);
};
// 구글 로그인
function onSignIn(response) {
const userObject = parseJwt(response.credential);
var id = userObject.sub;
var name = userObject.name;
var email = userObject.email;
var provider = "google";
post_to_url("/oauth/google/callback", {
id: id, name: name, email: email, provider: provider
});
}
function parseJwt(token) {
var base64Url = token.split('.')[1];
var base64 = base64Url.replace(/-/g, '+').replace(/_/g, '/');
var jsonPayload = decodeURIComponent(atob(base64).split('').map(function(c) {
return '%' + ('00' + c.charCodeAt(0).toString(16)).slice(-2);
}).join(''));
return JSON.parse(jsonPayload);
}
function post_to_url(path, params, method='post') {
const form = document.createElement('form');
form.method = method;
form.action = path;
for(const key in params) {
if(params.hasOwnProperty(key)) {
const hiddenField = document.createElement('input');
hiddenField.type = 'hidden';
hiddenField.name = key;
hiddenField.value = params[key];
form.appendChild(hiddenField);
}
}
document.body.appendChild(form);
form.submit();
}
</script>
</body>
</html>

- 카카오 로그인
- 카카오 로그인 버튼 > kakaoLogin() 함수 실행
- Kakao.Auth.login() 호출 > 카카오 인증 창
- 인증 성공 : Kakao.API.request('/v2/user/me')로 사용자 정보 요청
- 받은 사용자 정보에서 id, email, name, provider: "kakao" 추출
- PostMapping("/oauth/kakao/callback")에서 json 바디를 User 객체로 받음.
- 회원 존재 시 세션에 loginUser 등록 후 {status: "login_success"} JSON 응답
- 회원 없으면 세션에 임시 user 저장 후 {status: "signup_required"} JSON 응답
- 응답 결과에 따라 로그인 성공 시 /main으로, 회원가입 필요 시 /oauth/contract로 페이지 이
- 네이버 로그인
- 네이버 로그인 SDK 자동 초기화 (naver_id_login.init_naver_id_login())
- 네이버 로그인 버튼 클릭 시 팝업창에서 인증 진행
- 인증 완료 후 콜백 URL(http://localhost:9010/oauth/naver/callback)로 리다이렉트(기본 GET 요청)
- 사용자 정보를 받아서 직접 POST 요청으로 /oauth/naver/callback에 JSON 전송
- GET /oauth/naver/callback → 네이버 로그인 페이지(뷰) 반환
- POST /oauth/naver/callback → User 객체 JSON 바디로 받음
- DB에서 사용자 조회 후 로그인 성공 또는 회원가입 필요 여부 JSON으로 반환 (카카오와 동일)
- POST 응답에 따라 로그인 성공 시 메인 페이지, 회원가입 필요 시 회원가입 페이지로 이동
- 구글 로그인
- 페이지 로드 시 google.accounts.id.initialize()로 구글 로그인 초기화
- 구글 로그인 버튼 클릭 후 로그인 성공하면 onSignIn(response) 콜백 호출
- JWT 토큰(response.credential) 파싱해서 id, name, email, provider: "google" 추출
- post_to_url('/oauth/google/callback', {...}) 함수로 HTML form POST 요청 생성 및 제출
- POST /oauth/google/callback → @RequestParam으로 id, name, email, provider 받음
- DB에서 id로 회원 조회
- 회원 있으면 세션에 loginUser 등록 후 /oauth/autoLogin으로 리다이렉트 (로그인 성공 처리)
- 회원 없으면 세션에 임시 user 저장 후 /oauth/contract(회원가입 동의 페이지) 리다이렉트
구분 | 클라이언트 요청 | 서버 매핑 | 데이터 전달 방식 | 응답 처리 |
카카오 | JS fetch() POST JSON |
@PostMapping("/kakao/callback") | JSON 바디 | JSON 결과를 받고 JS에서 페이지 이동 |
네이버 | 네이버 SDK 팝업 > 콜백 URL 리다이렉션 > POST JSON | GET /naver/callback(뷰), POST /naver/callback | JSON 바디 (POST) | JSON 결과를 받고 JS에서 페이지 이동 |
구글 | JS에서 JWT 파싱 후 HTML form POST 제출 | POST /google/callback | @RequestParam 폼 데이터 | 서버에서 리다이렉트(로그인 성공/가입) |
5. Controller 계층 : 콜백 처리 및 세션 저장
package com.demo.controller;
import java.util.HashMap;
import java.util.Map;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.http.ResponseEntity;
import org.springframework.stereotype.Controller;
import org.springframework.ui.Model;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.ResponseBody;
import com.demo.domain.Member;
import com.demo.dto.User;
import com.demo.persistence.MemberRepository;
import com.demo.service.MemberService;
import com.demo.service.PasswordGenerator;
import jakarta.servlet.http.HttpSession;
import lombok.RequiredArgsConstructor;
@Controller
@RequestMapping("/oauth")
@RequiredArgsConstructor
public class OauthController {
@Autowired
private MemberService memberService;
@Autowired
private MemberRepository memberRepo;
// 카카오 로그인 콜백
@PostMapping("/kakao/callback")
@ResponseBody
public Map<String, String> kakaoLogin(@RequestBody User user, HttpSession session) {
System.out.println("Received User object: " + user); // User 객체 출력
// 회원 조회
Member kakaoUser = memberService.getMember(user.getId());
Map<String, String> response = new HashMap<>();
if (kakaoUser != null) {
session.setAttribute("loginUser", kakaoUser); // 실제 DB에서 조회한 Member 객체 저장
response.put("status", "login_success"); // 로그인 성공 응답
} else {
session.setAttribute("user", user);
System.out.println("User info stored in session: " + session.getAttribute("user"));
response.put("status", "signup_required"); // 회원가입 필요 응답
}
return response; // JSON 형식으로 응답
}
// 네이버 로그인 콜백
@GetMapping("/naver/callback")
public String naverCallback(HttpSession session) {
return "login/naver";
}
@PostMapping("/naver/callback")
@ResponseBody
public Map<String, String> naverLogin(@RequestBody User user, HttpSession session) {
System.out.println("Naver User: " + user);
Member naverUser = memberService.getMember(user.getId());
Map<String, String> response = new HashMap<>();
if (naverUser != null) {
session.setAttribute("loginUser", naverUser);
response.put("status", "login_success");
} else {
session.setAttribute("user", user);
System.out.println("User info stored in session: " + session.getAttribute("user"));
response.put("status", "signup_required");
}
return response;
}
// 구글 로그인
@PostMapping("/google/callback")
public String googleLogin(@RequestParam String id, @RequestParam String name,
@RequestParam String email, @RequestParam String provider, HttpSession session) {
System.out.printf("Google User info :: { ",id, ", ", name, ", ", email, ", ", provider,"}");
User user = new User();
user.setId(id);
user.setName(name);
user.setEmail(email);
user.setProvider(provider);
session.setAttribute("loginUser", user);
Member googleUser = memberService.getMember(user.getId());
if (googleUser != null) {
session.setAttribute("loginUser", googleUser);
return "redirect:/oauth/autoLogin";
} else {
session.setAttribute("user", user);
return "redirect:/oauth/contract";
}
}
@GetMapping("/contract")
public String contractPage(HttpSession session, Model model) {
User user = (User) session.getAttribute("user");
System.out.println("contract 진입 user: " + user);
if (user != null) {
String id = user.getId();
int exists = memberService.confirmId(id);
if (exists == 1) {
return "redirect:/oauth/autoLogin";
}
model.addAttribute("user", user);
}
return "login/contract";
}
@GetMapping("/autoLogin")
public String autoLogin(HttpSession session) {
User user = (User) session.getAttribute("user");
if (user != null) {
String id = user.getId();
// DB에서 해당 회원 정보를 조회
Member loginUser = memberService.getMember(id);
session.setAttribute("loginUser", loginUser);
session.removeAttribute("user"); // 불필요한 user 세션 제거
}
return "redirect:/main";
}
@GetMapping("/joinForm")
public String joinFormPage(HttpSession session, Model model) {
User user = (User) session.getAttribute("user");
if (user != null) {
model.addAttribute("name", user.getName());
model.addAttribute("email", user.getEmail());
model.addAttribute("id", user.getId());
model.addAttribute("provider", user.getProvider());
}
return "login/joinForm";
}
@PostMapping("/confirmEmail")
@ResponseBody
public int confirmEmail(@RequestParam String email) {
return memberService.confirmEmail(email);
}
@PostMapping("/joinAction")
public String joinAction(Member vo) {
Member member = new Member();
String randomPassword = PasswordGenerator.generateRandomPassword();
member.setId(vo.getId());
member.setName(vo.getName());
member.setEmail(vo.getEmail());
member.setTelephone(vo.getTelephone());
member.setProvider(vo.getProvider());
member.setAddress(vo.getAddress());
member.setAddressDetail(vo.getAddressDetail());
member.setPwd(randomPassword);
member.setMembercode(0);
memberRepo.save(member);
return "redirect:/main";
}
}
- 공통 흐름
- 사용자 인증은 각 소셜 플랫폼 SDK(JS)를 통해 클라이언트에서 진행
- 인증 성공 후, 사용자 정보(ID, NAME, EMAIL, PROVIDER 등)를 POST 요청으로 서버에 전달.
- memberService.getMember(id)로 DB에 사용자 존재 여부 확인
- 존재 시 : loginUser 세션 등록 > 로그인 완료.
- 미존재 시 : user 세션 등록 > /contract 페이지로 리다이렉션 후 회원가입 진행
- 카카오 로그인
- 클라이언트 요청 : JS에서 카카오 SDK를 통해 인증 및 사용자 정보 조회 후,
/oauth/kakao/callback에 User JSON 객체를 POST - 서버 처리 : @PostMapping("/kakao/callback")
- public Map<String, String> kakaoLogin(@RequestBody User user, HttpSession session)
- user.getId()로 DB 조회
- 있으면 loginUser 세션에 저장, login_success 응답
- 없으면 user 세션에 저장, signup_required응답(>> /oauth/contract 이동)
- public Map<String, String> kakaoLogin(@RequestBody User user, HttpSession session)
- 클라이언트 요청 : JS에서 카카오 SDK를 통해 인증 및 사용자 정보 조회 후,
- 네이버 로그인
- 클라이언트 요청 : 네이버 SDK 팝업 인증 > 콜백 URL로 리다이렉션 > 이후 User JSON으로 /naver/callback POST
- 서버 처리 : @GetMapping("/naver/callback") >> login/naver.html 반환 (프론트에서 사용자 정보 POST 전송 준비)
@PostMapping("/naver/callback")- public Map<String, String> naverLogin(@RequestBody User user, HttpSession session)
- DB 조회 후 동일한 방식으로 세션에 loginUser or user 저장
- 결과 JSON 응답
- 구글 로그인
- 클라이언트 요청 : 구글 로그인 후 받은 토큰을 파싱해 사용자 정보 추출
<form>을 만들어 POST /oauth/google/callback로 HTML form 전송 - 서버 처리 : @PostMapping("/google/callback")
- public String googleLogin(@RequestParam String id, @RequestParam String name, ...)
- 전달받은 파라미터로 User 객체 생성
- DB에서 id로 회원 존재 여부 확인
- 있으면 loginUser 세션 저장 후 /oauth/autoLogin 리다이렉션
- 없으면 user 세션 저장 후 /oauth/contract 리다이렉션
- 클라이언트 요청 : 구글 로그인 후 받은 토큰을 파싱해 사용자 정보 추출
6. 로그인 여부에 따른 페이지 이동
- 회원가입 진행 (/oauth/contract , /oauth/joinForm , /oauth/joinAction)
- contractPage(): 세션에 저장된 user 가져와서 확인, 이미 존재하면 자동 로그인
- joinFormPage(): 세션의 user 정보를 form에 미리 채워줌
- joinAction(): 전달된 Member 객체로 DB 저장 후 메인 페이지 이동
기능 테스트 (구글 계정)



비회원일 경


약관 동의 후 확인

회원 정보 입력 후 회원가입

이후에 구글 계정으로 로그인 할 시 계정을 선택하면 자동 로그인
(다른 sns계정인 네이버, 카카오도 마찬가지로 회원 조회 후 없으면
약관 페이지 > 회원가입 페이지 > 회원가입
이후로는 자동 로그인으로 같은 흐름으로 진행)
SNS 로그인 회원가입 및 자동 로그인 흐름 정리
1. 소셜 로그인 시작
플랫폼 SDK 또는 OAuth 인증을 통해 사용자가 로그인하면, 사용자 식별 정보(ID, EMAIL, NAME, PROVIDER)를 서버로 전달.
요청 방식 | 콜백 경로 | |
카카오 | POST + JSON | /oauth/kakao/callback |
네이버 | POST + JSON | /oauth/naver/callback |
구글 | POST + HTML form (application/x-www-form-relencoded) | /oauth/google/callback |
2. 회원 여부 확인
Member kakaoUser = memberService.getMember(user.getId());
Member naverUser = memberService.getMember(user.getId());
Member googleUser = memberService.getMember(user.getId());
회원 존재 O > loginUser를 세션에 저장 > 자동 로그인 처리
session.setAttribute("loginUser", kakaoUser);
session.setAttribute("loginUser", naverUser);
session.setAttribute("loginUser", googleUser);
response.put("status", "login_success"); // 카카오, 네이버
return "redirect:/oauth/autoLogin"; // 구글
회원 존재 X > user 세션에 임시 저장 > 약관 동의 페이지(/contract)로 이동
session.setAttribute("user", user);
response.put("status", "signup_required"); // 카카오, 네이버
return "redirect:/oauth/contract"; // 구글
3. 약관 동의 > 회원가입
- /oauth/contract : 약관 동의 확인
- /oauth/joinForm : User 정보로 회원가입 form 자동 입력
- /oauth/joinAction : DB에 신규 회원 저장
비밀번호는 자동 생성되며 저장됩니다. (member.setPwd(PasswordGenerator.generateRandomPassword());)
4. 이후 로그인 시
이미 가입된 SNS 계정으로 로그인하면 곧바로 로그인 처리되며, loginUser 세션을 통해 메인 페이지(/main)로 이동합니다.
아직 부족한 로그인 기능 구현
— 더 공부하고 확장해야 할 부분 정리 —
- 이메일 인증 기반 가입 처리
- SNS 로그인 시 제공되는 이메일 정보를 활용해서 사용자에게 이메일 인증을 요구하거나, 이메일 중복 가입 방지 등 보안성 있는 가입 절차.
- 이메일 인증 메일 발송 후 확인 절차 필요.
- 인증된 사용자만 회원가입 완료 처리.
- 기존 사용자와 이메일 중복 시 처리 로직 필요.
- SNS 로그인 시 제공되는 이메일 정보를 활용해서 사용자에게 이메일 인증을 요구하거나, 이메일 중복 가입 방지 등 보안성 있는 가입 절차.
- 자동 로그인 시 마지막 접속일 기록
- 유저의 활동 이력을 관리하기 위해 자동 로그인 또는 수동 로그인 시점에 마지막 접속일을 기록.
- 엔티티 클래스인 Member에 필드를 추가하고, 로그인 시점에 해당 날짜를 DB에 저장.
- 추후 관리자 페이지나 통계 기능에서 활용.
- 유저의 활동 이력을 관리하기 위해 자동 로그인 또는 수동 로그인 시점에 마지막 접속일을 기록.
- 로그인 이력 저장
- 로그인 시도에 대해서 로그 기록을 남겨 보안상 대응이나, 유저 활동 분석에 활용.
- 성공 or 실패 여부, 로그인 시각 등을 저장
- 별도의 테이블을 설계하고, Spring AOP 또는 필터/인터셉터를 활용해 공통 처리.
- 로그인 실패 / 예외 처리 메시지 세분화
- 현재는 성공 여부에 따른 단순 응답만 처리하지만 나중에는 상세한 에러 메시지와 상태 코드 분리 처리가 필요.
- 404 : 회원가입이 필요합니다.(회원 정보 없음)
- 400 : 잘못된 요청입니다.(잘못된 요청 포맷)
- 500 : 로그인 중 오류가 발생했습니다.(서버 오류)
- 현재는 성공 여부에 따른 단순 응답만 처리하지만 나중에는 상세한 에러 메시지와 상태 코드 분리 처리가 필요.
이번 SNS 로그인 기능 구현을 통해 세 개의 플랫폼(Kakao, Naver, Google) OAuth 인증 흐름 이해 / 세션 기반 로그인 상태 처리 / 회원가입과 자동 로그인 흐름 통합을 구현해 보았고, 로그인 기능은 단순히 되는 것보다, 안전하게 잘 되는 것이 훨씬 더 중요하다는 걸 이번 구현을 통해 느꼈다. 앞으로도 보완할 부분이 많지만, 계속해서 학습하고 개선하며 안정적이면서 확장 가능한 인증 시스템을 완성해 가고 싶다.