이번 시간에는 안정적인 프로그램을 만들기 위한 또 다른 포인트인 '참조무결성' 에 대해 알아보도록 하자.

참조(Referencing) 는 어떤 개체 a 가 다른 개체 b 의 존재에 대해 기억했다가, b 가 제공하는 서비스를

사용하는 것을 말한다. 여기서 개체는 여러가지가 될 수 있다. 같은 프로그램 내에서 동작하는 메모리

상의 오브젝트를 참조할 수도 있는 것이고, 네트웍 저편에 있는 오브젝트의 핸들을 참조할 수도 있는

것이고, DB 에 들어있는 레코드에 대한 ID 를 참조할 수도 있는 것이다.


MMORPG 에서 나타나는 몇가지 참조의 예를 들어서 생각해보자.

1. 플레이어가 어떤 오브젝트를 클릭한 경우, 예를 들면 몬스터를 클릭한 경우 몬스터를 향해 다가가서

   사정거리 안에 들어오면 공격을 하는 것이 일반적이다. 만약 몬스터가 이동하게 되더라도 플레이어는

   계속 몬스터를 따라가야만 한다. 이런 상황은 플레이어가 몬스터를 계속 참조하고 있는 상황이다.

   몬스터가 아니라 아이템이나 다른 오브젝트를 클릭했을 때도 마찬가지다.

2. 플레이어가 몬스터를 공격해서 몬스터를 죽이게 되면, 플레이어가 경험치를 얻게 된다. 그런데 여러

   명의 플레이어가 하나의 몬스터를 함께 공격하게 되면 일반적으로 공격의 공헌도, 즉 선빵을 누가

   때렸는가, 얼마나 많은 데미지를 입혔는가, 얼마나 많은 공격을 받았는가등에 의해 경험치를 분배해

   주어야 한다. 올바르게 분배가 되려면, 각 몬스터는 스스로 '나를 공격했던 플레이어들은 누구인가?'

   에 대한 리스트를 갖고 있다가, 자신이 죽는 순간에 그 리스트에 따라서 경험치를 나눠주어야 하는

   것이다. 이것은 복수의 참조, 즉 참조리스트의 경우이다.

이외에도 참조가 생기는 상황은 수없이 많다. 일반적으로 다수의 오브젝트가 서로 상호작용을 하는 거의

모든 상황이 참조관계로 볼 수 있을 것이다.

프로그래밍을 하면서 생기는 오류의 상황은 이런 참조에서의 예외상황이 상당부분을 차지한다. 어떤

경우가 참조의 예외상황이라고 할 수 있는 것일까?

1 번의 경우 플레이어가 몬스터를 향해 가고 있는데 (즉 몬스터의 좌표를 얻어서, 그 좌표를 향해 이동)

몬스터를 향해 가기 전에 만약에 다른 플레이어가 그 몬스터를 죽여버리면 어떻게 될까? 상식적으로

처리하려면, 중간에 자신이 참조하던 오브젝트가 소멸하게 되면 그 참조관계는 무효가 되고, 더 이상

몬스터를 향해 이동하는 행동을 하지 말아야 할 것이다.

2 번의 경우 플레이어가 열심히 몹을 잡다가 플레이어가 장렬히 전사를 했는데 다른 플레이어가 와서

그 몬스터를 어부지리로 죽였을 때의 경우를 생각해볼 수 있다. 몬스터의 입장에서는 자기가 경험치를

나눠줘야 할 플레이어들의 참조리스트에 있는 플레이어들에게 경험치를 주려고 하는데, 이미 처음의

플레이어는 소멸해버린 경우 역시 참조가 무효화되는 것이다.



참조 무결성은 이러한 참조의 상황들에서 각 예외상황이 발생해서 참조가 무효화되었을 때 안정적으로

예외처리를 해야만 보장될 수 있는 것이다.

참고로 예외와 에러는 엄밀히 구분해야 하는 것이다. 에러는 프로그램이 올바르게 디자인 되었을 때에는

