실제 GameObject 생성/삭제

목차

1. 요구 사항

2. 흐름도

3. 구현

        3.1. 배치된 오브젝트 저장 구조

       3.2. 빈 인덱스 탐색

       3.3. 구조물 회전 조회

       3.4. 구조물 배치

       3.5. 구조물 제거

       3.6. 전체 구조물 초기화

       3.7. 비활성화 시 Tween 정리

4. 개발 의도

1. 시스템 요구 사항

Grid 기반 건축 시스템에서 실제 구조물을 화면에 배치하는 단계는 단순히 오브젝트를 하나 생성하는 것으로 끝나지 않는다.

건축 시스템은 GridData를 통해 논리적인 배치 상태를 관리하지만, 플레이어가 실제로 보게 되는 것은 씬에 존재하는 GameObject이다.

따라서 건축 시스템에는 논리 데이터와 실제 오브젝트를 연결해 주는 실행 계층이 반드시 필요하다.

이 계층은 단순 생성 기능만 제공해서는 부족하다.

어떤 구조물이 몇 번째로 생성되었는지, 나중에 제거할 때 어떤 GameObject를 참조해야 하는지, 현재 회전 상태는 무엇인지까지 함께 관리할 수 있어야 한다.

특히 이 프로젝트에서는 PlacementGridData가 오브젝트의 위치와 점유 상태를 저장할 뿐만 아니라, 실제 씬 오브젝트와 연결되는 인덱스를 함께 저장하고 있으므로, StructurePlacer는 안정적인 참조 키를 반환하는 구조여야 한다.

그렇지 않으면 Grid 데이터와 실제 GameObject 사이의 연결이 끊기게 된다.

또한 건축 시스템은 배치와 제거가 반복적으로 일어나기 때문에, 단순히 리스트의 뒤에 계속 오브젝트를 추가하는 방식보다는 비어 있는 슬롯을 재사용할 수 있는 관리 구조가 필요하다.

그래야 제거 후 다시 배치하는 과정에서도 인덱스가 불필요하게 커지지 않고, 참조 관리가 단순해진다.

이와 함께 구조물 생성과 제거 시 시각적인 연출이 포함되면 사용자 경험이 더 좋아지므로, 구조물 생성 시 스케일 애니메이션, 제거 시 축소 애니메이션 같은 처리도 함께 지원할 수 있어야 한다.

2. 흐름도

PlacementManager.PlaceStructureAt()

   ↓

StructurePlacer.PlaceStructure()

   ↓

빈 인덱스 탐색

   ↓

GameObject 생성 및 부모 설정

   ↓

위치 / 회전 적용

   ↓

애니메이션 실행

   ↓

인덱스 반환

   ↓

PlacementGridData에 인덱스 저장

구조물 제거 요청

   ↓

StructurePlacer.RemoveObjectAt()

   ↓

Tween 종료

   ↓

축소 애니메이션 실행

   ↓

Destroy()

   ↓

해당 슬롯 null 처리

이 시스템의 흐름은 PlacementManager와 밀접하게 연결되어 있다.

배치 단계에서는 PlacementManager가 StructurePlacer에 prefab, 위치, 회전값을 전달하고, StructurePlacer는 실제 GameObject를 생성한 뒤 그 오브젝트가 저장된 인덱스를 반환한다.

PlacementManager는 이 인덱스를 다시 PlacementGridData에 기록함으로써, Grid 논리 데이터와 실제 오브젝트를 연결한다.

제거 단계에서는 반대로 PlacementGridData가 가지고 있는 인덱스를 사용해 StructurePlacer가 실제 GameObject를 찾고 제거한다.

이 과정에서 단순한 Destroy가 아니라 DOTween을 이용한 축소 애니메이션을 먼저 실행한 뒤 제거하도록 되어 있어, 시스템 상태 변화가 시각적으로 자연스럽게 전달된다.

또한 전체 초기화 시에는 리스트 안의 모든 오브젝트를 순회하며 씬 오브젝트와 참조를 동시에 정리한다.

3. 구현

3.1. 배치된 오브젝트 저장 구조
[SerializeField]
List<GameObject> placedObjects = new List<GameObject>();

[SerializeField]
private float scalingDelay = 0.3f, destroyDelay = 0.1f;

이 필드들은 StructurePlacer의 핵심 저장 구조와 연출 시간을 정의한다.

