웹개발/SpringBoot

보안 인증과 권한 시스템 - 1. Entity

bergerac 2025. 6. 2. 09:11

https://github.com/seoyoonchoi/Korea_sleep_Tech_springBoot/tree/main/src/main/java/com/example/korea_sleepTech_springboot

 

Korea_sleep_Tech_springBoot/src/main/java/com/example/korea_sleepTech_springboot at main · seoyoonchoi/Korea_sleep_Tech_springB

Contribute to seoyoonchoi/Korea_sleep_Tech_springBoot development by creating an account on GitHub.

github.com

실제 코드를 더 확인하고 싶다면 위 링크를 통해서 확인할 수 있다. 이 글에서는 간략하게 코드의 중요한 부분만 발췌하였다.

 

1. Entity란?

Entity는 객체 지향적으로 표현된 데이터베이스의 테이블이다.
JPA(Java Persistence API)에서는 Entity를 통해 자바 객체와 RDB 테이블 간 매핑을 자동으로 관리하게 된다.

즉, 데이터베이스를 직접 건드리지 않아도 엔티티 클래스를 통해 다음과 같은 작업을 할 수 있게 된다:

  • 테이블 생성 / 수정
  • CRUD 처리 (저장, 조회, 삭제 등)
  • 관계 맵핑 (1:1, 1:N, N:1, N:M)
com.example.korea_sleepTech_springboot
├── entity
│   ├── User.java
│   ├── Role.java
│   ├── UserRole.java
│   ├── UserRoleId.java
│   └── RoleChangeLog.java

 

권한 인증을 위해 설계하는 entity는 이렇게 5개의 file 이 있으며 각각의 entity의 역할에 대해서 아래에서 계속 서술하겠다.

 

2. User 엔티티 — 인증 시스템의 핵심

User 엔티티는 Spring Security와 연결되어 있어 단순한 사용자 정보만을 담는 구조가 아니다.
이 엔티티는 UserDetails 인터페이스를 구현함으로써 Spring Security가 인식 가능한 사용자 객체가 된다.

 

주요 필드 및 기능

  • id, email, password, createdAt, updatedAt: 기본적인 사용자 정보
  • Set<Role> roles: 사용자 권한. 다대다(@ManyToMany) 관계로 여러 Role을 가질 수 있음
  • getAuthorities(): 권한을 Spring Security 형식에 맞게 변환하여 반환
  • getUsername(): 이메일을 username으로 사용하도록 지정
  • 기타 계정 상태 관련 메서드: 계정 만료, 잠금 여부 등을 반환 (true로 고정)

설계에서의 포인트

  • 생성자는 @Builder를 사용해 명확하게 생성 관리
  • @NoArgsConstructor(access = AccessLevel.PROTECTED)로 외부에서 무분별한 객체 생성을 방지
  • 즉시 로딩(EAGER) 설정을 통해 인증 후 권한까지 함께 조회되도록 최적화
    // 다대다 관계 매핑
    @ManyToMany(fetch = FetchType.EAGER, cascade = CascadeType.PERSIST) 
    @JoinTable(
            name = "user_roles", 
            joinColumns = @JoinColumn(name = "user_id"), 
            inverseJoinColumns = @JoinColumn(name = "role_id") 
    )
    private Set<Role> roles;
    
    //사용자 권한을 반환하는 메서드
    @Override
    public Collection<? extends GrantedAuthority> getAuthorities() {
        return roles.stream()
                .map(role -> new SimpleGrantedAuthority("ROLE_" + role.getRoleName()))
                .collect(Collectors.toSet());
    }

    // 계정 만료 여부(true: 만료되지 않음)
    @Override public boolean isAccountNonExpired() { return true; }
    // 계정 잠김 여부(true: 잠기지 않음)
    @Override public boolean isAccountNonLocked() { return true;}
    // 인증 정보 만료 여부
    @Override public boolean isCredentialsNonExpired() {return true; }
    // 계정 활성화 여부
    @Override public boolean isEnabled() { return true;}
}

 

 

3. Role 엔티티 — 권한 도메인

Role은 시스템 내에서 부여될 수 있는 권한을 의미한다.
예: ROLE_USER, ROLE_ADMIN 등

  • id, roleName: 기본 정보
  • roleName은 유일해야 하므로 unique = true로 설정
  • @Builder 사용으로 객체 생성을 유연하게 처리

이처럼 역할은 유저와의 관계를 통해 인증 흐름을 완성시킨다.

@Entity
@Table(name = "roles")
@NoArgsConstructor(access = AccessLevel.PROTECTED)
// 기본 생성자를 protected로 생성
// : JPA에서 필요로 함
@Getter
public class Role {
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    @Column(name = "role_id") // DB 컬럼 이름과 매핑
    private Long id;

    @Column(name = "role_name", nullable = false, unique = true)
    private String roleName;

