노무현 대통령 배너

AS3.0 클래스 구조의 여러가지 상황에서 부모, 자식 객체의 참조 방법

by 세계의끝 on 3.29, 2010, under 고수들은 가르쳐주지 않는 AS3.0 입문

fla 파일의 프레임에 코드를 잔뜩 늘어놓았던 시절에는, 모든 함수와 변수의 스코프가 동일하므로 참조하는 방법에 대한 고민을 거의 하지 않아도 좋았습니다. 말하자면 fla 프레임의 코드는 하나의 클래스이면서, 생성자 함수외에는 존재하지 않는 클래스라고 볼 수 있을겁니다.

그러나 클래스를 이용하여 객체를 생성하는 경우에는 그렇게 단순하게만 돌아가지는 않습니다. 필연적으로, 원하지 않아도 부모 객체와 자식 객체를 생성하게 되는데, 몇 가지 규칙을 알고 있어야만 올바른 객체간 통신을 할 수 있게 됩니다.

이 내용은 AS2.0을 다루던 초보 개발자들이 AS3.0에 와서 가장 먼저 부딪히는 부분이며, 가장 많이 헤메는 부분이기도 합니다. 이제까지 MovieClip 이나 Sprite 객체를 만들어 마우스 클릭 이벤트만 걸고 노는 것에만 익숙했던 분들은 이 포스트를 정독하시면 많은 것을 얻으실 수 있을겁니다. 그런 즉슨 이 포스트는 AS3.0 에 막 입문한 초보분들을 위한 포스트 입니다.

아래에 설명한 방법들은 객체간 통신을 하기 위해 사용하는 여러가지 방법들 입니다. 물론 이 외에도 다른 방법이 있을 수 있습니다만, 가장 사용빈도가 높고 반드시 알아야 하는 몇 가지 방법을 소개하겠습니다.

A. 부모 객체가 자식 객체를 참조

객체간 관계에서 가장 흔한 상황이 되겠습니다. 부모 객체는 다수의 자식 객체를 가질 수 있고, 자식 객체에 public 으로 선언된 변수나 함수에 대해서는 자식 객체의 객체이름을 통하여 참조할 수 있다는것을 모두들 잘 알고 계실겁니다. 코드로 보면 아래와 같죠.
아래는 부모 객체를 생성한 부모 클래스의 코드 입니다.

0
1
2
3
4
5
6
7
8
9
10
11
12
13
package
{
	import flash.display.Sprite;
 
	public class ParentClass extends Sprite
	{
		public function ParentClass()
		{
			var _childInstance:ChildClass = new ChildClass();
			addChild( _childInstance );
			_childInstance.doSomething();
		}
	}
}

아래는 자식 객체를 생성한 자식 클래스의 코드 입니다.

0
1
2
3
4
5
6
7
8
9
10
11
package
{
	import flash.display.Sprite;
 
	public class ChildClass extends Sprite
	{
		public function doSomething():void
		{
			trace( "하위 클래스 : do something in ChildClass !!" );
		}
	}
}

B. 자식 객체가 부모 객체를 참조 – 첫 번째 방법

자, 그럼 거꾸로 자식 객체가 부모 객체에 있는 함수나 변수를 참조하려면 어떻게 하면 될까요? 자식 객체에서 parent 키워드를 사용하면 자신을 addChild 한 부모에게 접근할 수 있습니다만, DisplayObject 상속구조상의 메서드, 속성만 사용 가능할뿐 부모 객체에 정의된 것을 사용할 수는 없습니다.
이것의 논리적 원인을 따져보자면 이렇습니다. 우리가 부모, 자식 객체를 맺어주는데 사용한 메서드는 addChild() 입니다. addChild() 메서드는 Sprite 클래스가 바로 윗 단계에서 상속한 DisplayObjectContainer 클래스의 메서드죠. 아래의 레퍼런스 스샷에서 보시는 바와 같이 parent 를 사용해도 반환되어 나오는 것은 DisplayObjectContainer 입니다.

