[게시판 프로젝트] 소셜 로그인 기능 구현(3) - 카카오로 로그인 하기

2024. 8. 27. 03:34웹 개발/프로젝트

회원 정보 서비스 로직 구현 - UserAccountService


searchUser - 값이 있을 때와 없을 때의 호출자에게 위임하기 위해서 Optional <UserAccountDto> 사용
@Transactional(readOnly = true)
public Optional<UserAccountDto> searchUser(String username) {
    return userAccountRepository.findById(username)
            .map(UserAccountDto::from);
}

 

  • @Transactional(readOnly = true)
    • 이 메서드는 읽기 전용 트랜잭션으로 실행됩니다.
    • 데이터베이스에 변경을 가하지 않으므로 성능 최적화가 가능하며, 기본적으로 데이터베이스의 플러시 작업이 발생하지 않습니다.
  • Optional<UserAccountDto> searchUser(String username)
    • 사용자 이름(username)을 기반으로 사용자를 검색하는 메서드입니다.
    • 반환 타입은 Optional<UserAccountDto>입니다. 이는 검색 결과가 없을 경우 Optional.empty()를 반환하여, null 대신 안전한 방법으로 결과를 처리할 수 있게 합니다.
  • userAccountRepository.findById(username)
    • username으로 사용자 정보를 검색합니다.
    • findById는 Optional<UserAccount>를 반환합니다.
  • .map(UserAccountDto::from)
    • 검색된 UserAccount 객체를 UserAccountDto로 변환합니다.
    • UserAccountDto::from은 정적 메서드로, UserAccount 객체를 받아 UserAccountDto로 변환합니다.

saveUser
public UserAccountDto saveUser(String username, String password, String email, String nickname, String memo) {
    return UserAccountDto.from(
            userAccountRepository.save(UserAccount.of(username, password, email, nickname, memo, username))
    );
}

 

  • UserAccountDto saveUser(...)
    • 새로운 사용자를 데이터베이스에 저장하는 메서드입니다. 저장된 사용자의 정보를 UserAccountDto로 반환합니다.
  • UserAccount.of(...)
    • 정적 팩토리 메서드 of를 호출하여 UserAccount 객체를 생성합니다.
    • 이 메서드는 사용자 계정 정보를 받아 UserAccount 객체를 생성합니다. 
      • username: 사용자의 이름입니다. 여기서는 기본 키로 사용됩니다.
      • password: 암호화된 비밀번호입니다.
      • email: 사용자의 이메일 주소입니다.
      • nickname: 사용자의 닉네임입니다.
      • memo: 사용자에 대한 메모입니다.
      • 마지막 username 인자는 계정의 생성자(생성자 정보를 남길 때)로 사용될 수도 있습니다.
  • userAccountRepository.save(...)
    • UserAccountRepository의 save 메서드를 호출하여 UserAccount 객체를 데이터베이스에 저장합니다.
    • 저장된 객체는 영속화된 상태로 반환됩니다.
  • UserAccountDto.from(...)
    • 저장된 UserAccount 객체를 UserAccountDto로 변환합니다. 반환 값은 UserAccountDto입니다.

UserAccountServiceTest 로직 구현

 

searchUser - 존재하는 회원 ID를 검색하면, 회원 데이터를 Optional로 반환

searchUser - 존재하지 않는 회원 ID를 검색하면, 비어있는 Optional을 반환

saveUser - 회원 정보를 입력하면, 새로운 회원 정보를 저장하여 가입시키고 해당 회원 데이터를 반환
  • [명확한 검사 의도]
    • UserAccountDto result = sut.saveUser 기능을 사용하면
    • id, password, email, nickname, memo만 넣어줬는데 -> 검사식에는 createdBy와 modifiedAt이 포함되어있음
      • createdBy와 modifiedAt에서 getUserId()를 활용함


인증 설정 업데이트 : OAuth 인증 설정 추가 - OAuth2.0 보안 설정 - SecurityConfig

filterChain 수정
  • Before

  • After

  • SecurityFilterChain: Spring Security의 보안 필터 체인을 정의하는 빈입니다.
  • HttpSecurity: 이 객체는 웹 보안을 설정하는 데 사용됩니다.
  • OAuth2UserService<OAuth2UserRequest, OAuth2User>: OAuth2 로그인 과정에서 사용자 정보를 처리하는 서비스입니다.
  • authorizeHttpRequests: 요청에 대한 권한 설정을 정의합니다.
    • 정적 리소스(CSS, JS, 이미지 등)에 대한 요청은 모두 허용됩니다(permitAll()).
    • GET 요청 중 "/"(홈), "/articles", "/articles/search-hashtag" 경로에 대해서는 인증이 필요 없습니다.
    • 그 외의 모든 요청에 대해서는 인증이 요구됩니다(authenticated()).
  • formLogin(withDefaults()): 기본 로그인 페이지를 사용하도록 설정합니다.
  • logout: 로그아웃이 성공하면 "/" 경로로 리다이렉트됩니다.
  • oauth2Login: OAuth2 로그인 설정입니다.
    • userInfoEndpoint: 사용자 정보를 가져오는 엔드포인트 설정에서 OAuth2UserService를 사용해 사용자 정보를 처리합니다.

UserDatailsService 수정
  • Before

  • After

  • UserDetailsService는 스프링 시큐리티에서 사용자의 정보를 가져오는 서비스 인터페이스입니다.
  • userAccountService를 사용해 사용자를 검색하고, 결과를 BoardPrincipal 객체로 변환합니다.
  • 사용자를 찾지 못하면 UsernameNotFoundException을 발생시킵니다.