    @Builder
    public Role(String roleName) {
        this.roleName = roleName;
    }
}

4. UserRole & UserRoleId — 다대다 관계의 세분화

JPA에서는 @ManyToMany만으로도 다대다 관계 매핑이 가능하다.

@ManyToMany
@JoinTable(name = "user_roles", ...)

이 방식은 **중간 테이블(user_roles)**을 자동으로 생성하고 관리해주기 때문에, 단순한 유저-역할 연결에는 충분하다.

그러나 현실에서는 이 중간 테이블에 다음과 같은 추가적인 정보가 필요한 경우가 많다:

  • 역할 부여 일시 (assigned_at)
  • 현재 역할이 유효한지 여부 (is_active)
  • 부여한 관리자 ID (assigned_by)
  • 변경 사유 등…

이런 경우엔 user_roles 테이블을 독립적인 엔티티로 분리하여 관리해야 한다.
그렇게 하면 단순 연결 이상으로, 변경 이력, 권한 활성화/비활성화 상태, 부여자 추적 등이 가능하다

 

복합 키 클래스: UserRoleId

중간 테이블은 일반적으로 두 개의 외래 키를 복합 키로 사용한다.
JPA에서는 @Embeddable을 통해 이런 복합 키 클래스를 별도로 정의해야 한다.

@Embeddable // 복합 키로 사용할 수 있도록 지정
@NoArgsConstructor
@AllArgsConstructor
@Getter
@EqualsAndHashCode
public class UserRoleId implements Serializable {
    private Long userId;
    private Long roleId;
}

 

  • Serializable 필수
  • equals()와 hashCode()를 재정의해야 하므로 @EqualsAndHashCode 사용
  • 두 외래 키(userId, roleId)를 조합해 하나의 복합 키로 구성

 

중간 엔티티: UserRole

@Entity
@Table(name = "user_roles") // 중간 테이블을 명시적으로 엔티티화
@NoArgsConstructor(access = AccessLevel.PROTECTED)
public class UserRole {

    @EmbeddedId
    private UserRoleId id; // 복합 키

    @ManyToOne(fetch = FetchType.LAZY)
    @MapsId("userId")
    @JoinColumn(name = "user_id")
    private User user;

    @ManyToOne(fetch = FetchType.LAZY)
    @MapsId("roleId")
    @JoinColumn(name = "role_id")
    private Role role;

    // 예: 역할 부여 일시, 활성 상태, 부여자 정보 등 추가 가능
    // private LocalDateTime assignedAt;
    // private Boolean isActive;
    // private Long assignedBy;

    @Builder
    public UserRole(User user, Role role) {
        this.user = user;
        this.role = role;
        this.id = new UserRoleId(user.getId(), role.getId());
    }
}

주요 어노테이션 설명

  • @EmbeddedId: 복합 키 클래스 사용
  • @MapsId: 외래 키가 복합 키의 일부임을 명시
  • @ManyToOne(fetch = FetchType.LAZY): 중간 테이블에서도 지연 로딩 전략 사용 가능

분리해야 하는 이유

// 단순 연결이면 이렇게 충분하지만,
@ManyToMany
@JoinTable(name = "user_roles", ...)
Set<Role> roles;

// 아래처럼 추가 정보가 필요한 경우, 독립된 엔티티가 필요하다.
@Entity
@Table(name = "user_roles")
class UserRole {
    @EmbeddedId
    private UserRoleId id;

    private LocalDateTime assignedAt;
    private boolean isActive;
    private Long assignedBy;
}

 

5. RoleChangeLog - 역할 변경 로그 기록

이 엔티티는 관리자나 시스템에 의해 유저의 역할이 변경된 내역을 추적하기 위한 용도다.
이런 로그는 보안 감사를 위한 중요한 근거 자료가 된다.

  • userId, email: 변경 대상
  • prevRoles, newRoles: 변경 전후의 역할 (TEXT 타입 → @Lob 사용)
  • changedBy, changeType, changeReason: 누가 어떤 이유로 변경했는지 기록
  • changedAt: @PrePersist를 통해 자동 기록

이처럼 로그를 남김으로써 시스템의 투명성과 변경 이력 관리를 책임진다.

 

@PrePersist란?

 

  • JPA에서 제공하는 엔티티 생명주기 콜백 어노테이션 중 하나
  • 해당 엔티티가 처음으로 persist() 될 때, 즉 DB에 INSERT 되기 직전에 호출됨
  • 데이터베이스에 저장되기 바로 직전의 상태에서 어떤 처리를 하고 싶을 때 사용
@PrePersist
public void prePersist() {
    this.createdAt = LocalDateTime.now();
}

 

DB에 저장될 때마다 현재 시간을 자동으로 넣어주는 방식
이걸 통해 불필요하게 서비스나 컨트롤러에서 시간을 지정하지 않아도 되고, 일관성 있는 시간 처리가 가능