Java/SpringBoot

CustomUserDetails 클래스Builder 적용, JPA 활용한 UserEntity

dev.jelee 2024. 10. 28. 23:50

생각

기존 UserDetails 인터페이스의 인스턴스가 가진 필드 외에 email을 추가한 커스텀 UserDetails 클래스를 만들어 달라고 했다. 그 결과 아래와 같은 소스코드를 만들어 줬다.


일반 CustomUserDetails 클래스

1. CustomUserDetails 클래스

import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.core.userdetails.UserDetails;

import java.util.Collection;

public class CustomUserDetails implements UserDetails {
    private final String username;               // 사용자 이름
    private final String password;               // 비밀번호
    private final String email;                  // 이메일 필드 추가
    private final boolean enabled;                // 계정 활성화 여부
    private final boolean accountNonExpired;      // 계정 만료 여부
    private final boolean accountNonLocked;       // 계정 잠금 여부
    private final boolean credentialsNonExpired;  // 비밀번호 만료 여부
    private final Collection<? extends GrantedAuthority> authorities;  // 권한 목록

    // 생성자
    public CustomUserDetails(String username, String password, String email,
                             boolean enabled, boolean accountNonExpired,
                             boolean accountNonLocked, boolean credentialsNonExpired,
                             Collection<? extends GrantedAuthority> authorities) {
        this.username = username;
        this.password = password;
        this.email = email; // 이메일 초기화
        this.enabled = enabled;
        this.accountNonExpired = accountNonExpired;
        this.accountNonLocked = accountNonLocked;
        this.credentialsNonExpired = credentialsNonExpired;
        this.authorities = authorities;
    }

    // UserDetails 인터페이스 메서드 구현
    @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 accountNonExpired;
    }

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

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

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

    // 이메일 getter 추가
    public String getEmail() {
        return email;
    }
}

2. UserDetailsService 수정

  • loadUserByUsername 메서드에서 return문에 있는 CustomUserDetails를 수정해주면 된다.
@Override
public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
    User user = userMapper.findByUsername(username);
    
    if (user == null) {
        throw new UsernameNotFoundException("User not found with username: " + username);
    }

    List<GrantedAuthority> authorities = user.getRoles().stream()
            .map(role -> new SimpleGrantedAuthority(role.getName()))
            .collect(Collectors.toList());

    // CustomUserDetails 객체 생성
    return new CustomUserDetails(
            user.getUsername(),
            user.getPassword(),
            user.getEmail(), // 이메일 추가
            user.isEnabled(),
            true, // 만료 여부 (필요에 따라 설정)
            true, // 잠금 여부 (필요에 따라 설정)
            true, // 비밀번호 만료 여부 (필요에 따라 설정)
            authorities
    );
}

Builder 패턴 사용한 CustomUserDetails 클래스

1. CustomUserDetails 클래스 (Builder 패턴 사용)

import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.core.userdetails.UserDetails;

import java.util.Collection;

public class CustomUserDetails implements UserDetails {
    private final String username;
    private final String password;
    private final String email;
    private final boolean enabled;
    private final boolean accountNonExpired;
    private final boolean accountNonLocked;
    private final boolean credentialsNonExpired;
    private final Collection<? extends GrantedAuthority> authorities;

    // private 생성자
    private CustomUserDetails(Builder builder) {
        this.username = builder.username;
        this.password = builder.password;
        this.email = builder.email;
        this.enabled = builder.enabled;
        this.accountNonExpired = builder.accountNonExpired;
        this.accountNonLocked = builder.accountNonLocked;
        this.credentialsNonExpired = builder.credentialsNonExpired;
        this.authorities = builder.authorities;
    }

    // Builder 클래스
    public static class Builder {
        private String username;
        private String password;
        private String email;
        private boolean enabled;
        private boolean accountNonExpired;
        private boolean accountNonLocked;
        private boolean credentialsNonExpired;
        private Collection<? extends GrantedAuthority> authorities;

        public Builder username(String username) {
            this.username = username;
            return this;
        }

        public Builder password(String password) {
            this.password = password;
            return this;
        }

        public Builder email(String email) {
            this.email = email;
            return this;
        }

        public Builder enabled(boolean enabled) {
            this.enabled = enabled;
            return this;
        }

        public Builder accountNonExpired(boolean accountNonExpired) {
            this.accountNonExpired = accountNonExpired;
            return this;
        }

        public Builder accountNonLocked(boolean accountNonLocked) {
            this.accountNonLocked = accountNonLocked;
            return this;
        }

        public Builder credentialsNonExpired(boolean credentialsNonExpired) {
            this.credentialsNonExpired = credentialsNonExpired;
            return this;
        }

        public Builder authorities(Collection<? extends GrantedAuthority> authorities) {
            this.authorities = authorities;
            return this;
        }

        public CustomUserDetails build() {
            return new CustomUserDetails(this);
        }
    }

    // UserDetails 인터페이스 메서드 구현
    @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 accountNonExpired;
    }

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

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

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

    // 이메일 getter
    public String getEmail() {
        return email;
    }
}

2. UserDetailsService 수정

  • loadUserByUsername 메서드를 수정해서 Builder 패턴을 사용할 수 있다.
