Spring Security - UserDetails & UserDeatailsService 이해하기

이진일·2026년 4월 24일
post-thumbnail

Spring 사용자 인증 구현

회원가입/로그인 기능 구현하고 보니 현재 진행 중인 주문 플랫폼 서비스에서는 모든 도메인에서 인증된 사용자를 체크해야 했습니다. 하지만 모든 도메인의 컨트롤러에서 토큰을 파싱하여 권한 체크를 하면 반복되기도 하고 비즈니스 로직보다 권한 체크 로직이 더 커질 수도 있다고 생각했습니다.

그래서 Spring Security의 사용자 인증 구조를 도입하기로 했습니다.

Spring Security 인증 구조 흐름

이번 프로젝트에서 구현한 구조를 기반으로 Spring Security의 인증 절차가 이루어지는지 설명하도록 할 것입니다.


1. HTTP 요청 수신

  • 사용자가 JWT에 로그인 정보를 담아 요청을 보냅니다.
  • JwtAuthenticationFilter가 이 요청을 가장 먼저 가로챕니다.

2. 사용자 정보 로드(UserDetailsService)

  • JwtAuthenticationFilter에서 사용자 ID를 꺼내서 UserDetailServiceloadUserByUsername()을 호출하여 DB에서 사용자 정보를 받아옵니다.

3. 사용자 데이터 조회 및 UserDetails 생성

  • UserDetailService는 DB에서 찾은 사용자를 UserDetails 객체로 반환합니다.
  • 이 때 우리의 User엔티티에 맞게 만든 UserDetilsImpl이 사용됩니다.

4. 인증 객체(Authentication) 생성

  • UserDetails로 인증된 사용자 정보를 담아 Authentication 인증 객체를 생성합니다.

5. 인증 완료 및 저장

  • 생성된 인증 객체는 SecurityContextHolder라는 저장소에 보관됩니다.

위의 과정에서 두 가지 클래스를 구현하였습니다.

  • UserDetailsImpl: UserDetails의 구현체
  • UserDetailsServiceImpl: UserDetailsService의 구현체

UserDetailsImpl 클래스 구현

UserDetailsImpl은 Spring Security의 인터페이스인 UserDetails를 구현한 클래스입니다. 이 클래스는 User엔티티와 Spring Security 사이의 연결고리 역할을 합니다.

UserDetailsImpl 클래스를 구현하여 사용하는 이유(어댑터 패턴)

Spring Security는 우리 프로젝트에 User라는 엔티티가 있는지, 그 안에 어떤 필드가 있는지 알지 못합니다. Spring Security는 그저 정해진 규격(UserDetails)에 맞춰서 사용자 정보를 받기를 원합니다.

따라서 우리 도메인 객체인 UserUserDetails라는 규격으로 포장해주는 어댑터(Adapter)가 필요한데, 그것이 바로 UserDetailsImpl입니다.

주요 코드 설명

public class UserDetailsImpl implements UserDetails {

	private final User user; // 우리 도메인인 User엔티티를 필드로 가짐

	public UserDetailsImpl(User user) {
		this.user = user;
	}

	// 서비스 로직에서 유저 객체가 필요할 때 꺼내 쓰기 위한 메서드
	public User getUser() {
		return user;
	}

	// Spring Security가 인증에 사용할 비밀번호
	@Override
	public String getPassword() {
		return user.getPassword();
	}

	// Spring Security가 인증에 사용할 식별자(ID)
	@Override
	public String getUsername() {
		return user.getUsername();
	}

	// 권한 설정 (UserRole을 GrantedAuthority로 변환)
	@Override
	public Collection<? extends GrantedAuthority> getAuthorities() {
		UserRole role = user.getRole();
		String authority = role.getAuthority(); // 이전에 만든 "ROLE_CUSTOMER" 등 반환 메서드

		SimpleGrantedAuthority simpleGrantedAuthority = new SimpleGrantedAuthority(authority);
		Collection<GrantedAuthority> authorities = new ArrayList<>();
		authorities.add(simpleGrantedAuthority);

		return authorities;
	}

	// 계정 만료, 잠금, 패스워드 만료, 활성화 여부
    // 현재 프로젝트에서는 별도의 로직이 없으므로 모두 true(정상)를 반환하도록 설정
	@Override
	public boolean isAccountNonExpired() { return true; }

	@Override
	public boolean isAccountNonLocked() { return true; }

	@Override
	public boolean isCredentialsNonExpired() { return true; }

	@Override
	public boolean isEnabled() { return true; }
}

이 클래스를 통해 얻는 이점

  1. 관심사의 분리: UserDetailsImpl이 보안에 필요한 부가적인 기능을 담당하게 되어서 User엔티티는 비즈니스 데이터와 로직만 신경쓰면 됩니다.

  2. 편의성: Controller에서 @AuthenticationPrincipal UserDetailsImpl userDetails와 같은 방식으로 현재 로그인한 사용자의 정보를 쉽게 꺼낼 수 있습니다.

UserDetailsServiceImpl 클래스 구현

UserDetailsServiceImpl은 Spring Security의 UserDetailsService 인터페이스를 구현한 클래스입니다. 이 클래스의 목적은 "전달받은 사용자 식별자(ID)를 가지고 실제 사용자 정보를 찾아와서 보안 규격에 맞게 반환하는 것"입니다.

UserDetailsServiceImpl 클래스의 역할

Spring Security는 인증 과정에서 전달받은 사용자 식별자(ID)를 가진 유저가 있는지 검증합니다. 이때 구현체에 구현해둔 loadUserByUsername() 메서드가 실행됩니다. 즉, Spring Security와 Repository를 잇는 역할을 수행합니다.

주요 코드 설명

@Service // Bean으로 등록하여 Spring Security가 사용할 수 있도록 함.
@RequiredArgsConstructor
public class UserDetailsServiceImpl implements UserDetailsService {

	private final UserRepository userRepository;

	// username을 통해 DB에서 사용자 정보 조회
    // 조회된 정보를 앞서 만든 UserDetailsImpl에 담아 반환
	@Override
	public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
    	// UserRepository를 통해 DB에서 유저 조회
		User user = userRepository.findById(username)
			.orElseThrow(() -> new UsernameNotFoundException("Not Found " + username));

		// 조회된 유저 엔티티를 UserDetailsImpl로 감싸서 반환
		return new UserDetailsImpl(user);
	}
}

느낀점

1. "보안"과 "비즈니스 로직"의 완전한 분리
Code Shadowing 과제에서는 컨트롤러에서 비즈니스 로직을 분리해보았다면 이번에는 보안 체크 로직비즈니스 로직을 분리해본 경험이었습니다.

2. 확장성있는 설계
나중에 소셜 로그인이나 외부 인증 서버 방식으로 변경하더라도 도메인쪽 로직을 수정할 필요없이 Security 설정과 UserDetailsImpl만 수정하면 된다는 점에서 유연한 설계의 강점을 실감했습니다.

추후 MSA프로젝트를 진행할 때도 사용할 수 있을 것 같다는 생각이 들었습니다. UserDetailsServiceImpl클래스가 DB에서 사용자 정보를 받아온 것을 인증 서비스로 요청하도록 수정하여 사용할 수 있을 것 같다고 생각했습니다.

0개의 댓글