테스트 관련 강의를 들으면서 지금까지 테스트에 대해 너무 소홀히 했던 것이 아닌가 돌아보게 되었다. 그동안은 단순히 기능 개발에만 몰두해서 테스트는 나중에 직접 실행해보는 식으로 했었는데 언젠가 고쳐야지 생각만 하다가 이제서야 신경쓰게 되었다. 그런데 강의를 듣다보니까 좋은 테스트를 위한 코드가 좋은 설계를 위한 코드와 일맥상통한다는 점이 신기했다. 그러니까 테스트를 위한 코드를 작성하면 좋은 구조로 설계할 확률이 올라간다는 것이다. 그동안 공부했던 DDD와 추가로 알게된 헥사고날 아키텍처도 다른게 아니라 서로 연관이 있는 부분이 컸다.
도메인 위주로 리팩토링하기

위의 구조가 현재 프로젝트 구조이다.
어느 정도 계층을 나눠서 구현하려 했지만 도메인별로 파악하기가 쉽지 않고, 실제로 코드로 들어가보면 도메인 모델과 엔티티를 하나로 두고 사용하거나(JPA에 강결합), 추상화 수준이 부족하여 테스트 하기 어렵거나 하는 문제가 발생하고 있다.
그리고 기존의 controller, service, repository와 같이 구성하여 사용하는 레이어드 구조의 경우는 단순히 주어진 순서대로 실행하는 절차지향적인 성격을 가지고 있기 때문에 도메인 간의 객체지향적인 상호작용에는 뭔가 아쉬움이 있다. 그렇기 때문에 일단 패키지 구조를 도메인 위주로 변경하는 것부터 시작하기로 했다.

이렇게 도메인이 상단에 위치하도록 변경하여 도메인간 경계를 명확하게 하면서 개발할 수 있도록 했다.
하지만 패키지만 변경했다고 테스트하기 쉬워지는 것도 아니고, 좋은 설계를 한 것이라고 보기도 어렵다.
일단 도메인 구조를 살펴보자.

설계한 UML인데 애그리거트는 User, Badge, problem으로 구성했다. 내부에서도 따로 분리할 필요가 있는 부분은 VO로 빼놓았다.

위 엔티티는 User 엔티티인데 도메인 모델겸해서 같이 사용하고 있다. 그런데 여기서 문제가 발생한다. 응용 계층에서 구체적인 인프라 계층의 참조가 이루어진다는 것이다.

이런 식으로 User 엔티티가 응용 계층에 영향을 주게 된다. 물론 User를 엔티티겸 도메인 모델로 사용하고 있어서 domain 패키지에 위치시키긴 했지만 도메인과 JPA가 강결합되어 있어서 나중에 다른 기술로 바꾸게 된다면 수정해야할 부분이 많을 것이다.
그런데 이 부분도 잘 생각해봐야 한다. 서비스를 언제까지 운영할 것인지, 나중에 다른 기술로 바꿀 여지가 있는지를 잘 생각해보고 결정해야 한다.
나는 지금 상태로도 잘 동작이 되지만 나중에 다른 인프라 기술이나 프레임워크를 사용한다고 가정하고 도메인과 응용 계층을 특정 기술로부터 분리하는 경험을 하고 싶어서 엔티티 객체에서 도메인 객체를 분리할 것이다.
그런데 여기서 발생하는 문제가 JPA의 유용한 기능을 어디까지 사용할지를 결정해야 한다는 점이다. 도메인 객체는 그냥 자바 객체이기 때문에 JPA의 지연로딩 전략을 그대로 사용할 수 없다.
그리고 엔티티 객체의 M:N 관계를 @OneToMany와 @ManyToOne을 이용하여 매핑해주는데 이럴 경우 도메인 객체와 엔티티 객체간 변환 과정에서 순환 참조가 이루어져 문제가 발생한다.
순환 참조 문제 해결하기


