바버샵 프로젝트의 핵심은 뭘까? 뭐긴 뭐야 당연히 바버샵이지.
바버샵 테이블을 설계하면서 가장 고민한 부분이 '근무 중인 바버에 대한 정보는 어떻게 담을까'였다. 원장에 대한 정보는 바버샵 -> 사용자의 일대일 연관관계를 맺어주면 간단하게 해결할 수 있다. 문제는 근무 중인 바버에 대한 정보다. 개념적으로는 바버샵 -> 사용자의 일대다 연관관계가 된다. 반대로 보면 사용자 -> 바버샵의 다대일 관계로 풀어낼 수도 있다.
최근 프로젝트를 진행하고 JPA와 관련한 공부를 하면서 일대다 단방향 관계를 만드는 것은 썩 바람직하지 못하다고 생각하게 되었다. 데이터베이스 관점에서는 키 관리의 어려움이 있으며, 코드 레벨에서는 더 중요한 도메인이 덜 중요한 도메인에 의존하게 된다. 예를 들어, 게시글과 댓글이라는 예시를 들어보자.
게시글과 댓글은 일대다 단방향 관계로 설정할 수 있겠다. 데이터베이스에서는 다에 해당하는 테이블에 일에 해당하는 테이블의 외래 키가 칼럼으로 들어가는데, 일대다 단방향 관계에서는 List로 관계를 표현하다보니 외래 키가 존재하는 테이블에는 외래 키가 없는 문제가 발생한다. 일대다 연관을 맺었기 때문에 N+1문제도 당연히 발생하게 된다. 특히 게시글의 경우 게시판이라는 형태로 여러 엔티티를 페이징 조회하는 경우가 많기 때문에 N+1문제를 반드시 해결해야한다. 이를 도메인 레벨로 가져오면, 게시글이라는 개념적으로 앞선 도메인에 댓글이라는 더 낮은 수준의 도메인이 List의 형태로 필드에 남게된다. 그러나 게시글은 댓글이 없다고 존재할 수 없는 도메인은 아니다. 댓글이 있던 없건 게시글은 게시글대로 존재한다.
따라서 게시글과 댓글은 다대일 단방향 관계로 설정하는 것이 낫다. 댓글에 게시글의 id값을 넣는 것이다. 코드 레벨에서는 댓글 클래스의 필드로 게시글 객체가 있어야 할 것이다. 그럼 게시글을 읽을 때마다 댓글 테이블을 조회해서 읽어와야하는 문제가 남지만, 엘라스틱 서치 같은 역인덱싱 기능을 적절히 활용하면 충분히 해결가능하지 않을까 싶다.
이런 단순한 경우만 생각하면 일에 해당하는 테이블이 더 중요하고 다에 해당하는 테이블이 덜 중요한 경우가 종종 있다. 그러나 이번 문제는 조금 달랐다. 다에 해당하는 회원 테이블이 비즈니스와 코드 수준에서 보았을 때 훨씬 더 중요한 도메인이다. 물론 서비스 전체의 핵심은 바버샵일 수 있으나, 회원이라는 도메인은 바버샵이고 뭐고 모든 서비스의 중심이 되는 도메인이다. 여기서 문제가 발생했다.
다른 도메인들의 문제는 일대다 단방향 또는 양방향 대신 다대일 단방향 연관관계를 설정함으로써 문제없이 해결할 수 있었다. 동시에 데이터베이스 영역의 코드를 작성할 때도 굳이 엔티티 간 연관을 맺어주는 대신 Long을 이용해 id값을 칼럼에 추가시켜 불필요한 n+1이슈를 최소화했다. 그러나 이 문제의 경우 회원에 바버샵 id를 넣어주는 방식으로는 해결할 수 없다. 회원이라는 코어 도메인에 다른 도메인이 스며들기 때문이다. 동시에, 바버가 아닌 회원의 경우에는 회원 클래스에 바버샵이 들어갈 자리 자체가 없다. 애초에 다른 도메인이 침범하는 것도 썩 바람직하지 않은데, 그 도메인이 회원이라는 도메인과 전혀 관계가 없는 것이다.
문제를 해결하기 위한 방안이 필요하다. 크게 두 가지 방법을 고안해봤다.
- 회원 도메인 자체를 여러 개의 세부 도메인으로 나눈다. 고객, 바버, 관리자의 형태다. 크게는 Member라는 도메인으로 표현하지만, 내부적으로는 Customer, Barber, Admin의 세 도메인이 존재한다. 회원 테이블도 세 개로 나눈다. 고객과 바버, 관리자의 세 테이블이 회원이라는 테이블을 상속하는 형태로 사용한다.
- 조인 테이블을 하나 둔다. 해당 테이블에는 바버샵의 id와 회원의 id가 담겨있다. 바버가 근무 중인 샵을 등록하거나, 바버샵을 조회할 때 해당 테이블을 활용한다.
두 방법 중 어느 방법을 선택해야할지 명확하게 해결책이 나오지 않는다. 문제가 발생할 법한 상황들을 좀 더 살펴보면서 두 방법 중 해결책을 선택해보자.
- 바버 회원이 가입하는 경우
- 근무 중인 바버샵을 입력한다. 해당 샵의 존재 여부는 샵을 입력할 때 검증한다.
- 이 때 해당 회원 엔티티와 바버샵 엔티티의 연관관계도 설정한다.
- 특정 바버샵을 검색하는 경우
- 근무 중인 바버 회원들의 정보를 출력한다.
- 바버 회원을 검색하는 경우
- 근무 중인 바버샵의 정보를 출력한다.
- 바버 회원이 근무 중인 샵을 수정하는 경우
- 기존의 연관 관계를 끊는다.
- 새로운 연관 관계를 수립한다.
- 바버샵에 달린 리뷰에 답글을 작성하는 경우
- 해당 바버샵에 근무 중인 바버인지 확인한다.
이렇게 나누고 나니, 일단 회원 도메인은 여러 세부 도메인으로 나뉘는게 맞는 것 같다. 기본적으로 바버 회원과 고객 회원의 비즈니스 로직 처리가 다르게 되어야 하기 때문이다. 회원 엔티티를 등록, 수정, 조회할 때마다 isBarber()등의 함수로 조건 분기를 처리하는 것보다. 추상화된 회원이라는 개념을 다형적으로 사용해서 여러 도메인에서 처리하는 것이 더 낫다고 생각된다.
특히 조회의 경우에는 클라이언트에 넘겨줘야 하는 정보가 달라진다. 이는 API계층에서 사용자가 넘겨준 정보에 따라 비즈니스 로직 처리를 해야하는 결과를 야기한다. 뭐 물론 넘겨주는 Member Reponse DTO를 추상화해서 Barber Response와 Customer Response를 사용할 수도 있겠지만, 이 방법은 도메인 분리를 했을 때도 동일하게 적용될 수 있다.
까지 생각했는데, '클라이언트 입장에서 특정 회원이 바버인지 고객인지 알 수가 있나?' 하는 생각이 문득 들었다. 또한 게시글 검색에 작성자 검색 같은 기능을 사용하면, 해당 사용자는 어떻게 처리해야할까. 결국 바버인지 고객인지를 파악할 수 있는 건 서버가 아닐까? 일단은 회원 도메인을 분리한다는 큰 관점에서 문제 해결을 시도해보자. 전체적인 로직은 내부에서 조건 분기 처리를 하되 도메인 엔티티만 나누는 등의 해결 방안이 있을 수도 있다. 안되면 일단 뭉쳐서 진행해야하고. 어쨋든 회원이라는 도메인에 많은 개념이 섞인 형태인 것은 맞으니까 말이다.
그럼 이제 데이터베이스 관점으로 돌아와서, 회원 테이블 상속을 사용할 것인가, 조인 테이블을 사용할 것인가의 문제가 남았다. 어느 해결책을 선택해야 하나를 고민해봤는데 상속 테이블을 사용하는 편이 더 낫다고 생각된다.
- 연관관계를 수정, 삭제하는 부분에서 조인 테이블을 사용하면 불필요한 작업이 추가된다.
- 예를 들어 바버샵 id가 1, 회원 아이디가 4인 경우, 조인 테이블에서 해당 칼럼을 찾기 위해 검색하는 로직이 추가된다.
- 반면 테이블 상속을 사용하면 해당 경우를 한 번에 처리할 수 있다. 그냥 바버 엔티티와 연관된 샵의 id값만 수정하면 된다.
- 바버샵을 조회하는 경우, 상속 테이블이나 조인 테이블이나 별 차이가 없다. 오히려 조회를 위한 연관관계 수정, 삭제에서 조인 테이블이 더 불리하다.
조회 성능에는 별 차이가 없는데, 바버가 N명, 바버샵이 M개 있다고 해보자. 최악의 검색 시간을 생각하더라도 결국은 두 방법 모두 O(N)만큼 나올 수밖에 없다. 바버샵과 회원이 일대다 매핑이기 때문이다. 다대다면 조인 테이블을 사용할 시 O(N * M)까지 조회 시간이 발생할 수 있지만, 결국 바버 한 명은 하나의 바버샵에만 종속되기 때문에 조인 테이블의 칼럼 수도 N개로 제한된다. 그러면 결국 둘의 성능에 그렇게 큰 차이가 있나 싶다.
반면 연관관계를 수정, 삭제하는 경우에는 큰 차이가 있다. 조인 테이블도 함께 수정해야하기 때문이다. 최악의 경우에는 O(N) 이상만큼의 시간이 더 발생한다. 불필요한 조회, 수정 || 삭제가 추가되기 때문이다. 반면 상속 테이블을 사용하면, 그냥 바버와 바버샵의 연관관계만 끊으면 된다. 아니면 수정하던지. 끝이다.
나름대로 꽤 다방면에서 고민해봤는데, 이게 옳은 선택일지는 명확하지 않다. 내가 숙고해서 내린 결정이니 내가 아니면 누가 믿어주겠냐만은, 다른 이들과 이것과 관련한 얘기를 더 나눠봐야겠다.
'Programming > Study' 카테고리의 다른 글
롤백 - 회원을 왜 나눠 (0) | 2024.02.28 |
---|---|
구체 클래스 vs 추상 클래스 vs 인터페이스 - 고객 / 바버 추상화하기 (0) | 2024.02.27 |
DB Entity -> Domain Entity로의 전환은 어느 계층에서 담당하는 것이 옳을까 (2) | 2024.02.14 |
웹소켓은 왜 쓸까? (0) | 2024.02.07 |
JWT 이대로 괜찮은가? (0) | 2024.01.02 |
댓글