진행하고 있는 프로젝트인 코끼리끼리에서 성능 개선의 일환으로 코드에서의 의존성 리팩터링을 진행했다. 프로젝트는 크게 Member, Roadmap, GoalRoom 3개의 도메인으로 구성되어 있는데, 초기 설계 단계에서는 의존성이 어떻게 흐르는지 크게 신경쓰지 않고 구현을 했었다. 그러다 우테코에서 의존성 리팩토링 미션을 진행해보기도 하였고, [우아한테크세미나]에서 조영호님의 우아한테크세미나를 본 뒤 언젠가 코끼리끼리 프로젝트도 의존성 분리를 진행하지 않을까 싶었는데 이번에 하게 되었다. 의존성의 방향은 GoalRoom이 Member와 Roadmap을, Roadmap이 Member를 의존하도록 정했고, 각 큰 도메인을 하나씩 맡아 자신이 생각하는 방식대로 하위 도메인들의 의존성 리팩터링을 진행한 뒤 코드 리뷰를 하는 방식으로 진행했다.
의존성 리팩터링은 왜 할까?
변경에 대한 영향을 최소화하기 위해서
A가 B를 의존한다면 B가 변경될 때 A가 같이 변경될 가능성이 있다. 따라서 변경에 대한 영향을 최소한으로 하기 위해 실제로 의존할 필요가 없다면 최소화하는 게 좋다
트랜잭션의 경계를 확실히 하기 위해서
함께 변경되어야 하는 객체들은 함께 변경되는 것이 당연하지만 그럴 필요가 없는 객체들 간에 연관관계가 존재한다면 트랜잭션의 범위가 지나치게 길어지게 된다. 일반적인 예시를 들어보자.
웹사이트에서 새로운 사용자가 회원 가입을 할 때 가입에 성공하고 알림을 보내주는 기능이 있다고 생각해보자. 코드적으로 생각해보면 사용자의 정보를 DB에 저장한 뒤 id를 받아서 해당 id로 알람을 보내도록 구현할 수 있을 것이다. 그런데 사용자의 정보가 정상적으로 입력되면 가입이 성공되어야 함에도 불구하고 알람을 보낼 때 트랜잭션에서 예외가 발생한다면 가입까지 실패하게 되는 문제가 발생할 수 있다. 이는 의존성을 적절히 끊어줌으로써 회원 가입과 알림 발송을 다른 트랜잭션으로 관리할 수 있다.
의존성 사이클을 제거하기 위해서
의존한다는 의미는 같이 변경될 가능성이 크다는 것을 의미한다. 서로 다른 패키지에서 의존성 사이클이 존재하면 한 패키지의 변경이 다른 패키지에도 영향을 미칠 가능성이 높아진다. 또한 의존성을 관리하기 위한 동기화 작업도 까다로울 수 있다. 의존성 사이클을 한 방향으로 흐르도록 잘 관리한다면 MSA로의 전환도 용이해지는 장점이 있다.
의존성 분리 작업

내가 담당한 로드맵 부분에서 리팩터링 하기 전의 모든 도메인의 의존성 사이클을 그려보았다.
살펴보면 Roadmap과 RoadmapContent, RoadmapReview 사이에 의존성 사이클이 있는 것을 볼 수 있다. 처음 팀원들과 엔티티를 설계할 때는 연관관계를 가질 필요가 없다면 가지지 않는 것을 기준으로 했기에 생각보다 많지는 않았다. 양방향 연관관계인 경우에는 연관관계 편의 메서드를 잘 작성하는 식으로 개발했다.
리팩터링을 진행하기 전에 내가 세운 원칙은 간단하다
- 의존성 사이클은 모두 제거한다
- 직접 참조가 반드시 필요한 것이 아니라면 id를 통해 간접 참조한다
- 직접 참조의 기준은 생명 주기로 판단한다
그렇게 수정한 엔티티들의 의존성은 다음과 같다

RoadmapReview와 Roadmap의 생명주기가 다르다고 판단했기 때문에 직접 참조를 끊어버리고 id를 통한 간접 참조 역시 한 방향으로만 흐르도록 바뀌었다. 그와 다르게 RoadmapContent 내부의 많은 다른 도메인들은 RoadmapContent가 생명주기가 같고 조회할 때 같이 불러오는 경우가 대부분이기에 의존성 사이클만 제거해주었다. 또한 전체적으로 도메인 내에서 Member를 직접 참조하는 경우를 없애고 서비스 레이어에서 각 클래스에 있는 memberId를 통해 조회하도록 했다.
로드맵 패키지 내부의 도메인 사이에서의 의존성 리팩터링을 끝낸 후 이제 Member나 GoalRoom 패키지와의 의존성을 정리해주었다. 기본적인 골자는 Roadmap 패키지에서는 Member 패키지를 의존하고 GoalRoom 패키지는 의존하지 않는 것이다. 이를 위해 인터페이스를 사용한 의존성 역전이나 도메인 이벤트를 발행하는 방식을 사용했다.
인터페이스를 사용한 의존성 역전
수정 전

수정 후

GoalRoom 패키지에 존재하는 클래스인 GoalRoomMember를 Roadmap에서 직접 호출하지 않도록 리팩터링했다. 인터페이스인 RoadmapGoalRoomService는 Roadmap 패키지에 두고 그 구현체를 GoalRoomMember를 실제로 가지고 있는 GoalRoom 패키지에서 구현해서 의존성을 역전시켜주었다.
도메인 이벤트 발행
수정 전

수정 후

