본문 바로가기
Programming/Study

바버 - 바버샵 최종장 : 중간 테이블... 써야겠지?

by JKROH 2024. 3. 3.
반응형

 회원과 바버샵 간의 연관관계를 다루는 (아마) 최종 게시글이다. 결국 중간에 테이블을 하나 사용하기로 했다.

 

 그렇다고 조인해서 정보를 사용하는 경우는 아마 없을 것 같긴하다. 일대다의 방향성을 지닌 연관관계를 사용하지 않고자 최대한 노력하고있고, 그럼 굳이 테이블을 조인해서 어떤 정보를 찾아올 이유도 없다. 게시글을 조회하면 댓글 리스트에 대한 조회 요청이 같이 오는 것과 마찬가지로, 바버샵을 조회하면 해당 조인 테이블에 대한 조회 요청도 같이 보낸다. 이렇게 엔티티 간 연관을 끊고 id에 의한 약한 연관관계를 유지하면서, 좀 더 상위 도메인을 방어적으로 다룰 수 있다.

 

 아무튼 처음의 생각으로 돌아가보자. 처음 생각했던 테이블 설계는 아래 그림과 같았다.

 Member도메인 자체를 두 개의 도메인으로 분리하고, 분리된 Barber도메인은 BarberShop과 연관시킨다. 이렇게 기조를 잡고 Member의 공통 기능에 대한 추상화도 해보고, 반환되어야 하는 정보를 담는 DTO에 대해서도 추상화를 진행해보고자 했다. 결과는 대실패였다. 이유는 하나였다. 회원과 바버샵을 연관짓는 바버샵 정보는 도메인 구분의 핵심이기 때문에, 이 정보를 빼고 진행하는 추상화는 공통 기능에 대한 추상화가 아니라, 분리된 Customer영역의 기능에 제한된다. 공통 기능으로 추상화가 불가능하니, 결국 두 도메인을 사용하기 위한 인스턴스를 추상화된 타입으로 만들 수가 없었다.

 

 추상화가 막히고 하루 종일 고민하다가 이전 글과 같은 결론에 도달했다. 회원이라는 코어를 지키고 싶은 마음과 코어에 다른 도메인을 스며들게 만드려는 행동이 충돌해 이러지도 저러지도 못하는 결과만 낳은 것이다.  결과적으로 두 테이블의 연관관계를 표현하기 위해서는 중간에 테이블이 하나 필요하다는 결론을 내렸다.

 중간에 테이블을 두게 된다면 바버샵 -> 회원의 일대다 단방향 연관관계를 테이블 -> 회원의 일대일 단방향 연관관계와 테이블 -> 바버샵의 다대일 단방향 연관관계로 나타낼 수 있다. 데이터베이스 조회 관점과, 내부 로직 처리에서 불리한 부분이 있을 수 있지만 핵심 도메인을 지킨다는 관점에서 이 방법이 더 낫다고 판단했다.

 

 문제는 이 테이블을 어떻게 다룰 것인가인데, 이 부분에 대해서는 고민이 있었다. 두 가지 방법이 있었다. 하나는 @ElementalCollection을 이용해 List<Long>으로 나타나는 값 컬렉션을 바버샵 테이블에 넣는 것이다. 코드로 표현하면 아래처럼 사용할 수 있겠다.

public class BarberShopEntity {

    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    @Column(name = "BARBER_SHOP_ID")
    private Long barberShopId;

    private String name;

    private Long ownerId;

    @ElementCollection
    @CollectionTable(
            name = "EMPLOYMENT",
            joinColumns = @JoinColumn(name = "WORK_PLACE_ID")
    )
    @Column(name = "WORKER_ID")
    private List<Long> workerIdList = new ArrayList<>();
}

 

 다른 방법은 아예 하나의 도메인으로 해당 영역을 빼고, 테이블도 하나 만드는 것이다. 

public class EmploymentEntity {

    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long employmentId;

    @Column(name = "WORKER_ID")
    @NotNull
    private Long memberId;

    @Column(name = "WORK_PLACE_ID")
    @NotNull
    private Long barberShopId;

