팀 프로젝트/ARTIE ( 아르티 ) : 2022.12 ~

외래키 제거 고민하기

밍 끄적 2023. 5. 5. 01:15
728x90

아래는 이 게시글에 관련된 우리 프로젝트의 이슈이다.

 

Category 테이블의 User 외래키 제약 제거를 통한 의존성 제거 · Issue #121 · YAPP-Github/21st-ALL-Rounder-Team

https://github.com/YAPP-Github/21st-ALL-Rounder-Team-2-BE/blob/develop/src/main/java/com/yapp/artie/global/deprecated/LoadUserJpaEntityApi.java LoadUserJpaEntity.java는 UserJpaEntity를 로드하는 API를 수행하는...

github.com

현재 우리 프로젝트의 ERD는 아래와 같다.

현재 프로젝트에서 사용하고 있는 JPA 엔티티는 아래와 같은 구조를 가진다.

 

외래키 제약을 고려한 이유

외래키 제약을 제거하는 것을 생각하게된 이유는 아래와 같다.

1. 카테고리 테이블의 유저 FK 제약 제거 : ID로 유저는 찾는 로직이 여기 저기서 사용된다.

 

GitHub - YAPP-Github/21st-ALL-Rounder-Team-2-BE: 🎨 간편한 전시 관람 서비스, 아르티 ARTIE 🎨

🎨 간편한 전시 관람 서비스, 아르티 ARTIE 🎨. Contribute to YAPP-Github/21st-ALL-Rounder-Team-2-BE development by creating an account on GitHub.

github.com

Notice 도메인을 리팩토링하기 전, 우리는 User 도메인에 대한 리팩토링을 거쳤고, 그 과정에서 User에 대한 영속성 모델(UserJpaEntity)과 도메인 모델을 분리했다. 

위의 LoadUserJpaEntity.java는 User에 대한 영속성 모델인 UserJpaEntity를 ID를 이용해 로드하는 메소드를 가지고 있는 인터페이스이다.


외부 클라이언트 코드(서비스, 도메인 레이어 등 ) 에서는 이 메소드를 이용해서, 데이터의 CRUD시 User에 대한 제약이 필요할 때 이 메소드를 통해 유저를 로드하여 사용하거나, 주어진 유저 자신에 대한 리소스에 접근하고 있는지 검증할 때 등등에 사용하고 있다.

기존에는, 이 findById 메소드는 domain/user 패키지의 서비스로서 존재했다.

 

하지만, 외부 클라이언트에서 위와 같은 이유로 UserJpaEntity를 의존하게 되어서, 외부에서 유저에 대한 의존성을 최소한으로 유지하기 위해서 deprecated 패키지로 분류하고, ( java doc에도 deprecated 임을 명시하고 ), 글로벌하게 사용되었으므로 global 패키지로 이동시켰다.

 

최종적으로, 의존성을 제거하기 위해서는 Category.ownerId로 변경이 필요하다. 그러니까 Category의 User의 FK 제약이 논리적으로는 존재하지만, 물리적으로 존재하지 않아야한다.

현재는, Category 엔티티에서 외래키 제약을 검사하기 위해서 UserJpaEntity를 합성 관계로 가지고 있다. Category가 User에 대한 외래키를 가지고 있는 것이다.

이를,

- User 도메인 엔티티(JPA 영속성 모델X)를 가지고 있거나,

- Long 타입의 회원 ID를 가지고 있거나,

- 아예 값 타입으로 MemberId를 가지고 있는

구조로 개선해, 의존성을 제거할 수 있는 방안이 논의되었다.

 

결론적으로 의존성 제거를 위해 FK 제약을 물리적으로 없애는게 바른 것인가에 대해 고민이 필요하다.

2. 대부분의 FK 제약을 제거 : 테스트 시 불편함 개선

현재 테스트를 할 때, 부모 데이터가 꼭 필요하다.

 

아래는 현재 단위 테스트로 개선될 예정인 레거시 통합 테스트 코드로, 특정 작품에 대표 작품(isMain=true)으로 설정하는 setMainArtwork 서비스 메소드가 정상 작동하는지 확인하는 테스트이다.

@Test
  @DisplayName("대표 작품이 아니었던 작품에 대해 대표 작품을 설정하는 요청이 있다면, 기존 대표 작품은 대표 해제 되고, 주어진 작품이 대표 작품으로 설정되어야합니다.")
  public void setMainArtwork_대표_작품이_아니었던_작품을_대표_작품으로_설정() {
    UserJpaEntity user = createUser("user", "tu");
    Category defaultCategory = categoryRepository.findCategoryEntityGraphById(user.getId());
    Exhibit exhibit = exhibitRepository.save(
        Exhibit.create("test", LocalDate.now(), defaultCategory, user, null));
    artworkRepository.save(Artwork.create(exhibit, true, "sample-uri"));
    artworkRepository.save(Artwork.create(exhibit, false, "sample-uri-2"));
    em.flush();
    em.clear();

    artworkService.setMainArtwork(2L, user.getId());
    em.flush();
    em.clear();

    Artwork preMainArtwork = em.find(Artwork.class, 1L);
    Artwork nowMainArtwork = em.find(Artwork.class, 2L);

    assertThat(preMainArtwork.isMain()).isFalse();
    assertThat(nowMainArtwork.isMain()).isTrue();
  }