placedObjects는 현재 씬에 배치된 구조물의 실제 GameObject 참조를 저장하는 리스트이다.

이 리스트의 중요한 의미는 단순한 컬렉션이 아니라, 인덱스가 구조물의 참조 키 역할을 한다는 것이다.

PlacementManager가 구조물을 배치하면 StructurePlacer는 이 리스트 안에 오브젝트를 저장하고, 해당 위치의 인덱스를 반환한다.

이후 GridData는 그 인덱스를 저장하고 있다가, 제거나 이동 시 다시 사용한다.

따라서 이 리스트는 단순히 화면에 있는 오브젝트를 모아두는 용도가 아니라, 논리 데이터와 씬 오브젝트를 연결하는 중간 맵 역할을 한다.

[SerializeField]를 사용한 이유도 의미가 있다.

placedObjects는 private 수준으로 관리하고 싶지만, Unity Inspector에서 디버깅하거나 현재 배치 상태를 눈으로 확인할 수 있게 만들기 위해 직렬화한 것이다.

Unity의 SerializeField는 캡슐화를 유지하면서도 에디터에서 값을 볼 수 있게 해주는 기능이다.

장점은 디버깅과 에디터 가시성이 좋아진다는 점이고, 단점은 런타임 상태가 에디터에 노출되기 때문에 잘못 건드리면 혼란을 줄 수 있다는 점이다.

scalingDelay와 destroyDelay는 DOTween 애니메이션 지속 시간을 제어한다.

구조물 생성 시 Y축 스케일을 키우는 연출과 제거 시 Y축 스케일을 줄이는 연출에 각각 사용되며, 단순히 보기 좋게 만들기 위한 값이 아니라, 플레이어가 지금 배치되었거나 지금 제거되었을 때 상태 변화를 더 명확히 인식할 수 있도록 도와주는 UX 파라미터다.

3.2. 빈 인덱스 탐색
private int GetFreeIndex()
{
    int indexOfNull = placedObjects.IndexOf(null);
    if (indexOfNull > -1)
    {
        return indexOfNull;
    }
    placedObjects.Add(null);
    return placedObjects.Count - 1;
}

이 함수는 새로운 구조물을 저장할 위치를 찾는 역할을 한다.

단순히 placedObjects.Count를 반환해 맨 뒤에 계속 추가하는 방식이 아니라, 리스트 안에 이미 null로 비어 있는 슬롯이 있으면 그 위치를 재사용하는 구조를 택하고 있다.

이것이 중요한 이유는 구조물 제거 이후에도 인덱스를 효율적으로 관리할 수 있기 때문이다.

만약 제거된 구조물의 자리를 무시하고 계속 뒤에만 추가한다면, 플레이가 길어질수록 리스트 크기가 불필요하게 커지고, 인덱스가 실제 활성 오브젝트 수보다 훨씬 더 커지는 문제가 생긴다.

placedObjects.IndexOf(null)은 리스트 안에서 첫 번째 null 위치를 찾아준다.

C#의 List<T>.IndexOf는 특정 값을 가진 첫 번째 요소의 인덱스를 반환하고, 없으면 -1을 반환한다.

장점은 구현이 매우 단순하다는 점이다.

단점은 리스트를 선형 탐색하므로 오브젝트 수가 아주 많아질 경우 성능 비용이 생길 수 있다는 점이다.

그러나 건축 시스템에서 동시에 배치되는 구조물 수가 수만 단위가 아니라면, 단순성과 가독성을 우선한 현재 방식이 충분히 합리적이다.

null 슬롯이 존재하면 그 위치를 반환하고, 없으면 리스트 끝에 null을 하나 추가한 뒤 마지막 인덱스를 반환한다.

여기서 먼저 null을 추가하는 이유는 자리 예약 때문이다.

이후 실제 오브젝트 생성이 끝나면 이 인덱스 위치에 GameObject가 저장된다.

즉, 이 함수는 단순히 숫자를 반환하는 것이 아니라, 리스트 안에 새 구조물의 저장 자리를 확보하는 역할까지 함께 수행한다.

3.3. 구조물 회전 조회
public Quaternion GetObjectsRotation(int index)
{
    Quaternion rotationToReturn = Quaternion.identity;
    if (index >= 0 && index < placedObjects.Count && placedObjects[index] != null)
        rotationToReturn = placedObjects[index].transform.GetChild(0).rotation;
    return rotationToReturn;
}

