-
Notifications
You must be signed in to change notification settings - Fork 0
refactor: 사용자 구조를 팀 레포지토리 기반 아키텍처에 맞게 정렬 #12
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -3,12 +3,14 @@ | |
| import com.zimdugo.auth.domain.OAuth2UserInfo; | ||
| import com.zimdugo.auth.domain.OAuth2UserInfoFactory; | ||
| import com.zimdugo.user.domain.SocialAccount; | ||
| import com.zimdugo.user.domain.SocialAccountReader; | ||
| import com.zimdugo.user.domain.SocialAccountStore; | ||
| import com.zimdugo.user.domain.User; | ||
| import com.zimdugo.user.domain.UserRole; | ||
| import com.zimdugo.user.domain.UserStatus; | ||
| import com.zimdugo.user.domain.UserStore; | ||
| import com.zimdugo.user.infrastructure.SocialAccountJpaRepository; | ||
| import com.zimdugo.user.infrastructure.UserJpaRepository; | ||
| import java.util.HashMap; | ||
| import java.util.List; | ||
| import java.util.Map; | ||
| import lombok.RequiredArgsConstructor; | ||
| import org.springframework.security.core.authority.SimpleGrantedAuthority; | ||
| import org.springframework.security.oauth2.client.userinfo.DefaultOAuth2UserService; | ||
|
|
@@ -21,18 +23,13 @@ | |
| import org.springframework.stereotype.Service; | ||
| import org.springframework.transaction.annotation.Transactional; | ||
|
|
||
| import java.util.HashMap; | ||
| import java.util.List; | ||
| import java.util.Map; | ||
|
|
||
| @Service | ||
| @RequiredArgsConstructor | ||
| @Transactional | ||
| public class CustomOAuth2UserService implements OAuth2UserService<OAuth2UserRequest, OAuth2User> { | ||
|
|
||
| private final UserStore userStore; | ||
| private final SocialAccountReader socialAccountReader; | ||
| private final SocialAccountStore socialAccountStore; | ||
| private final UserJpaRepository userJpaRepository; | ||
| private final SocialAccountJpaRepository socialAccountJpaRepository; | ||
|
|
||
| @Override | ||
| public OAuth2User loadUser(OAuth2UserRequest userRequest) throws OAuth2AuthenticationException { | ||
|
|
@@ -48,67 +45,51 @@ public OAuth2User loadUser(OAuth2UserRequest userRequest) throws OAuth2Authentic | |
|
|
||
| Map<String, Object> attributes = new HashMap<>(oAuth2User.getAttributes()); | ||
| attributes.put("userId", user.getId()); | ||
| attributes.put("email", user.getEmail()); // null 가능 | ||
| attributes.put("email", user.getEmail()); | ||
| attributes.put("nickname", user.getNickname()); | ||
| attributes.put("role", user.getRoleOrDefault().name()); | ||
|
|
||
| String nameAttributeKey = resolveNameAttributeKey(user, userInfo); | ||
| String nameAttributeKey = resolveNameAttributeKey(user); | ||
|
|
||
| return new DefaultOAuth2User( | ||
| List.of(new SimpleGrantedAuthority(toAuthority(user.getRoleOrDefault()))), | ||
| attributes, | ||
| nameAttributeKey | ||
| List.of(new SimpleGrantedAuthority(toAuthority(user.getRoleOrDefault()))), | ||
| attributes, | ||
| nameAttributeKey | ||
| ); | ||
| } | ||
|
|
||
| private void validateRequiredFields(OAuth2UserInfo userInfo, String registrationId) { | ||
| if (userInfo.getProviderUserId() == null || userInfo.getProviderUserId().isBlank()) { | ||
| throw new OAuth2AuthenticationException( | ||
| new OAuth2Error("invalid_user_info"), | ||
| registrationId + " 사용자 식별값(providerUserId)을 가져오지 못했습니다." | ||
| new OAuth2Error("invalid_user_info"), | ||
| registrationId + " 사용자 식별자(providerUserId)를 가져오지 못했습니다." | ||
|
Comment on lines
+64
to
+65
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 이부분은 사용자에게 어떻게 핸들링되서 내려가게 되나요?
Collaborator
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 현재 OAuth2 실패 핸들러는 별도 등록하지 않아 Spring Security 기본 failure flow(리다이렉트)로 처리되고 있습니다. 응답 일관성을 위해 OAuth2 성공/실패를 한 방식으로 통일하는 게 맞다고 생각합니다.. 놓친부분 확인해 주셔서 감사합니다! 현재 브라우저 기반 소셜 로그인 흐름을 고려하면 리다이렉트 방식으로 맞추는 방향이 더 자연스럽다고 보고 있습니다.
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 넵 그리고 리다이렉트시에 어디로 보낼지는 프론트와 협의가 필요한 부분이니 이야기 나눠보시면 좋을 것 같습니다. |
||
| ); | ||
| } | ||
| } | ||
|
|
||
| private User findOrCreateUser(OAuth2UserInfo userInfo) { | ||
| return socialAccountReader | ||
| return socialAccountJpaRepository | ||
| .findByProviderAndProviderUserId(userInfo.getProvider(), userInfo.getProviderUserId()) | ||
| .map(socialAccount -> syncAndGetUser(socialAccount, userInfo)) | ||
| .map(SocialAccount::getUser) | ||
| .orElseGet(() -> createNewUser(userInfo)); | ||
| } | ||
|
|
||
| private User syncAndGetUser(SocialAccount socialAccount, OAuth2UserInfo userInfo) { | ||
| socialAccount.updateProviderProfile( | ||
| normalize(userInfo.getEmail()), | ||
| normalize(userInfo.getProfileImageUrl()) | ||
| ); | ||
| SocialAccount saved = socialAccountStore.store(socialAccount); | ||
| return saved.getUser(); | ||
| } | ||
|
|
||
| private User createNewUser(OAuth2UserInfo userInfo) { | ||
| String email = normalize(userInfo.getEmail()); | ||
| String nickname = resolveNickname(userInfo); | ||
| String profileImageUrl = normalize(userInfo.getProfileImageUrl()); | ||
|
|
||
| User user = new User( | ||
| email, // 카카오는 null일 수 있음 | ||
| nickname, | ||
| profileImageUrl, | ||
| UserStatus.ACTIVE | ||
| ); | ||
|
|
||
| User savedUser = userStore.store(user); | ||
| User user = new User(email, nickname, profileImageUrl, UserStatus.ACTIVE); | ||
| User savedUser = userJpaRepository.save(user); | ||
|
|
||
| SocialAccount socialAccount = new SocialAccount( | ||
| savedUser, | ||
| userInfo.getProvider(), | ||
| userInfo.getProviderUserId(), | ||
| email, // null 가능 | ||
| profileImageUrl | ||
| savedUser, | ||
| userInfo.getProvider(), | ||
| userInfo.getProviderUserId(), | ||
| email, | ||
| profileImageUrl | ||
| ); | ||
|
|
||
| socialAccountStore.store(socialAccount); | ||
| socialAccountJpaRepository.save(socialAccount); | ||
|
|
||
| return savedUser; | ||
| } | ||
|
|
@@ -135,7 +116,7 @@ private String normalize(String value) { | |
| return trimmed.isBlank() ? null : trimmed; | ||
| } | ||
|
|
||
| private String resolveNameAttributeKey(User user, OAuth2UserInfo userInfo) { | ||
| private String resolveNameAttributeKey(User user) { | ||
| if (user.getEmail() != null && !user.getEmail().isBlank()) { | ||
| return "email"; | ||
| } | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -1,45 +1,46 @@ | ||
| package com.zimdugo.user.application; | ||
|
|
||
| import com.zimdugo.user.domain.SocialAccount; | ||
| import com.zimdugo.user.domain.SocialAccountReader; | ||
| import com.zimdugo.user.domain.User; | ||
| import com.zimdugo.user.domain.UserReader; | ||
| import com.zimdugo.core.exception.BusinessException; | ||
| import com.zimdugo.core.exception.ErrorCode; | ||
| import java.util.List; | ||
| import com.zimdugo.user.infrastructure.SocialAccountJpaRepository; | ||
| import com.zimdugo.user.infrastructure.UserJpaRepository; | ||
| import lombok.RequiredArgsConstructor; | ||
| import org.springframework.stereotype.Service; | ||
| import org.springframework.transaction.annotation.Transactional; | ||
|
|
||
| import java.util.List; | ||
|
|
||
| @Service | ||
| @RequiredArgsConstructor | ||
| @Transactional(readOnly = true) | ||
| public class UserQueryService { | ||
|
|
||
| private final UserReader userReader; | ||
| private final SocialAccountReader socialAccountReader; | ||
| private final UserJpaRepository userJpaRepository; | ||
| private final SocialAccountJpaRepository socialAccountJpaRepository; | ||
|
|
||
| public UserProfileResponse getProfile(Long userId) { | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 아키텍처 규칙상 Application Layer에서 바로 Response를 반환하면 안됩니다. 별도의 DTO로 변환 후 컨트롤러에서 Response로 변환해주세요. |
||
| User user = findById(userId); | ||
|
|
||
| List<SocialAccount> socialAccounts = socialAccountReader.findAllByUserId(userId); | ||
| List<SocialAccount> socialAccounts = | ||
| socialAccountJpaRepository.findAllByUserId(userId); | ||
|
|
||
| List<String> providers = socialAccounts.stream() | ||
| .map(sa -> sa.getProvider().name().toLowerCase()) | ||
| .toList(); | ||
| .map(sa -> sa.getProvider().name().toLowerCase()) | ||
| .toList(); | ||
|
|
||
| return new UserProfileResponse( | ||
| user.getId(), | ||
| user.getEmail(), | ||
| user.getNickname(), | ||
| user.getProfileImageUrl(), | ||
| user.getStatus().name(), | ||
| providers | ||
| user.getId(), | ||
| user.getEmail(), | ||
| user.getNickname(), | ||
| user.getProfileImageUrl(), | ||
| user.getStatus().name(), | ||
| providers | ||
| ); | ||
| } | ||
|
|
||
| // AuthController에서 재발급 시 User 조회용 | ||
| public User findById(Long userId) { | ||
| return userReader.findById(userId) | ||
| .orElseThrow(() -> new BusinessException(ErrorCode.USER_NOT_FOUND)); | ||
| return userJpaRepository.findById(userId) | ||
| .orElseThrow(() -> new IllegalArgumentException("user not found. id=" + userId)); | ||
| } | ||
| } | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -1,8 +1,8 @@ | ||
| package com.zimdugo.identity.domain; | ||
| package com.zimdugo.user.domain; | ||
|
|
||
| public enum AuthProvider { | ||
| GOOGLE, | ||
| KAKAO, | ||
| NAVER, | ||
| } | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -1,51 +1,78 @@ | ||
| package com.zimdugo.user.domain; | ||
|
|
||
| import com.zimdugo.identity.domain.AuthProvider; | ||
| import java.time.LocalDateTime; | ||
| import jakarta.persistence.Column; | ||
| import jakarta.persistence.Entity; | ||
| import jakarta.persistence.EnumType; | ||
| import jakarta.persistence.Enumerated; | ||
| import jakarta.persistence.FetchType; | ||
| import jakarta.persistence.GeneratedValue; | ||
| import jakarta.persistence.GenerationType; | ||
| import jakarta.persistence.Id; | ||
| import jakarta.persistence.JoinColumn; | ||
| import jakarta.persistence.ManyToOne; | ||
| import jakarta.persistence.PrePersist; | ||
| import jakarta.persistence.Table; | ||
| import jakarta.persistence.UniqueConstraint; | ||
| import lombok.AccessLevel; | ||
| import lombok.Getter; | ||
| import lombok.NoArgsConstructor; | ||
|
|
||
| import java.time.LocalDateTime; | ||
|
|
||
| @Entity | ||
| @Table( | ||
| name = "social_account", | ||
| uniqueConstraints = { | ||
| @UniqueConstraint( | ||
| name = "uk_provider_provider_user_id", | ||
| columnNames = {"provider", "provider_user_id"} | ||
| ) | ||
| } | ||
| ) | ||
| @Getter | ||
| @NoArgsConstructor(access = AccessLevel.PROTECTED) | ||
| public class SocialAccount { | ||
|
|
||
| @Id | ||
| @GeneratedValue(strategy = GenerationType.IDENTITY) | ||
| private Long id; | ||
|
|
||
| @JoinColumn(name = "user_id", nullable = false) | ||
| @ManyToOne(fetch = FetchType.LAZY, optional = false) | ||
| private User user; | ||
|
|
||
| @Enumerated(EnumType.STRING) | ||
| @Column(nullable = false, length = 20) | ||
| private AuthProvider provider; | ||
|
|
||
| @Column(name = "provider_user_id", nullable = false, length = 100) | ||
| private String providerUserId; | ||
|
|
||
| @Column(length = 100) | ||
| private String providerEmail; | ||
|
|
||
| @Column(length = 255) | ||
| private String providerProfileImageUrl; | ||
| private LocalDateTime linkedAt; | ||
|
|
||
| public SocialAccount( | ||
| User user, | ||
| AuthProvider provider, | ||
| String providerUserId, | ||
| String providerEmail, | ||
| String providerProfileImageUrl | ||
| ) { | ||
| this(null, user, provider, providerUserId, providerEmail, providerProfileImageUrl, null); | ||
| } | ||
| @Column(nullable = false) | ||
| private LocalDateTime linkedAt; | ||
|
|
||
| @SuppressWarnings("checkstyle:ParameterNumber") | ||
| public SocialAccount( | ||
| Long id, | ||
| User user, | ||
| AuthProvider provider, | ||
| String providerUserId, | ||
| String providerEmail, | ||
| String providerProfileImageUrl, | ||
| LocalDateTime linkedAt | ||
| User user, | ||
| AuthProvider provider, | ||
| String providerUserId, | ||
| String providerEmail, | ||
| String providerProfileImageUrl | ||
| ) { | ||
| this.id = id; | ||
| this.user = user; | ||
| this.provider = provider; | ||
| this.providerUserId = providerUserId; | ||
| this.providerEmail = providerEmail; | ||
| this.providerProfileImageUrl = providerProfileImageUrl; | ||
| this.linkedAt = linkedAt; | ||
| } | ||
|
|
||
| public void updateProviderProfile(String providerEmail, String providerProfileImageUrl) { | ||
| this.providerEmail = providerEmail; | ||
| this.providerProfileImageUrl = providerProfileImageUrl; | ||
| @PrePersist | ||
| protected void onCreate() { | ||
| this.linkedAt = LocalDateTime.now(); | ||
| } | ||
| } |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Repository 네이밍에 JPA라는 기술을 사용한다는걸 명시적으로 알려줄 필요는 없을 것 같아요