////
Search
Duplicate
🛎️

Chapter 05. 순환 참조

순환 참조는 두 개 이상의 객체나 컴포넌트가 서로를 참조함으로써 의존 관계에 사이클이 생기는 상황을 말한다.
예를 들어 객체 A가 객체 B를 참조하고 객체 B가 다시 객체 A를 참조하는 양방향 참조가 대표적이다.
이러한 순환 참조는 소프트웨어 설계에서 볼 수 있는 대표적인 안티패턴 중 하나다.
우리는 일상생활 속에서 JPA의 양방향 매핑이라는 이름의 순환 참조를 거리낌없이 사용하곤 한다.

1. 순환 참조의 문제점

그렇다면 순환 참조는 왜 나쁜 것일까? 앞서 살펴본 문제점 외의 문제를 알아보자.

1. 무한 루프

순환 참조가 있다는 말은 시스템에 무한 루프가 발생할 수 있다는 말이다.
무한 루프는 왜 문제가 될까? 이는 예상치 못한 오류를 만들어내기 때문이다.
무한 루프는 개발자가 메소드 호출 과정에 신경 쓴다고 해결할 수 있는 문제가 아니다. 그러므로 순환 참조로 인해 발생할 잠재적 위험을 안고 갈 이유가 없다.

2. 시스템 복잡도

순환 참조는 시스템의 복잡도를 높인다. 순환 참조가 있으면 어떤 객체에 접근할 수 있는 경로가 너무 많아진다.
경로가 많다는 것은 의존 관계가 복잡하게 얽혀 있다는 의미이기 때문에 좋지 않다.
가능한 도메인 모델들에 단일 진입점을 만들어서 필요한 객체가 있을 때 단방향으로 접근하도록 만드는 것이 좋다.

2. 순환 참조를 해결하는 방법

1. 불필요한 참조 제거

불필요한 참조를 제거한다는 말은 양방향 참조가 꼭 필요한지 재고해 본다는 의미다.
꼭 필요하지 않은 참조를 제거하거나 필요에 따라 관계를 표현하긴 해야 한다면 한쪽이 다른 한쪽의 식별자를 갖고 있게끔 하여 간접 참조 형태로 관계를 바꾸는 것이다.
이처럼 불필요한 참조를 약간만 집중하여 의식적으로 추적해 보면 상당히 많이 제거할 수 있게 될 것이다.

2. 간접 참조 활용

순환 참조를 제거하는 데 간접 참조를 활용할 수도 있다.
식별자를 이용해 필요한 데이터를 가져오게끔 하면 된다.
영속성 객체로 하여금 참조 대상의 식별자를 가지게 하여 간접 참조하는 식이다. 데이터가 필요할 땐, 해당 식별자로 데이터를 요청하면 된다.

3. 공통 컴포넌트 분리

만약 서비스 같은 컴포넌트에 순환 참조가 있고, 이것이 각 컴포넌트의 설정상 필수적이라면 이는 어떻게 해결할 수 있을까?
가장 간단하고 효과적인 방법으로 다음과 같이 공통 컴포넌트를 분리하는 식의 방법이 있다.
즉, 양쪽 서비스 컴포넌트에 있던 공통 기능을 하나의 컴포넌트로 분리하는 것이다. 그러고 나서 양쪽 서비스가 컴포넌트에 의존하도록 바꾸면 순환 참조가 없어진다.
이 방법의 또 다른 장점으로는 공통 기능을 분리하는 과정에서 책임 분배가 적절하게 재조정된다는 점이다.
컴포넌트의 기능적 분리는 결과적으로 과하게 부여되었던 책임을 분산하며, 그 결과 기능적 응집도를 높이는 효과를 가져온다.

4. 이벤트 기반 시스템 사용

