포스트

Attribute에 대해서

Attribute

C# 코드에 추가할 수 있는 메타 데이터

메타 데이터

  • 코드 자체에 대한 정보
  • 데이터 안의 데이터로 Attribute, Reflection을 통해 얻는 정보

image (86)

[Obsolete]

더 이상 사용하지 않는 코드에 대해 경고를 남길 때 사용한다.

image (77)

image (78)

위와 같이 IDE에서 경고를 인식할 수 있도록 도와주기도 하며,

1
[Obsolete("이 메서드는 더 이상 사용하지 않는다.", true)]

위와 같이 두번째 인자로 true를 집어넣으면, 이 요소를 사용한 부분에 컴파일 오류로 처리하게 된다.

image (79)

[SerializeField]

Unity에서 비공개(private) 필드에 직렬화(Serialization)를 허용하여 Inspector View에서 노출하고 싶을 때 사용한다. 이에 대해서는 잘 알려주는 곳이 많으니 패스.

그 밖에 유니티에서 자주 사용되는 Attribute들

[AddComponentMenu]

  • 이 스크립트를 GameObject에 컴포넌트로 추가할 때 메뉴에서 이 스크립트를 찾는 경로를 지정한다. string 타입의 매개변수의 사용이 필수적이다.
  • /로 디렉토리 계층을 만들 수 있다.
  • 이 매개변수의 값이 GameObject에 컴포넌트로 붙을 때의 이름에 영향을 미친다.

    image (9)

    이렇게 [AddComponentMenu] 를 작성해주면

    image (12)

    image (13)

    지정한 경로를 통해 GameObject에 컴포넌트를 붙일 수 있다.

[UnityEditor.MenuItem]

  • 유니티 에디터의 상단에 메뉴를 만들고, 해당 메뉴를 메서드와 연결한다. string 타입의 매개변수의 사용이 필수적이다.
  • static 메서드만 연결 가능하다.

    image (14)

    위와 같이 작성해주면

    image (15)

    이렇게 상단 메뉴에서 해당 메서드를 실행시킬 수 있다.

[ContextMenu]

  • GameObject에 스크립트가 컴포넌트로 부착되어있을 때, InspectorView에서 해당 함수를 호출할 수 있는 방법을 제공한다. string 타입의 매개변수의 사용이 필수적이다.

    image (16)

    위와 같이 작성하면

    image (17)

    이렇게 InspectorView에서 해당 함수를 바로 실행시킬 수 있다.

[ContextMenuItem()]

  • InspectorView에 드러나는 필드에서 바로 특정한 함수를 실행할 수 있도록 연결하는 Attribute
  • 필드의 바로 위에서 선언하며, 두 개의 string 매개변수를 필요로한다.
    • 첫 번째 string으로 필드에서 함수를 연결할 이름을 지정하고
    • 두 번째 string으로 연결할 함수의 이름을 지정한다.

    image (18)

    위와 같이 Value라는 필드에 ResetValue 함수와 RandomValue 함수를 연결했다.

    image (19)

    InspectorView에서 해당 필드에 마우스 커서를 올려놓고 우클릭하면 위와 같이 해당 함수를 실행시킬 수 있다.

[Tooltip()]

  • InspectorView 에서 해당 필드에 마우스 커서를 올려놓으면 나타날 툴팁의 내용을 지정하는 Attribute
  • string 타입의 인자로 툴팁의 내용을 지정한다.

    image (20)

[HelpURL()]

image (21)

  • 스크립트가 컴포넌트로 부착되어있는 상태에서 물음표 버튼을 클릭해 특정한 웹페이지를 열어주는 Attribute
  • 해당 스크립트에 대한 설명이 있는 웹페이지를 연결해주는 용도로 사용할 수 있겠다.
  • string 타입의 인자에는 연결하고자 하는 웹페이지의 주소를 넣어주면 된다.