DisplayObjectContainer 의 읽기 전용 속성인 parent. 반환되어 나오는 것도 DisplayObjectContainer.

다시말해, DisplayObject 상속 체인 구조[01] 에서 부모와 자식 객체간의 결합은 DisplayObjectContainer 가 담당하고 있다는 것이죠… 라는 내용은 당연스러운 사실이지만, 무심코 사용한 parent 역시 DisplayObjectContainer 형(type)을 반환 하므로, DisplayObjectContainer 를 상속한 Sprite 도, 그 Sprite 를 상속한 개별 클래스(ParentClass) 역시 DisplayObjectContainer 까지의 메서드, 속성만 사용할 수 있습니다.[02]
약간 어려운가요? 논리적 원인을 따지자니 오히려 어려워진 감이 있는데, 어쨌건 ParentClass 의 메서드나 속성을 사용하기 위해서는 ParentClass로 형 변환을 해줘야 한다는 것만 이해하고 있다면 오케이 입니다.

아래는 부모 객체를 만드는데 사용한 클래스 입니다.

0
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
package
{
	import flash.display.Sprite;
 
	public class ParentClass extends Sprite
	{
		public function ParentClass()
		{
			var childInstance:ChildClass = new ChildClass();
			addChild( childInstance );
		}
 
		public function doSomething():void
		{
			trace( "상위 클래스 : do something in ParentClass !!" );
		}
	}
}

아래는 자식 객체를 만드는데 사용한 클래스 입니다.

0
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
package
{
	import flash.display.Sprite;
	import flash.events.MouseEvent;
 
	public class ChildClass extends Sprite
	{
		public function ChildClass()
		{
			// 클릭을 위한 RoundRect
			this.graphics.beginFill( 0x336699 );
			this.graphics.drawRoundRect( 10, 10, 100, 100, 60 );
 
			this.buttonMode = true;
			this.addEventListener( MouseEvent.CLICK, clickHandler );
		}
 
		private function clickHandler( $e:MouseEvent ):void
		{
			trace( this.parent ); // 출력 : [object ParentClass]
 
			// this.parent 는 DisplayObject 구조의 메서드나 속성만을 사용할 수 있음.
			// 상위 클래스 형(type)으로 형변환(casting:캐스팅) 하면 public 멤버를 사용할 수 있음
			ParentClass( this.parent ).doSomething();
 
			// 아래는 컴파일 에러
			// this.parent.doSomething();
		}
	}
}

trace( this.parent ) 출력 결과에서 보는것과 같이 parent 는 확실히 부모 객체 자체 까지는 참조 가능하지만 parent 가 가리키고 있는 것은 부모 객체의 DisplayObjectContainer 형태이기 때문에 부모 객체의 doSomething() 을 호출하게 되면 컴파일 에러가 발생합니다.
그래서 위와 같이 ParentClass로 형변환을 해야 부모 객체의 메서드를 사용할 수가 있습니다.

또한 형변환은 아래와 같이 할 수도 있습니다.

0
1
var parentInstance:ParentClass = this.parent as ParentClass;
parentInstance.doSomething();

C. 자식 객체가 부모 객체를 참조 – 두 번째 방법

자식 객체에서 부모 객체의 public 메서드를 사용하는 방법은 B 케이스와 같이 형변환 하는 방법 외에도 한가지 더 있습니다. 부모객체가 자식객체에게 부모 객체 자신을 참조할 수 있는 참조(레퍼런스)를 넘겨주는 방법입니다. 아래와 같이 말이죠.

