IT박스

순환 참조가 해로운 것으로 간주되는 이유는 무엇입니까?

itboxs 2020. 10. 29. 07:57
반응형

순환 참조가 해로운 것으로 간주되는 이유는 무엇입니까?


개체가 첫 번째 개체를 다시 참조하는 다른 개체를 참조하는 것이 왜 나쁜 디자인입니까?


클래스 간의 순환 종속성 이 반드시 해로운 것은 아닙니다. 실제로 어떤 경우에는 바람직합니다. 예를 들어, 애플리케이션이 애완 동물과 그 소유자를 다루는 경우 Pet 클래스에는 애완 동물의 소유자를 가져 오는 메서드가 있고 Owner 클래스에는 애완 동물 목록을 반환하는 메서드가 있어야합니다. 물론 이것은 메모리 관리를 더 어렵게 만들 수 있습니다 (비 GC 언어에서). 그러나 순환 성이 문제에 내재되어있는 경우이를 제거하려고하면 더 많은 문제가 발생할 수 있습니다.

반면에 모듈 간의 순환 종속성 은 유해합니다. 이는 일반적으로 잘못 생각한 모듈 구조 및 / 또는 원래의 모듈화를 고수하지 못했음을 나타냅니다. 일반적으로 제어되지 않는 상호 종속성을 가진 코드 기반은 깔끔하고 계층화 된 모듈 구조를 가진 코드 기반보다 이해하기 더 어렵고 유지 관리하기도 더 어렵습니다. 적절한 모듈이 없으면 변경의 영향을 예측하기가 훨씬 더 어려울 수 있습니다. 그리고 이는 유지 관리를 더 어렵게 만들고 잘못된 패치로 인한 "코드 붕괴"로 이어집니다.

(또한 Maven과 같은 빌드 도구는 순환 종속성이있는 모듈 (아티팩트)을 처리하지 않습니다.)


순환 참조가 항상 해로운 것은 아닙니다 . 매우 유용 할 수있는 몇 가지 사용 사례가 있습니다. 이중 연결 목록, 그래프 모델 및 컴퓨터 언어 문법이 떠 오릅니다. 그러나 일반적으로 개체 간의 순환 참조를 피해야하는 몇 가지 이유가 있습니다.

  1. 데이터 및 그래프 일관성. 순환 참조로 객체를 업데이트하면 항상 객체 간의 관계가 유효한지 확인하는 데 문제가 발생할 수 있습니다. 이러한 유형의 문제는 개체-관계형 모델링 구현에서 종종 발생하며 엔티티 간의 양방향 순환 참조를 찾는 것이 드문 일이 아닙니다.

  2. 원자 적 작업 보장. 순환 참조의 두 객체에 대한 변경이 원 자성인지 확인하는 것은 특히 멀티 스레딩이 관련된 경우 복잡해질 수 있습니다. 여러 스레드에서 액세스 할 수있는 개체 그래프의 일관성을 보장하려면 스레드가 불완전한 변경 집합을 보지 않도록하는 특수 동기화 구조 및 잠금 작업이 필요합니다.

  3. 물리적 분리 문제. 두 개의 다른 클래스 A와 B가 서로 순환 방식으로 참조하는 경우 이러한 클래스를 독립 어셈블리로 분리하는 것이 어려울 수 있습니다. A와 B가 구현하는 인터페이스 IA 및 IB를 사용하여 세 번째 어셈블리를 만드는 것은 확실히 가능합니다. 각 인터페이스를 통해 서로를 참조 할 수 있습니다. 순환 종속성을 끊는 방법으로 약한 형식의 참조 (예 : 개체)를 사용할 수도 있지만 이러한 개체의 메서드 및 속성에 대한 액세스는 쉽게 액세스 할 수 없으므로 참조를 갖는 목적을 무효화 할 수 있습니다.

  4. 변경 불가능한 순환 참조 적용. C # 및 VB와 같은 언어는 개체 내 참조를 변경할 수 없도록 (읽기 전용) 키워드를 제공합니다. 불변 참조를 사용하면 프로그램에서 참조가 객체의 수명 동안 동일한 객체를 참조하도록 할 수 있습니다. 불행히도, 순환 참조가 변경 될 수 없도록하기 위해 컴파일러 강제 불변성 메커니즘을 사용하는 것은 쉽지 않습니다. 한 개체가 다른 개체를 인스턴스화하는 경우에만 수행 할 수 있습니다 (아래 C # 예제 참조).

    class A
    {
        private readonly B m_B;
        public A( B other )  { m_B = other; }
    }
    
    class B 
    { 
        private readonly A m_A; 
        public A() { m_A = new A( this ); }
    }
    
  5. 프로그램 가독성 및 유지 보수 용이성. 순환 참조는 본질적으로 깨지기 쉽고 깨지기 쉽습니다. 이는 부분적으로 순환 참조를 포함하는 코드를 읽고 이해하는 것이이를 피하는 코드보다 어렵다는 사실에서 기인합니다. 코드를 이해하고 유지하기 쉽게 유지하면 버그를 방지하고보다 쉽고 안전하게 변경할 수 있습니다. 순환 참조가있는 개체는 서로 격리하여 테스트 할 수 없기 때문에 단위 테스트가 더 어렵습니다.

  6. 개체 수명 관리. .NET의 가비지 수집기는 순환 참조를 식별하고 처리 할 수 ​​있고 이러한 개체를 올바르게 처리 할 수 ​​있지만 모든 언어 / 환경이 그렇게 할 수있는 것은 아닙니다. 가비지 수집 체계 (예 : VB6, Objective-C, 일부 C ++ 라이브러리)에 참조 계수를 사용하는 환경에서 순환 참조로 인해 메모리 누수가 발생할 수 있습니다. 각 개체는 다른 개체를 유지하므로 참조 횟수는 0에 도달하지 않으므로 수집 및 정리 후보가되지 않습니다.