[ColorUsage()]

  • UnityEngine.Color 타입 필드가 InspectorView에 보일 때, Color의 alpha 값 사용 여부를 조절할 수 있는 Attribute.
  • 첫번째 인자로 false를 사용하면 Color 필드의 값을 에디터에서 조절할 때 Alpha 값을 나타나지 않는다.
  • 두번째 인자는 HDR 표시 여부를 나타낸다.

    image (22)

    위는 [ColorUsage(false, true)] 로 선언한 결과.

  • InspectorView에 보이는 필드들을 묶는 데 사용되는 Attribute
  • Header는 Bold로 표시된다.

    image (23)

[Space()]

  • InspectorView에 보이는 요소들 사이의 수직 여백을 추가하는 Attribute
  • 인자로 int 값을 넣어 수직 여백의 길이를 조절할 수 있다.

    image (24)

    크기 20의 수직 여백을 넣은 모습

[MultiLine()]

  • InspectorView에 string 타입 필드의 입력칸을 위 아래로 늘리는 Attribute

    image (25)

    [Multiline(5)] 로 설정한 모습

[TextArea()]

  • string 타입의 필드에 쓰인다.
  • 두 개의 int 타입 인자를 받는다. 각각 최소, 최대 열의 수를 의미한다.

[ExecuteInEditMode]

  • Play 모드에 진입하지 않고도 해당 스크립트가 동작하게 만드는 Attribute
  • 클래스의 선언 직전에 쓰인다.

[RequireComponent()]

  • 이 선언이 사용된 스크립트의 컴포넌트를 붙일 때, 여기서 지정한 컴포넌트가 함께 붙도록 만든다.
  • 클래스의 선언 직전에 쓰인다.
  • 예를 들어 RigidBody를 지정할 때는 [RequireComponent(typeof(Rigidbody))] 와 같이 지정하면 된다.
  • 스크립트의 동작에 반드시 필요한 다른 컴포넌트가 동일한 GameObject에 있는 것을 보장할 수 있는 Attribute.

[DisallowMultipleComponent]

  • 한 GameObject에 동일한 스크립트의 컴포넌트를 여러 개 붙이지 않도록 해주는 Attribute.
  • 클래스의 선언 직전에 쓰인다.

[System.NonSerialized]

  • 필드의 직렬화를 푸는 Attribute
  • 직렬화가 풀려, 해당 필드가 InspectorView에도 보이지 않게 된다.

[HideInInspector]

  • 필드가 InspectorView에서 보이지 않도록 감추는 Attribute.
  • 직렬화는 유지한다.

사용자 정의 Attribute

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
37
38
39
40
41
42
43
44
45
using System;
using System.Reflection;
using UnityEngine;

public class MyCustomAttribute : Attribute
{
    public string Desc { get; }

    public MyCustomAttribute(string desc)
    {
        Desc = desc;
    }
}

public class MyTestClass
{
    [MyCustomAttribute("테스트용 메서드")]
    public void TestMethod()
    {
        Debug.Log("테스트 메서드 실행");
    }
}

public class AboutAttribute : MonoBehaviour
{
    private void Start()
    {
        TestMethod();
    }

    public void TestMethod()
    {
        Type myTestClass = typeof(MyTestClass);
        foreach (var methodInfo in myTestClass.GetMethods())
        {
            var attribute = (MyCustomAttribute)methodInfo.GetCustomAttribute(typeof(MyCustomAttribute));

            if (attribute != null)
            {
                Debug.Log(attribute.Desc);
                methodInfo.Invoke(Activator.CreateInstance(typeof(MyTestClass)), null);
            }
        }
    }
}

위 코드는 Reflection을 이용하는 코드다. 위 코드의 동작을 설명하자면