0
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
package
{
	import flash.display.Sprite;
 
	public class ParentClass extends Sprite
	{
		public function ParentClass()
		{
			var childInstance:ChildClass = new ChildClass();
			addChild( childInstance );
 
			// 이렇게 자식 객체에게 this(부모 자신의 객체 레퍼런스)를 넘겨줌
			childInstance.updateParent( this );
		}
 
		public function doSomething():void
		{
			trace( "상위 클래스 : do something in ParentClass !!" );
		}
	}
}
0
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
package
{
	import flash.display.Sprite;
	import flash.events.MouseEvent;
 
	public class ChildClass extends Sprite
	{
		private var _parent:ParentClass;
 
		public function ChildClass()
		{
			// 클릭을 위한 RoundRect
			this.graphics.beginFill( 0x336699 );
			this.graphics.drawRoundRect( 10, 10, 100, 100, 60 );
 
			this.buttonMode = true;
			this.addEventListener( MouseEvent.CLICK, clickHandler );
		}
 
		/**
		 * 부모 객체의 레퍼런스를 저장함 (부모 객체에서 호출)
		 * @param	부모객체를 인자로 받아 전역변수에 저장
		 */
		public function updateParent( $parent:ParentClass ):void
		{
			this._parent = $parent;
		}
 
		private function clickHandler( $e:MouseEvent ):void
		{
			this._parent.doSomething();
		}
	}
}

마치…부모가 통장과 도장과 비밀번호를 자식에게 알려주는 것과 같은 느낌이네요.

D. 자식 객체가 부모 객체에게 이벤트를 보냄

위의 B, C 케이스를 이벤트로 통신을 하는 형태로 바꿔보면 어떨까요? 흔히 사용하는 MouseEvent 도 이에 해당하지만, MouseEvent 같이 사용자의 인터랙션에 의한 이벤트는, 이벤트를 발생하는 객체(dispatcher:디스패쳐)가 하는일이 없는것처럼 보이므로 아래와 같이 일부러 디스패쳐를 사용하는 상황을 구성해보았습니다.

0
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
package
{
	import flash.display.Sprite;
	import flash.events.Event;
 
	public class ParentClass extends Sprite
	{
		public function ParentClass()
		{
			var childInstance:ChildClass = new ChildClass();
			addChild( childInstance );
			childInstance.addEventListener( Event.COMPLETE, animationComplete );
		}
 
		private function animationComplete( $e:Event ):void
		{
			trace( "부모 객체에서 자식 객체에 걸어놓은 Event.COMPLETE 를 캐치함 !!" );
		}
	}
}

자식 객체를 만든 클래스 입니다. 클릭을 하게 되면 객체 자신에게 Event.ENTER_FRAME 을 걸게 되고 매 프레임마다 enterFrameHandler() 핸들러를 실행하게 됩니다. 자세한 내용은 주석을 참고하세요.

0
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
package
{
	import flash.display.Sprite;
	import flash.events.Event;
	import flash.events.MouseEvent;
 
	public class ChildClass extends Sprite
	{
		public function ChildClass()
		{
			// 클릭을 위한 RoundRect
			this.graphics.beginFill( 0x336699 );
			this.graphics.drawRoundRect( 10, 10, 100, 100, 60 );
 
			this.buttonMode = true;
			this.addEventListener( MouseEvent.CLICK, clickHandler );
		}
 
		private function clickHandler( $e:MouseEvent ):void
		{
			this.addEventListener( Event.ENTER_FRAME, enterFrameHandler );
		}
 
		private function enterFrameHandler( $e:Event ):void
		{
			// x 가 200이 될 때까지 감속운동 (easeOut)
			this.x += ( 200 - this.x ) * 0.1;
 
			// 200이 되면 엔터프레임을 중단하고 Event.COMPLETE 를 dispatchEvent() 함
			if ( Math.round( this.x ) >= 200 )
			{
				this.removeEventListener( Event.ENTER_FRAME, enterFrameHandler );
				this.dispatchEvent( new Event( Event.COMPLETE ) );
			}
		}
	}
}

실제 작업에서는 이런 종류의 코드를 작성해야 하는 경우가 많을 겁니다. 자식 객체에서 발생한 어떤 상황을 모두 실행한 후, 그 결과를 부모에게 알리고 싶은거죠.

E. 부모 객체가 자식 객체에게 이벤트를 보냄

