[일정 관리 앱] N : M(다대다) 관계 풀어내기

2024. 10. 12. 18:37내일배움캠프/Schedule Management

 Lv.4 요구사항은 단순하게 보면 '유저' 정보를 갖는 엔티티를 추가하는 것이다. 하지만 세부 요구사항을 보면 'N : M' 관계에 대해 더 집중해야 함을 예상할 수 있었다.

 

1. ERD

간략하게 요구사항을 정리하면 '유저' 엔티티가 추가되며 '일정' 엔티티는 작성자에 대한 정보로 '유저' 엔티티의 식별자 값을 가져야 한다. 더불어 일정 생성시 작성자(유저)는 일정을 관리(담당)해 줄 '관리자' 를 설정할 수 있어야 한다.

  • 한 명의 유저는 여러개의 일정을 작성 가능 - "유저 엔티티 : 일정 엔티티 = 1 : N"
  • 하나의 일정에 여러명의 일정 관리자(=유저)를 지정 가능 - "일정 엔티티 : 유저 엔티티 = 1 : N"

즉, 유저 엔티티와 일정 엔티티는 'N : M(다대다)' 관계를 맺어야 하는 것이다. 솔직히 JPA 가 '@ManyToMany' 를 통해 'N : M' 관계 매핑을 지원하긴 하나 개인적으로 'N : M' 매핑을 통한 기능 구현은 엄두가 나지 않았다. 이러한 생각이 든 가장 큰 이유는 "내가 제대로 흐름을 읽고 컨트롤 할 수 있을까?" 라는 생각이 들었기 때문이다. 그래서 아래와 같이 '중간 테이블(엔티티)' 를 생성해 'N : M' 관계를 풀어내 구현하기로 하였다.

Level.04 요구사항에 대한 ERD

 

일단 '유저' 테이블이 추가되며 '일정' 테이블은 기존의 'author(작성자명)' 대신 '유저' 엔티티의 식별 값(=유저 ID)을 참조 키로 가지게 되었고 '일정' 테이블과 '유저' 테이블간 'N : 1' 관계가 맺어지게 되었다.

 

그리고 '일정' 테이블과 '유저' 테이블의 '1 : N' 관계를 해결하기 위해 '일정 매니저' 테이블을 중간에 추가하게 되었다. 해당 테이블의 레코드는 '일정의 관리자' 정보를 나타낸다. '일정 매니저' 테이블 은 아래와 같은 연관관계를 갖는다.

  • 하나의 일정에 여러명의 일정 매니저 지정 가능 - 일정 : 일정 매니저 = 1 : N
  • 한 명의 유저는 여러 일정의 매니저로 지정 가능 - 유저 : 일정 매니저 = 1 : N

ERD 작성을 작성하고 "어? 하나의 일정에 여러 일정 매니저를 지정할 수 있다면 그냥 '일정 매니저 - 유저' 관계를 ' 1 : N' 으로 해도되는거 아닌가?" 라는 생각이 잠깐 들기도 했다. 하지만 '일정에 해당하는 일정 매니저' 정보를 얻기 위해서 '일정 매니저' 테이블 뿐만 아니라 '유저' 테이블 또한 조회해야 한다는 점과 '일정' 테이블 삭제시 '일정 매니저' 테이블도 함께(영속성 전이) 삭제하고 싶었기에 '고아 객체' 가 생기는 것을 막고자 결국 위와 같은 연관관계를 선택하게 되었다.

 

2. 구현 후 API 테스트

위의 ERD 를 토대로 코드를 수정 및 추가 작성해 구현을 완료했고 이번에 작성한 코드는 여기서 확인할 수 있다. 구현 이후 API 테스트 결과는 아래와 같다.

일정 생성 - 정상 요청

 

먼저 수정사항에 대해 말하자면 유저(=작성자)의 ID 를 전달 받기 위해 쿼리 파라미터가 추가 됬으며 생성 일정에 일정 관리자들을 지정할 수 있게 RequestBody 에 'scheduleManagers' 파라미터가 추가 되었다. 요청 수행 후 반환 값은 기존과 같으며 응답 또한 '201 Created' 로 같다. 반환 값에 일정 매니저 정보가 없으니 DB 의 scheduleManager 테이블을 확인해 보면

scheduleManager 테이블

 

