💡 오늘 학습 키워드
- UserDetails
- UserDetailsService
🎯 학습 내용 정리
Spring Security를 사용하다 보면 UserDetails, UserDetailsService, AuthenticationProvider와 같은 인터페이스를 자연스럽게 접하게 된다. 처음에는 단순히 Spring Security가 요구하는 인터페이스이기 때문에 구현한다고 생각하기 쉽다.
하지만 내부 동작을 이해하면 UserDetails와 UserDetailsService는 단순한 규약이 아니라 Spring Security와 애플리케이션의 도메인 모델을 연결하는 핵심 계층이라는 것을 알 수 있다.
로그인 인증 과정에서 UserDetails는 언제 사용될까?
사용자가 로그인을 요청하면 Spring Security는 단순히 비밀번호만 비교하는 것이 아니라 다음과 같은 절차를 수행한다.
Client
↓
POST /login
↓
UsernamePasswordAuthenticationFilter
↓
AuthenticationManager
↓
AuthenticationProvider
↓
UserDetailsService
↓
UserDetails
↓
PasswordEncoder.matches()
↓
Authentication 생성
이 과정에서 사용자 조회를 담당하는 객체가 UserDetailsService, 조회된 사용자를 표현하는 객체가 UserDetails이다.
1. UserDetails
UserDetails는 Spring Security가 인증된 사용자를 표현하기 위한 인터페이스이다.
실제 구현체는 다음과 같이 작성한다.
public class CustomUserDetails implements UserDetails {
private final Long userId;
private final String username;
private final String password;
private final Collection<? extends GrantedAuthority> authorities;
public CustomUserDetails(
Long userId,
String username,
String password,
Collection<? extends GrantedAuthority> authorities
) {
this.userId = userId;
this.username = username;
this.password = password;
this.authorities = authorities;
}
public Long getUserId() {
return userId;
}
@Override
public Collection<? extends GrantedAuthority> getAuthorities() {
return authorities;
}
@Override
public String getPassword() {
return password;
}
@Override
public String getUsername() {
return username;
}
@Override
public boolean isAccountNonExpired() {
return true;
}
@Override
public boolean isAccountNonLocked() {
return true;
}
@Override
public boolean isCredentialsNonExpired() {
return true;
}
@Override
public boolean isEnabled() {
return true;
}
}
Spring Security는 이 객체를 이용해 인증과 권한 검사를 수행한다.
왜 Entity를 직접 사용하지 않을까?
많은 프로젝트에서 다음과 같은 의문이 생긴다. UserEntity가 있는데 왜 CustomUserDetails를 또 만들어야 할까?
가장 큰 이유는 관심사의 분리(Separation of Concerns) 이다.
UserEntity는 데이터베이스와 매핑되는 도메인 객체이다.
@Entity
public class UserEntity {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
private String username;
private String password;
private UserRole role;
private String nickname;
}
반면 UserDetails는 Spring Security가 인증을 수행하기 위한 객체이다.
도메인 모델과 보안 프레임워크를 분리하면 다음과 같은 장점이 있다.
- Spring Security에 대한 의존성을 Entity에서 제거할 수 있다.
- 인증에 필요한 정보만 노출할 수 있다.
- 도메인 모델 변경이 Security 구현에 미치는 영향을 줄일 수 있다.
- 테스트와 유지보수가 쉬워진다.
2. UserDetailsService
UserDetailsService는 사용자를 조회하는 역할을 담당하는 인터페이스이다.
Spring Security는 로그인 과정에서 loadUserByUsername()을 호출하여 사용자를 조회한다.
@Service
@RequiredArgsConstructor
public class CustomUserDetailsService
implements UserDetailsService {
private final UserRepository userRepository;
@Override
public UserDetails loadUserByUsername(String username) {
UserEntity user = userRepository.findByUsername(username)
.orElseThrow(() ->
new UsernameNotFoundException(username));
return new CustomUserDetails(
user.getId(),
user.getUsername(),
user.getPassword(),
List.of(
new SimpleGrantedAuthority(
user.getRole().name()
)
)
);
}
}
중요한 점은 UserEntity를 그대로 반환하는 것이 아니라 UserDetails로 변환하여 반환한다는 것이다.
loadUserByUsername()은 언제 호출될까?
로그인 요청이 들어오면 Spring Security는 내부적으로 다음과 같은 코드를 수행한다.
UserDetails user =
userDetailsService.loadUserByUsername(username);
조회된 UserDetails를 이용하여 PasswordEncoder가 비밀번호를 검증한다.
passwordEncoder.matches(
rawPassword,
user.getPassword()
);
즉, loadUserByUsername()은 로그인 과정에서 가장 먼저 실행되는 비즈니스 로직이다.
DaoAuthenticationProvider는 무엇을 할까?
Spring Security의 기본 인증 구현체는 DaoAuthenticationProvider이다.
인증 과정은 다음과 같다.
AuthenticationManager
↓
DaoAuthenticationProvider
↓
UserDetailsService
↓
UserDetails
↓
PasswordEncoder.matches()
↓
Authentication 생성
직접 구현하지 않아도 Spring Security가 내부적으로 처리한다.
JWT 기반 인증에서도 UserDetails를 사용할까?
많은 개발자가 다음과 같은 의문을 가진다. JWT에서는 이미 토큰으로 인증하는데 UserDetails가 필요한가?
정답은 상황에 따라 다르다이다.
JWT 인증에서는 일반적으로 토큰의 Claims를 이용하여 Authentication을 생성한다.
예를 들어 사용자 ID와 권한을 Claims에 저장했다면 다음과 같이 구현할 수 있다.
Claims claims = extractClaims(token);
Long userId = Long.valueOf(claims.getSubject());
Collection<? extends GrantedAuthority> authorities =
List.of(new SimpleGrantedAuthority(
claims.get("role", String.class)
));
UserDetails principal =
new CustomUserDetails(
userId,
null,
null,
authorities
);
JWT 자체로 인증을 수행하더라도 Authentication의 Principal은 여전히 UserDetails를 사용하는 경우가 많다.
이는 Spring Security의 일관된 인증 모델을 유지하기 위함이다.
UserDetails를 구현할 때 고려할 점
1. 필요한 정보만 포함한다.
인증과 무관한 필드는 포함하지 않는 것이 좋다.
예를 들어 주소, 프로필 이미지, 가입일 등의 정보는 대부분 인증 과정에 필요하지 않다.
2. Entity를 그대로 상속하지 않는다.
Entity와 인증 모델은 역할이 다르므로 별도의 클래스로 관리하는 것이 좋다.
3. 권한은 GrantedAuthority로 관리한다.
List.of(
new SimpleGrantedAuthority("ROLE_ADMIN")
);
Spring Security는 권한 정보를 GrantedAuthority 컬렉션으로 관리한다.
4. Controller에서는 UserDetails를 직접 활용한다.
@GetMapping("/me")
public UserResponse me(
@AuthenticationPrincipal CustomUserDetails user
) {
return userService.findById(user.getUserId());
}
직접 JWT를 파싱하지 않아도 현재 로그인한 사용자를 사용할 수 있다.
지금까지 살펴본 내용을 하나의 흐름으로 정리하면 다음과 같다.
Login Request
↓
UsernamePasswordAuthenticationFilter
↓
AuthenticationManager
↓
DaoAuthenticationProvider
↓
UserDetailsService
↓
UserEntity 조회
↓
CustomUserDetails 생성
↓
PasswordEncoder.matches()
↓
Authentication 생성
↓
SecurityContext 저장
↓
Controller
실무에서 자주 하는 실수는 다음과 같다.
1. Entity가 UserDetails를 직접 구현하는 경우
다음과 같은 구현은 가능하지만 권장되지 않는다.
@Entity
public class UserEntity implements UserDetails {
...
}
Entity가 Spring Security 인터페이스를 구현하게 되면 도메인 계층이 보안 프레임워크에 의존하게 된다. 도메인 모델과 인증 모델의 역할을 분리하기 위해 CustomUserDetails와 같은 별도 클래스를 두는 것이 유지보수에 유리하다.
2. UserDetails에 너무 많은 정보를 담는 경우
UserDetails는 인증을 위한 객체이다. 화면 표시를 위한 프로필 정보나 통계 정보까지 포함하면 객체의 책임이 불필요하게 커질 수 있다. 인증에 필요한 최소한의 정보만 포함하는 것이 좋다.
3. UsernameNotFoundException 대신 일반 예외를 사용하는 경우
loadUserByUsername()에서는 사용자를 찾지 못했을 때 UsernameNotFoundException을 사용하는 것이 Spring Security의 기본 인증 흐름과 자연스럽게 연동된다.
throw new UsernameNotFoundException(username);
UserDetails는 인증된 사용자를 표현하는 객체이고, UserDetailsService는 사용자를 조회하여 UserDetails로 변환하는 역할을 담당한다. Spring Security는 로그인 과정에서 loadUserByUsername()을 호출하고, 반환된 UserDetails를 이용해 비밀번호를 검증한 뒤 Authentication을 생성한다.
또한 UserEntity와 UserDetails를 분리하면 도메인 모델과 보안 모델의 책임을 명확하게 구분할 수 있으며, 유지보수성과 확장성도 함께 향상된다. JWT 기반 인증을 사용하더라도 Authentication의 Principal로 UserDetails를 활용하면 Spring Security의 인증 모델을 일관성 있게 유지할 수 있다.
📚 한줄 정리
UserDetails는 인증된 사용자를 표현하는 객체이고, UserDetailsService는 도메인 사용자를 Spring Security가 이해할 수 있는 인증 모델로 변환하는 연결 고리이다.
'Bootcamp > Implementation' 카테고리의 다른 글
| AuthenticationEntryPoint와 AccessDeniedHandler 예외 처리 (0) | 2026.07.13 |
|---|---|
| AuthenticationManager와 AuthenticationProvider (0) | 2026.07.10 |
| Authentication과 SecurityContextHolder (0) | 2026.07.08 |
| JWT 기반 로그인 구현과 Access Token 발급 (0) | 2026.07.07 |
| Spring Security 구조 설계 (0) | 2026.07.06 |