MyTestClass의 타입을 추출해 해당 클래스의 메서드 목록에 대해 순회한다. MyCustomAttribute 애트리뷰트가 있는 메서드라면, 애트리뷰트의 Desc를 콘솔에 출력하고 MyTestClass 인스턴스를 만들어 해당 메서드를 실행시킨다.

  • GetCustomAttribute()
    • 지정한 타입의 애트리뷰트를 해당 멤버(메서드, 클래스 등)에서 가져오는 메서드

그 결과는 다음과 같다.

image (80)

Attribute의 활용방안

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
public class AboutAttribute : MonoBehaviour
{
    public Rigidbody rigid;
    public BoxCollider coll;
    public AudioSource audi;

    private void Start()
    {
        Transform tr1 = Util.FindChild("Target1", transform);
        rigid = tr1.GetComponent<Rigidbody>();
        coll = tr1.GetComponent<BoxCollider>();

        Transform tr2 = Util.FindChild("Target2", transform);
        audi = tr2.GetComponent<AudioSource>();
    }
}

위와 같은 스크립트를 지금 작성 중에 있다고 하자. 컴포넌트에 대한 레퍼런스가 비어있는 상태에서, 코드로 레퍼런스를 채워주는 기능을 지금 작성 중에 있다. 참고로 Util.FindChild 함수에 대한 정의는 다음과 같다.

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
public class Util
{
    /// <summary>
    /// 게임 오브젝트의 Transform을 찾는 재귀 함수
    /// </summary>
    /// <param name="name">타겟 이름</param>
    /// <param name="tr">시작 위치</param>
    /// <returns>찾은 Transform</returns>
    public static Transform FindChild(string name, Transform tr)
    {
        if (tr.name == name)
            return tr;

        for (int i = 0; i < tr.childCount; i++)
        {
            Transform findTr = FindChild(name, tr.GetChild(i));
            if (findTr != null)
                return findTr;
        }
        return null;
    }
    
    // ...
    
}

Hierarchy는 다음과 같은 상황이다.

image (81)

Root에는 위 스크립트가, Target1에는 BoxCollider와 Rigidbody가, 그리고 Target2에는 AudioSource가 있다.

image (82)

Root의 스크립트에는 이렇게 필드의 값이 비어있는 상태다.

이제 Play 모드에 진입하면 스크립트의 필드가 알아서 채워질 것이다. 그런데 스크립트에서 이렇게 참조해야 할 컴포넌트가 늘어나면 어떻게 될까? 참조할 컴포넌트의 수가 늘어나면 늘어날수록, AboutAttribute.Start메서드는 덩치가 점점 커질 것이다. 그렇게 주구장창 길어지는 걸 어떻게 해결할 수 없을까?

Attribute를 통해, 몇몇 부분을 자동화해보자.

Attribute에 특정한 값을 저장할 수 있다는 점에 집중해보자.

1
2
3
4
5
6
7
8
9
10
[AttributeUsage(AttributeTargets.Field)]
public class FindComponentAttribute : Attribute
{
    public string _gameObjectName { get; }

    public FindComponentAttribute(string gameObjectName)
    {
        _gameObjectName = gameObjectName;
    }
}

위 코드에서 AttributeUsage를 볼 수 있는데, 그 자세한 정보는 다음과 같다.

AttributeUsage

  • Custom Attribute를 정의할 때 Attribute를 적용할 수 있는 대상 (메서드, 클래스) 과 사용 규칙을 정의한다.
  • 주요 속성
    • AttributeTargets

      Attribute 를 적용할 수 있는 코드 요소를 지정한다.

      AttributeTargets 열거형의 플래그 조합 가능

      image (83)

      해당 어트리뷰트가 상속되는지 여부를 설정

      • true: 자식 클래스에 상속됨.
      • false: 상속되지 않음.
    • AllowMultiple

      동일한 요소에 어트리뷰트를 여러 번 적용할 수 있는지 여부를 설정

      • true: 여러 번 적용 가능.
      • false: 한 번만 적용 가능.

그리고 Util 클래스에 다음의 함수가 더 있다고 가정해보자

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
public class Util
{
	
