카카오 로그인은 카카오계정으로 다양한 서비스에 로그인할 수 있도록 하는 OAuth 2.0 기반의 소셜 로그인 서비스이다.
OAuth = Open Authorization으로, 권한 부여를 위한 개방형 표준 프로토콜이다.
사용자가 앱에 로그인 사용자 인증 정보를 증명하지 않아도 앱이 사용자의 보호된 리소스를 제한적으로 액세스하도록 허용한다.

1.1 카카오 로그인 요청
사용자가 프론트 단에서 카카오 로그인 버튼 클릭
이떄 서비스 서버는 카카오 인증 서버로 리다이렉트 할 URL 생성
1.2 인가 코드 받기 요청
1.3 로그인
로그인 페이지에서 카카오 계정으로 로그인
1.4 인가 코드 발급
카카오 인증 서버는 사용자를 우리가 지정한 redirect_url로 리다이렉트 시킨다.
Rederiect URI 의 위치비교
프론트 vs 서버
- 프론트 -> 소셜 -> 프론트 (인가코드) -> 백엔드 -> 프론트
- 프론트 -> 백엔드(인가코드) -> 소셜 -> 백엔드 -> 프론트
프론트단에서 인가 코드를 받아 백엔드로 로그인 요청을 보내는 방법과,
백엔드에서 모든 작업을 완수해준 뒤, 프론트로 리다이렉트를 처리해주는 방법이 있다.
사용자 경험 관점에서 왜 프론트 URI를 선호할까?
- 로그인 직후 프론트에서 상태 변경이 가능해 빠르게 보여 줄 수 있다.
- 사용자 경험을 위해서 많은 서비스에서 프론트로 URI를 설정하는 방식으로 진행한다.
- 그러나 인가코드 노출의 위험이 있기 떄문에 보안 측면에서는 서버에서 처리하는 것이 낫다.
🔁 전체 로그인 흐름 요약
1. 사용자가 URL에 접근
2. Kakao 로그인 페이지로 리다이렉트됨
3. 사용자가 Kakao에 로그인 하면 -> 카카오가 code를 백엔드에 넘겨줌
4. Spring security가 자동으로:
- code를 이용해 액세스 토큰 요청
- 액세스 토큰으로 사용자 정보 조회
5. CustomOAuth2UserService에서 사용자 정보 파싱해서 처리 (회원가입, 세션 저장 등)
처음에 사용자를 카카오 인증 페이지로 리다이렉트 시키는 "인가 코드 요청 URL" 을 생성하는 메서드가 필요하다.
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Component;
import org.springframework.web.util.UriComponentsBuilder;
@Component
public class KakaoOauthService {
@Value("${spring.security.oauth2.client.registration.kakao.client-id}")
private String clientId;
@Value("${spring.security.oauth2.client.registration.kakao.redirect-uri}")
private String redirectUri;
public String getKakaoAuthorizeUri() { // Kakao 인증 URL 생성 서비스
return UriComponentsBuilder.fromUriString("https://kauth.kakao.com/oauth/authorize")
.queryParam("response_type", "code")
.queryParam("client_id", clientId)
.queryParam("redirect_uri", redirectUri)
.queryParam("scope", "profile_nickname, account_email")
.build()
.toUriString();
}
}
주의할점
카카오에 권한 요청할때 요청하는 동의항복과 카카오에 설정된 동의항목이 일치해야한다.
맞지 않으면 인증 단계에서 오류가 발생한다.
처음에 동의항목에서 이메일을 체크하지 않아서 205 에러가 발생했다.

