1. 배경
인가 시스템을 고민하면서, 수직적인 권한과 수평적인 권한을 분리했다.
- 수직적 권한 : (비회원->미승인회원->일반회원->회장단->회장->관리자)
- 수평적 권한 : 부서 (IT 부, 운영부, 총무, 홍보부 등)
수평적 권한을 처리하기 위해 Team 이라는 도메인을 추가하기로 했다.
지금 동아리 운영 상황을 보자면,
두 부서에서 동시에 활동하는 사람은 없지만
한 부서에서 다른 부서로 이동하는 경우, 중간에 임시로 동시 활동하는 경우 등이 있었고,
꼭 한 부서만 속해있으리라는 법은 없으니까.
Member 와 Team 을 다대다 관계로 설정했다.
2. 다대다의 문제
jpa 에서 ManyToMany 관계를 지원하고 있어서, 해당 어노테이션을 사용해 풀어내면 정말 쉽다.
하지만 두 엔티티 사이의 관계가 그 자체로 어떤 부가적인 의미를 갖게 될 때,
이런 방식으로는 의미를 담아낼 수 없으며, 서비스의 변화에도 대응할 수 없다.
예로, Member 와 Team을 매핑하는 member-team 테이블이 있다고 하자.
ManyToMany 어노테이션을 이용한다면, 이 테이블은 member_id 와 team_id 를 매핑하는 용도로만 사용될 것이다.
그런데,
- "회원이 언제 부서에 합류했는지 날짜도 볼 수 있었으면 좋겠어요"
- "부서 안에서도 여러 세부 부서로 나뉘어졌어요, 부서장이 직접 관리할 수 있는 기능을 추가해주세요"
등의 요구 사항이 생겼다고 해보자. 위 경우들이 회원과 팀 사이의 관계 자체가 부가적인 의미를 갖게 되는 상황이다.
(해당 기능을 추가하기 위한 테이블을 직접 설계해보라.)
ManyToMany 어노테이션은 member_id 와 team_id 컬럼만을 갖는 테이블을 매핑하도록 되어있기 때문에
이런 요구 사항을 해결할 수 없다.
따라서 매핑테이블에 부가 정보를 담을 수 있도록 컬럼을 추가하고
어플리케이션 단에서는 중간 객체를 추가해서 (Member - MemberTeam - Team) 일대다 다대일로 풀어내야한다.
3. 엔티티 생성
(1) Member 에 연관관계 메소드 추가
IbasInformation 이라는 wrapper type이 있는데,
학교관련 정보와 동아리(IBAS) 자체 정보를 분리하기 위한 용도이다.
중간 객체를 일대다 매핑해주었다.
Cascade type 은 Remove이다. 뒤에 설명하겠다.
@Entity
@NoArgsConstructor(access = AccessLevel.PROTECTED)
public class Member {
/* ... 생략 ... */
// 연관관계 메소드 추가
public void addTeam(MemberTeam team) {
this.ibasInformation.addTeam(team);
}
/* ... 생략 ... */
}
@Embeddable
@Getter
@NoArgsConstructor(access = AccessLevel.PROTECTED)
public class IbasInformation {
/* ... 생략 ... */
@OneToMany(mappedBy = "member", cascade = CascadeType.REMOVE)
private List<MemberTeam> teamList = new ArrayList<>();
public void addTeam(MemberTeam team) {
this.teamList.add(team);
}
/* ... 생략 ... */
}
(2) Team 엔티티 생성
마찬가지로 중간객체를 일대다 매핑해주었다.
Cascade type 은 Remove이다. 뒤에 설명하겠다.
@Entity
@Table(name = "team")
@NoArgsConstructor(access = AccessLevel.PROTECTED)
public class Team {
@Id @GeneratedValue(strategy = GenerationType.IDENTITY)
private Integer id;
@Embedded
private TeamName name;
@OneToMany(mappedBy = "team", cascade = CascadeType.REMOVE)
private List<MemberTeam> memberList = new ArrayList<>();
/* 연관관계 메소드 */
public void addMember(MemberTeam member) {
memberList.add(member);
}
/* ... getter & setter ...*/
}
(3) 중간객체 생성
지금은 단순하게 회원과 팀만 연결해놓은 상태이다.
추후에 부가적인 정보를 추가할 수 있다.
연관관계를 맺는 방법을 유심히 봐보자! (개인적으로 이렇게 구현해봤는데 맞는건지 잘 모르겠다.)
@Table(name = "user_team")
@NoArgsConstructor(access = AccessLevel.PROTECTED)
public class MemberTeam {
@Id @GeneratedValue(strategy = GenerationType.IDENTITY)
private Integer id;
@ManyToOne(optional = false, fetch = FetchType.LAZY)
@JoinColumn(name="user_id", foreignKey = @ForeignKey(name = "fk_to_user"))
private Member member;
@ManyToOne(optional = false, fetch = FetchType.LAZY)
@JoinColumn(name="team_id", foreignKey = @ForeignKey(name = "fk_to_team"))
private Team team;
/* constructor */
public MemberTeam(Member member, Team team) {
setMember(member);
setTeam(team);
}
/* relational methods */
private void setMember(Member member) {
this.member = member;
member.addTeam(this);
}
private void setTeam(Team team) {
this.team = team;
team.addMember(this);
}
}
생성자를 통해서 생성할 때, setter를 이용해 연관관계를 맺도록 강제한다.
"회원과 부서를 매핑" 한다는 비지니스 로직의 흐름을 생각해봤을 때, "회원"과 "부서"는 "매핑 행위"보다 항상 먼저 존재할 수 밖에 없다. 즉 MemberTeam 이라는 중간객체를 생성하는 것은, 항상 Member 와 Team 이 주어지고 난 뒤일 수밖에 없다. 따라서 MemberTeam 을 생성할 때 해당 생성자를 통해서 연관관계를 모두 맺어준다면, 개발자는 두 엔티티의 연관관계를 생성 시에 신경쓰지 않아도 된다!
4. 영속성 전이 설정.
영속성 전이는 객체의 생명주기 안에서의 특정 상태가 전이되는 특성이다.
부서(Team) 엔티티를 통해 자세히 살펴보자.
public class Team {
/* ... 생략 ...*/
@OneToMany(mappedBy = "team", cascade = CascadeType.REMOVE)
private List<MemberTeam> memberList = new ArrayList<>();
/* ... 생략 ...*/
}
Member - MemberTeam - Team 은 말했다시피 일대다, 다대일 관계이다.
MemberTeam 에게 외래키가 있으므로, Member 와 Team 이 부모이고 MemberTeam 이 자식이라고 볼 수 있다.
만약 한 부서가 사라진다면 서비스에서는 어떻게 처리해야할까?
해당 부서에 있는 모든 사람들을 부서에서 내보내야 할것이다. Member 와 Team 의 매핑정보를 모두 없애야한다.
Team 의 입장에서 보면, Team 이 사라졌을 때 관련된 MemberTeam(매핑정보)는 사라져야하는 정보이다.
즉, Team 의 특정 영속 상태(remove) 는 MemberTeam 에게 필수적으로 전달되어야한다.
위 코드(cascade)가 바로 그 뜻이다. "나의 특정 영속상태를, 이 친구들에게 전달해줘~" (Team에서 영속상태는 remove)
그래서 실제로 Team 을 삭제했을 때, 연관된 MemberTeam 을 삭제하는 쿼리가 같이 생성된다.
(뒤에 테스트 코드 확인)
그러면 거꾸로 MemberTeam(매핑정보)가 삭제된다면? Member 나 Team 도 삭제되어야할까?
회원이 부서에서 나간다는 의미이므로, 회원을 삭제한다거나 부서를 삭제한다는 건 말이 안된다.
수정, 생성 등의 경우도 그렇다. 그래서 MemberTeam 의 코드를 보면 영속성 전이 설정을 아무것도 하지 않았다.
Member 입장에서도 Team 과 동일하다.
회원이 삭제된다면 해당 회원이 부서 안에 존재한다는 사실(매핑정보)도 삭제되어야 할 것이다.
따라서 Member 엔티티에도 remove 에 대한 전이 설정을 해주었다.
주의해야할 점은, 보통 연관관계 부모 엔티티에 (cascade.ALL, orphanRemoval=true) 를 설정하는 경우가 있는데,
이런 경우에는 한쪽 부모를 삭제하게 되면, 자식 엔티티가 자동으로 삭제되지 않는다.
오히려 참조 무결성 오류가 발생하게 된다.
자세한 내용은 이 블로그에서 확인해보자!
영속성 전이를 이런 경우에는 어떻게 설정하고, 저런 경우에는 어떻게 설정해야한다~ 라는 법칙이 있는게 아니다.
도메인 안에서 엔티티 간의 관계와 운영하고자 하는 서비스에 따라 영속성을 그에 맞게 잘 설정해줘야한다.
5. 테스트 코드 작성
@DefaultDataJpaTest // 커스텀 어노테이션
public class MemberTeamTest {
@Autowired private TestEntityManager entityManager;
@Autowired private MemberTeamRepository memberTeamRepository; // 중간객체용
@Autowired private TeamRepository teamRepository; // 부서
@Autowired MemberRepository memberRepository; // 회원
private Team IT;
private Member member;
@BeforeEach
public void setUp() {
IT = entityManager.persist(new Team("IT 부서"));
member = entityManager.persist(
new Member(12171652, "유동현", "010-0000-0000", "",
SchoolInformation.ofUnderGraduate("건축공학과", 3),
new IbasInformation(Role.BASIC_MEMBER)));
memberTeamRepository.save(new MemberTeam(member, IT));
entityManager.flush();
entityManager.clear();
}
@DisplayName("팀을 삭제하면, 해당 팀에 속한 멤버들이 팀에서 방출된다.")
@Test
public void expelledByDeletingTeamsTest() {
//when
teamRepository.deleteById(IT.getId()); // 부서를 삭제할 때, 매핑정보도 삭제하는 쿼리가 추가로 생성되어야 한다.
entityManager.flush();
entityManager.clear();
//then
assertThat(entityManager.find(Team.class, IT.getId())).isNull();
assertTrue(memberTeamRepository.findAll().isEmpty());
}
@DisplayName("회원을 삭제하면, 부서 회원 목록에서 사라진다.")
@Test
public void vanishedByDeletingMemberProfileTest() {
//when
memberRepository.deleteById(member.getId()); // 회원을 삭제할 때, 매핑정보도 삭제하는 쿼리가 추가로 생성되어야 한다.
entityManager.flush();
entityManager.clear();
//then
assertThat(entityManager.find(Member.class, member.getId())).isNull();
assertTrue(memberTeamRepository.findAll().isEmpty());
}
}
'웹 프로젝트 (IBAS) > SpringBoot api 개편' 카테고리의 다른 글
[RFC 표준] OAuth 2.0를 쉽고 정확하게 알아보자! (기초 개념 및 용어 정리) (1) | 2022.05.08 |
---|---|
[Spring Boot] 4. 로컬 개발을 위한 CORS 설정 - (2) Spring MVC 와 Spring Security (0) | 2022.04.15 |
[Spring boot] 5. 멀티모듈? MSA? 좋은 아키텍쳐가 뭐야?! (1) | 2022.03.09 |
[Spring Boot] 4. 로컬 개발을 위한 CORS 설정 - (1) w3c recommendation (0) | 2022.02.28 |
[Spring Boot] 3. OAuth2 인증 설계 및 구현 (feat. Security FilterChain 분석) (0) | 2022.02.28 |