절대 일어나서는 안되는 것이며, 예외는 프로그램이 올바르게 디자인되었더라도, 유저의 조작등에 의해

얼마든지 일어날 수 있는 정상적인 동작의 일부분인 것이다.


참조를 구현 하는 가장 간단하고도 불안한 방법은 직접 포인터를 저장하는 것이다. 오브젝트 a 가 b 를

참조해야 할 경우, a 가 b 의 포인터를 멤버변수로 갖고 있는 것이다.

그런데 만약 위와 같은 경우로 해서 b 가 사라져버리게 될 경우에는 어떻게 될까? b 가 이미 delete 되어

버렸다면, b 에 대해 어떤 멤버함수를 부르게 될 때 잘못된 연산을 수행하게 되고 프로그램은 다운되어

버리게 될 것이다. 상당히 많은 수의 프로그램 다운이 위와 같은 이유로 일어나는 잘못된 참조인 것이다


이것에 대한 해결책은 몇가지가 있다.

첫번째로 포인터 대신 다른 참조 방식을 이용하는 것이다. 대표적인 다른 참조방식이 핸들(HANDLE)

이다. 핸들은 중앙집중적으로 관리되는 오브젝트 매니저를 통해서 발급받고, 핸들을 통해 다른 오브젝트

를 참조할 때에도 오브젝트 매니저가, 그 핸들의 유효성을 먼저 검사한 후에 비로소 포인터를 발급해주는

것이다. 이 방법을 쓸 때 주의해야 할 점은, 새로 생긴 오브젝트가 똑같은 핸들번호를 갖고 있다면

엉뚱한 참조를 할 수도 있다는 점이다. 예를 들어서 오브젝트 a 가 오브젝트 b 의 핸들을 갖고 있었는데

그 핸들의 번호가 11 이었다고 하자. 그런데 오브젝트 b 가 소멸되었다가 다른 새로운 오브젝트 c 가

생겨났는데 핸들 11 을 발급받았다고 하면 나중에 오브젝트 a 는 11 이라는 핸들을 갖고 b 를 참조하는

것이 아니라 c 를 참조하게 되어버리는 상황이 발생할 수 있는 것이다.


두번째로는 레퍼런스 카운팅과 좀비상태를 이용하는 것이다. 각각의 오브젝트는 참조를 받을 때마다

(dereference) 자신이 참조되고 있다는 카운트를 증가시키고, 더 이상 참조를 할 필요가 없게 되었을

때에는 카운트를 감소시킨다. 오브젝트가 소멸되어야 할 경우인데 아직 참조 카운트가 0 이 아니라면

즉, 누군가 아직 자신을 참조하고 있다면 자신을 zombie 상태로 두고 소멸을 지연시킨다. 나중에 참조

카운트가 0 이 되었을 때에야 완전히 소멸되도록 오브젝트 매니저가 감시하는 (garbage collection)

방식을 말하는 것이다. 만약에 자신이 참조하려는 대상이 좀비상태라면, 참조를 끊어주고 레퍼런스

카운트를 감소시켜주면 될 것이다.

이것은 위와 같은 잘못된 참조가 일어날 가능성이 없지만, 명시적으로 참조를 걸고 끊고 하는 절차가

필요하다는 불편함이 있다. 하지만 안정적인 프로그램을 만들기 위해서는 불가피한 투자가 될 것이다

템플릿을 다룰 줄 아는 사람이라면 스마트 포인터를 응용해서 자동으로 참조를 관리하게 만들 수도

있을 것이다. 하지만 그럼에도 불구하고, 스마트 참조에 NULL 을 대입해서 명시적으로 참조를 끊어

주어야 할 필요는 여전히 남는다. 왜냐하면 주로 참조는 함수안에서만 쓰는 것이 아니라, 멤버변수로

오랫동안 보관하게 되기 때문이다.

imcgames 의 김학규입니다