부모 객체가 자식 객체에게 이벤트를 보낼 경우도 있겠죠? 부모 객체 입장에서는 자식 객체의 public 멤버들은 모두 참조가 가능하므로 자식 객체의 메서드나 변수를 직접 참조하여 데이터를 전달하거나 메서드를 호출할수도 있습니다. 바로 A케이스와 같은거죠.
그러나 A 케이스의 경우 부모 객체가 자식객체의 public 멤버들이 뭔지 알고 있어야 합니다. 물론 Flash(Flex) Builder 나 FlashDevelop, 또는 FDT, 그리고 Flash CS5 같이 코드 힌트를 지원하는 에디터에서는 자식객체가 가지고 있는 public 멤버들을 볼 수 있지만, 여기서 말하는 것은 그런 기능적인 부분을 이야기 하는 것이 아니라 객체지향 프로그래밍에서의 설계 원칙의 문제입니다. 예를 들어보죠.

만약 부모 객체와 자식 객체가 서로 분리되야할 상황이 생겼습니다. A나 B 케이스는 부모 자식객체간에 서로 직접 참조하고 있으므로, 이 두 객체를 만든 클래스가 분리되는 순간 서로를 참조하고 있던 부분은 모두 컴파일 에러가 발생합니다. 이런 것을 보고 두 객체간 강한 결합을 했다고 표현하죠.
한편, D나 E 케이스는 이벤트를 통해 이 결합을 다소 약하게 만들 수 있습니다. 데이터를 이벤트를 통해 전달하고 이벤트를 보냈다고 어떤 행동을 강제하지 않습니다. 이벤트를 보내는 입장에서는 보내는 걸로 역할을 다 한 것이고 받는 입장에서는 받긴 받았지만 이것을 실행할지 말지는 자신이 결정할 수 있기 때문입니다.

0
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
package
{
	import flash.display.Sprite;
	import flash.events.Event;
	import flash.events.MouseEvent;
 
	public class ParentClass extends Sprite
	{
		private var _roundRect:Sprite = new Sprite;
		private var _childInstance:ChildClass = new ChildClass;
 
		public function ParentClass()
		{
			// 클릭을 위한 RoundRect
			_roundRect.graphics.beginFill( 0x996633 );
			_roundRect.graphics.drawRoundRect( 10, 10, 100, 100, 60 );
 
			_roundRect.buttonMode = true;
			_roundRect.addEventListener( MouseEvent.CLICK, clickHandler );
 
			addChild( _roundRect );
 
			addChild( _childInstance );
		}
 
		private function clickHandler( $e:MouseEvent ):void
		{
			// _roundRect 의 MouseEvent.CLICK 를 캐치하면
			// _childInstance 객체에 MouseEvent.ROLL_OUT 이벤트를 보냄
			_childInstance.dispatchEvent( new MouseEvent( MouseEvent.ROLL_OUT ) );
		}
	}
}

부모 객체에서 사용자의 인터랙션에 의해 발생한 MouseEvent.CLICK과는 다른 이벤트를 고르느라 자식 객체에는 MouseEvent.ROLL_OUT 를 보내는 다소 엉뚱한 예제 입니다만, 이벤트는 이렇게도 할 수 있다는것을 보여주는 예시라고 볼 수도 있겠습니다.

0
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
package
{
	import flash.display.Sprite;
	import flash.events.MouseEvent;
 
	public class ChildClass extends Sprite
	{
		public function ChildClass()
		{
			// ChildClass 객체의 존재를 확인하기 위한 RoundRect
			this.graphics.beginFill( 0x336699 );
			this.graphics.drawRoundRect( 120, 10, 100, 100, 60 );
 
			this.buttonMode = true;
			this.addEventListener( MouseEvent.ROLL_OUT, rollOutHandler );
		}
 
		private function rollOutHandler( $e:MouseEvent ):void
		{
			trace( "부모 객체가 자식 객체에게 dispatchEvent() 한 MouseEvent.ROLL_OUT 을 캐치함 !!" );
		}
	}
}

부모 객체(왼쪽에 그려진 RoundRect 그림)를 클릭하면 자식 객체에게 MouseEvent.ROLL_OUT 이벤트를 발생시킬수가 있습니다. 또한 자식객체에는 addEventListener( MouseEvent.ROLL_OUT ) 가 걸려 있으므로 자식 객체를 표시한 오른쪽 RoundRect 그림에 마우스를 올렸다가 밖으로 빼도 동일한 이벤트가 발생하게 됩니다.