새로 생성된 일정에 지정된 일정 매니저(유저)의 정보가 추가된 것을 확인할 수 있다. 테이블을 보면 일정 매니저마다 'scheduleManager' 엔티티를 생성한 것을 볼 수 있는데 이 때문에 JpaRepository 의 'saveAll()' 메서드를 사용했다. 그래서 현재 일정에 지정된 매니저 수만큼 'insert' 쿼리를 날리는데 이 부분은 '배치 인서트' 를 활용하면 한 번에 쿼리를 날릴 수 있다는 정보를 확인했지만 '배치 인서트' 를 사용할 만큼의 대규모 데이터를 다루는 것이 아니라 판단 적용하지 않는 결정을 하였다.

 

정리하면 이제 '일정 생성' 기능은 일정을 생성할 때 일정 매니저를 지정하지 않는다면 일정 엔티티만을 생성하고, 지정했다면 일정 엔티티와 함께 일정 매니저 엔티티(들을)를 생성하게 되었다.

 

요청 파라미터가 추가됨에 따라 요청 파라미터 관련 예외 처리도 추가하였다.

일정 생성 - 비정상 요청(1)

 

만약 존재하지 않는 유저 ID(작성자 ID) 를 쿼리 파라미터로 전달할 경우 요청을 보낸 사용자는 일정 관리 앱의 유저가 아니므로 일정을 작성(생성)할 권한이 없다 생각해 위 이미지와 같은 예외 코드 및 메시지와 '401 Unauthorized' 응답을 반환하도록 하였다.

 

참고로 유저 ID 를 쿼리 파라미터로 전달한 이유는 이후 도전 요구사항에서 JWT 를 활용한 인증/인가를 다루면서 삭제될 요청 파라미터이기에 수정에 용이하도록 쿼리 파라미터로 전달하게 되었다.

일정 생성 - 비정상 요청(2)

 

요청 파라미터가 유효하지 않은 경우 이전과 같이 예외 코드 및 메시지와 유효성 검증을 통과하지 못한 파라미터와 통과하지 못한 이유를 반환해두고 '400 Bad Request' 를 응답한다.'scheduleManagers' 의 경우 '@NotNull' 어노테이션을 지정해 검증을 하도록 구현하였다.

 

마지막으로 '일정 삭제' 에 대한 API 테스트이다. 삭제 요청 전 'schedule, scheduleManager' 테이블의 상태는 아래와 같다.

일정 삭제 전 schedule 테이블
일정 삭제전 scheduleManager 테이블

 

schedule 테이블에서 'id = 16' 에 해당하는 일정을 삭제해 보겠다.

일정 삭제 요청

 

일정 삭제의 경우 요청 URL 이 "/schedule/{scheduleId}/{memberId}" 로 수정되었다. 작성한 유저만 일정을 삭제할 수 있도록 쿼리 파라미터를 추가한 것이다. 일정 삭제의 경우 '204 No Content' 응답만을 반환하므로 요청 수행후 DB 를 살펴봐야 한다.

일정 삭제 후 schedule 테이블
일정 삭제 후 scheduleManager 테이블

 

앞서 말한대로 일정이 삭제되면 일정에 지정된 일정 매니저 정보 또한 삭제되도록 구현했기에 'id = 16' 일정이 삭제되자 scheduleManager 테이블에서 'schedule_id = 16' 인 레코드들이 모두 삭제 되었다.

 

 

3. 정리

 새로운 테이블을 한 번에 2개 추가하면서 수정을 하다보니 미처 신경쓰지 못한 부분(변수명, 메서드명 등)이 여럿 있을 거라 생각한다. 이후 인증/인가를 반영하고 나서는 기존 코드에 영향이 크게 가는 요구사항이 없을거라 판단 인증/인가에 대한 요구사항을 모두 반영 후 모든 API 테스트를 다시 한 번 진행하며 해결할 생각이다.

 

또한 '1 : N' 관계에서의 전체 조회를 제외하고는 대부분의 요청 수행에 사용되는 쿼리를 신경쓰지 못한 것을 인지, 이 부분에 대해서도 청 수행에 필요한 쿼리만을 사용하게 끔 재확인할 필요성을 느꼈다. 이 부분도 인증/인가를 반영하고 나면 진행해볼 생각이다.