예를 들어 위와 같이 되어 있을 때, UserBadge를 UserBadgeEntity로 변환한다고 생각해보자.
UserBadge에는 Badge가 포함되어 있기 때문에 Badge도 BadgeEntity로 변환해주어야 한다.
그런데 BadgeEntity로 변환을 위해 BadgeEntity의 from()을 호출하는데 Badge도 UserBadge를 포함하고 있기 때문에 다시 UserBadgeEntity의 from()을 호출하게 되고, 이 과정에 반복되어 결국 제대로된 변환을 하지 못한다.
그래서 결국 한쪽의 참조를 끊어주어야 한다.
지금 원하는 기능은 회원의 뱃지를 조회하는 기능밖에 없다. 그렇기 때문에 BadgeEntity 쪽에서 UserBadgeEntity를 참조할 일이 없다.
그래서 나는 BadgeEntity의 참조를 없애버렸다.
이렇게 해도 연관 관계의 주인은 일대다 관계에서 다가 가지고 있기 때문에 UserBadgeEntity 쪽에서 BadgeEntity를 조회할 수 있다.


이렇게 해서 순환 참조의 고리를 끊어버렸다.
현실적인 문제 맞닥뜨리기
그런데 리팩토링하던 도중...
이상적인 것만으로는 현실적인 문제를 해결하지 어렵다는 것을 깨달았다.
도메인 위주로 설계하다보면 성능적인 부분을 포기해야 한다.
내가 맞닥뜨린 문제는 JPA와 관련된 부분이었다.
도메인을 서로 참조하지 않도록 분리하면서 Fetch Join을 하지 못한다거나 VO를 외부 테이블로 저장하다보니 이를 불러올 때 생기는 N + 1 문제 등 성능적으로 포기해야 할 부분이 너무 많았다.
그래서 어떻게 할지 며칠간 고민을 했다...
DDD관련 읽고 있는 도서에는 엔티티 객체를 그대로 사용해서 내 경우와는 맞지 않았고, 인터넷을 검색해봐도 모두 같은 책을 읽고 내용을 그대로 가져왔는지 도움이 되는 내용이 없었다.
그러던 중, 인프런의 김영한님의 답변이 내 생각을 바꾸게 해주었다.
DDD 적용시 JPA 설계에 대해 궁금한 사항 - 인프런 | 커뮤니티 질문&답변
누구나 함께하는 인프런 커뮤니티. 모르면 묻고, 해답을 찾아보세요.
www.inflearn.com
모든 기술에는 트레이드 오프가 있다.
나는 완전히 이상적인 형태로만 구현하고 싶은 마음에 좁은 시각으로 접근했던 것 같다.
더군다나 나는 이제 막 DDD 관련해서 공부하고 있던 참이라 첫 술에 배부를 수는 없는 법, 그런 구조는 나중에 경험이 많이 쌓인 이후에 시도해보기로 했다.
이번에는 완전한 애그리거트 구조를 약간 포기하고 엔티티를 몇 개 합쳐서 구성하기로 했다.

그래서 위와 같이 구조를 약간 변경하기로 했다.
완전한 애그리거트를 구성하지는 못했지만 최대한 타협을 보고 성능도 가져올 수 있었다.
'공부 > Java' 카테고리의 다른 글
| [Java] Entity 객체에서 불필요한 Setter 잡아내기 (0) | 2024.11.15 |
|---|---|
| [Java] 자바 Reflection(리플렉션)을 이용하여 동적 개수 파라미터 받아 문자열로 된 메서드 실행하기 (0) | 2024.11.01 |
| [Java] 문자열 그대로 클래스로 만들어 컴파일 후 실행하기 (0) | 2024.09.28 |
| [Java] 퀵정렬 (QuickSort) 구현하기 (0) | 2024.09.24 |
| [Java] 동적 프록시 (Dynamic Proxy) (0) | 2024.07.23 |