F. 이벤트 발생위치를 바꿈

E 케이스의 코드는 부모객체가 자식객체에게[03] dispatchEvent() 메서드를 사용한 것이지만, 반대로 부모객체 내부에서 dispatchEvent() 를 하고 자식객체에서 이것을 addEventListener() 할 수도 있습니다.

0
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
package
{
	import flash.display.Sprite;
	import flash.events.Event;
	import flash.events.MouseEvent;
 
	public class ParentClass extends Sprite
	{
		private var _roundRect:Sprite = new Sprite;
		private var _childInstance:ChildClass = new ChildClass;
 
		public function ParentClass()
		{
			// 클릭을 위한 RoundRect
			_roundRect.graphics.beginFill( 0x996633 );
			_roundRect.graphics.drawRoundRect( 10, 10, 100, 100, 60 );
 
			_roundRect.buttonMode = true;
			_roundRect.addEventListener( MouseEvent.CLICK, clickHandler );
 
			addChild( _roundRect );
 
			addChild( _childInstance );
		}
 
		private function clickHandler( $e:MouseEvent ):void
		{
			// 이번에는 부모 객체인 this 에 dispatchEvent() 를 함
			// _childInstance 객체에서 addEventListener() 하고 있건 말건 무조건 이벤트가 발생
			this.dispatchEvent( new Event( Event.SELECT ) );
		}
	}
}

아래는 자식 객체를 만드는 클래스 입니다. 자세한 내용은 주석을 참고하세요.

0
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
package
{
	import flash.display.Sprite;
	import flash.events.Event;
 
	public class ChildClass extends Sprite
	{
		public function ChildClass()
		{
			// ChildClass 객체의 존재를 확인하기 위한 RoundRect
			this.graphics.beginFill( 0x336699 );
			this.graphics.drawRoundRect( 120, 10, 100, 100, 60 );
 
			// 이 객체가 addChild() 되면 핸들러 실행
			// 즉, 어떤 객체의 자식 객체가 된 상황
			this.addEventListener( Event.ADDED, addedHandler );
		}
 
		private function addedHandler( $e:Event ):void
		{
			// 어떤 부모 객체인지는 모르겠지만 부모 객체에 addEventListener() 를 걸어놓음
			// addEventListener() 는 ParentClass 형(type)이 아니더라도 사용할 수 있으므로 캐스팅이 필요 없음
			this.parent.addEventListener( Event.SELECT, selectHandler );
		}
 
		private function selectHandler( $e:Event ):void
		{
			trace( "부모 객체의 이벤트를 자식 객체에서 캐치함 !!" );
		}
	}
}

또는 반대로, 자식 객체에서 this.addEventListener() 하지 않고, 부모객체에서 이벤트가 일어나도록this.parent.dispatchEvent() 한 후, 부모객체가 this.addEventListener() 까지 하게 할 수도 있습니다. 이벤트가 발생(dispatchEvent)하는 위치만 다를뿐 부모, 자식 객체간은 서로를 자유롭게 참조할 수 있으므로 addEventListener() 메서드는 어디나 걸어 놓을 수 있는 것이죠.

이렇게 부모, 자식 객체간의 여러가지 상황에서 서로를 참조하는 방법에 대해 알아보았습니다. 어떤 방법이 절대적으로 옳지 않으므로 상황에 따라 적절하게 사용하면 됩니다. 또한, 어떤 방법이 절대적으로 옳은건 아니지만, 모든 방법을 제대로 이해하고 있어야 합니다. 그 만큼 객체 지향 프로그래밍에서 가장 기초적이고 중요한 내용입니다.

이해가 잘 안되는 케이스가 있다면, 빈 fla 파일에 ParentClass 를 도큐먼트 클래스로 설정하고 컴파일을 해 보세요.

* * *