이제 그들은 실제로 하나의 단일 객체이기 때문에. 둘 중 하나를 따로 테스트 할 수 없습니다.

하나를 수정하면 동반자에게도 영향을 미칠 수 있습니다.


Wikipedia에서 :

순환 종속성은 소프트웨어 프로그램에서 원치 않는 많은 영향을 줄 수 있습니다. 소프트웨어 설계 관점에서 가장 문제가되는 것은 단일 모듈의 개별 재사용을 줄이거 나 불가능하게 만드는 상호 의존적 모듈의 긴밀한 결합입니다.

순환 종속성은 한 모듈의 작은 로컬 변경이 다른 모듈로 확산되고 원치 않는 전역 효과 (프로그램 오류, 컴파일 오류)가있을 때 도미노 효과를 일으킬 수 있습니다. 순환 종속성으로 인해 무한 재귀 또는 기타 예기치 않은 오류가 발생할 수도 있습니다.

순환 종속성은 또한 매우 원시적 인 특정 자동 가비지 수집기 (참조 계산을 사용하는)가 사용하지 않는 개체의 할당을 해제하지 못하도록하여 메모리 누수를 일으킬 수 있습니다.


이러한 객체는 생성 및 삭제가 어려울 수 있습니다. 비원 자적으로 수행하려면 먼저 하나를 생성 / 파괴하기 위해 참조 무결성을 위반 한 다음 다른 하나를 위반해야하기 때문입니다 (예 : SQL 데이터베이스가이 문제를 해결할 수 있습니다). 가비지 수집기를 혼동 할 수 있습니다. 가비지 수집을 위해 간단한 참조 계산을 사용하는 Perl 5는 (도움없이) 메모리 누수를 일으킬 수 없습니다. 두 객체가 이제 다른 클래스이면 단단히 결합되어 분리 될 수 없습니다. 이러한 클래스를 설치할 패키지 관리자가 있으면 순환 종속성이 여기에 퍼집니다. 테스트 하기 전에 패키지 모두 설치 하는 것을 알아야합니다. (빌드 시스템의 관리자라고 말하면) PITA입니다.

즉, 이러한 모든 문제를 극복 할 수 있으며 순환 데이터가 필요한 경우가 많습니다. 현실 세계는 깔끔한 방향성 그래프로 구성되어 있지 않습니다. 많은 그래프, 나무, 지옥, 이중 연결 목록은 원형입니다.