    @Builder
    private EmploymentEntity(Long employmentId, Long memberId, Long barberShopId){
        this.employmentId = employmentId;
        this.memberId = memberId;
        this.barberShopId = barberShopId;
    }
}

 

 두 방법의 차이는 사실 크게 없다. 두 방법 모두 테이블을 생성한다. 그럼 둘 중 어떤 방법을 쓰는게 나을까. 두 가지 관점에서 생각해봐야했다.

 

 먼저, 당연하지만 비즈니스 관점에서 살펴봐야한다. 바버샵이라는 도메인과 고용이라는 도메인을 하나의 영역으로 다뤄도 되는가? 사례를 들어 살펴보자.

  • 바버를 바버샵에 등록하는 경우
  • 바버가 근무 중인 바버샵 정보를 삭제하는 경우
    • 바버가 근무 중인 바버샵 정보를 수정하는 경우 : 기존 바버샵에서 정보를 삭제하고 새로운 바버샵에 정보를 추가한다.
  • 원장이 바버샵에 새로운 직원을 추가하는 경우
  • 원장이 바버샵에 근무 중인 직원을 제거하는 경우

 이렇게 다섯 가지가 생각난다. 다른 네 경우는 하나의 바버샵에만 직접적으로 영향을 주는 유즈케이스로 볼 수 있다. 즉, 바버샵이라는 도메인 내에서 처리하는 것에 충분한 근거가 있다. 원장이 해당 정보를 건드리는 경우는 다른 영역에 이벤트 처리를 맡기면 그만이다. 그러나 바버가 근무 중인 바버샵 정보를 수정하는 경우에는 '고용 정보'만 바뀐다. 물론 고용 정보가 바버샵의 하위 개념에 속하기 때문에, 바버샵이라는 도메인의 하위 도메인으로 존재하지만 동시에 여러 바버샵을 건드리는 유일한 유즈케이스라는 점에서 독특하다.

 

 그러면 저 케이스를 어떻게 처리해야할까에 대한 고민이 이어졌다. 바버샵이라는 도메인 내부에서 처리할까? 아니면 고용이라는 도메인을 새롭게 만들어야할까? 일단은 비즈니스적인 관점에서 둘을 분리하는게 좀 더 낫다고 생각된다. 바버샵과 고용 정보는 포함되어있는 관계지만 엄연히 분리되어야 할 영역이다. 하지만 이 점은 여전히 헷갈리는 부분이 있다.

 

 결국 데이터베이스 관점에서 해당 문제를 살펴봐야했다. 데이터베이스 관점에서 생각하면 거의 어지간해선 도메인을 분리하고 테이블을 만들어놓는 것이 낫다. 이유는 값 컬렉션을 사용한 삭제, 추가 로직이 지닌 불리함에 있다.

 

 값 컬렉션은 기본 식별자가 없다. 대신 연관된 테이블의 식별자를 외래 키이자 식별자로 사용한다. 즉, 하나의 특정한 애트리뷰트를 찾기가 어렵다는 것이다. 그런데, 여러 애트리뷰트 중에 특정 애트리뷰트만 찾아 사용하는 것이 가능할까? 당연히 어렵다. 따라서 JPA에서는 값 컬렉션에 변화가 생기면 냅다 전부 지우고 새롭게 다시 전부 추가한다. 예를 들어, id가 1인 바버샵의 값 컬렉션에 3개의 Long값이 저장되어있었는데, 해당 컬렉션에 하나의 값이 추가되면 테이블을 돌며 b.id = 1 인 애트리뷰트를 전부 지우고, 다시 4번의 insert문이 수행된다는 것이다. 당연히 비효율적이다.

 

 이런 고로, 중간에 Employment라는 테이블을 하나 만들어 사용하는 형태로 프로젝트를 진행하기로 했다. 이런저런 고민이 계속 이어지다 보니 막상 구현은 차일피일 미뤄진다, 심지어 다음 주에는 면접 일정도 잡혀있어 당분간은 면접 대비도 함께 진행해야한다. 그럼에도 불구하고 애정이 담긴 프로젝트이니만큼 우선순위를 많이 미루고싶지는 않다. 하루빨리 구현에 착수해야겠다.

반응형

댓글