Skip to content

Commit

Permalink
Refactor: OauthService 회원 가입, 로그인, 탈퇴 시 Oauth id token, access token …
Browse files Browse the repository at this point in the history
…요청/응답과 db 트랜잭션 분리 (#276)

* Refactor: OauthService에서 socialProfile 생성 로직을 SocialProfileService로 분리

* Refactor: SignupEvent와 WithdrawEvent 추가

* Refactor: MemberWithdrawService에서 탈퇴 시 러닝 데이터가 없어도 탈퇴 id를 로깅하도룩 수정

* Fix: social profile 조회 시 member도 fetch join하도록 수정

* Test: OauthServiceTest에 리팩토링 사항 반영
  • Loading branch information
Jaewon-pro authored Oct 28, 2024
1 parent e6d098d commit 4e4e322
Show file tree
Hide file tree
Showing 9 changed files with 151 additions and 114 deletions.
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
package com.dnd.runus.application.member;

import com.dnd.runus.application.member.event.SignupEvent;
import com.dnd.runus.application.member.event.WithdrawEvent;
import lombok.RequiredArgsConstructor;
import org.springframework.context.event.EventListener;
import org.springframework.stereotype.Component;

@Component
@RequiredArgsConstructor
public class MemberEventHandler {

private final MemberService memberService;
private final MemberWithdrawService memberWithdrawService;

@EventListener
public void handleSignupEvent(SignupEvent signupEvent) {
memberService.initMember(signupEvent.member());
}

@EventListener
public void handleWithdrawEvent(WithdrawEvent withdrawEvent) {
memberWithdrawService.deleteAllDataAboutMember(withdrawEvent.memberId());
}
}
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
package com.dnd.runus.application.member;

import com.dnd.runus.domain.level.Level;
import com.dnd.runus.domain.member.Member;
import com.dnd.runus.domain.member.MemberLevel;
import com.dnd.runus.domain.member.MemberLevelRepository;
import com.dnd.runus.presentation.v1.member.dto.response.MyProfileResponse;
Expand All @@ -13,6 +14,11 @@
public class MemberService {
private final MemberLevelRepository memberLevelRepository;

@Transactional
public void initMember(Member member) {
memberLevelRepository.save(new MemberLevel(member));
}

@Transactional(readOnly = true)
public MyProfileResponse getMyProfile(long memberId) {
MemberLevel.Current memberCurrentLevel = memberLevelRepository.findByMemberIdWithLevel(memberId);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -44,11 +44,15 @@ public void deleteAllDataAboutMember(long memberId) {
scaleAchievementRepository.deleteByMemberId(member.memberId());
socialProfileRepository.deleteByMemberId(member.memberId());

// running_record 조회
deleteRunningRecordAndRelatedData(member);

memberRepository.deleteById(member.memberId());
log.info("멤버 삭제 완료: memberId={}", member.memberId());
}

private void deleteRunningRecordAndRelatedData(Member member) {
List<RunningRecord> runningRecords = runningRecordRepository.findByMember(member);
if (runningRecords.isEmpty()) {
// running_record가 없으면 멤버 삭제 후 리턴
memberRepository.deleteById(member.memberId());
return;
}

Expand All @@ -63,11 +67,6 @@ public void deleteAllDataAboutMember(long memberId) {
challengeAchievementRepository.deleteByIds(challengeAchievementIds);
}

// running_record 삭제
runningRecordRepository.deleteByMemberId(member.memberId());

memberRepository.deleteById(member.memberId());

log.info("멤버 삭제 완료: memberId={}", member.memberId());
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
package com.dnd.runus.application.member.event;

import com.dnd.runus.domain.member.Member;

public record SignupEvent(Member member) {}
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
package com.dnd.runus.application.member.event;

public record WithdrawEvent(long memberId) {}
73 changes: 16 additions & 57 deletions src/main/java/com/dnd/runus/application/oauth/OauthService.java
Original file line number Diff line number Diff line change
@@ -1,17 +1,14 @@
package com.dnd.runus.application.oauth;

import com.dnd.runus.application.member.MemberWithdrawService;
import com.dnd.runus.application.member.event.SignupEvent;
import com.dnd.runus.application.member.event.WithdrawEvent;
import com.dnd.runus.auth.exception.AuthException;
import com.dnd.runus.auth.oidc.provider.OidcProvider;
import com.dnd.runus.auth.oidc.provider.OidcProviderRegistry;
import com.dnd.runus.auth.token.TokenProviderModule;
import com.dnd.runus.auth.token.dto.AuthTokenDto;
import com.dnd.runus.domain.member.*;
import com.dnd.runus.global.constant.MemberRole;
import com.dnd.runus.domain.member.SocialProfile;
import com.dnd.runus.global.constant.SocialType;
import com.dnd.runus.global.event.AfterTransactionEvent;
import com.dnd.runus.global.exception.BusinessException;
import com.dnd.runus.global.exception.NotFoundException;
import com.dnd.runus.global.exception.type.ErrorType;
import com.dnd.runus.presentation.v1.oauth.dto.request.SignInRequest;
import com.dnd.runus.presentation.v1.oauth.dto.request.SignUpRequest;
Expand All @@ -23,7 +20,6 @@
import lombok.extern.slf4j.Slf4j;
import org.springframework.context.ApplicationEventPublisher;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;

@Slf4j
@Service
Expand All @@ -33,73 +29,56 @@ public class OauthService {
private final OidcProviderRegistry oidcProviderRegistry;
private final TokenProviderModule tokenProviderModule;

private final MemberRepository memberRepository;
private final SocialProfileRepository socialProfileRepository;
private final MemberLevelRepository memberLevelRepository;

private final MemberWithdrawService memberWithdrawService;
private final SocialProfileService socialProfileService;
private final ApplicationEventPublisher eventPublisher;

@Transactional
public SignResponse signIn(SignInRequest request) {
OidcProvider oidcProvider = oidcProviderRegistry.getOidcProviderBy(request.socialType());
Claims claim = oidcProvider.getClaimsBy(request.idToken());
String oauthId = claim.getSubject();
String email = extractAndValidateEmail(claim, request.socialType());

SocialProfile socialProfile = socialProfileRepository
.findBySocialTypeAndOauthId(request.socialType(), oauthId)
.orElseThrow(
() -> new BusinessException(ErrorType.USER_NOT_FOUND, "socialType: " + request.socialType()));
SocialProfile socialProfile = socialProfileService.findOrThrow(request.socialType(), oauthId, email);

updateEmailIfChanged(socialProfile, email);
AuthTokenDto tokenDto = tokenProviderModule.generate(
String.valueOf(socialProfile.member().memberId()));
return SignResponse.from(socialProfile.member().nickname(), socialProfile.oauthEmail(), tokenDto);
}

@Transactional
public SignResponse signUp(SignUpRequest request) {
OidcProvider oidcProvider = oidcProviderRegistry.getOidcProviderBy(request.socialType());
Claims claim = oidcProvider.getClaimsBy(request.idToken());
String oauthId = claim.getSubject();
String email = extractAndValidateEmail(claim, request.socialType());

// 기존 사용자 없을 경우 insert
SocialProfile socialProfile = socialProfileRepository
.findBySocialTypeAndOauthId(request.socialType(), oauthId)
.orElseGet(() -> createMember(oauthId, email, request.socialType(), request.nickname()));
SocialProfile socialProfile =
socialProfileService.findOrCreate(request.socialType(), oauthId, email, request.nickname());

updateEmailIfChanged(socialProfile, email);
AuthTokenDto tokenDto = tokenProviderModule.generate(
String.valueOf(socialProfile.member().memberId()));

eventPublisher.publishEvent(new SignupEvent(socialProfile.member()));

return SignResponse.from(socialProfile.member().nickname(), socialProfile.oauthEmail(), tokenDto);
}

@Transactional(readOnly = true)
public boolean revokeOauth(long memberId, WithdrawRequest request) {

memberRepository.findById(memberId).orElseThrow(() -> new NotFoundException(Member.class, memberId));

OidcProvider oidcProvider = oidcProviderRegistry.getOidcProviderBy(request.socialType());

String oauthId = oidcProvider.getClaimsBy(request.idToken()).getSubject();

socialProfileRepository
.findBySocialTypeAndOauthId(request.socialType(), oauthId)
.filter(profile -> profile.member().memberId() == memberId)
.orElseThrow(() -> new AuthException(
ErrorType.INVALID_CREDENTIALS,
String.format(
"socialType: %s, oauthId: %s, memberId: %s", request.socialType(), oauthId, memberId)));
if (!socialProfileService.isSocialMemberExists(request.socialType(), oauthId, memberId)) {
String message =
String.format("socialType: %s, oauthId: %s, memberId: %s", request.socialType(), oauthId, memberId);
throw new AuthException(ErrorType.INVALID_CREDENTIALS, message);
}

try {
String accessToken = oidcProvider.getAccessToken(request.authorizationCode());
oidcProvider.revoke(accessToken);
log.info("토큰 revoke 성공. memberId: {}, socialType: {}", memberId, request.socialType());

AfterTransactionEvent withDrawEvent = () -> memberWithdrawService.deleteAllDataAboutMember(memberId);
eventPublisher.publishEvent(withDrawEvent);
eventPublisher.publishEvent(new WithdrawEvent(memberId));

return true;
} catch (Exception e) {
Expand All @@ -108,20 +87,6 @@ public boolean revokeOauth(long memberId, WithdrawRequest request) {
}
}

private SocialProfile createMember(String oauthId, String email, SocialType socialType, String nickname) {
Member member = memberRepository.save(new Member(MemberRole.USER, nickname));

// default level 설정
memberLevelRepository.save(new MemberLevel(member));

return socialProfileRepository.save(SocialProfile.builder()
.member(member)
.socialType(socialType)
.oauthId(oauthId)
.oauthEmail(email)
.build());
}

private String extractAndValidateEmail(Claims claim, SocialType socialType) {
String email = (String) claim.get("email");
if (StringUtils.isBlank(email)) {
Expand All @@ -130,10 +95,4 @@ private String extractAndValidateEmail(Claims claim, SocialType socialType) {
}
return email;
}

private void updateEmailIfChanged(SocialProfile socialProfile, String email) {
if (!email.equals(socialProfile.oauthEmail())) {
socialProfileRepository.updateOauthEmail(socialProfile.socialProfileId(), email);
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@
package com.dnd.runus.application.oauth;

import com.dnd.runus.domain.member.Member;
import com.dnd.runus.domain.member.MemberRepository;
import com.dnd.runus.domain.member.SocialProfile;
import com.dnd.runus.domain.member.SocialProfileRepository;
import com.dnd.runus.global.constant.MemberRole;
import com.dnd.runus.global.constant.SocialType;
import com.dnd.runus.global.exception.NotFoundException;
import lombok.RequiredArgsConstructor;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;

@Service
@RequiredArgsConstructor
public class SocialProfileService {

private final SocialProfileRepository socialProfileRepository;
private final MemberRepository memberRepository;

@Transactional(readOnly = true)
public boolean isSocialMemberExists(SocialType socialType, String oauthId, long memberId) {
return socialProfileRepository
.findBySocialTypeAndOauthId(socialType, oauthId)
.filter(profile -> profile.member().memberId() == memberId)
.isPresent();
}

@Transactional
public SocialProfile findOrThrow(SocialType socialType, String oauthId, String email) {
SocialProfile socialProfile = socialProfileRepository
.findBySocialTypeAndOauthId(socialType, oauthId)
.orElseThrow(() -> new NotFoundException(SocialProfile.class, oauthId));

updateEmailIfChanged(socialProfile, email);
return socialProfile;
}

@Transactional
public SocialProfile findOrCreate(SocialType socialType, String oauthId, String email, String nickname) {
Member member = memberRepository.save(new Member(MemberRole.USER, nickname));

SocialProfile socialProfile = socialProfileRepository
.findBySocialTypeAndOauthId(socialType, oauthId)
.orElseGet(() -> createSocialProfile(member, socialType, oauthId, email));

updateEmailIfChanged(socialProfile, email);
return socialProfile;
}

private SocialProfile createSocialProfile(Member member, SocialType socialType, String oauthId, String email) {
return socialProfileRepository.save(SocialProfile.builder()
.member(member)
.socialType(socialType)
.oauthId(oauthId)
.oauthEmail(email)
.build());
}

private void updateEmailIfChanged(SocialProfile socialProfile, String email) {
if (!email.equals(socialProfile.oauthEmail())) {
socialProfileRepository.updateOauthEmail(socialProfile.socialProfileId(), email);
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -2,12 +2,14 @@

import com.dnd.runus.global.constant.SocialType;
import com.dnd.runus.infrastructure.persistence.jpa.member.entity.SocialProfileEntity;
import org.springframework.data.jpa.repository.EntityGraph;
import org.springframework.data.jpa.repository.JpaRepository;

import java.util.Optional;

public interface JpaSocialProfileRepository extends JpaRepository<SocialProfileEntity, Long> {

@EntityGraph(attributePaths = {"member"})
Optional<SocialProfileEntity> findBySocialTypeAndOauthId(SocialType socialType, String oauthId);

void deleteByMemberId(long memberId);
Expand Down
Loading

0 comments on commit 4e4e322

Please sign in to comment.