왜 이 글을 작성하였을까
Spring-data-jpa를 활용하여, 회원(Member Entity)의 회원권한(MemberRole)에 관한 테이블 설계를 진행하고 있었습니다.
이때, 회원과 회원권한을 OneToMany를 사용하여 설계를 진행하려고 하였지만,
관련 정보를 좀 더 찾아보다가 제 프로젝트 같은경우,
회원권한에 들어가는 값들은 단순히 (USER, ADMIN, MANAGER) 이런식으로 단순한 고정 Value들이 들어갈 것이기에 OneToMany를 활용하지말고 ElementCollection을 활용하여 회원권한을 컬렉션형태로 사용하는 것이 더 나을 것같아 관련 글을 남기게 되었습니다.
다만, 해당글은 ElementCollection을 사용하는 하나의 예시일뿐, 회원권한관리에 대하여서는 일반적으로 OneToMany를 사용하는 것을 더 추천합니다.
@OneToMany를 활용할경우,
1. 회원(Member Entity)의 회원권한(MemberRole)을
2. @OneToMany 형태로 Member Entity의 고유값을 MemberRole에 FK로 두어서 도메인을 설계한다.
이렇게 할경우
@OneToMany 어노테이션의 장점인
1. 연관된 엔티티를 통해 엔티티의 라이플사이클에 따라 컬렉션 값들을 관리할 수 있다.
2. 컬렉션 값들에 대해 검색과 조인이 용이하며, 복잡한 쿼리 작성이 가능하다.
@OneToMany 어노테이션의 단점으로는
1. 별도의 FK키를 사용하여 연관 엔티티를 저장하기에 테이블 구조가 복잡해집니다.
@ElementCollection을 활용할경우,
1. 회원(Member Entity)의 회원권한(MemberRole)을
2. @ElementCollection 을 활용하여 Collection을 HashSet으로 관리합니다.
이렇게 할경우
@ElementCollection 어노테이션의 장점인
1. Primitive types, String, Int 등 과 같은 타입들을 엔티티의 일부로 사용가능합니다. 예를 들어, 엔티티가 컬렉션 속성을 갖고 있을경우, ElementCollection을 사용하여 컬렉션의 요소를 매핑할 수 있습니다.
2. 단일테이블에 컬렉션 값들이 저장되어, 별도의 조인 테이블이 필요하지 않습니다.
3. 컬렉션을 지원하는 데이터타입을 사용할 수 있습니다. Set, List, Map 을 활용할 수 있습니다.
@ElementCollection 어노테이션의 단점인
1. 연관된 엔티티가 존재하지 않아서 엔티티의 생명주기에 종속되지 않습니다.
2. 컬렉션 값들에 대한 검색이나 조인이 어려울 수 있습니다.
이러한 장점과 단점들을 알아보았을때, 제 프로젝트 같이 단순히 USER, ADMIN, MANAGER 이런 권한이 들어갈경우 ElementCollection을 활용하여 권한 컬렉션들을 관리하는것이 좀 더 관리하기 편할 것 입니다.
ElementCollection을 사용한 예시코드
MemberEntity
@Entity
@Builder
@AllArgsConstructor
@NoArgsConstructor
@Getter
@ToString
public class Member extends BaseEntity{
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long mno;
private String email;
private String password;
/**
* ElementCollection을 활용하여 Member 객체의 일부로만 사용합니다.
*/
@ElementCollection(fetch = FetchType.LAZY)
@Builder.Default
private Set<MemberRole> roleSet = new HashSet<MemberRole>();
public void addMemberRole(MemberRole memberRole){
roleSet.add(memberRole);
}
}
MemberRole.java
public enum MemberRole {
USER, MANAGER, ADMIN,
}
위와 같이 코드를 작성할경우, hibernate 에서 자동으로 MemberRole 테이블도 만들어주어 관리를 해줍니다.
MemberRepository.java
public interface MemberRepository extends JpaRepository<Member, Long> {
/**
* 이메일을 기준으로 조회합니다.
* @param email : email
* @return
*/
@EntityGraph(attributePaths = {"roleSet"}, type = EntityGraph.EntityGraphType.LOAD)
@Query("select m from Member m where m.email = :email")
Optional<Member> findByEmail(String email);
}
EntityGraph를 사용하여 자동으로 'left outer join'을 작동하게 합니다.
@SpringBootTest
public class MemberRepositoryTests {
@Autowired
private MemberRepository memberRepository;
@DisplayName("Member 테스트데이터 삽입")
@Test
public void insertMembers(){
IntStream.rangeClosed(1, 20).forEach(i ->{
Member member = Member.builder()
.email("user"+i+"@hello.com")
.password("password")
.build();
//default Role
member.addMemberRole(MemberRole.USER);
if( i > 10) member.addMemberRole(MemberRole.ADMIN);
memberRepository.save(member);
});
}
@DisplayName("Member 데이터 조회")
@Test
public void testRead(){
Optional<Member> result = memberRepository.findByEmail("user1@hello.com");
Member member = result.get();
System.out.println(member);
}
}
직접 insert한 뒤에 testRead를 할경우 올바르게 나오는 것을 알 수 있습니다.
마무리
이번에 elementCollection을 활용하여 고정된 value들을 사용할 경우 직관적으로 DB를 설계할 수 있다는 것을 알게되었던 것 같습니다.
또 추가로 elementCollection에 대해 찾아보니, Address(주소)에서와 같이 서울시, 경기도 이런식으로 고정된 값들을 사용하는곳에서도 사용되고 있던 것도 기억에 남습니다.