개인정보 동의 항목은 닉네임, 이메일을 사용하였다.
2) 인가 코드 받아서 액세스 토큰 요청 + 사용자 정보 요청
http://localhost:8080/login/oauth2/code/kakao
스프링 시큐리티 기본 OAuth2 로그인 기능을 쓰려면 콜백 URI는 이런 형식으로 설정해야 한다.
이렇게 설정해야 별도 콜백 처리 컨트롤러 없이 loadUser()가 정상 호출돼서 회원가입/로그인 처리가 된다.
import com.fasterxml.jackson.annotation.JsonIgnoreProperties;
import com.fasterxml.jackson.annotation.JsonProperty;
import lombok.Getter;
@JsonIgnoreProperties(ignoreUnknown = true) // 필요없는 필드 무시
@Getter
public class KakaoInfoDto {
private Long id;
@JsonProperty("kakao_account")
private KakaoAccount kakaoAccount;
@JsonIgnoreProperties(ignoreUnknown = true)
@Getter
public static class KakaoAccount {
private Profile profile;
private String email;
@JsonIgnoreProperties(ignoreUnknown = true)
@Getter
public static class Profile {
private String nickname;
}
}
}
카카오 OAuth2 로그인 시 카카오에서 전달하는 사용자 정보 JSON을 받기 위한 DTO이다.
카카오 API에서 내려주는 사용자 이메일, 닉네임 등의 정보를 객체로 변환하기 위한 용도이다.
public enum Platform {
LOCAL, KAKAO
}
로컬 로그인과 카카오 로그인을 구분하기 위한 Enum을 생성하였다.
import jakarta.persistence.*;
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Generated;
import lombok.Getter;
import lombok.NoArgsConstructor;
import java.time.LocalDateTime;
@Entity
@Builder
@Generated
@Getter
@AllArgsConstructor
@NoArgsConstructor
public class Member {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long memberId;
@Column(name = "user_id", unique = true, length = 30)
private String userId;
@Column(unique = true)
private String kakaoId;
@Column(unique = true)
private String email;
@Column(length = 50)
private String nickname;
@Enumerated(EnumType.STRING)
private Role role;
@Column(unique = true, length = 20)
private String password;
@Column(unique = true)
private String memberNumber;
@Enumerated(EnumType.STRING)
private Platform platform;
@Column
private String name;
@Column(unique = true, length = 20)
private String phone_number;
@Column(unique = true)
private String address;
@Column
private String grade;
@Column(name = "created_at")
private LocalDateTime createdAt;
@Column(name = "is_deleted")
private boolean isDeleted;
public void setIsDeleted() {
this.isDeleted = true;
}
}
import com.example.shopping_mall.domain.Member;
import com.example.shopping_mall.domain.type.Platform;
import com.example.shopping_mall.domain.type.Role;
import com.example.shopping_mall.dto.KakaoInfoDto;
import com.example.shopping_mall.repository.MemberRepository;
import com.example.shopping_mall.security.KakaoMemberDetails;
import com.fasterxml.jackson.databind.ObjectMapper;
import lombok.RequiredArgsConstructor;
import org.springframework.security.oauth2.client.userinfo.DefaultOAuth2UserService;
import org.springframework.security.oauth2.client.userinfo.OAuth2UserRequest;
import org.springframework.security.oauth2.core.OAuth2AuthenticationException;
import org.springframework.security.oauth2.core.user.OAuth2User;
import org.springframework.stereotype.Component;
import java.time.LocalDateTime;
@Component
@RequiredArgsConstructor
public class CustomOAuth2UserService extends DefaultOAuth2UserService {
private final MemberRepository memberRepository;
@Override
public OAuth2User loadUser(OAuth2UserRequest userRequest) throws OAuth2AuthenticationException {
OAuth2User oAuth2User = super.loadUser(userRequest);
ObjectMapper objectMapper = new ObjectMapper();
KakaoInfoDto kakaoInfo = objectMapper.convertValue(oAuth2User.getAttributes(), KakaoInfoDto.class);
String kakaoId = String.valueOf(kakaoInfo.getId());
String nickname = kakaoInfo.getKakaoAccount().getProfile().getNickname();
String email = kakaoInfo.getKakaoAccount().getEmail();
Member member = memberRepository.findByKakaoId(kakaoId)
.orElseGet(() -> memberRepository.save(Member.builder()
.userId("kakao_" + kakaoId)
.kakaoId(kakaoId)
.nickname(nickname)
.email(email)
.platform(Platform.KAKAO)
.role(Role.ROLE_USER)
.createdAt(LocalDateTime.now())
.isDeleted(false)
.build()
));
return new KakaoMemberDetails(member, oAuth2User.getAttributes());
}
}
스프링 시큐리티 OAuth2 로그인 처리 시 카카오 사용자 정보를 받아와서 내부 로직에 맞게 가공 및 인증 처리를 하는 클래스이다.
카카오에서 받은 JSON을 KakaoInfoDto로 변환하여 DB에 있는지 확인 후 없으면 저장한다.
import com.example.shopping_mall.domain.Member;
import lombok.Getter;
import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.core.authority.SimpleGrantedAuthority;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.oauth2.core.user.OAuth2User;
import java.util.Collection;
import java.util.Collections;
import java.util.Map;
@Getter
public class KakaoMemberDetails implements UserDetails, OAuth2User {
private final Member member;
private final Collection<? extends GrantedAuthority> authorities;
private final Map<String, Object> attributes; // OAuth2User의 attributes 저장
public KakaoMemberDetails(Member member, Map<String, Object> attributes) {
this.member = member;
this.attributes = attributes;
this.authorities = Collections.singletonList(new SimpleGrantedAuthority(member.getRole().name()));
}
@Override
public Map<String, Object> getAttributes() {
return attributes;
}
@Override
public String getName() {
return member.getUserId(); // OAuth2User의 고유 name 값
}
@Override
public Collection<? extends GrantedAuthority> getAuthorities() {
return authorities;
}
@Override
public String getPassword() {
return member.getPassword(); // 카카오 로그인은 비밀번호 null 가능
}
@Override
public String getUsername() {
return member.getUserId();
}
@Override
public boolean isAccountNonExpired() {
return true;
}
@Override
public boolean isAccountNonLocked() {
return true;
}
@Override
public boolean isCredentialsNonExpired() {
return true;
}
@Override
public boolean isEnabled() {
return !member.isDeleted();
}
}
로그인 된 사용자 정보를 스프링 시큐리티 표준(UserDetails + OAuth2User)에 맞춰 포장하는 클래스이다.
최종적으로 KakaoMemberDetails를 만들어 반환한다.
Spring Security Authentication에 저장된 후 로그인 성공!
이후 컨트롤러에서
@AuthenticationPrincipal KakaoMemberDetails user
로 접근이 가능하다.

로그인 후 DB에 저장된 것을 확인하였다.
'spring & java' 카테고리의 다른 글
| Spring + Redis를 이용한 Email 인증 구현 (1) | 2025.08.19 |
|---|---|
| 스프링 시큐티리 + JWT 를 이용한 로그인 처리 (2) | 2025.08.03 |
| Spring Event를 이용한 비동기 이벤트 처리 (0) | 2025.02.23 |
| 쿠키 & 세션 / JWT (1) | 2024.08.22 |
| JPA 영속성 컨텍스트 / 영속성 전이 (0) | 2024.08.13 |