0. 회원가입 플로우
1. 소셜 로그인을 진행하고 KakaoId 혹은 GoogleId를 최종적으로 가져온다.
2. 소셜ID + ACTIVE 유저가 DB에 있다면 로그인 처리를 진행한다.
3. 소셜ID + INACTVIE or 해당 소셜ID로 조회되지 X -> 회원가입 진행을 한다.
- 이때 소셜ID를 담고 INACTIVE인 임시 유저를 생성하고 회원가입 단계에서 점차 사용자 정보를 update한다.
4. 인증 코드를 받을 학교 이메일을 입력한다.
5. 인증 코드를 입력한다.
6. 약관 동의를 한다.
7. 사용자 추가 정보를 입력한다.
- 이때 최종적으로 유저 상태를 ACTIVE로 전환
1. 카카오, 구글 소셜 로그인 구현 중
카카오와 구글 로그인을 구현하는 도중 개발하는 서비스 특징 상 고민거리가 발생했다.
가치택시에서는 카카오, 구글 소셜로 로그인해서 사용자 인증을 거친 뒤, 실제로는 학교 이메일을 받아서 회원에 저장한다. (카카오 Id, 구글 Id도 저장한다.)
또한 약관 동의, 추가 사용자 정보를 순차적으로 받아서 업데이트 한 뒤 마지막에 UserStatus를 ACTIVE로 바꾼다.
따라서 최종 회원가입이 완료되면 DB에 다음과 같이 저장된다. (약관 동의, 추가 정보 컬럼은 생략)
ex 카카오로 회원가입 완료 한 경우
2. 고민거리
이때 내가 구글로 로그인 하려고 한다면?
소셜 로그인 페이지에서 인증 후 다음과 같이 임시 유저가 생성되게 된다.
이러고 난 뒤, 학교 이메일을 koreaioi@gachon.ac.kr로 입력하면 이미 가입된 이메일이라서 회원가입 진행이 불가능하다.
즉,
나는 카카오, 구글 로그인 방식이 모두 있는 서비스에서 둘 중 하나로 로그인 되는 서비스를 본 적이 없었다.
그래서 FE와 BE분들과 이야기를 나눴다.
개발자들끼리 이야기를 나눴을 때 사용자 한명당 카카오, 구글 계정을 연동하는 것으로 결정했다.
그래서 열심히 밤새워 로직도 생각하고 코드도 짜서 완성했다.
3. 비지니스 로직 (엣지 케이스까지)
아 정말 골치 아팠습니다.
통합 로그인을 진행하는 경우는 다음과 같습니다.
이미 카카오 계정으로 최초 회원가입을 진행한 유저가 구글 계정으로 소셜 로그인 하려는 경우
이미 구글 계정으로 최초 회원가입을 진행한 유저가 카카오 계정으로 소셜 로그인 하려는 경우
아래와 같은 상황일 경우 위 통합 로그인을 진행하는 기준이 됩니다.
인증 코드를 받을 학교 이메일을 입력하는 단계에서
학교 이메일 + ACTIVE한 유저가 있는 지 조회합니다.
기존 멤버가 카카오 ID를 가진 경우 + 기존 멤버가 구글 ID에 값이 없는 경우 + 임시 유저가 구글 ID에 값이 있는 경우
반대 상황이라면
기존 멤버가 구글 ID를 가진 경우 + 기존 멤버가 카카오 ID에 값이 없는 경우 + 임시 유저가 카카오 ID에 값이 있는 경우
위 두 가지 경우에는 KAKAO_INTEGRATE, GOOGLE_INTEGRATE를 반환하도록 했습니다. (정상적인 경우는 MAIL_SUCCESS)
3 - 1. 왜 임시 유저의 소셜ID 존재 여부도 검사하는지?
임시 유저 소셜ID 존재 여부를 검사하는 이유는 테스트에서 직접 경험해봤기 때문이다.
임시 유저 소셜ID 존재 여부를 검사하지 않는다면, 다음과 같다.
기존 멤버가 카카오 ID를 가진 경우 + 기존 멤버가 구글 ID에 값이 없는 경우
기존 멤버가 구글 ID를 가진 경우 + 기존 멤버가 카카오 ID에 값이 없는 경우
이 경우에는 다음과 같은 엣지 케이스가 발생할 수 있다.
(같은 학교 이메일로 인증한다는 가정 하)
1. 이미 구글 계정으로 최초 회원가입을 완료한 유저가 다른 구글 계정으로 소셜 로그인 하려는 경우
2. 이미 카카오 계정으로 최초 회원가입을 완료한 유저가 다른 구글 계정으로 소셜 로그인 하려는 경우
3. 모든 연동이 끝난 사용자 계정의 학교 이메일로 회원가입 하려는 경우
위는 예시이다. (임시 유저 소셜ID 검증을 하지 않는다면?)
기존 유저의 kakaoId, googleId가 하나만 존재하기 때문에 MAIL_SUCCESS가 반환된다.
이메일 인증 코드를 제대로 입력하면 유저2의 이메일이 koreaioio@gachon.ac.kr로 update되어야 하는데 이는 유니크 조건에 의해서 거절된다.
(이는 개발자가 의도한 커스텀 예외 처리가 이루어지지 않기에, 개발자가 직접 예외 처리를 해줘야한다고 생각한다.)
4. 코드
AuthController의 일부
memberService의 IsAlreadySignEmail에서 기존 가입자 인지 아닌 지 판단합니다.
@PostMapping("/code/mail")
@Operation(summary = "이메일 인증 코드를 보내는 API입니다. 기존 가입자의 경우 통합 로그인을 해주세요.")
public ApiResponse sendEmail(
@RequestBody @Valid EmailAddressDto emailDto,
@CurrentMemberId Long userId // 임시 토큰에서 가져온 임시 유저 ID
) {
MemberMailResponseDto dto = memberService.IsAlreadySignEmail(emailDto.email(), userId);
emailService.sendEmail(emailDto.email());
return ApiResponse.response(OK, EMAIL_SEND_SUCCESS.getMessage(), dto);
}
MemberService의 IsAlreadySignEmail 실제 코드입니다.
멤버가 소셜ID 소유 여부를 도메인 로직으로 만들었습니다.
public MemberMailResponseDto IsAlreadySignEmail(String email, Long tmpId) {
Optional<Members> findMembers = memberRepository.findByEmailAndStatus(email, ACTIVE);
if(findMembers.isPresent()) { // 이미 가입되어 있는 회원 가입 한 유저면
Members members = findMembers.get(); // 이미 가입되어있는 멤버
Members tmpMembers = findById(tmpId);
if (members.hasKakaoId() && !members.hasGoogleId() && tmpMembers.hasGoogleId()) {
return new MemberMailResponseDto(KAKAO_INTEGRATE, email, null, tmpMembers.getGoogleId());
}
if (members.hasGoogleId() && !members.hasKakaoId() && tmpMembers.hasKakaoId()){ // 구글 Id가 있다는 의미므로 GOOGLE_INTEGRATE
return new MemberMailResponseDto(GOOGLE_INTEGRATE, email, tmpMembers.getKakaoId(), null);
}
throw new DuplicatedEmailException();
}
return new MemberMailResponseDto(MAIL_SUCCESS, email, null, null);
}
AuthController 중 인증 코드 + 통합 로그인 전용 API이다.
통합 로그인이 완료되면 AccessToken과 RefreshToken을 발급해 응답에 반환한다.
@PatchMapping("/code/integration")
@Operation(summary = "인증코드 검증 + 통합 로그인을 진행하는 API 입니다. 성공 시 토큰을 발행합니다. ")
public ApiResponse checkAuthCodeAndKakaoIntegration(
@RequestBody MemberIntegrationRequestDto dto,
@CurrentMemberId Long userId,
HttpServletResponse response
) {
emailService.checkEmailAuthCode(dto.email(), dto.authCode());
JwtTokenDto jwtTokenDto = generateIntegration(dto, userId);
responseToken(jwtTokenDto, response);
return ApiResponse.response(OK, INTEGRATION_SUCCESS.getMessage());
}
// 리팩토링
private JwtTokenDto generateIntegration(MemberIntegrationRequestDto dto, Long userId){
if(dto.kakaoId() != null){ // 카카오Id에 값이 있으면 구글로 통합 로그인
return jwtService
.generateJwtToken(memberService.IntegrationMemberToGoogle(dto, userId));
}else{ //카카오로 통합 로그인
return jwtService
.generateJwtToken(memberService.IntegrationMemberToKakao(dto, userId));
}
}
실제로 기존 멤버와 임시 멤버를 통합하는 비지니스 로직입니다.
여기서 중요한 건 flush()
각 소셜 Id는 Unique 하기 때문에 임시 멤버를 지우고 기존 멤버의 다른 소셜 ID를 갱신해야합니다.
@Transactional
public MemberTokenDto IntegrationMemberToKakao(MemberIntegrationRequestDto dto, Long tmpId) {
Members existsMembers = findActiveByEmail(dto.email());
memberRepository.deleteById(tmpId);
memberRepository.flush();
existsMembers.updateGoogleId(dto.googleId());
return MemberTokenDto.from(existsMembers);
}
@Transactional
public MemberTokenDto IntegrationMemberToGoogle(MemberIntegrationRequestDto dto, Long tmpId) {
Members existsMembers = findActiveByEmail(dto.email());
memberRepository.deleteById(tmpId);
memberRepository.flush();
existsMembers.updateKakaoId(dto.kakaoId());
return MemberTokenDto.from(existsMembers);
}
5. 구현 결과
잘 작동한다!
실제 DB도 잘 적용된 모습이다!
6. 통합 로그인 과연??
기존에 카카오와 구글 로그인을 동시에 할 수 없는 문제점을 발견하고 통합 로그인을 먼저 구현해봤다.
통합 로그인에 대해서 장단점도 파악할 수 있었다.
이에 대해서 전체적인 회의를 거쳐본 결과
통합 로그인은 아직 시기상조 or 굳이 엣지 케이스를 늘리는 위험이라는 팀원들의 의견을 반영 해, 구현하지 않기로 했다!
즉, 카카오로 회원가입을 한 경우 구글로 회원가입을 진행할 때, 이미 가입된 학교 이메일을 입력하는 경우 예외 처리하기로 했다!
밤새서 테스트 삼아 구현해봤지만, 막상 통합로그인을 택하지 않으니 아쉽긴하다... 흑
'프로젝트에서 일어난 일' 카테고리의 다른 글
졸프: 라즈베리파이와 MSA를 곁들인 (0) | 2025.04.10 |
---|---|
Jwt토큰 인가 검증에서 일어난 간단한 사건! (0) | 2025.02.04 |
N+1 싹둑(Slice)해버리기 (0) | 2025.02.01 |
허용한 경로까지 막아버리는 불효자 코드 물효자로 만들기 (0) | 2025.01.14 |
AccessToken을 상쾌하게 만드는 RefreshToken (0) | 2025.01.14 |