순환 참조 (Circular Reference)의 개념과 해결방법

shjk·2025년 4월 1일

개요

소프트웨어 개발에서 순환 참조(Circular Reference)란 두 개 이상의 모듈, 객체 또는 클래스가 서로를 직접적 또는 간접적으로 참조할 때 발생하는 현상이다. 순환 참조는 소프트웨어의 유지보수를 어렵게 하고, 메모리 누수(memory leak)나 성능 저하 등의 문제를 유발할 수 있으므로, 이를 방지하거나 해결하는 것이 중요하다.

이번 포스팅에서는 순환 참조의 발생 원인과 예시, 문제점, 그리고 이를 해결하는 방법까지 자세히 알아본다.


순환 참조의 정의 및 발생 원인

순환 참조는 다음과 같은 형태로 발생할 수 있다.

  • 클래스 또는 모듈 간 서로를 직접 참조할 때

    ABA \leftrightarrow B
  • 복수의 클래스나 모듈이 연쇄적으로 서로 참조할 때 (간접 참조)

    ABCAA \rightarrow B \rightarrow C \rightarrow A

이러한 순환 구조는 주로 다음과 같은 상황에서 발생한다.

  • 객체 간 양방향(Bidirectional) 참조가 필요한 설계
  • 상호 의존적인 모듈의 구조
  • 무분별한 객체 간의 관계 설정 및 책임 분리 실패

순환 참조 예시

다음은 간단한 예시로 설명한 클래스 간 순환 참조이다.

# a.py
from b import ClassB

class ClassA:
    def __init__(self):
        self.b = ClassB()

# b.py
from a import ClassA

class ClassB:
    def __init__(self):
        self.a = ClassA()

위와 같이 클래스 ClassAClassB가 서로를 직접 참조하면 순환 참조가 발생하게 된다.


순환 참조의 문제점

순환 참조로 인해 발생할 수 있는 주요 문제점은 다음과 같다.

  • 의존성 관리의 어려움: 모듈 또는 객체 간 의존성이 복잡해져 유지보수가 어려워진다.
  • 메모리 누수 (Memory Leak): 참조 카운트(reference count) 기반의 가비지 컬렉션(Garbage Collection) 방식에서, 순환 참조가 존재하면 객체가 적절히 해제되지 않을 수 있다.
  • 빌드 과정의 복잡성 증가: 순환 참조로 인해 빌드 순서가 꼬일 수 있고, 컴파일 에러가 발생할 수 있다.

순환 참조 해결 방법

순환 참조 문제를 해결하는 방법은 크게 다음과 같이 정리할 수 있다.

1. 추상화 계층 도입 (인터페이스 사용)

순환 참조 문제를 해결하는 가장 일반적인 방법은 중간 추상 계층(인터페이스)을 두어 직접적인 참조를 끊는 것이다.

# interface.py
from abc import ABC, abstractmethod

class IClassB(ABC):
    @abstractmethod
    def method_b(self):
        pass

class IClassA(ABC):
    @abstractmethod
    def method_a(self):
        pass

위 인터페이스를 통해 간접 참조로 변환하면 다음과 같이 구현한다.

# a.py
from interface import IClassB

class ClassA:
    def __init__(self, b: IClassB):
        self.b = b

# b.py
from interface import IClassA

class ClassB:
    def __init__(self, a: IClassA):
        self.a = a

이러한 방식을 사용하면 순환 참조를 피할 수 있다.


2. 모듈이나 클래스를 분리하여 단방향 참조로 전환

객체의 책임을 분명히 하고 클래스를 잘게 나누어 단방향 참조 구조로 변경한다.

순환 참조:
A ↔ B

해결된 구조:
A → C ← B

위와 같이 공통적으로 의존하는 클래스를 새로 만들어 클래스 간 직접 참조를 끊는다.


3. 의존성 주입 (Dependency Injection, DI)

객체의 생성을 외부에서 주입하여 클래스 내에서 직접 객체를 생성하지 않도록 한다. DI를 통해 순환 참조를 방지할 수 있다.

예를 들어, Python 코드에서 DI 적용은 다음과 같이 나타난다.

# main.py
from a import ClassA
from b import ClassB

a = ClassA(None)
b = ClassB(a)
a.b = b

이 방식은 객체의 생성과 관리를 외부에서 담당하여 순환 참조 문제를 제거한다.


4. Weak Reference 사용

Python에서는 약한 참조(Weak Reference)를 통해 순환 참조를 방지할 수 있다. 이는 객체가 서로를 참조하지만 참조 카운트를 증가시키지 않게 하여, 메모리 누수를 방지한다.

import weakref

class A:
    def __init__(self):
        self.b = None

class B:
    def __init__(self, a):
        self.a = weakref.ref(a)  # 약한 참조 사용

a = A()
b = B(a)
a.b = b

이 방식으로 참조하면 객체는 가비지 컬렉터가 자동으로 수거할 수 있게 된다.


순환 참조를 피하기 위한 권장사항

  • 객체 간 의존성을 최소화하여 단방향 참조를 유지한다.
  • 명확한 책임 분리(Single Responsibility Principle, SRP)를 적용하여 클래스 설계를 진행한다.
  • 인터페이스나 추상화를 적극적으로 활용하여 의존성을 느슨하게 만든다.
  • 의존성 주입(DI)과 같은 패턴을 통해 객체 생성 책임을 분리한다.

결론

순환 참조는 소프트웨어 설계 및 유지보수 측면에서 중대한 문제를 야기할 수 있다. 이러한 순환 참조를 해결하기 위해서는 추상화 계층의 도입, 의존성 주입(DI), 클래스의 책임 분리 및 Weak Reference와 같은 기술적 해결책을 적극적으로 활용하는 것이 중요하다. 개발자는 소프트웨어 설계 초기에 이러한 순환 참조 문제를 인지하고 예방하는 습관을 들여야 한다.


profile
백엔드 개발자

0개의 댓글