이 함수는 특정 인덱스에 저장된 구조물의 회전값을 반환한다.

이 기능은 특히 이동 시스템에서 중요하다.

이미 배치된 구조물을 선택해 다른 위치로 옮길 때, 단순히 위치만 가져오는 것이 아니라 원래 회전 상태까지 유지해야 하기 때문이다.

그래서 PlacementManager의 이동 처리 로직에서는 StructurePlacer에서 이 함수를 호출해 기존 구조물의 회전값을 읽어 온다.

함수 시작에서 Quaternion.identity를 기본값으로 설정하는 이유는 안전한 초기값을 보장하기 위해서다.

Quaternion.identity는 Unity에서 회전이 전혀 없는 기본 회전을 의미한다.

즉, 조회가 실패하더라도 시스템이 null이나 잘못된 회전 데이터를 받지 않고, 최소한 기본 회전 상태라도 받도록 만드는 방어 코드다.

이후 조건문에서는 인덱스 범위를 검사하고, 해당 위치에 실제 오브젝트가 존재하는지도 확인한다.

index >= 0 && index < placedObjects.Count는 리스트 범위 밖 접근을 방지하기 위한 가장 기본적인 보호 처리이고, placedObjects[index] != null은 제거되어 빈 슬롯이 된 위치를 걸러내기 위한 조건이다.

실제 회전값은 transform.GetChild(0).rotation에서 가져온다.

이것은 단순히 루트 오브젝트의 회전을 보는 것이 아니라 첫 번째 자식의 회전을 참조하고 있다는 뜻인데, 현재 프로젝트의 구조물 프리팹이 루트 오브젝트를 컨테이너처럼 쓰고 실제 시각적 메쉬는 첫 번째 자식에 배치되어 있기 때문으로 해석할 수 있다.

즉, 이 코드는 프로젝트의 프리팹 구조를 반영한 구현이다.

일반적인 범용 코드는 아니지만, 현재 구조에 맞춘 실용적 설계다.

3.4. 구조물 배치
public int PlaceStructure(GameObject objectToPlace, Vector3 position, 
			Quaternion rotation, float yHeight, bool animate = true)
{
    int freeIndex = GetFreeIndex();
    GameObject newObject = Instantiate(objectToPlace);
    newObject.transform.SetParent(transform);
    Vector3 placementPosition = new Vector3(position.x, yHeight, position.z);
    newObject.transform.position = placementPosition;

    newObject.transform.GetChild(0).rotation = rotation;
    
    placedObjects[freeIndex] = newObject;
    if(animate)
    {
        newObject.transform.localScale = new Vector3(1, 0, 1);
        newObject.transform.DOScaleY(1, scalingDelay);
    }
    return freeIndex;
}

이 함수는 StructurePlacer의 핵심 함수이며, 실제 구조물을 씬에 생성하고 그 참조 인덱스를 반환한다.

PlacementManager의 PlaceStructureAt에서 이 함수를 호출하며, 여기서 반환된 인덱스가 PlacementGridData에 저장되어 이후 제거나 이동 시 참조 키로 사용된다.

즉, 이 함수는 단순 Instantiate가 아니라, 오브젝트 생성과 인덱스 기반 관리 시작점을 동시에 담당한다.

가장 먼저 GetFreeIndex()를 호출해 저장할 슬롯을 확보한다.

이 단계가 먼저 수행되는 이유는, 생성 이후 곧바로 어느 위치에 저장할지 확정되어야 하기 때문이다.

즉, 이 함수는 '자리 확보 → 오브젝트 생성 → 슬롯에 저장'이라는 흐름을 가진다.

그 다음 Instantiate(objectToPlace)로 실제 구조물을 생성한다.

Unity의 Instantiate는 프리팹이나 기존 오브젝트를 복제하는 함수이며, 씬 안에 새 오브젝트를 만들 때 가장 기본적으로 사용된다.

장점은 간단하고 직관적이라는 점이고, 단점은 런타임에 잦은 생성/삭제를 반복하면 비용이 클 수 있다는 점이다.

하지만 건축 배치는 본질적으로 이벤트성 동작이고, 즉시 사용자 반응이 필요한 상황이므로 현재 구조에서는 적절하다.

