동현 유
척척석사
동현 유
전체 방문자
오늘
어제
  • 분류 전체보기 (178)
    • BlockChain (48)
      • [paper] Consensus (13)
      • [paper] Execution (19)
      • [paper] Storage (5)
      • [paper] ZKP (1)
      • [paper] Oracle (1)
      • Blockchains (9)
    • Java (19)
      • Java의 정석 (13)
      • Java 파헤치기 (5)
    • Python (20)
      • Python 뜯어보기 (6)
      • 데이터 분석 기초 (5)
      • Python 기초 강의 (6)
      • Python 기초 강의 부록 (3)
    • Golang (0)
    • MySQL (3)
      • programmers (2)
      • 기본 문법 (0)
    • 웹 프로젝트 (IBAS) (36)
      • Django 레거시 (14)
      • SpringBoot api 개편 (14)
      • Infra (3)
      • 서버 장애 기록 (4)
      • 신입팀원 교육 자료 (1)
    • CS (30)
      • Operating System (22)
      • Computer Security (3)
      • Network (4)
      • DBMS (1)
    • 책 (10)
      • 도메인 주도 설계 철저 입문 (9)
      • Real MySQL 8.0 (1)
    • BOJ 문제 풀이 (3)
    • 이러쿵저러쿵 (7)
    • 회고 (1)

인기 글

최근 댓글

최근 글

hELLO · Designed By 정상우.
동현 유

척척석사

웹 프로젝트 (IBAS)/SpringBoot api 개편

[SpringBoot] 6. ManyToMany 를 "일대다/다대일"로 풀어서 사용하기 (+ 영속성 전이 문제)

2022. 3. 20. 01:32

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
    동현 유
    동현 유
    Fault Tolerant System Researcher for more Trustful World and Better Lives. (LinkedIn: https://www.linkedin.com/in/donghyeon-ryu-526b8a276/)

    티스토리툴바