마이페이지 구조 개선
해당 task가 리팩토링을 진행하면서 마주한 가장 큰 task 였던것 같다. DDD관점을 고민하며 리팩토링을 하다 보니 기존의 경계들을 어떻게 구분지어줄지에 대해서도 큰 고민을 하게 됬었고, 무엇보다 DB Entity레벨에서 너무많은 서로다른 엔티티들이 무분별하게 양방향으로 연관관계 매핑을 한상태로 있다 보니 그 관계들을 어느정도 합리적으로 끊어내는데 있어서 상당히 힘든 경험을 하게 된 것 같다.
글을 시작하기에 앞서, Entity의 매핑전략으로써 어떤 전략을 채택할지를 먼저 정하고 가면 좋을것 같다. 우선 쿠스토랑은 당연히 모놀리식 아키텍처이고 그렇다할 큰 트래픽이나 대용량 데이터는 당연히 더더욱 없다. 그에 따라서 entity간 양방향 매핑에서 오는 이점은 사실 무시하기엔 너무 큰게 사실이였다. 오히려 일부 매핑 전략을 유지하는것이 합리적인 선택일 수 있음을 전혀 부정하지 않는다. 하지만 관점이다. 예상치 못한 파급범위나 확장가능성의 관점에서 봤을때는 전혀 좋지 못한 선택임일 수도 있겠다는 생각은 이번에 뼈저리게 경험했기에 공감이 어느정도 되기 시작한것 같다.
매핑관계가 필요하다면 가능한 단방향 매핑을 하도록 하였고, entity자체를 참조하기보단 필요한 엔티티의 id를 fk로써 가지며 참조하는 방향으로 가져가기로 하였다.
(확장 가능성 관점에서는, 서로 다른 도메인간의 관계매핑은 절대 하지 않는것이 유효하다는것이 현재 스스로 내린 결론이다.)
우선, 마이페이지 조회에는 다양한 정보들이 나타나게 된다.

웹에서만 보더라도 개인 정보들 부터 시작해서 하단의 특정 정보들에 대한 수치,
각 개인이 저장한 맛집들, 평가한 맛집들 부터 해서
만약 평가한 맛집들이라면 점수나 코멘트 등과 같은 평가 포함 요소들,
커뮤니티 관련 정보들이라면 제목이나 받은 좋아요 수 등등과 같은 요소들이 드러나게 된다.
즉, 다양한 도메인에 대한 정보들을 가져와야 할 필요가 있는 서비스의 성격을 띄고 있다.
물론 몇몇 정책이나 요구사항들을 수정하는 방법또한 있지만, 맛집 관련된 서비스의 성격이라는 점에서 어느정도 보편타당하게 마이페이지 내에서 조회가능한 정보들이라 판단하였다.
하지만 이제는 DDD 관점에 따라 진행하면서 도메인별로 분리가 진행되었고, 그렇게 다음 질문에 맞닥뜨리게 되었다.
서로 다른 도메인간의 DB 조회를 해도되나 ..?
앞서 언급했듯 우리는 여러가지 정보가 DB엔티티레벨에서부터 @manyToMany, @manyToOne 등 여러 관계로 마구 양방향 매핑 되있었다. DDD관점에서는 어느정도 경계가 중요하다 볼 수 있는데 복잡하게 연관관계를 맺고 있으면 서로다른 관심사로 묶인 집합(애그리거트 라고도 부른다)간의 결합도가 복잡하게 얽혀버리게 된다. 굳이 DDD의 관점이 아니더라도 JPA의 성능관점에서 또한 좋지 못할수밖에 없다. 무분별하게 연관된 테이블들끼리는 쿼리 발생시 그 파급효과가 어떻게 미칠지 예측이 불가하며 전형적인 N+1 문제에 직관할수밖에 없다.
따라서 최적화를 하는데 있어서 도메인 중심의 개발 관점에서 시작해서 기존의 몇 도메인들에서 사용중이였던 테이블 구조에 대한 문제를 포함하여 여러 문제 및 방법들, N+1 문제와 이를 해결하기 위해 query dsl을 사용할지, fetch join을 사용할지 부터 시작해서 jpql을 사용해야 할지 native query를 사용해야 할지 여러 잡다하게 흩어진 정보들로 부터 어떤 패턴이, 어떤 방식이 가장 합리적인 방식일지 스스로 판단이 머리속에 어질러이 내팽겨쳐져 스스로 정리되지 않는 지경에 이르렀다.
그때서야 깨달았다...
아 이거 크게 한번 제대로 정리 다시 하고 가야겠다.
그리고 스스로 판단하고 결정할 수 있어야겠다.
그래서 학습을 하고 돌아왔다. 우선 다양한 문제점들을 살펴보자.
도움을 준 책도 하나 소개한다 -> "도메인 주도 개발 시작하기" https://wcwdfu.tistory.com/35
예시로 아래는 우리의 포스트 테이블 중 일부이다.
@Table(name="posts_tbl")
public class PostEntity {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Integer postId;
private String postTitle;
...생략
@ManyToOne
@JoinColumn(name = "user_id")
private UserEntity user;
@OneToMany(mappedBy = "post")
private List<PostCommentEntity> postCommentList = new ArrayList<>();
@OneToMany(mappedBy = "post")
private List<PostScrapEntity> postScrapList = new ArrayList<>();
@OneToMany(mappedBy = "post")
private List<PostLikeEntity> postLikesList = new ArrayList<>();
@OneToMany(mappedBy = "post")
private List<PostDislikeEntity> postDislikesList = new ArrayList<>();
...생략
}
Post Entity는 이미 댓글 엔티티와 유저 엔티티등 여러가지 엔티티들 그대로 가져와서 여러 양방향 매핑 관계를 가지고 있다.
타 도메인들의 db Entity또한 비슷하게 여러가지 엔티티들과 양방향 매핑을 가지고 있다.
따라서 마이페이지의 조회로직을 구현하기 전에 이런 Post 같이 타 도메인에서 조회에 필요한 DB엔티티들의 불필요한 연관관계를 끊는 개선이 먼저 필요했다. 타 도메인들의 엔티티간 관계에 따라 마이페이지의 조회 로직이 달라지기 때문이다.
개선 방향으로는 다음과 같다.
- 개별 db entity에서 과하거나 불필요한 참조 관계는 없애버려야 한다. 우리는 확장가능성 까지 염두에 두었기에 같은 도메인 내에서 필요하다면 단방향 매핑이라던가 fk키를 참조한 뒤, 후에 join형태로 데이터를 결합하는 형식으로 가기로 하였다.
(타 글에서도 그렇고, msa구조에서도 그렇고 fk또한 잘 두지 않는다는 글을 본적이 있다. 그냥 주어진 상황에 가장 합리적인 의사결정을 하는것이 올바른것 같다.)
- 현재의 like,dislike개별 테이블 전략 또한 합리적으로 보이진 못한다.(여기선 더깊게 짚지 않고 넘어간다)
- 또한 조회를 위한 목적으로 좋아요 집계 값을 누적해서 저장하는 필드에 대한 필요성을 생각해볼 수 있을것 같다.
(물론 유의미한 차이가 발생하기엔 너무 미미한 트래픽이라 아무렇게 해도 큰 차이가 없지만, 여기서 의사결정은 "확장가능성"에 주안을 두었다.)
좋아요 집계 값을 기존 entity에서 단순히 필드값으로 가져간다고 해도, 락경합 확률또한 희박하지만 동시성 문제를 고려하여 락을 적용할경우, 낙관락이나 비관락을 걸었을때 조회에 대한 접근에 대해서 차단이나 병목이 생기지 않기 때문에 해당 락 전략을 적용시켜도 문제없을 것이라 판단된다. 하지만 비관락을 걸 경우, 누군가 좋아요를 하는 동시에 해당 글 작성자가 게시글을 수정하려고 하는 경우에는 예외가 발생할것이다. 별도의 좋아요 집계테이블을 생성하여 그쪽에서 관리하는것이 훨씬 깔끔해 보인다.
이제 그러면 양방향 매핑이 끊어진 현 상황에서 데이터들의 조회는 어떤식으로 해볼 수 있을까?
DTO프로젝션 방법을 사용해볼 수 있을것 같다. 데이터 조회를 위한 DAO를 만들어서 jpql을 사용해 join을 이용해서 id참조방식을 이용해서 필요한걸 한번에 가져오는 방법이다. 그러면 fetchType과 무관하게 데이터를 로딩해올 수 있게 된다. 여기서 queryDsl방식을 사용하면 컴파일 타임에 에러를 잡을 수도 있고 동적 쿼리를 작성하는데도 편리한 이점이 있어 우리는 queryDsl을 이용하기로 하였다.
패키지 구조는 어떻게 됬을까?