생성한 오브젝트는 곧바로 newObject.transform.SetParent(transform)을 통해 StructurePlacer의 자식으로 넣는다.

이 처리는 매우 중요하다.

부모를 명확히 두면 Hierarchy 관리가 쉬워지고, 나중에 전체 제거나 씬 정리 시 관련 오브젝트를 한 계층 아래에서 통합 관리할 수 있다.

Unity의 SetParent는 Transform 계층 구조를 설정하는 함수이며, 장점은 씬 정리와 그룹 관리가 쉬워진다는 점이다.

이후 위치를 설정할 때 yHeight를 별도로 사용한다.

Vector3 placementPosition = new Vector3(position.x, yHeight, position.z);

이 구조는 XZ 평면의 Grid 좌표를 기준으로 건축을 수행하면서도, Y축 높이는 별도로 제어할 수 있게 해준다.

현재 코드에서는 배치 시 0이 전달되지만, 향후 바닥 높이 차이나 다른 레이어 배치를 지원할 경우 확장성을 확보할 수 있다.

회전은 역시 newObject.transform.GetChild(0).rotation = rotation으로 적용한다.

이 부분은 앞서 GetObjectsRotation과 마찬가지로, 실제 시각적 회전 대상이 루트가 아니라 첫 번째 자식이라는 프로젝트 구조를 반영한 코드다.

그 다음 placedObjects[freeIndex] = newObject를 통해 확보해둔 슬롯에 참조를 저장한다.

이 줄이 매우 중요하다.

이 시점부터 해당 구조물은 단순한 씬 오브젝트가 아니라, StructurePlacer가 관리하는 인덱스 기반 오브젝트가 된다.

마지막 부분은 애니메이션 처리다.

if(animate)
{
    newObject.transform.localScale = new Vector3(1, 0, 1);
    newObject.transform.DOScaleY(1, scalingDelay);
}

여기서는 DOTween의 DOScaleY를 사용해 구조물이 아래에서 위로 자라나는 것처럼 보이는 연출을 만든다.

DG.Tweening은 Unity에서 자주 사용하는 트윈 라이브러리로, 위치, 회전, 스케일, 색상 등을 시간 기반으로 부드럽게 바꿀 수 있게 해준다.

장점은 애니메이션 코드를 매우 짧고 읽기 쉽게 만들 수 있다는 점이고, 단점은 외부 라이브러리에 의존하게 되며 런타임 중 관리가 필요하다는 점이다.

생성 직후 Y 스케일을 0으로 두고 DOScaleY(1, scalingDelay)를 호출하면, scalingDelay 시간 동안 Y축이 1까지 증가한다.

이 연출은 플레이어에게 지금 구조물이 새로 배치되었다는 피드백을 자연스럽게 전달하는 역할을 한다.

마지막으로 freeIndex를 반환함으로써, PlacementManager가 이를 GridData에 기록할 수 있게 한다.

3.5. 구조물 제거
public void RemoveObjectAt(int index)
{
    GameObject newObject = placedObjects[index];
    newObject.transform.DOKill();
    newObject.transform.DOScaleY(0, destroyDelay).OnComplete(()=> Destroy(newObject));
    placedObjects[index] = null;
}

이 함수는 특정 인덱스에 저장된 구조물을 제거하는 역할을 한다.

PlacementManager의 제거 로직에서는 PlacementGridData에서 가져온 인덱스를 StructurePlacer에 넘기고, 이 함수가 실제 씬 오브젝트를 없앤다.

즉, 배치의 반대 흐름에서 StructurePlacer가 수행하는 핵심 함수다.

먼저 placedObjects[index]를 가져와 newObject에 저장한다.

이후 가장 먼저 DOKill()을 호출한다.

newObject.transform.DOKill();

이 코드는 현재 오브젝트의 Transform에 걸려 있는 모든 DOTween 애니메이션을 종료하는 역할을 한다.

이 처리가 중요한 이유는, 구조물이 생성 직후 아직 확대 애니메이션 중일 수 있기 때문이다.

그런 상태에서 제거 애니메이션을 바로 시작하면 Tween이 겹치거나 예상하지 못한 상태가 나올 수 있다.

따라서 먼저 기존 Tween을 정리하고, 그 다음 제거 연출을 수행하는 것이다.