코드 가독성이 떨어집니다. 그리고 순환 종속성에서 스파게티 코드에 이르기까지 아주 작은 단계가 있습니다.


다음은 순환 종속성이 나쁜 이유를 설명하는 데 도움이되는 몇 가지 예입니다.

문제 # 1 : 무엇을 먼저 초기화 / 구성합니까?

다음 예를 고려하십시오.

class A
{
  public A()
  {
    myB.DoSomething();
  }

  private B myB = new B();
}

class B
{
  public B()
  {
    myA.DoSomething();
  }

  private A myA = new A();
}

어떤 생성자가 먼저 호출됩니까? 완전히 모호하기 때문에 확신 할 방법이 없습니다. DoSomething 메서드 중 하나 또는 다른 메서드가 초기화되지 않은 개체에서 호출되어 잘못된 동작이 발생하고 예외가 발생할 가능성이 큽니다. 이 문제를 해결하는 방법이 있지만 모두보기 흉하고 모두 비 생성자 이니셜 라이저가 필요합니다.

문제 # 2 :

이 경우 .NET 구현이 의도적으로 문제를 숨길 수 있기 때문에 관리되지 않는 C ++ 예제로 변경했습니다. 그러나 다음 예에서는 문제가 매우 명확 해집니다. 나는 .NET이 메모리 관리를 위해 실제로 참조 계산을 사용하지 않는다는 것을 잘 알고 있습니다. 여기서는 핵심 문제를 설명하기 위해서만 사용하고 있습니다. 여기에서 문제 # 1에 대한 한 가지 가능한 해결책을 시연했습니다.

class B;

class A
{
public:
  A() : Refs( 1 )
  {
    myB = new B(this);
  };

  ~A()
  {
    myB->Release();
  }

  int AddRef()
  {
    return ++Refs;
  }

  int Release()
  {
    --Refs;
    if( Refs == 0 )
      delete(this);
    return Refs;
  }

  B *myB;
  int Refs;
};

class B
{
public:
  B( A *a ) : Refs( 1 )
  {
    myA = a;
    a->AddRef();
  }

  ~B()
  {
    myB->Release();
  }

  int AddRef()
  {
    return ++Refs;
  }

  int Release()
  {
    --Refs;
    if( Refs == 0 )
      delete(this);
    return Refs;
  }

  A *myA;
  int Refs;
};

// Somewhere else in the code...
...
A *localA = new A();
...
localA->Release(); // OK, we're done with it
...

At first glance, one might think that this code is correct. The reference counting code is pretty simple and straightfoward. However, this code results in a memory leak. When A is constructed, it initially has a reference count of "1". However, the encapsulated myB variable increments the reference count, giving it a count of "2". When localA is released, the count is decremented, but only back to "1". Hence, the object is left hanging and never deleted.

As I mentioned above, .NET doesn't really use reference counting for its garbage collection. But it does use similar methods to determine if an object is still being used or if it's OK to delete it, and almost all such methods can get confused by circular references. The .NET garbage collector claims to be able to handle this, but I'm not sure I trust it because this is a very thorny problem. Go, on the other hand, gets around the problem by simply not allowing circular references at all. Ten years ago I would have preferred the .NET approach for its flexibility. These days, I find myself preferring the Go approach for its simplicity.


It is completely normal to have objects with circular references e.g. in a domain model with bidirectional associations. An ORM with a properly written data access component can handle that.


Refer to Lakos’s book, in C++ software design, cyclic physical dependency is undesirable. There are several reasons:

  • It makes them hard to test and impossible to reuse independently.
  • It makes them difficult for people to understand and maintain.
  • It will increase the link-time cost.

Circular references seem to be a legitimate domain modelling scenario. An example is Hibernate and many other ORM tools encourage this cross association between entities to enable bi-directional navigation. Typical example in an online Auction system, a Seller entitiy may maintain a reference to the List of entities He/She is selling. And every Item can maintain a reference to it's corresponding seller.


The .NET garbage collector can handle circular references so there is no fear of memory leaks for applications working on the .NET framework.

참고URL : https://stackoverflow.com/questions/1897537/why-are-circular-references-considered-harmful

반응형