OAuthUserService 메서드 추가

  • 이 코드는 Spring Security의 OAuth2 인증 과정에서 사용자 정보를 처리하기 위해 사용자 정의 OAuth2UserService를 생성하는 메서드입니다.
  • 이 메서드는 사용자가 OAuth2 인증을 통해 로그인할 때 호출되며, 사용자의 정보를 로드하고 애플리케이션의 사용자로 매핑하는 역할을 합니다. 
이 코드 블록은 Spring Security에서 OAuth2 인증을 처리하는 로직을 정의합니다.

카카오를 통한 OAuth2 인증 시, 사용자의 정보를 받아 애플리케이션 내에서 적절한 형태로 변환하고 저장하며, 저장된 사용자를 반환하는 과정을 구현하고 있습니다.

이 과정에서 기본적으로 제공되는 DefaultOAuth2UserService를 사용하여 사용자의 정보를 로드하고, 그 정보를 기반으로 사용자 계정의 생성 또는 검색을 처리합니다.

OAuthUserService 메서드 코드 분석
@Bean
public OAuth2UserService<OAuth2UserRequest, OAuth2User> oAuth2UserService(
        UserAccountService userAccountService,
        PasswordEncoder passwordEncoder
) {
  • 이 메서드는 OAuth2UserService<OAuth2UserRequest, OAuth2User> 타입의 빈을 생성합니다.
  • @Bean 어노테이션을 통해 이 메서드는 Spring 컨텍스트에 등록됩니다.
  • userAccountService는 사용자 계정을 관리하는 서비스입니다.
  • passwordEncoder는 비밀번호를 암호화하는 도구입니다.

DefaultOAuthUserService 객체 생성

final DefaultOAuth2UserService delegate = new DefaultOAuth2UserService();
  • DefaultOAuth2UserService기본적으로 OAuth2 인증 과정에서 사용자 정보를 가져오는 데 사용되는 서비스입니다.
  • delegate라는 변수로 이 기본 서비스를 생성합니다. 이 서비스는 이후에 사용됩니다.

람다 표현식으로 OAuthUserService 구현

return userRequest -> {
  • 이 람다 표현식은 OAuth2UserService<OAuth2UserRequest, OAuth2User> 인터페이스를 구현합니다. userRequest라는 매개변수를 받아 처리합니다.
  • userRequest는 OAuth2UserRequest 객체로, 사용자가 로그인할 때 OAuth2 공급자로부터 반환된 정보가 담겨 있습니다.

사용자 정보 로드

OAuth2User oAuth2User = delegate.loadUser(userRequest);
  • delegate.loadUser(userRequest)를 통해 OAuth2 공급자로부터 사용자의 정보를 로드합니다.
  • 이 정보는 OAuth2User 객체에 저장됩니다.

KakaoOAuthResponse 객체 생성

KakaoOAuth2Response kakaoResponse = KakaoOAuth2Response.from(oAuth2User.getAttributes());
  • oAuth2User.getAttributes()사용자의 속성을 Map<String, Object> 형태로 반환합니다.
  • KakaoOAuth2Response.from() 메서드를 사용해 이 속성을 KakaoOAuth2Response 객체로 변환합니다.
  • 이 변환 과정은 카카오의 OAuth2 응답을 처리하기 위한 것입니다.

사용자 정보 생성

String registrationId = userRequest.getClientRegistration().getRegistrationId();
String providerId = String.valueOf(kakaoResponse.id());
String username = registrationId + "_" + providerId;
  • registrationId: 어떤 OAuth2 공급자를 통해 인증했는지를 나타냅니다 (예: kakao).
  • providerId: 사용자의 고유 ID입니다. 카카오에서는 이 ID가 kakaoResponse.id()로부터 추출됩니다.
  • username: registrationId와 providerId를 결합하여 고유한 사용자 이름을 생성합니다. 이는 애플리케이션 내에서 사용자를 식별하는 데 사용됩니다.

임시 비밀번호 생성

String dummyPassword = passwordEncoder.encode("{bcrypt}" + UUID.randomUUID());
  • dummyPassword: 임시 비밀번호를 생성합니다. 이 비밀번호는 bcrypt 방식으로 암호화되며, 고유한 UUID로 생성된 값이 사용됩니다.

사용자 검색 및 생성

return userAccountService.searchUser(username)
        .map(BoardPrincipal::from)
        .orElseGet(() ->
                BoardPrincipal.from(
                        userAccountService.saveUser(
                                username,
                                dummyPassword,
                                kakaoResponse.email(),
                                kakaoResponse.nickname(),
                                null
                        )
                )
        );
  • userAccountService.searchUser(username): 데이터베이스에서 사용자를 검색합니다. username은 이전에 생성된 사용자 이름입니다.
  • .map(BoardPrincipal::from): 사용자가 존재하면, 이를 BoardPrincipal 객체로 변환하여 반환합니다.
  • .orElseGet(() -> ...): 사용자가 존재하지 않으면, 새로운 사용자를 생성합니다.
    • userAccountService.saveUser(...)를 통해 새로운 사용자를 데이터베이스에 저장합니다.
    • 저장된 사용자를 BoardPrincipal 객체로 변환하여 반환합니다.

최종 반환

};
  • 이 람다 표현식은 최종적으로 OAuth2UserService를 구현하는 익명 함수로서 반환됩니다. 이 구현체는 OAuth2 로그인 과정에서 사용자 정보를 로드하고, 해당 사용자가 시스템에 이미 존재하는지 확인한 후, 사용자 정보를 반환하는 역할을 합니다.