다음 줄은 제거 연출이다.

newObject.transform.DOScaleY(0, destroyDelay).OnComplete(()=> Destroy(newObject));

여기서는 Y축 스케일을 0까지 줄이는 애니메이션을 실행하고, 애니메이션이 끝나면 Destroy(newObject)를 호출한다.

즉, 구조물이 위에서 아래로 접히듯 사라지는 연출이다.

OnComplete는 DOTween에서 애니메이션 종료 후 실행할 콜백을 등록하는 함수다.

이 구조 덕분에 애니메이션이 끝나기 전에 오브젝트가 즉시 사라지는 문제를 막을 수 있다.

마지막으로 placedObjects[index] = null을 통해 리스트 슬롯을 비운다.

이 줄도 매우 중요하다.

씬 오브젝트 제거 자체는 Destroy로 처리되지만, 논리적으로는 이미 이 인덱스 슬롯을 더 이상 사용 중이 아님을 즉시 표시해야 하기 때문이다.

이렇게 해야 GetFreeIndex 가 이후 이 자리를 재사용할 수 있다.

즉 이 코드는 단순 null 대입이 아니라, 이 슬롯은 다시 사용 가능하다는 표시다.

3.6. 전체 구조물 초기화
public void ClearAllPlacedObjects()
{
    for(int i = 0; i < placedObjects.Count; i++)
    {
        if (placedObjects[i] != null)
        {
            Destroy(placedObjects[i]);
            placedObjects[i] = null;
        }
    }

    placedObjects.Clear();
    Debug.Log("All placed objects have been cleard");
}

이 함수는 현재 배치된 모든 구조물을 한 번에 제거하고, 내부 참조 리스트까지 완전히 비우는 역할을 한다.

이 함수는 저장 데이터 복원 전에 기존 상태를 비울 때나, 새 게임 시작 시 전체 초기화가 필요할 때 사용될 수 있다.

PlacementManager에서는 ClearAllPlacedObjects 함수를 호출한 뒤 gridData.Clear()를 함께 수행하여, 씬 상태와 Grid 논리 상태를 동시에 초기화하는 구조를 갖고 있다.

함수 내부의 주석이 말해주듯, 여기서 중요한 점은 단순히 placedObjects.Clear()만 하면 안 된다는 것이다.

리스트를 비우는 것은 C# 컬렉션 내부 참조만 제거할 뿐, 실제 씬의 GameObject는 그대로 남아 있을 수 있다.

그래서 먼저 루프를 돌며 각 오브젝트를 실제로 Destroy하고, 그 참조 슬롯도 null로 만든다.

그 다음에 리스트 자체를 Clear 한다.

이 처리 순서는 매우 중요하다.

먼저 실제 씬 오브젝트를 제거하지 않고 리스트만 비우면, 화면에는 구조물이 남아 있는데 시스템은 아무것도 없는 것처럼 보이는 불일치 상태가 된다.

그래서 이 함수는 단순 컬렉션 초기화가 아니라, '씬 오브젝트 제거 + 참조 제거 + 리스트 비우기' 를 한꺼번에 수행하는 리셋 함수라고 볼 수 있다.

3.7. 비활성화 시 Tween 정리
private void OnDisable()
{
    foreach (GameObject obj in placedObjects) 
    {
        if(obj != null)
            obj.transform.DOComplete();
    }
}

이 함수는 StructurePlacer가 비활성화될 때 실행되는 Unity 생명주기 함수다.

OnDisable은 MonoBehaviour가 꺼지거나 씬 전환, 오브젝트 비활성화가 일어날 때 호출된다.

이 시점에서 중요한 것은, 중간에 재생 중이던 Tween이 어정쩡한 상태로 남지 않도록 정리하는 것이다.

DOComplete 는 현재 Transform에 걸려 있는 Tween을 즉시 완료 상태로 만드는 DOTween 함수다.

DOKill()이 강제 종료라면, DOComplete는 끝까지 진행한 것으로 처리에 가깝다.

이 코드를 사용한 이유는 StructurePlacer가 꺼질 때 모든 구조물이 반쯤 커진 상태나 반쯤 줄어든 상태로 남는 것을 막기 위해서다.

즉, 비활성화 직전 시각 상태를 정돈하는 역할을 한다.