	// ...
	
	public static void InjectComponents(object o)
	{
	    Type type = o.GetType();
	    MonoBehaviour script = o as MonoBehaviour;
	
	    FieldInfo[] fields = type.GetFields(BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.Instance);
	
	    foreach (var field in fields)
	    {
	        var attribute = (FindComponentAttribute)field.GetCustomAttribute(typeof(FindComponentAttribute));
	
	        Type fieldType = field.FieldType;
	        Transform tr = FindChild(attribute._gameObjectName, script.transform);
	
	        Component component = tr.GetComponent(fieldType);
	        
	        field.SetValue(script, component);
	    }
	}

	// ....
	
}

그러면, 앞서 작성했던 AboutAttribute 컴포넌트는 다음과 같은 방식으로 수정할 수 있다. 물론 이렇게 해도 동작은 기존과 동일하다.

1
2
3
4
5
6
7
8
9
10
11
public class AboutAttribute : MonoBehaviour
{
    [FindComponent("Target1")] public Rigidbody rigid;
    [FindComponent("Target1")] public BoxCollider coll;
    [FindComponent("Target2")] public AudioSource audi;

    private void Start()
    {
        Util.InjectComponents(this);
    }
}

Util.InjectComponents 함수는 C#의 Reflection을 이해한다면 이해할 수 있는 코드다.

이렇게 수정한다면, AboutAttribute의 필드가 늘어난다해도 그에 맞춰 코드를 추가적으로 짤 필요가 사라지고 Start에서는 Util.InjectComponents(this); 한줄로 모든 필드에 대해 대응할 수 있게 된다. 대신 필드마다 FindComponentAttribute가 붙긴 해야 한다.

위 코드를 한번 업그레이드 해보자. params 키워드를 이용해 배열타입의 필드에 대해서도 적용할 수 있도록 개선해보자.

  • params
    • 메서드가 정해지지 않은 개수의 인수를 받을 수 있게 하는 키워드.
    • 배열 타입 앞에 사용되며, 이를 통해 여러개의 값을 배열로 처리할 수 있다.

FindComponentsAttribute 를 다음과 같이 새롭게 정의해보자. 기존의 FindComponentAttribute 를 대신할 수 있다.

1
2
3
4
5
6
7
8
9
10
11
[AttributeUsage(AttributeTargets.Field)]
public class FindComponentsAttribute : Attribute
{
    public string[] _gameObjectNames { get; }

    public FindComponentsAttribute(params string[] gameObjectNames)
    {
        _gameObjectNames = gameObjectNames;
    }
}

그리고 Util 클래스의 InjectComponents 메서드를 수정해주자.

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
37
38
39
40
41
42
43
44
45
46
public static void InjectComponents(object o)
{
    Type type = o.GetType();
    MonoBehaviour script = o as MonoBehaviour;

    FieldInfo[] fields = type.GetFields(BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.Instance);

    foreach (var field in fields)
    {
        var attribute = (FindComponentsAttribute)field.GetCustomAttribute(typeof(FindComponentsAttribute));

        Type fieldType = field.FieldType;

        if (fieldType.IsArray)      // 배열인 경우
        {
            Type elementType = fieldType.GetElementType();

            List<Component> componentsList = new List<Component>();

            foreach (var gameObjectName in attribute._gameObjectNames)
            {
                Transform tr = FindChild(gameObjectName, script.transform);

                Component component = tr.GetComponent(elementType);
                
                componentsList.Add(component);
            }

            Array componentArray = Array.CreateInstance(elementType, componentsList.Count);
            for (int i = 0; i < componentsList.Count; i++)
            {
                componentArray.SetValue(componentsList[i], i);
            }
            
            field.SetValue(script, componentArray);
        }
        else                        // 배열이 아닌 경우
        {
            Transform tr = FindChild(attribute._gameObjectNames[0], script.transform);

            Component component = tr.GetComponent(fieldType);
        
            field.SetValue(script, component);
        }
    }
}

