UserDetails와 OAuth 2.0 반환 객체 통합하기

Lord·2024년 10월 12일

Problem Solving Skills

목록 보기
1/17
post-thumbnail

이번 포스팅에서는 Spring Security를 사용하여 일반 회원가입과 OAuth 2.0 소셜 로그인을 동시에 구현하면서 겪었던 트러블 슈팅 경험을 공유하려고 한다. 특히 반환되는 인증 객체의 차이로 인해 발생한 문제를 해결했던 과정을 자세히 다뤄보려고 한다.

문제 상황: 로그인 유형에 따라 다른 객체 반환

Spring Security를 사용하면서 일반 회원가입 로그인과 카카오 OAuth 2.0 소셜 로그인을 함께 구현하게 되었다. 이때 각각의 로그인 방식이 서로 다른 객체를 반환한다는 점에서 문제가 발생했다.

  • 일반 로그인: UserDetails 객체 반환
  • OAuth 2.0 로그인: OAuth2User 객체 반환

이러한 차이 때문에 @AuthenticationPrincipal 어노테이션을 사용하여 현재 인증된 사용자의 정보를 가져올 때마다, 로그인 유형에 따라 컨트롤러 로직을 다르게 작성해야 했다. 이는 코드의 복잡성과 유지보수성 측면에서 큰 비효율을 초래했다. 컨트롤러마다 분기 처리를 추가해야 했고, 로그인 방식에 따라 다른 객체를 다뤄야 했기 때문에 코드의 중복이 발생했다. 이러한 상황은 유지보수와 확장성 측면에서 큰 단점으로 작용했다.

해결 방법: PrincipalDetails 객체 커스텀

이 문제를 해결하기 위해 UserDetailsOAuth2User를 동시에 구현하는 커스텀 PrincipalDetails 객체를 만들기로 했다. 이렇게 함으로써 로그인 유형에 상관없이 동일한 타입의 객체를 사용할 수 있게 되었고, 코드의 간결성과 유지보수성을 크게 향상시킬 수 있었다. PrincipalDetails를 사용하면 로그인 방식에 관계없이 하나의 객체를 통해 사용자 정보를 다룰 수 있기 때문에, 인증 객체를 일관되게 처리할 수 있었다.

PrincipalDetails 클래스 주요 내용

  • PrincipalDetails 클래스는 UserDetailsOAuth2User 인터페이스를 모두 구현하여, 일반 회원과 소셜 로그인 사용자 모두를 동일하게 처리할 수 있도록 했다.
  • 주요 필드로는 사용자 식별자인 email, id, password, role 등이 있으며, OAuth 2.0 로그인을 위한 attributes 필드를 추가로 포함했다.
  • 인증 객체를 생성하기 위한 여러 생성자와 메서드를 제공하여, 다양한 로그인 방식에 유연하게 대응할 수 있도록 설계했다.

이를 통해 인증 객체를 일관되게 다룰 수 있게 되었으며, 로그인 방식에 따라 달라지는 컨트롤러의 중복 코드를 제거할 수 있었다. 이를 통해 코드의 유지보수가 훨씬 수월해졌으며, 확장성도 개선되었다. 또한, 새로운 소셜 로그인 기능을 추가하더라도 동일한 PrincipalDetails 객체를 사용할 수 있어 일관된 구조를 유지할 수 있었다.

PrincipalDetailsService 구현

또한, UserDetailsService를 구현한 PrincipalDetailsService를 통해 일반 로그인 사용자에 대한 처리를 수행하도록 했다. 이 서비스는 사용자 이메일을 기반으로 Member 객체를 조회하고, 조회된 Member 객체로 PrincipalDetails를 생성하여 반환한다. 이를 통해 일반 로그인 시에도 동일한 인증 객체를 사용할 수 있도록 했다.

public class PrincipalDetailsService implements UserDetailsService {
    private final MemberRepository memberRepository;

    @Override
    public UserDetails loadUserByUsername(String email) throws UsernameNotFoundException {
        Member member = memberRepository.findByEmail(email)
                .orElseThrow(() -> {
                    log.debug("loadUserByUsername exception occur email {}", email);
                    return new BusinessLogicException(ExceptionCode.MEMBER_NOT_FOUND);
                });

        if (member.isBlocked()) {
            log.debug("Blocked user tried to login, email {}", email);
            throw new MemberBlockedException("비활성화된 계정입니다.");
        }

        return PrincipalDetails.of(member);
    }
}

위와 같이 PrincipalDetailsService를 통해 일반 로그인 시에도 PrincipalDetails 객체를 반환하도록 하여, 컨트롤러에서 인증 객체를 쉽게 다룰 수 있게 했다. 이를 통해 개발자는 로그인 방식에 대한 고민 없이 동일한 객체로 사용자의 인증 정보를 처리할 수 있게 되었고, 코드의 일관성과 가독성이 크게 향상되었다. 또한, 계정 비활성화 여부 등 추가적인 검증 로직을 손쉽게 추가할 수 있어 보안 측면에서도 이점이 있었다.

트러블 슈팅 결과 및 배운 점

이와 같은 접근 방식 덕분에 두 가지 로그인 방식을 통합적으로 처리할 수 있는 구조를 만들 수 있었다. 이를 통해 코드의 중복을 줄이고 유지보수성을 크게 향상시킬 수 있었다. 또한, 인증 로직의 일관성을 유지하면서 다양한 인증 방식을 효과적으로 지원할 수 있는 구조를 설계하는 중요성을 깨닫게 되었다.

트러블 슈팅 과정에서 많은 시행착오를 겪었지만, 이를 통해 얻은 경험은 앞으로의 개발에서도 큰 도움이 될 것이라 확신한다. (계속 다른 객체가 반환되고 통합하는 과정에서 디버깅을 하는데 spring security의 oauth2.0의 코드를 하나 하나 보느라 많이 힘들었다..) 특히, 커스텀 객체를 통해 복잡한 문제를 해결하는 방법을 배우면서, 더 나은 코드 품질과 개발 생산성을 목표로 하는 자세를 갖게 되었다. 이러한 트러블 슈팅 경험을 통해 복잡한 문제를 해결하는 과정에서의 성취감을 느낄 수 있었고, 문제를 깊이 파고들어 해결하는 능력 또한 성장할 수 있었다. 또한, 사용자 경험을 개선하고 보안을 강화하는 측면에서 많은 것을 배울 수 있었다.

마무리

이번 포스팅에서는 Spring Security와 OAuth 2.0 소셜 로그인을 통합하면서 발생한 문제와 이를 해결하기 위한 트러블 슈팅 과정을 이야기해 보았다. 비슷한 문제를 겪고 있다면, 커스텀 객체를 활용해 로그인 방식을 통합적으로 처리하는 방법을 고려해보길..! 커스텀 객체를 사용하는 것은 코드를 단순화하고 유지보수성을 높이는 좋은 방법이 될 수 있다.

profile
다재다능한 Backend 개발자에 도전하는 개발자

0개의 댓글