@Override
public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
    User user = userMapper.findByUsername(username);
    
    if (user == null) {
        throw new UsernameNotFoundException("User not found with username: " + username);
    }

    List<GrantedAuthority> authorities = user.getRoles().stream()
            .map(role -> new SimpleGrantedAuthority(role.getName()))
            .collect(Collectors.toList());

    // Builder 패턴을 사용하여 CustomUserDetails 객체 생성
    return new CustomUserDetails.Builder()
            .username(user.getUsername())
            .password(user.getPassword())
            .email(user.getEmail())
            .enabled(user.isEnabled())
            .accountNonExpired(true) // 필요에 따라 설정
            .accountNonLocked(true)   // 필요에 따라 설정
            .credentialsNonExpired(true) // 필요에 따라 설정
            .authorities(authorities)
            .build();
}

빌더를 사용하고 안 하고의 차이

1. 가독성

  • Builder 사용: 각 필드를 메서드 체인으로 설정할 수 있어서, 코드가 명확하고 읽기 쉽다. 
  • Builder 미사용: 모든 필드를 생성자에서 한 번에 전달해야 하므로, 길어지고 복잡해질 수 있다.

2. 유연성

  • Builder 사용: 선택적인 필드를 쉽게 처리할 수 있다. 필요 없는 필드는 설정하지 않으면 된다. 기본값을 설정해두면 생략 가능하다.
  • Builder 미사용: 모든 필드를 반드시 설정해야 하므로, 생성자 매개변수가 많아질 경우 관리가 힘들어질 수 있다. 모든 값을 명시적으로 설정해야한다.

3. 확장성

  • Builder 사용: 새로운 필드를 추가할 때 Builder에만 추가하면 되므로, 기존 코드에 영향을 덜 끼친다.
  • Builder 미사용: 기존 생성자에 필드를 추가하면 모든 호출 부분을 수정해야 할 수도 있다.

▼ Builder 사용

public static class Builder {
    // ... 다른 필드
    private boolean accountNonExpired = true;      // 기본값 설정
    private boolean accountNonLocked = true;       // 기본값 설정
    private boolean credentialsNonExpired = true;  // 기본값 설정
    
    // ... 빌더 메서드
}

// 사용 시
return new CustomUserDetails.Builder()
        .username(user.getUsername())
        .password(user.getPassword())
        .email(user.getEmail())
        .enabled(user.isEnabled())
        .authorities(authorities)
        .build();

 

▼ Builder 미사용

return new CustomUserDetails(
        user.getUsername(),
        user.getPassword(),
        user.getEmail(),
        user.isEnabled(),
        true,  // 계정 만료 여부
        true,  // 계정 잠금 여부
        true,  // 비밀번호 만료 여부
        authorities
);

JPA를 활용한 UserEntity 클래스

1. UserEntity 클래스

import javax.persistence.*;
import java.util.List;

@Entity
@Table(name = "users")  // 데이터베이스 테이블 이름
public class UserEntity {

    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;  // 고유 식별자

    @Column(nullable = false, unique = true)
    private String username;  // 사용자 이름

    @Column(nullable = false)
    private String password;  // 비밀번호

    @Column(nullable = false)
    private String email;  // 이메일

    @Column(nullable = false)
    private boolean enabled;  // 계정 활성화 여부

    @OneToMany(fetch = FetchType.EAGER)
    @JoinColumn(name = "user_id")  // 외래 키 설정
    private List<RoleEntity> roles;  // 사용자 역할 목록

    // Getters와 Setters 생략
}

2. RoleEntity 클래스

  • 역할 정보를 나타내는 RoleEntity 클래스
import javax.persistence.*;

@Entity
@Table(name = "roles")
public class RoleEntity {

    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;  // 고유 식별자

    @Column(nullable = false, unique = true)
    private String name;  // 역할 이름 (예: ROLE_USER, ROLE_ADMIN)

    // Getters와 Setters 생략
}

3. UserRepository 인터페이스

  • JPA를 사용하면 Repository 인터페이스를 만들어 데이터베이스 작업을 수행할 수 있다.
import org.springframework.data.jpa.repository.JpaRepository;

public interface UserRepository extends JpaRepository<UserEntity, Long> {
    UserEntity findByUsername(String username);  // 사용자 이름으로 찾기
}

4. UserDetailsService 구현

  • UserDetailsService를 구현할 때 UserRepository를 사용해 사용자 정보를 조회하는 방식으로 변경할 수 있다.
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.core.userdetails.UsernameNotFoundException;
import org.springframework.stereotype.Service;

import java.util.List;
import java.util.stream.Collectors;

@Service
public class CustomUserDetailsService implements UserDetailsService {

    @Autowired
    private UserRepository userRepository;  // JPA Repository

    @Override
    public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
        UserEntity userEntity = userRepository.findByUsername(username);
        
        if (userEntity == null) {
            throw new UsernameNotFoundException("User not found with username: " + username);
        }

        List<GrantedAuthority> authorities = userEntity.getRoles().stream()
                .map(role -> new SimpleGrantedAuthority(role.getName()))
                .collect(Collectors.toList());

        return new CustomUserDetails.Builder()
                .username(userEntity.getUsername())
                .password(userEntity.getPassword())
                .email(userEntity.getEmail())
                .enabled(userEntity.isEnabled())
                .authorities(authorities)
                .build();
    }
}

 

요약

  • JPA: 데이터베이스와의 매핑을 간편하게 해주는 프레임워크.
  • Repository: 데이터베이스 작업을 수행할 수 있도록 도와주는 인터페이스.
  • CustomUserDetailsService: UserRepository를 사용하여 사용자 정보를 조회하고, CustomUserDetails를 반환하는 서비스.