여기서 null 체크를 하는 이유는 리스트 안에 비어 있는 슬롯이 있을 수 있기 때문이다.

이 프로젝트는 제거된 자리를 재사용하기 위해 null 슬롯을 남겨두는 구조이므로, 모든 요소가 실제 GameObject라는 가정이 성립하지 않는다.

따라서 안전하게 null 여부를 확인한 뒤 Tween 완료를 호출한다.

이 함수는 작아 보이지만 꽤 중요한 안정화 코드다.

씬 전환이나 오브젝트 비활성화 순간에 애니메이션이 비정상 상태로 끊어지면, 다음 활성화 시 구조물이 잘못된 스케일을 가진 채 남을 수 있기 때문이다.

따라서 이 코드는 시각적 상태 정합성을 보장하기 위한 후처리라고 볼 수 있다.

4. 개발 의도

StructurePlacer의 핵심 설계 의도는 건축 시스템에서 실제 GameObject의 생성과 제거 책임을 완전히 분리하는 것이다.

건축 시스템은 Grid 기반 데이터와 실제 씬 오브젝트라는 두 개의 상태를 동시에 관리해야 하는 구조를 가지고 있기 때문에, 이 두 영역이 서로 뒤섞이기 시작하면 시스템 전체의 복잡도가 급격하게 증가하게 된다.

특히 PlacementManager나 Command 시스템은 어디에 무엇을 배치할 것인가를 결정하는 계층이지, 실제로 GameObject를 생성하거나 제거하는 역할까지 수행하는 계층이 아니다.

이 두 책임을 하나의 클래스에서 처리하게 되면, 배치 로직과 씬 오브젝트 관리 로직이 강하게 결합되며, 이후 기능 확장이나 수정 시 영향 범위가 커지는 문제가 발생한다.

이를 방지하기 위해 StructurePlacer는 씬에 존재하는 오브젝트 상태를 관리하는 전용 실행 계층으로 분리되었다.

PlacementManager는 단지 배치 요청을 전달하고, StructurePlacer는 그 요청을 받아 실제 GameObject를 생성하거나 제거하는 역할만 수행한다.

이 구조를 통해 건축 시스템은 데이터 중심 로직과 씬 실행 로직이 명확하게 분리된 형태를 유지할 수 있다.

또한 StructurePlacer는 단순히 Instantiate를 감싸는 수준의 클래스가 아니라, 생성된 오브젝트를 내부 리스트로 관리하고 인덱스를 반환하는 구조를 가지고 있다.

이 인덱스는 Grid 데이터 시스템과 연결되며, 이후 구조물 제거, 이동, Undo와 같은 기능에서 동일한 오브젝트를 정확하게 식별하는 기준이 된다.

즉 이 시스템은 단순 생성기가 아니라, 씬 오브젝트의 식별성과 참조 안정성을 보장하는 역할까지 수행한다.

이러한 설계는 Undo 시스템과도 강하게 연결된다.

Command Pattern 기반 구조에서는 특정 명령을 되돌리기 위해 어떤 오브젝트를 생성했는지 또는 어떤 오브젝트를 제거했는지를 정확하게 알아야 한다.

StructurePlacer가 오브젝트를 중앙에서 관리하고 인덱스를 반환하는 구조를 사용함으로써, 명령 객체는 해당 인덱스를 기반으로 안정적으로 Undo를 수행할 수 있다.

또한 StructurePlacer 내부에서 오브젝트 생성과 제거를 일관된 방식으로 처리하기 때문에, 향후 애니메이션, 이펙트, 풀링(Object Pooling)과 같은 기능을 추가하더라도 수정 범위가 이 클래스 내부로 제한된다.

예를 들어 현재는 Instantiate/Destroy를 사용하고 있지만, 성능 최적화를 위해 Object Pooling으로 변경해야 할 경우에도 PlacementManager나 Command 시스템을 수정할 필요 없이 StructurePlacer만 변경하면 된다.

결과적으로 StructurePlacer는 단순한 오브젝트 생성 유틸리티가 아니라,
건축 시스템에서 씬 상태를 책임지고 관리하는 실행 계층(Runtime Execution Layer)으로 설계되었으며,
데이터 시스템(GridData), 명령 시스템(Command), 상태 시스템(BuildingState)과의 결합도를 낮추면서도 이들을 안정적으로 연결하는 핵심 역할을 수행한다.