작품에 테스트를 거쳐야하기 때문에, 부모 데이터가 되는 유저, 카테고리, 전시 테스트 데이터를 다 만들어주어야한다.

하지만, 이 서비스 메소드에서 정말 테스트하고 싶은것은 주어진 작품에 isMain = true가 잘 설정되는지의 여부이다.

 

물론, 위 예제는 단위테스트를 적용하면, 간단한 로직으로 개선될 수는 있다. 하지만, 통합 테스트가 필요하다거나, 최소한의 부모 데이터가 있어야한다. 이는 개발시에 불편함을 만들 수 있다.

 

이 부분에 있어서, 개발 기간 중에는 FK를 Disable 시켰다가, 릴리즈 때는 활성화시키는 방안을 고려해봤는데, 우리 프로젝트 처럼 기능에 대한 수정, 도입이 활발한 경우에는 적합하지 않은 것 같다.

3. 주어진 User의 소유가 맞는지 확인할 때, 불필요한 Select User 쿼리가 발생한다.

이 전시 데이터가 주어진 User의 소유가 맞는지 확인할 때, select user 데이터 from user where user_id = userId; 문을 수행한다.

그런데 사실, userId 일치 여부만 확인하면 되지 않을까? 전시 데이터에 저장될 userId와 주어진 userId가 일치하는지만 비교해도, 소유 여부를 확인할 수 있다.

 

이러한 소유 여부 확인을 위한 User 확인 쿼리는 서버 전반에 존재하고 있다.

따라서, 대부분의 엔티티에서 User에 관한 FK 제약을 제거하고, userId로만 확인해도 좋아보인다.

 

물론, 이 user가 적합한 유저인지 검증하기 위해서, 요청시에 select user 문을 사용한다면 적합한 접근이지만, 이는 비즈니스 로직을 수행하기 전 시큐리티 단계에서 검증함으로써, 불필요한 로직 수행 없이 올바르지 않은 유저의 요청으로 예외 처리하는 것이 좋다고 생각한다.

외래키가 주는 영향

외래키를 쓰면, 설계자의 의도를 알 수 있고, 엄격하게 정책을 수립할 수 있다.

 

하지만, 아래와 같은 불편함을 초래할 수 있다.

  • row를 삭제할 때, 제약사항이 생겨 불편하다.
  • Insert / Update 시 정합성을 체크하는 것이 오버헤드이다.
    • DB가 아닌 application 단계에서 검증하는 것이 예외처리 측면에서도 유리하다.
    • ex ) 자식 테이블에 데이터를 추가할 때, 부모 테이블에 PK가 존재하는지 확인하는 검사 과정이 선행된다.
  • 부모 테이블 구조를 변경해야하는 경우, 자식 테이블에 영향이 없을지 추가적인 확인이 필요하다.
    • 프로그램 로직이 바뀌거나, 서비스 확장시, 자식 테이블에 데이터 생성시, 부모 row가 미리 생성되어야 한다던지 등의 기존 제약이 문제를 일으킬 수 있어, 개발이 번거로워질 수 있다.
  • 외래키가 체크하는 무결성 검사는 아래와 같다.
    • 자식 테이블 INSERT시, 이 FK에 해당하는 부모 데이터가 부모 테이블에 존재하는가
      자식 테이블에서 FK는 UPDATE할 때, 변경 후 값이 부모 테이블에 있는가
      부모 테이블에서 DELETE 할 때, 자식 테이블에서 참조하고 있는가 
    • FK가 없어도 위에 대한 무결성 검사는 진행하는 것이 올바를텐데, 그렇다면 검사를 위해 필요한 데이터를 사전에 select해야하는 문제가 있다.
  • FK가 있다면, DML 작업의 순서가 필요하다.
    • INSERT시, 부모 데이터가 생성이 먼저 되어야, 자식 데이터가 생성될 수 있다.
      UPDATE시, 자식 데이터가 먼저 삭제 되어야, 부모 데이터가 삭제될 수 있다.
    • 따라서, FK로 인한 일종의 제약이 발생한다. ( 물론, 논리적으로 위 순서를 지켜주는 것이 맞다. )
      이를 위해, 개발 과정에서 순서의 제약에 따른 번거로움을 없애주기 위해 FK를 없앨 수 있다.
    • 혹은, FK 생성시 deferred 옵션으로 설정할 수 있다. deferred 옵션으로 FK를 생성하면, Validation을 트랜잭션 단위로 수행한다. 즉, Commit 시에 Validate를 하게되고, 쿼리간 순서 제약이 해소된다.
      기본 설정인 Not Deferred 옵션은 각 쿼리 문장 단위로 검증하기 때문에, 각 INSERT, UPDATE, DELETE 문 마다 검증한다.
      따라서, 개발 과정에서는 FK를 deferred 옵션으로 설정하고, 개발 후에 Not Deferred 옵션으로 변경하면 된다.

위와 같은 영향을 고려해서, 우리 서비스의 전반적인 외래키 제약을 없애는 것을 생각해보았다. 

 

DDD 책에서 위와 관련된 내용을 담고 있다고 조언받아서, 관련된 내용을 찾아서 추가적인 글을 작성하려 한다.

또한, 우리 프로젝트에서 어떻게 적용하였는지도 회고하면서 추가적인 글을 작성할 예정이다.

728x90