글을 쓰는 이유
Spring Boot 프로젝트를 진행하면서 회원과 회원권한 관련 Entity 설계에 있어서 초기에는 Member와 MemberRole 관계를 단순히 @OneToMany로 설정하여 진행할 생각이었습니다.
다만, Role을 일종의 Code값( "USER", "Admin", "SUPERADMIN" ) 형식으로 관리하는 것이 더 깔끔하게 관리될 것 이라는 생각이 들었고, 그 과정에서 Member - [BRIDGE TABLE] - Role 아래의 구조처럼 될 것 입니다.
사실, @ManyToMany 가 결국 지금 위의 그림과 똑같이 Member_Role이라는 테이블을 자동으로 생성해주어 처리해주는 것과 같은 역할이지만, 위의 구조로 변화시키는것만으로도 이후에 Member_Role 관련한 데이터를 가져오거나 할떄 의존성이 줄어드므로 이후에 원하는 데이터를 넣기 편해지고, 조작하기 쉬워질 것 입니다.
이제 실제로 구현해보겠습니다.
구현해보기
1. Member Entity 설계
@Entity
@Builder
@AllArgsConstructor
@NoArgsConstructor
@Getter
@ToString
//@ToString(exclude = {"member_role_set"})
public class Member extends BaseEntity{
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long member_no;
@Column(length = 100, nullable = false, unique = true, name = "member_id")
private String member_id;
@Column(length = 100, nullable = false)
private String member_password;
@Column(length = 100, nullable = true)
private String member_nickname;
@Column(nullable = false)
private boolean member_from_social;
@Column(nullable=true)
private LocalDateTime del_dt;
@OneToMany(mappedBy = "member",fetch = FetchType.LAZY, cascade=CascadeType.ALL, orphanRemoval = true)
@Builder.Default
private Set<Member_Role> member_role_set = new HashSet<>();
public void setMember_nickname(String member_nickname){
this.member_nickname = member_nickname;
}
public void addMemberRole(Member_Role member_role){
member_role_set.add(member_role);
}
}
- 여기서 주시해야할 점은 private Set<Member_Role> member_role_set = new HashSet<>(); 부분입니다.
- OneToMany 로 처리했고, HashSet을 통해 이후에 USER 권한인지, ADMIN 권한인지 더 빨리 확인할 수 있게 처리했습니다.
- 여기서 mappedBy = "member" 로 되어있는데, 해당 내용은 중간 테이블에서 Member Entity 를 가리키는 변수 이름을 설정합니다. 저는 Member_Role에서 Member를 "member"로 가리키고 있습니다.
- OneToMany에서 mappedBy는 사실 mappedBy가 있는곳이 주인 관계가 아니지만, 현재는 1:N, M:1 관계를 풀어써서 진행하고 있기에 어쩔 수 없이 방식으로 진행합니다.(ManyToOne에는 MappedBy 속성이 없음) 관련내용으로 아래 답변을 통해 이해하면 좋을 것 같습니다. https://www.inflearn.com/questions/18042/manytoone-%EC%97%90%EB%8A%94-%EC%99%9C-mappedby-%EC%86%8D%EC%84%B1%EC%9D%B4-%EC%97%86%EC%9D%84%EA%B9%8C%EC%9A%94
2. MembeR_Role Entity ( 중간테이블 )
@Entity
@Builder
@AllArgsConstructor
@NoArgsConstructor
@Getter
@ToString(exclude = "member")
public class Member_Role extends BaseEntity{
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long member_role_no;
@ManyToOne(fetch = FetchType.LAZY)
@JoinColumn(name = "member_no")
private Member member; //연관관계 지정
@ManyToOne(fetch = FetchType.LAZY)
@JoinColumn(name = "role_no")
private Role role;
public void setMember(Member member) {
this.member = member;
}
}
- Member_Role은 Member Entity를 @ManyToOne으로 바라봅니다.
- Member_Role은 Role Entity를 @ManyToOne으로 바라봅니다.
- FetchType.Lazy로 설정합니다. ( 그래야 Member_Role 이 필요하지는 않을때 호출하지 않습니다. )
3. Role Entity
@Entity
@Builder
@AllArgsConstructor
@NoArgsConstructor
@Getter
@ToString(exclude = {"member_list"})
public class Role extends BaseEntity{
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long role_no;
@OneToMany(mappedBy = "role", fetch = FetchType.LAZY)
private List<Member_Role> member_list;
@Enumerated(EnumType.STRING)
@Column(nullable = false, unique = true)
private RoleType role_type;
@Column(nullable=true)
private LocalDateTime del_dt;
}
- Role Entity는 Member_Role을 OneToMany로 바라봅니다.
- "Role Entity에서 관리자 권한을 가진 사람은 몇명이야?" 라는 기능이 필요할지도 모르므로 관련 연관관계를 넣어줍니다. ( 만약 해당 연관관계가 필요하지 않다면 추가하지 않으셔도 됩니다. )
- 여기서 추가로 RoleType 객체가 등장합니다. 물론, String Role_Name 이런식으로해서 회원가입할때 "ADMIN", "USER", "MANAGER" 이런식으로 넣으셔도 됩니다. 하지만, RoleType을 전체적으로 한곳에서 관리한다면 이후에 RoleType 을 관리하기가 편할 것 입니다.
4. RoleType
public enum RoleType {
USER, MANAGER, ADMIN
}
- 이로써 연관관계 매핑은 끝났습니다.
실제로 RepositoryTests를 통하여 관계를 확인해보겠습니다.6. MemberRepository 생성
public interface MemberRepository extends JpaRepository<Member, Long> {
@EntityGraph(attributePaths = {"member_role_set.role"}, type = EntityGraph.EntityGraphType.LOAD)
@Query("Select m From Member m WHERE m.member_id = :member_id AND del_dt is null")
Optional<Member> findByMember_id(String member_id);
}
- 아래 Test에 필요한 Query 들입니다.
- EntityGraph를 활용하여 member_role_set.role 을 자동으로 JOIN하여 가져옵니다.
- 현재 FetchType.LAZY 이기에 관련 정보가 필요할떄 Member_Role과 Role이 따로따로 필요할때 2번 실행됩니다. 이때, EntityGraph를 활용하여 Member Entity의 member_role_set.role을 설정으로 잡아서 한번에 모든 테이블들이 Join되서 가져옵니다.
7. RoleRepository 생성.
public interface RoleRepository extends JpaRepository<Role, Long> {
@Query("SELECT COUNT(*) FROM Role r WHERE r.role_type = :role_type AND del_dt is null")
Long countByRole_type(@Param("role_type") RoleType role_type);
@Query("SELECT r FROM Role r WHERE r.role_type = :role_type AND del_dt is null")
Optional<Role> findByRole_type(@Param("role_type") RoleType role_type);
}
- 아래 Test에 필요한 Query 들입니다.
5. [src/test/Repository/MemberRepositoryTests.class] 에서 테스트 진행합니다.
@SpringBootTest
public class MemberRepositoryTests {
@Autowired
private MemberRepository memberRepository;
@Autowired
private RoleRepository roleRepository;
private Member member;
private final String member_id= "hello@naver.com";
private final String member_password= "123";
private final String member_nickname = "hello";
private final boolean member_from_social = false;
@BeforeEach
public void setup() throws Exception{
// Create member
Member member = Member.builder()
.member_id(member_id)
.member_password(member_password)
.member_nickname(member_nickname)
.member_from_social(member_from_social)
.build();
Role adminRole = roleRepository.findByRole_type(RoleType.ADMIN)
.orElseGet(() -> roleRepository.save(Role.builder().role_type(RoleType.ADMIN).build()));
Role userRole = roleRepository.findByRole_type(RoleType.USER)
.orElseGet(() -> roleRepository.save(Role.builder().role_type(RoleType.USER).build()));
// Create member_role
Member_Role memberRole = Member_Role.builder()
.member(member)
.role(adminRole)
.build();
// ADD Set member_role in member
member.addMemberRole(memberRole);
//Save Member
memberRepository.save(member);
}
@DisplayName("findByMember_id Test")
@Test
public void testGetWithMember_id(){
// given, when
Optional<Member> member = memberRepository.findByMember_id(member_id);
// then
assertNotNull(member.get());
System.out.println(member.get());
}
}
- 테스트를 진행하면, 아래와 같은 SQL과 모든값들이 JOIN되서 나옵니다.
- 이떄 만약, 본인은 Member정보만 출력되길 원한다면 ToString Exclude를 Member ENtity에서 을 추가합니다.
@ToString(exclude = {"member_role_set"})
SQL 쿼리와 Member 정보들
select
m1_0.member_no,
m1_0.del_dt,
m1_0.inst_dt,
m1_0.member_from_social,
m1_0.member_id,
m1_0.member_nickname,
m1_0.member_password,
m2_0.member_no,
m2_0.member_role_no,
m2_0.inst_dt,
r1_0.role_no,
r1_0.del_dt,
r1_0.inst_dt,
r1_0.role_type,
r1_0.updt_dt,
m2_0.updt_dt,
m1_0.updt_dt
from
member m1_0
left join
member_role m2_0
on m1_0.member_no=m2_0.member_no
left join
role r1_0
on r1_0.role_no=m2_0.role_no
where
m1_0.member_id=?
and m1_0.del_dt is null
Member(member_no=10, member_id=daeho.kang2@naver.com, member_password=$2a$10$y82mtcYMtOgZ9dADOE4STuArv/X6uD//ABxNF51b5Ez0gplO3gDMS, member_nickname=hello, member_from_social=false, del_dt=null, member_role_set=[Member_Role(member_role_no=10, role=Role(role_no=9, role_type=ADMIN, del_dt=null))])
개선해야할점들
현재 EntityGraph를 활용하여 모든 내용들이 다 JOIN되어 나오게 처리해놓은 상태입니다.
모든 Member 정보 쿼리를 뽑을때마다 이렇게 처리할 수는 없으니 MemberRepository에서 그에 맞는 Query를 따로 추가하여 진행해야합니다.
이후의 작업
이제 회원과 회원 권한 관련 포스팅이 완료되었으니, Spring Security와 JWT를 REST API 인증/인가 기능에 대한 내용을 정리해보겠습니다.