서비스를 공통 컴포넌트로도 분리할 수 없다면 이벤트 기반 프로그래밍을 시스템에 적용할 수 있다.
이벤트 기반 프로그래밍을 적용한다는 것은 시스템 설계를 다음과 같은 순서로 변경한다는 것이다.
시스템에서 사용할 중앙 큐를 만든다.
필요에 따라 컴포넌트들이 중앙 큐를 구독하게 한다.
컴포넌트들은 자신의 역할을 수행하던 중 다른 컴포넌트에 시켜야 할 일이 있다면 큐에 이벤트를 발행한다.
이벤트가 발행되면 큐를 구독하고 있는 컴포넌트들이 반응한다.
컴포넌트들은 이벤트를 확인하고 자신이 처리해야 하는 이벤트면 이를 읽어 처리한다.
컴포넌트들은 자신이 처리하지 않아도 되는 이벤트라면 무시한다.
이 구조에서 서비스는 더 이상 서로를 참조하지 않게 되며 대신 이벤트와 이벤트 큐에 의존한다. 이벤트와 이벤트 큐가 인터페이스이자 곧 메시지가 되는 것이다.
이벤트 기반 시스템은 객체 간의 통신을 이벤트로 이뤄지게 하여 결합을 느슨하게 만들어 순환 참조를 피할 수 있게 도와준다.
이벤트 기반 시스템은 컴포넌트들의 상호 의존성을 끊어내면서도 시스템 설계를 단순하게 만들어준다.
스프링을 이용하면 이러한 이벤트 시스템을 쉽게 구현할 수 있다. 스프링에서 지원하는 ApplicationEvent, ApplicationEventPublisher, EventListener 등을 사용하면 된다.
물론 스프링을 이용하지 않아도 된다. 중앙화된 큐를 만들어 두기만 한다면 어디서든 적용할 수 있기 때문이다. 이벤트 큐에 쌓인 이벤트를 어떻게 처리하느냐에 따라 동기 처리로 만들 수도 있고 비동기 처리로 만들 수도 있다.
이러한 방식의 프로그래밍을 이벤트 기반 프로그래밍이라고 한다.
이벤트 큐를 전역 변수가 아닌 카프카 같은 메시지 시스템을 이용해 구현한다면 어떨까? 이벤트 큐로 중앙 시스템 인프라를 이용하는 것이다.
이렇게 되면 이벤트 기반 시스템이 단일 서버에서만 동작하는 것이 아니라 멀티 서버에서 동작하게 할 수 있을 것이다.
이러한 설계 방식으로 멀티 시스템을 구성하는 방식을 가리켜 이벤트 기반 아키텍처라고 한다. 이 설계 방식은 시스템 각각이 곧 기능이 되는 MSA 환경에서 자주 선택되는 전략이기도 하다.

3. 양방향 매핑

JPA에 양방향 매핑이라는 개념이 있긴 하지만 이는 양방향 매핑을 적극적으로 사용해도 된다는 의미는 아니다.
순환 참조는 어떻게든 없애는 것이 좋으며 대부분 없앨 수 있다. 순환 참조를 사용하는 데는 정말 신중에 신중을 기해야 하며 같은 맥락으로 양방향 매핑도 사용할 때 신중에 신중을 기해야 한다.
양방향 매핑을 사용하지 않아도 얼마든지 개발이 가능하다. JPA는 수단일 뿐이다. 따라서 수단인 JPA로 인해 시스템 설계가 영향을 받아서는 안 된다. 심지어 영향을 받아 만들어진 설계 결과물이 안 좋은 방향이라면 더더욱 안 된다.
우리는 순환 참조가 없는 순수한 도메인을 먼저 구성해야 한다. 그리고 그 다음 JPA를 연동하는 식으로 개발해야 한다. JPA는 절대 어플리케이션의 핵심이될 수 없다.

4. 상위 수준의 순환 참조

순환 참조는 객체뿐만 아니라 패키지 사이나 시스템 수준에서도 발생할 수 있는 문제다. 해당 수준에서 발생한다면 객체 간 순환 참조보다 더 큰 문제를 야기할 수 있다.
우리는 패키지나 모듈, 시스템에서 발생하는 순환 참조를 경계하고 독립된 무언가를 만들 수 있어야 한다. 그리고 이렇게 독립된 무언가를 만드는 것은 퍼즐 조각을 만드는 것과 유사하다.
패키지나 시스템, 모듈 수준에서 순환 참조가 발생하면 분리와 유연성이 제한된다. 따라서 개발자는 클래스뿐만 아니라 상위 수준에서 발생하는 순환 참조를 방지하기 위해 주의해야 한다.
순환 참조는 명백한 안티패턴이며 순환 참조를 이용해 개발하는 것은 일시적으로는 편리할 수 있지만 클린 코드 관점에서 다양한 문제를 만든다.
순환 참조 자체를 만들지 않아 순환 참조로 발생할 수 있는 문제를 미연에 차단해야 한다. 그렇게 하는 것이 시스템의 독립성과 유지보수성, 확장성을 높일 수 있는 길이다.