순환 참조(Circular Reference)란 무엇인가?
순환 참조는 서로 다른 두 객체가 각각 상대방에 대한 참조를 가지고 있는 상황을 말합니다. 즉, A 객체가 B 객체를 참조하고, 동시에 B 객체도 A 객체를 참조하여 참조 관계가 고리처럼 이어지는 구조입니다.
과거에는 메모리 누수의 주범이었다
오래된 브라우저 환경에서는 순환 참조가 메모리 누수(memory leak)의 대표적인 원인이었습니다. 당시의 가비지 컬렉터는 서로를 참조하는 객체들을 정리하지 못해 메모리가 계속 남아있는 문제가 발생했기 때문입니다.
하지만 현대의 가비지 컬렉션 알고리즘은 순환 구조(cycle)와 순환 의존성을 충분히 잘 처리할 수 있게 발전했습니다. 따라서 오늘날 자바스크립트에서 순환 참조가 곧바로 메모리 누수로 이어지는 경우는 드물어졌습니다.
그래도 여전히 나쁜 이유: 설계 관점의 문제
런타임 성능의 문제가 사라졌다고 해서 순환 참조가 좋아진 것은 아닙니다. 순수한 설계 관점에서 보면 순환 참조는 여전히 나쁜 패턴이며, 대표적인 코드 스멜(code smell)로 여겨집니다.
그 이유는 명확합니다. 서로를 참조하는 두 객체는 강하게 결합(tight coupling)되어 있다는 뜻이기 때문입니다. 한 객체의 구조나 동작을 변경하면, 그 영향이 참조하고 있는 다른 객체에도 전파되어 연쇄적인 수정이 필요할 수 있습니다. 이러한 결합도는 코드의 유지보수성과 테스트 용이성을 크게 떨어뜨립니다.
순환 참조를 피하는 방법
자바스크립트에서 순환 참조를 피하는 정해진 단 하나의 방법은 없습니다. 사용 사례와 상황에 따라 접근 방식이 달라지며, 경우에 따라서는 오히려 순환 참조가 필요할 수도 있습니다. 간단한 예제를 통해 살펴보겠습니다.
예제: Dog와 Person 객체
개(Dog) 객체만으로도 주인(owner)에 접근할 수 있고, 사람(Person) 객체만으로도 애완동물(pet)에 접근할 수 있도록 만들고 싶다고 가정해 봅니다.
let dog = new Dog();
let person = new Person();
// 순환 참조 생성
dog.owner = person;
person.pet = dog;
실용적인 해결 전략
대부분의 상황에서는 실제로 한쪽 객체에만 접근하고, 반대쪽 객체는 그것으로부터 파생해내면 됩니다. 이 경우라면 양방향 링크 중 하나를 끊어 순환 참조를 제거할 수 있습니다.
양방향 접근이 꼭 필요하다면, 맵(Map)을 활용해 역방향 조회를 별도로 관리하는 방법도 있습니다. 다만 이런 식의 편법(hack)을 남용하기보다는, 불가피한 상황이라면 순환 참조를 그대로 두는 것이 더 깔끔한 선택일 수 있습니다. 핵심은 무조건 제거하는 것이 아니라, 설계상 꼭 필요한지 신중하게 판단하는 것입니다.