아래와 같다. 커다란 하나의 user패키지 내에 user, mypage, rank가 자리한다.
mypage 패키지 에서는 읽기 전용 레포지토리(이하 쿼리 레포)를 두었고, 해당 레포지토리에는 dip를 적용하지 않았다.
패키지 구조 안건들 중에서는 쿼리레포에 dip를 적용시킨후 인터페이스는 mypage, 구현체는 개별 도메인으로 두자는 제안도 있었지만 그렇게 할 경우 db에서 쿼리한 후 결과로 가져올 dto객체에 대한 의존성 문제가 생겼다. 이를 경계분리라는 목적성을 갖고 개별 응답 dto를 다시 개별 도메인에 둘 수 도 있겠지만 이렇게 되면 mypage의 queryService에서 다시 response 객체로 재매핑 해주는 비효율적인 과정이 발생하고, 테스트 코드 작성 까지의 관점에서 확장해 봤을때 애초에 쿼리레포를 fake나 목을 두어 테스트하는건 의미가 없다고 생각하였고, mypage는 여러 도메인에서 정보를 취합하는 서비스의 성격을 띈다는 판단하에 통합테스트로써 실제 SpringBootTest 애노테이션을 활용하여 실제 db와 통신하는 테스트 작성 대상에 포함, 작성 하기로 하였다.
또한 queryRepo를 개별로 mypage에 두지 말고 개별 도메인들이 가져가야 하지 않느냐는 의견도 주었지만 이는 응집성 측면에서 상당히 좋지않다고 생각하였다. 마이페이지의 ui랜더링을 위한 조회로직을 개별 도메인들이 갖는것 자체가 srp위반이란 생각이 들었다.
-- 작성 진행 예정 --
1. 일단 동작되는데 주안점을 두어 만들어논 queryDSL 문을 분석하고 성능 친화적으로 계산되었는지 분석한후 개선한다.
2. 성능 테스트를 통해 수치적으로 얼만큼 개선이 되었는지 확인한다.
'개발[프로젝트] > 쿠스토랑' 카테고리의 다른 글
| 웹 세션/앱 jwt - Naver/Apple 로그인 통합 리팩토링 (로그인2탄) (0) | 2025.06.13 |
|---|---|
| 전역 에러 핸들링 구조 개선하기 (0) | 2025.06.10 |
| 스키마 관리 자동화를 위한 Flyway 도입기 (0) | 2025.06.02 |
| [쿠스토랑] 리팩토링(1) - 테스트코드 작성과 리팩토링의 시작 (0) | 2025.03.11 |
| 쿠스토랑 이란? (0) | 2025.01.09 |