그러면 다음과 같이 필드가 배열 타입인 경우에도 사용할 수 있게 된다.

1
2
3
4
5
6
7
8
9
[FindComponents("Target1")] public Rigidbody rigid;
[FindComponents("Target1")] public BoxCollider coll;
[FindComponents("Target2")] public AudioSource audi;
[FindComponents("Target3", "Target4")] public AudioSource[] audiArray = new AudioSource[2];

private void Start()
{
    Util.InjectComponents(this);
}

image (84)

위와 같이 Target3와 Target4에 있는 AudioSource 컴포넌트를 찾아 필드에 할당해준다.

필드가 배열 타입일 때를 고려했으니, List타입일 때도 고려해 볼 수 있겠다.

List일때도 동작하게 해보자.

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
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
public static void InjectComponents(object o)
{
    Type type = o.GetType();
    MonoBehaviour script = o as MonoBehaviour;

    FieldInfo[] fields = type.GetFields(BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.Instance);

    foreach (var field in fields)
    {
        var attribute = (FindComponentsAttribute)field.GetCustomAttribute(typeof(FindComponentsAttribute));

        Type fieldType = field.FieldType;

        // 배열인 경우
        if (fieldType.IsArray)
        {
            Type elementType = fieldType.GetElementType();

            List<Component> componentsList = new List<Component>();

            foreach (var gameObjectName in attribute._gameObjectNames)
            {
                Transform tr = FindChild(gameObjectName, script.transform);

                Component component = tr.GetComponent(elementType);
                
                componentsList.Add(component);
            }

            Array componentArray = Array.CreateInstance(elementType, componentsList.Count);
            for (int i = 0; i < componentsList.Count; i++)
            {
                componentArray.SetValue(componentsList[i], i);
            }
            
            field.SetValue(script, componentArray);
        }
        // List인 경우
        else if (fieldType.IsGenericType && fieldType.GetGenericTypeDefinition() == typeof(List<>))
        {
            Type elementType = fieldType.GetGenericArguments()[0];

            IList componentsList = (IList)Activator.CreateInstance(fieldType);

            foreach (var gameObjectName in attribute._gameObjectNames)
            {
                Transform tr = FindChild(gameObjectName, script.transform);

                Component component = tr.GetComponent(elementType);
                
                componentsList.Add(component);
            }
            
            field.SetValue(script, componentsList);
        }
        // 배열이 아닌 경우
        else
        {
            Transform tr = FindChild(attribute._gameObjectNames[0], script.transform);

            Component component = tr.GetComponent(fieldType);
        
            field.SetValue(script, component);
        }
    }
}

Util 클래스의 InjectComponents 메서드만 위와 같이 수정해주자. 기존에 배열 타입, 단일 타입만 고려했던 것에 List타입에 대해서도 동작하게 경우의 수를 추가했다.

1
2
3
4
5
6
7
8
9
10
[FindComponents("Target1")] public Rigidbody rigid;
[FindComponents("Target1")] public BoxCollider coll;
[FindComponents("Target2")] public AudioSource audi;
[FindComponents("Target3", "Target4")] public AudioSource[] audiArray = new AudioSource[2];
[FindComponents("Target5", "Target6")] public List<Rigidbody> rigidList = new List<Rigidbody>();

private void Start()
{
    Util.InjectComponents(this);
}

그러면 위와 같이 List타입의 필드에 대해서도 적용되는 것을 볼 수 있다.

image (85)

레퍼런스

다음은 내가 Attribute에 대해 배우고 이해할 수 있게 해준 영상이다. 항상 양질의 영상을 올려주시는 채널장분께 감사의 인사를 드린다.

유니티 C# 고급문법 Attribute

이 기사는 저작권자의 CC BY 4.0 라이센스를 따릅니다.