class 정의 시의 순환참조 오류란?
class 정의 구문을 이용하여 하나의 클래스를 정의할 때 만약 그 정의 내부에서 해당 클래스를 참조한다면 닭이 먼저냐 알이 먼저냐의 문제에 봉착하여 클래스 정의가 실패하는 현상입니다. 말로 하면 어려우니 코드로 보겠습니다.
class Test{
private var instance:Test = new Test;
public function Test(){}
}
위의 코드를 보시면 Test라는 클래스가 정의되려면 instance에 기본값으로 Test의 인스턴스를 할당한다라는 행위가 성립되어야 합니다. 하지만 아직 Test라는 클래스가 정의되지 않았으므로 인스턴스를 할당할 수 없고 인스턴스를 할당한다는 행위를 성공시킬 수 없으니 Test라는 클래스는 정의될 수 없습니다. 그야말로 닭이 먼저냐 알이 먼저냐의 상황입니다. 이러한 경우 해답은 외부에서 instance를 초기화해야 한다는 것입니다. 즉 아래와 같이 바꿔도 여전히 동일한 문제가 됩니다.
class Test{
private var instance:Test;
public function Test(){}
public function init():void{
instance = new Test;
}
}
왜냐면 Test 클래스가 성립하려면 반드시 init를 파싱하는데 성공해야하는데 init를 파싱하려면 instance = new Test를 파싱할 수 있어야 하고 저걸 파싱하려면 Test의 정의가 로드 되어 있어야 하기 때문입니다. 따라서 유일한 해답은 instance의 할당을 외부에서 하는 것 밖에 없습니다.
class Test{
public var instance:Test;
public function Test(){}
}
var t:Test = new Test;
//외부에서 할당한다.
t.instance = new Test;
해서 간단히 말해 클래스 내부에서 자기 자신을 new 하려는 행위를 하면 순환참조 오류에 빠지게 된다는 것입니다. class 정의 시 일어나는 순환참조 오류는 사실 매우 어려운 오류입니다. 처음 당하시는 분은 당췌 이게 왜 발생한 건지 이해하는데도 오래 걸립니다. 클래스 정의 순환참조가 어려운 이유를 간단히 정리해볼까요.
1. 일단 컴파일에러가 아닌 런타임 에러로 발생합니다. 특히 아래와 같은 생소한 메세지를 받게 됩니다.
Error: Error #2136: SWF 파일 file:///Main.swf 에 유효하지 않은 데이터가 포함되어 있습니다.
at Test()[F:Test.as:23]
2. 따라서 순환참조라는 관점을 갖지 않고 그 코드를 쳐다봐도 전혀 알 수 없고 유효하지 않은 데이터라는 에러 메세지도 의미를 알 수 없습니다.
3. 결국 위에 설명한대로 해결 방법은 설계를 바꾸는 것만이 유일한 해결책인데 스스로를 초기화해야 할 경우도 있긴 합니다.
Vector와 순환참조
Vector의 경우 Vector.<type>을 통해 순환참조 가능성을 볼 수 있습니다. 즉 아래와 같은 경우입니다.
class Test{
private var tests:Vector.<Test> = new Vector.<Test>();
public function Test(){}
}
하지만 이 경우엔 문제없이 잘 굴러갑니다. 왜냐면 Vector는 new 되는 시점에는 Test를 생성하지 않기 때문이죠.
static을 이용한 순환참조 해소
static을 사용해도 class의 정의 내에서 해당 static 함수를 호출하는 이상 안에 new가 있어서는 안됩니다. 즉 아래와 같이 해도 동일하게 에러상황입니다.
class Test{
static public function init( test:Test ):void{
test.instance = new Test;
}
private var instance:Test;
public function Test(){
init();
}
}
왜냐면 init를 호출하여 new Test가 일어나는 과정이 Test의 정의 시 일어나 순환참조에 빠지기 때문이죠. 그렇다고 static으로 빠진 init함수가 의미가 없는건 아닙니다. 만약 생성자에서 init()를 호출하지만 않는다면 정상적으로 컴파일 됩니다. static의 경우 해당 클래스의 인스턴스에 있는 private에도 전부 접근이 가능합니다. 따라서 클래스가 구지 자신의 private를 노출하는 세터함수를 제공하지 않아도 됩니다. 즉 static을 사용하지 않는 경우 instance를 설정하게 하는 방법은 아래와 같은 세터입니다.
class Test{
private var instance:Test;
public function setInstance( $t:Test ):void{
instance = $t;
}
public function Test(){}
}
하지만 이 경우 반드시 Test의 인스턴스를 받아와야합니다. 즉 아래와 같은 호스트 코드가 됩니다.
var t:Test = new Test;
t.setInstance( new Test ); //호스트에서 new를 해서 넘겨준다.
하지만 static은 내부에서 new를 정의할 수 있으니까 상관없습니다.
class Test{
static public function setInstance( $t:Test ):void{
$t.instance = new Test;
}
private var instance:Test;
public function setInstance( $t:Test ):void{
instance = $t;
}
public function Test(){}
}
var t:Test = new Test;
Test.setInstance( t );
as3의 봉인된 클래스의 구조가 Tca를 따로 잡아주는걸 생각해보면 static구문 안에는 왜 new를 이용한 자기 자신 생성이 허용되는지 쉽게 이해할 수 있습니다.
결론
클래스 순환참조 정의는 설계를 복잡하게 해야 하는 큰 어플리케이션에서 종종 만나볼 수 있는 문제입니다. 정말 이해할 수 없는 에러가 나오거나 분명히 컴파일 에러가 없는데 해당 클래스의 정의를 찾을 수 없습니다 따위의 에러를 만나시면 이 문제를 한 번쯤 떠오실 필요가 있을지 모릅니다.