해당 예시는 도메인 이벤트를 발행해서 의존성을 분리한 예시는 아니지만 기능의 역할을 분리했다는 것에 그 목적이 있다.
Roadmap이 삭제될 때 해당 로드맵에 대해 작성된 리뷰들도 같이 삭제된다는 규칙을 정해놓은 상태다. 하지만 이러한 규칙은 미래에 충분히 바뀔 가능성이 존재한다. (리뷰를 작성한 사용자가 자신이 어떤 리뷰들을 작성했는지 보는 기능을 제공하는 등의 이유가 있을 수 있겠다)
결국 delteRoadmap() 이라는 메서드는 로드맵을 삭제하는 것을 기대하는 것이고, 로드맵의 삭제에 따른 추가적인 규칙들은 얼마든지 바뀔 수 있다. 이럴 때는 '로드맵이 삭제된다'라는 의미의 이벤트를 발행해서 그에 따른 규칙들에 영향을 받는 객체들이 이벤트를 구독한 뒤 처리하도록 할 수 있다.
현재 수정된 방식은 데이터베이스에서 FK의 제약 조건 때문에 모든 리뷰가 삭제된 뒤에야 로드맵을 삭제할 수 있기에 트랜잭션 분리는 하지 않았다. 하지만 만약 DB의 스키마 변경이 있다면 트랜잭션 분리까지 할 수 있게 된다. 로드맵 1개를 지우려고 하는데 관련된 리뷰를 삭제할 때 문제가 생겨서 롤백이 되는 상황을 막을 수도 있고, 요청에 대한 응답 속도도 더 빨라질 수 있을 것이라고 기대한다.
느낀 점
의존성 리팩터링의 목표는 추후에 있을 유지보수를 더 용이하게 만들기 위함이라고 생각한다. 어떤 서비스를 개발할 때 초기 개발에 들어가는 비용보다 유지보수에 들어가는 비용이 더 크기 때문에 (시간이 지날수록 더더욱) 이러한 개선은 매우 중요할 것이다. 하지만 프로젝트를 대대적으로 뜯어고칠 일이 많지 않으니 이론적으로는 이해가 되어도 실제로 유지보수가 더 편할 것인가? 하는 측면에서는 체감하기 어려운 것이 아쉽다. 아니면 아직 내가 코드를 보는 눈이 좋지 않아 그런 것일 수도…
그래도 여러 가지 장/단점을 느낄 수 있었다.
장점
1. 트랜잭션의 단위를 필요한 만큼으로만 제한할 수 있다.
가장 크게 느낀 장점이다. 구현하면서 트랜잭션이 어디까지 이어져야 할지 고민하면서 했던 거 같은데, 의존성 리팩터링하면서 각 객체가 책임져야 할 부분이 무엇인지 다시 한 번 생각하니 수정할 부분이 더 쉽게 보였다.
2. 테스트 작성의 용이성
기존 코드의 연관관계가 너무 복잡했는데 이게 테스트 코드를 꼼꼼히 짠다는 규칙과 맞물려 테스트 코드를 고치는 시간이 엄청 걸리곤 했었다. 중간에 다른 팀원이 중복된 테스트 코드들을 Fixture로 정리하면서 훨씬 나아지긴 했지만 원하는 상황을 테스트하기 위해 선언해주어야 하는 객체들이 너무 많았다. 그런데 연관관계들을 끊었으니 당연히 테스트 코드들도 수정했어야 했는데 의외로 수정이 그리 오래 걸리지 않았다. 게다가 불필요하게 선언했던 객체들도 삭제할 수 있어 작성해야 하는 코드의 양도 많이 줄어들었다.
단점
1. JPA를 사용한다면 그 특성을 잘 활용하지 못할 수 있다.
연관관계와 cascade를 잘 활용하면 연관된 객체에 대한 생성과 삭제를 JPA가 알아서 잘 관리해주었는데 이를 활용하기가 어렵다. 위의 코드의 예시에서도 원래 로드맵을 삭제하면 해당 로드맵을 FK로 가지는 모든 객체들이 알아서 삭제되도록 구현했는데, 이를 개발자가 잘 신경써주어야 한다.
2. 중복된 코드가 많아진다
여러 패키지에서 사용되던 코드를 분리하게 되니 중복된 코드가 많아졌다. ‘그런 부분을 따로 클래스를 분리한 뒤 그 클래스를 참조하면 되지 않나’ 라고 말할 수 있지만 개인적으로는 좀 애매했던 부분이 있었다. 하지만 각 객체에서 필요한 로직이라면 당연히 두 군데에 각각 존재하는 것이 맞다는 생각도 들어 큰 단점이라고 느껴지진 않았다.
참고 : 주의할 점
기존의 테스트 코드의 데이터베이스 스키마는 실제 서비스의 데이터베이스의 구조와 똑같게 JPA 엔티티를 통해 만들어졌다. 하지만 연관관계를 수정하면서 테스트 DB의 스키마가 실제와 달라졌다. 원래 FK 제약조건으로 인한 예외가 발생해야 하는데 테스트가 전부 멀쩡하게 통과하길래 발견했다. DB 마이그레이션은 상당한 회의가 필요한 일이기 때문에 테스트를 돌릴 때 사용할 SQL 문서를 넣어주어야 한다.
'우아한 테크코스 5기' 카테고리의 다른 글
| [우아한 테크코스] 로드맵 검색 기능 개선기 (0) | 2024.08.07 |
|---|---|
| [우아한 테크코스] 레벨 2 - 쇼핑 주문(협업) 미션 회고 (0) | 2023.06.04 |
| [우아한 테크코스] 레벨 1 - 사다리 미션 회고 (0) | 2023.03.19 |
| [우아한 테크코스] 레벨 1 - 자동차 경주 회고 (5) | 2023.02.23 |
| [우아한 테크코스] 프리코스 회고 (1) | 2023.02.23 |