•
순환 참조는 두 개 이상의 객체나 컴포넌트가 서로를 참조함으로써 의존 관계에 사이클이 생기는 상황을 말한다.
◦
예를 들어 객체 A가 객체 B를 참조하고 객체 B가 다시 객체 A를 참조하는 양방향 참조가 대표적이다.
•
이러한 순환 참조는 소프트웨어 설계에서 볼 수 있는 대표적인 안티패턴 중 하나다.
◦
우리는 일상생활 속에서 JPA의 양방향 매핑이라는 이름의 순환 참조를 거리낌없이 사용하곤 한다.
1. 순환 참조의 문제점
•
그렇다면 순환 참조는 왜 나쁜 것일까? 앞서 살펴본 문제점 외의 문제를 알아보자.
1. 무한 루프
•
순환 참조가 있다는 말은 시스템에 무한 루프가 발생할 수 있다는 말이다.
•
무한 루프는 왜 문제가 될까? 이는 예상치 못한 오류를 만들어내기 때문이다.
•
무한 루프는 개발자가 메소드 호출 과정에 신경 쓴다고 해결할 수 있는 문제가 아니다. 그러므로 순환 참조로 인해 발생할 잠재적 위험을 안고 갈 이유가 없다.
2. 시스템 복잡도
•
순환 참조는 시스템의 복잡도를 높인다. 순환 참조가 있으면 어떤 객체에 접근할 수 있는 경로가 너무 많아진다.
◦
경로가 많다는 것은 의존 관계가 복잡하게 얽혀 있다는 의미이기 때문에 좋지 않다.
•
가능한 도메인 모델들에 단일 진입점을 만들어서 필요한 객체가 있을 때 단방향으로 접근하도록 만드는 것이 좋다.
2. 순환 참조를 해결하는 방법
1. 불필요한 참조 제거
•
불필요한 참조를 제거한다는 말은 양방향 참조가 꼭 필요한지 재고해 본다는 의미다.
◦
꼭 필요하지 않은 참조를 제거하거나 필요에 따라 관계를 표현하긴 해야 한다면 한쪽이 다른 한쪽의 식별자를 갖고 있게끔 하여 간접 참조 형태로 관계를 바꾸는 것이다.
•
이처럼 불필요한 참조를 약간만 집중하여 의식적으로 추적해 보면 상당히 많이 제거할 수 있게 될 것이다.
2. 간접 참조 활용
•
순환 참조를 제거하는 데 간접 참조를 활용할 수도 있다.
◦
식별자를 이용해 필요한 데이터를 가져오게끔 하면 된다.
•
영속성 객체로 하여금 참조 대상의 식별자를 가지게 하여 간접 참조하는 식이다. 데이터가 필요할 땐, 해당 식별자로 데이터를 요청하면 된다.
3. 공통 컴포넌트 분리
•
만약 서비스 같은 컴포넌트에 순환 참조가 있고, 이것이 각 컴포넌트의 설정상 필수적이라면 이는 어떻게 해결할 수 있을까?
•
가장 간단하고 효과적인 방법으로 다음과 같이 공통 컴포넌트를 분리하는 식의 방법이 있다.
◦
즉, 양쪽 서비스 컴포넌트에 있던 공통 기능을 하나의 컴포넌트로 분리하는 것이다. 그러고 나서 양쪽 서비스가 컴포넌트에 의존하도록 바꾸면 순환 참조가 없어진다.
•
이 방법의 또 다른 장점으로는 공통 기능을 분리하는 과정에서 책임 분배가 적절하게 재조정된다는 점이다.
◦
컴포넌트의 기능적 분리는 결과적으로 과하게 부여되었던 책임을 분산하며, 그 결과 기능적 응집도를 높이는 효과를 가져온다.
4. 이벤트 기반 시스템 사용
•
서비스를 공통 컴포넌트로도 분리할 수 없다면 이벤트 기반 프로그래밍을 시스템에 적용할 수 있다.
•
이벤트 기반 프로그래밍을 적용한다는 것은 시스템 설계를 다음과 같은 순서로 변경한다는 것이다.
◦
시스템에서 사용할 중앙 큐를 만든다.
◦
필요에 따라 컴포넌트들이 중앙 큐를 구독하게 한다.
◦
컴포넌트들은 자신의 역할을 수행하던 중 다른 컴포넌트에 시켜야 할 일이 있다면 큐에 이벤트를 발행한다.
◦
이벤트가 발행되면 큐를 구독하고 있는 컴포넌트들이 반응한다.
◦
컴포넌트들은 이벤트를 확인하고 자신이 처리해야 하는 이벤트면 이를 읽어 처리한다.
◦
컴포넌트들은 자신이 처리하지 않아도 되는 이벤트라면 무시한다.
•
이 구조에서 서비스는 더 이상 서로를 참조하지 않게 되며 대신 이벤트와 이벤트 큐에 의존한다. 이벤트와 이벤트 큐가 인터페이스이자 곧 메시지가 되는 것이다.
•
이벤트 기반 시스템은 객체 간의 통신을 이벤트로 이뤄지게 하여 결합을 느슨하게 만들어 순환 참조를 피할 수 있게 도와준다.
◦
이벤트 기반 시스템은 컴포넌트들의 상호 의존성을 끊어내면서도 시스템 설계를 단순하게 만들어준다.
•
스프링을 이용하면 이러한 이벤트 시스템을 쉽게 구현할 수 있다. 스프링에서 지원하는 ApplicationEvent, ApplicationEventPublisher, EventListener 등을 사용하면 된다.
•
물론 스프링을 이용하지 않아도 된다. 중앙화된 큐를 만들어 두기만 한다면 어디서든 적용할 수 있기 때문이다. 이벤트 큐에 쌓인 이벤트를 어떻게 처리하느냐에 따라 동기 처리로 만들 수도 있고 비동기 처리로 만들 수도 있다.
◦
이러한 방식의 프로그래밍을 이벤트 기반 프로그래밍이라고 한다.
•
이벤트 큐를 전역 변수가 아닌 카프카 같은 메시지 시스템을 이용해 구현한다면 어떨까? 이벤트 큐로 중앙 시스템 인프라를 이용하는 것이다.
◦
이렇게 되면 이벤트 기반 시스템이 단일 서버에서만 동작하는 것이 아니라 멀티 서버에서 동작하게 할 수 있을 것이다.
•
이러한 설계 방식으로 멀티 시스템을 구성하는 방식을 가리켜 이벤트 기반 아키텍처라고 한다. 이 설계 방식은 시스템 각각이 곧 기능이 되는 MSA 환경에서 자주 선택되는 전략이기도 하다.
3. 양방향 매핑
•
JPA에 양방향 매핑이라는 개념이 있긴 하지만 이는 양방향 매핑을 적극적으로 사용해도 된다는 의미는 아니다.
•
순환 참조는 어떻게든 없애는 것이 좋으며 대부분 없앨 수 있다. 순환 참조를 사용하는 데는 정말 신중에 신중을 기해야 하며 같은 맥락으로 양방향 매핑도 사용할 때 신중에 신중을 기해야 한다.
•
양방향 매핑을 사용하지 않아도 얼마든지 개발이 가능하다. JPA는 수단일 뿐이다. 따라서 수단인 JPA로 인해 시스템 설계가 영향을 받아서는 안 된다. 심지어 영향을 받아 만들어진 설계 결과물이 안 좋은 방향이라면 더더욱 안 된다.
•
우리는 순환 참조가 없는 순수한 도메인을 먼저 구성해야 한다. 그리고 그 다음 JPA를 연동하는 식으로 개발해야 한다. JPA는 절대 어플리케이션의 핵심이될 수 없다.
4. 상위 수준의 순환 참조
•
순환 참조는 객체뿐만 아니라 패키지 사이나 시스템 수준에서도 발생할 수 있는 문제다. 해당 수준에서 발생한다면 객체 간 순환 참조보다 더 큰 문제를 야기할 수 있다.
•
우리는 패키지나 모듈, 시스템에서 발생하는 순환 참조를 경계하고 독립된 무언가를 만들 수 있어야 한다. 그리고 이렇게 독립된 무언가를 만드는 것은 퍼즐 조각을 만드는 것과 유사하다.
•
패키지나 시스템, 모듈 수준에서 순환 참조가 발생하면 분리와 유연성이 제한된다. 따라서 개발자는 클래스뿐만 아니라 상위 수준에서 발생하는 순환 참조를 방지하기 위해 주의해야 한다.
•
순환 참조는 명백한 안티패턴이며 순환 참조를 이용해 개발하는 것은 일시적으로는 편리할 수 있지만 클린 코드 관점에서 다양한 문제를 만든다.
•
순환 참조 자체를 만들지 않아 순환 참조로 발생할 수 있는 문제를 미연에 차단해야 한다. 그렇게 하는 것이 시스템의 독립성과 유지보수성, 확장성을 높일 수 있는 길이다.