• [.NET / C#] List의 Enumerator는 왜 public struct 일까?

    2022. 1. 17.

    by. xxx123

     

     

    타입의 가시성

     

    책 More Effective C#의 Item 13을 보면 "타입의 가시성을 제한하라"는 주제를 던진다.

    이 말은 "굳이 불필요하게 타입을 외부로 공개하지 말라"는 의미이다.

    어떤 클래스를 public으로 만드는 건 많은 상황에서 불편함을 줄여주기에 우리는 클래스를 쉽게 public으로 만드는 경우가 많지만 불필요한 타입의 노출은 다른 사용자에 의한 오용을 발생시키기도 하며, 해당 타입이 변경 되었을 때는 시스템의 많은 코드를 손봐야 하는 경우 또한 생길 수 있다.

    그렇기에 클래스를 공개하지 않는 건 꽤나 좋은 습관이라고 볼 수 있겠다.

    class MyList<T> : IEnumerable<T>
    {
    
    ...
    
        private class Enumerator : IEnumerator<T> // private class
        {
        	public Enumerator(List<T> list)
            {
            	...
            }
            
            public bool MoveNext()
            {
            	...
            }
        }
        
        public Enumerator GetEnumerator()
        {
        	return new Enumerator(this);
        }
    
    ...
    
    }

     

    위 MyList<T>클래스 외부에서는 Enumerator라는 내부 타입을 알 필요가 없다.

    왜냐하면 GetEnumerable()을 통해 얻은 타입을 IEnumerable로 받아서 처리하면 되기 때문이다.

    MyList<int> list = new MyList<int>();
    IEnumerator<int> enumerator = list.GetEnumerator(); // Enumerator를 몰라도 IEnumerator로 받으면 된다.
    while (enumerator.MoveNext())
    {
       int current = enumerator.Current;
    }

    이렇게 외부에 클래스를 최대한 감춤으로써 불필요한 오용 및 혼란을 막을 수 있게 된다.

     

     

     

     

     

    List의 Enumerator

     

    그럼에도 불구하고 .NET의 List의 경우 Enumerator가 'public'으로 공개되어 있으며 심지어 'struct'로 되어 있는 것을 볼 수 있다.

    잠깐 .NET의 List 클래스를 보자.

    // List.cs
    
    public class List<T> : IList<T>, System.Collections.IList, IReadOnlyList<T>
    {
    ...
    	public struct Enumerator : IEnumerator<T>, System.Collections.IEnumerator // public struct
    	{
    		...
    	}
     ...
     }

    어떤 이유로 타입 가시성을 보호하지 않고 public으로 공개 해 놓은 것일까?

     

    그 이유는 관리힙 할당을 최소화하여 GC로부터 관리의 부담을 줄여주기 위함이다.

    foreach를 사용하여 루프를 돌 경우 아쉽게도 퍼포먼스 측면에서는 for문에 비해 떨어지는 부분이 존재하는데, 그 중 하나가 바로 Enumerator 클래스의 관리힙 할당이다.

    만약 Enumerator가 class라고 가정을 한다면 우리는 foreach 호출 시 매번 Enumerator 클래스를 관리힙에 할당하게 된다. (물론 지금은 아니지만 이전의 .NET에서는 Enumerator가 class로 되어 있었다.)

     

    MyList와 같이 Enumerator가 class일 경우를 보자.

        private class Enumerator : IEnumerator<T> // class
        {
        	public Enumerator(List<T> list)
            {
            	...
            }
            
            public bool MoveNext()
            {
            	...
            }
        }
        
        public Enumerator GetEnumerator()
        {
        	return new Enumerator(this); // 관리 힙 할당 발생
        }

     

    이러한 낭비를 막고자 빈번하게 foreach를 호출하는 List 같은 클래스를 struct로 만들게 되는 것이다.

        private struct Enumerator : IEnumerator<T>  // struct로 변경
        {
            ...
        }
        
        public Enumerator GetEnumerator()
        {
        	return new Enumerator(this); // struct이므로 더 이상 힙할당이 발생하지 않음.
        }

     

    이제 모든 문제가 해결된 것 처럼 보이지만 문제는 GetEnumerator()를 호출하는 곳에서 발생한다.

    struct인 Enumerator를 MyList<T>와 마찬가지로 IEnumerator로 받아서 처리하게 되면 boxing이 발생하게 된다.

    혹을 떼려다가 또 다른 혹을 붙이게 되는 것이다.

            MyList<int> list = new List<int>();
            IEnumerator enumerator = list.GetEnumerator(); // struct를 interface로. // boxing
            while (enumerator.MoveNext())
            {
                 int current = enumerator.Current;
            }

     

    그리하여 List와 같이 foreach가 빈번하게 발생하는 타입의 경우 Enumerator의 가시성을 제한하는 것을 포기하고 성능에 좀 더 향상 시키기 위해 'public struct'를 사용하여 IEnumerator가 아닌 struct 타입 자체로 GetEnumerator()의 return 값을 받아 들이도록 구현된 것이다.

    List<int> list = new List<int>();
    List<int>.Enumerator enumerator = list.GetEnumerator(); // struct 타입 자신으로 받기 때문에 boxing이 발생하지 않음.
            
    while (enumerator.MoveNext())
    {
     	int current = enumerator.Current;
    }

     

     

    Enumerator가 public으로 공개되었다 하여 외부에서 마구 가져다 써도 좋다는 의미는 아니므로 foreach에게만 사용을 양보 해주기로 하자.

     

     

     

    댓글