만약 이벤트와 함께 데이터를 보내야 할 필요가 있을 경우에는 커스텀 이벤트를 사용하면 됩니다. 커스텀 이벤트는 이 포스트의 범위를 벗어나는 내용이므로 “이벤트에 뭔가 같이 보내보자 – 커스텀 이벤트 만들고 사용해보기” 를 읽어보시기 바랍니다.

또한, 객체간 더욱 약한 결합을 구현하기 위한 별도의 방법으로는 옵저버 디자인 패턴을 이용하는 방법이 있습니다. “Observer Pattern 옵저버 패턴 – 이벤트 디스패처를 이용해 구현” 을 참고하세요.

이 포스트는 부모, 자식 객체간 참조 방법을 다룬 글이었습니다. 객체가 아닌 상위 클래스와 하위 클래스의 참조 방법에 대한 글 “AS3.0 클래스의 상속(extends)구조에서 상위, 하위 클래스의 메서드 호출방법” 도 초보 개발자 여러분들에게 많은 도움이 될 것입니다.

이 글을 복사해서 퍼가시는건 허용하지 않습니다. 글의 주소를 다른곳에 알려주시는 것은 환영합니다.
  1. Object – EventDispatcher – DisplayObject – InteractiveObject – DisplayObjectContainer – Sprite []
  2. Object, EventDispatcher, DisplayObject, InteractiveObject, DisplayObjectContainer 의 메서드, 속성만 사용할 수 있고, Sprite 이하의 메서드, 속성은 사용할 수 없습니다. []
  3. 즉, 자식객체의 입장에서 보면 this.dispatchEvent() 된 것입니다. []

관련된 글

:, , , , , , , , , , , , ,

8 Comments for this entry

  • semomuriNo Gravatar

    안녕하세요^^
    객체간의 통신에 대한 문제는 항상 고민하고 있었던 부분인데..
    어플리케이션 단위가 커지면 커질수록 저도 모르게 커스텀 이벤트의 남발을 하게 되는경우도
    있더군요.
    이벤트로 먼가를 주고 받지 않을경우에는 굳이 커스텀 이벤트를 쓰지 않구 내장 이벤트 타입으로만 주고 받는편이 좋을지…
    Tweener 형태에서 많이 쓰이는 오브젝트형태로 만들어 통째루 날려 버리는 방법 또한 고민되어 지는 부분이더군요.
    오늘 하루도 좋은 하루 되시고 좋은 정보에 대한 다시 한번 감사의 말씀 드립나다^^

    • 세계의끝No Gravatar

      선택의 문제겠죠.
      커스텀 이벤트는 전달해야할 데이터가 있을경우엔 확실히 만들수밖에 없겠고요.
      Event.COMPLETE 정도만 보내도 무방하다면 그것도 역시 불필요한 커스텀 클래스를 만들필요는 없는거죠.

      그런데 한편으로는 보내는 데이터가 없어도 이벤트의 성격을 명확히 하고자 커스텀 이벤트를 만들기도 합니다. 그러니까…
      SortEvent 라던지, GalleryEvent 같은것 말이죠.
      아무래도 다른사람이(물론 코드를 작성한 본인조차도) 코드를 읽기가 수월해지겠죠?

      방문 고맙습니다 ^^

  • No Gravatar

    안녕하세요. 좋은 글 잘 읽고 갑니다. ^^

  • 티오엠No Gravatar

    매번 고민하던 내용이었는데 잘 정리해주셔서 도움이 많이 되었습니다.

  • solNo Gravatar

    안녕하세요 구글 검색해서 이것저거 보다가 여기까지 왔습니당.

    아직도 가물가물해서 읽고 또읽고 가야하네요 ㅠㅠ

    좋은글 감사합니다!

    • 세계의끝No Gravatar

      읽어보는 것도 물론 중요하지만, 간단하게나마 코딩을 직접 해보는것이 훨씬 빠르게 이해 될겁니다.
      마치… 손가락으로 체득하는 느낌이랄까요? ^^

Leave a Reply

Looking for something?

Use the form below to search the site:

Still not finding what you're looking for? Drop a comment on a post or contact us so we can take care of it!