ABOUT ME

-

Today
-
Yesterday
-
Total
-
  • [C#] Public Field vs Auto-Implemented Property
    프로그래밍 언어 2024. 4. 6. 22:41

    유니티로 게임을 개발하다보면, Property를 자주 접하게 된다. 그런데 Public Field와 Auto-Implemented Property 중 어느 것을 사용해야하는지 매번 고민하게 된다. 이번 글에선 C#의 Property에 대해 알아보고, Property를 사용하는 이유는 무엇인지, Public Field와 Auto-Implemented Property의 차이점이 무엇인지에 대해 작성해보고자 한다.

     

    Property란?

    객체 지향 프로그래밍에선 캡슐화는 매우 중요한 요소 중 하나이다. 클래스의 데이터를 외부 클래스로부터 보호하여, 의도치 않은 기능을 수행하지 않도록 각 객체의 데이터 오염을 방지한다. 

     

    예를 들어, 캐릭터를 구현한다고 가정해보자. 캐릭터 클래스는 캐릭터의 위치 정보를 갖고 있는 position와 이동 속도 정보를 갖고 있는 moveSpeed를 맴버 변수로 갖고 있고, 플레이어의 입력 방향을 파라미터로 전달받아 캐릭터를 이동시키는 Move 맴버 함수를 갖고 있다.

    public class Character
    {
        #region Variables
        
        // 캐릭터의 위치 정보
        public Vector3 position;
        
        // 캐릭터의 이동 속도 정보
        public float moveSpeed;
        
        #endregion Variables
        
        #region Methods
        
        // 캐릭터를 파라미터로 전달받은 방향으로 이동
        public void Move(Vector3 direction)
        {
            position += moveSpeed * direction;
        }
        
        #endregion Methods
    }

     

    캡슐화가 갖춰져있지 않다면, 외부 클래스에서 Move 함수로 캐릭터를 이동시키는 것이 아닌, 캐릭터의 위치를 직접 설정할 수 있다. 또한 moveSpeed에 음수를 대입하여 플레이어가 왼쪽 방향을 입력해도 캐릭터가 오른쪽으로 이동하는, 개발자가 의도하지 않은 기능이 수행될 수 있다.

    public class Program
    {
        public static int Main(string[] args)
        {
            Character character = new Character();
            
            // Wrong : Move 함수를 사용하지 않고 맴버 변수에 직접 접근하여 캐릭터를 이동
            character.position = new Vector3(10f, 10f, 10f);
            
            // Wrong : 캐릭터의 이동 속도를 음수로 설정
            character.moveSpeed = -10f;
            
            return 0;
        }
    }

     

    이를 방지하기 위해 private, protected, public 접근 지정자를 사용하고, 외부 클래스에서 이를 흔히 Setter, Getter라고 불리는 함수를 통해 private 맴버 변수에 접근하여 클래스를 캡슐화하고, 정보 은닉 목표를 달성할 수 있다. 

    public class Character
    {
        #region Variables
        
        // 캐릭터의 위치 정보
        // private으로 설정하여 외부 클래스에서 직접 수정할 수 없도록 구현
        private Vector3 position;
        
        // 캐릭터의 이동 속도 정보
        private float moveSpeed;
        
        #endregion Variables
        
        #region Setter / Getter
        
        public void SetMoveSpeed(float value)
        {
            // 외부 클래스에서 이동 속도를 0보다 작은 값을 대입할 경우에 대한 예외 처리
            if (value < 0f)
            {
                throw new System.Exception("The speed of movement cannot be less than 0");
            }
            
            moveSpeed = value;
        }
        
        public float GetMoveSpeed()
        {
            return moveSpeed;
        }
        
        #endregion Setter / Getter
        
        #region Methods
        
        // 캐릭터를 파라미터로 전달받은 방향으로 이동
        public void Move(Vector3 direction)
        {
            position += moveSpeed * direction;
        }
        
        #endregion Methods
    }

     

    하지만 개발을 진행할수록 클래스의 크기가 커짐에 따라 맴버 변수에 접근하기 위한 메소드가 계속해서 추가되면 클래스가 비대해져서 관리하기가 어려워진다. 이를 해결하기 위한 방안으로 접근 지정자를 설정할 수 있는, Public 변수와 같은 역할을 수행하는 Property가 존재한다. Microsoft의 C# 가이드에선 Property를 다음과 같이 소개하고 있다.

    A property is a member that provides a flexible mechanism to read, write, or compute the value of a private field. Properties can be used as if they're public data members, but they're special methods called accessors. This feature enables data to be accessed easily and still helps promote the safety and flexibility of methods.

     

    위 클래스를 Property를 사용하여 다음과 같이 수정할 수 있다. 더 나아가 Set과 Get의 다른 로직이 추가되지 않는다면, Auto-Implemented Property를 이용하여 코드를 더욱 간소화할 수 있다.

    public class Character
    {
        #region Variables
        
        // 캐릭터의 위치 정보
        // private으로 설정하여 외부 클래스에서 직접 수정할 수 없도록 구현
        private Vector3 position;
        
        // 캐릭터의 이동 속도 정보
        private float moveSpeed;
        
        #endregion Variables
        
        #region Property
        
        public float MoveSpeed
        {
            set
            {
                // 외부 클래스에서 이동 속도를 0보다 작은 값을 대입할 경우에 대한 예외 처리
                if (value < 0f)
                {
                    throw new System.Exception("The speed of movement cannot be less than 0");
                }
            
                moveSpeed = value;
            }
            get => moveSpeed; // 람다식도 사용 가능
        }
        
        // 추가 로직을 작성하지 않아도 되는 Auto-Implemented Property
        public float Anything { set; get; }
        
        #endregion Property
        
        #region Methods
        
        // 캐릭터를 파라미터로 전달받은 방향으로 이동
        public void Move(Vector3 direction)
        {
            position += moveSpeed * direction;
        }
        
        #endregion Methods
    }

     

    Public Field와 Auto-Implemented Property

    그렇다면, 외부 클래스에서 Setter와 Getter가 필요하고, 값을 대입하거나 반환할 때 추가적인 로직이 필요없다면, 왜 Auto-Implemented Property가 필요할까? Public Field를 사용해도 똑같지 않을까? 라는 생각이 들었다.

     

    public class PublicField
    {
        public int value;
    }
    
    public class AutoImplementedProperty
    {
        public int value { set; get;}
    }

     

    우선 위 코드를 ILSpy 툴을 이용하여 Decompiling을 해봤다. .Net Framework에선 C# 언어를 CIL (Common Intermediate Language)라는 언어로 번역되어 .exe나 .dll 확장자의 실행 파일을 생성한다. 그 결과는 다음과 같고, 빨간 박스를 집중해서 확인해보자.

    ILSpy 툴을 이용하여 코드를 Decompiling한 결과

     

    PublicField 클래스에선 public 변수인 value가 출력된 반면, AutoImplementedProperty 클래스에선 값을 저장하기 위한 변수인 backing field와 Setter와 Getter를 위한 함수들도 같이 출력된 것을 확인할 수 있다. 이를 미루어 보아, Auto-Implemented Property는 겉보기엔 Public Field와 똑같아보이지만, Set과 Get을 수행하기위해 함수를 호출하는 것을 알 수 있다.

    // PublicField class decomiling 결과
    // Fields
    .field public int32 'value'
    
    // AutoImplementedProperty class decompiling 결과
    // Fields
    .field private int32 '<value>k__BackingField'
    
    // Properties
    .property instance int32 'value'()
    {
        .get instance int32 ClassLibrary1.AutoImplementedProperty::get_value()
        .set instance void ClassLibrary1.AutoImplementedProperty::set_value(int32)
    }

     

    함수를 호출하는 것 자체로 오버헤드가 발생하기 때문에, Public Field가 Auto-Implemented Property보다 빠를 수 밖에 없다. 테스트를 위해 Public Field와 Auto-Implemented Property에 1씩 1억번 더하는 로직을 100번 반복하고 평균 시간을 출력해봤다. 1을 더하기 위해 메모리에 접근하는 Getter를 1회 수행하고 1을 더한 값을 설정하는 Setter를 1회 수행한다.

    using System.Diagnostics;
    
    public class PerformanceTest
    {
        private const int ITERATIONS = 100000000;
    
        public int PublicField;
        public int AutoImplementedProperty { set; get; }
    
        public void TestPublicField()
        {
            PublicField = 0;
            long totalTime = 0, averageTime = 0;
    
            for (int i = 0; i < 100; i++)
            {
                var sw = Stopwatch.StartNew();
    
                for (int j = 0; j < ITERATIONS; j++)
                {
                    PublicField += 1;
                }
    
                sw.Stop();
    
                totalTime += sw.ElapsedMilliseconds;
            }
    
            averageTime = totalTime / 100;
    
            Console.WriteLine($"Calculation time of public field : {averageTime}");
        }
    
        public void TestAutoImplementedProperty()
        {
            AutoImplementedProperty = 0;
            long totalTime = 0, averageTime = 0;
            
            for (int i = 0; i < 100; i++)
            {
                var sw = Stopwatch.StartNew();
    
                for (int j = 0; j < ITERATIONS; j++)
                {
                    AutoImplementedProperty += 1;
                }
    
                sw.Stop();
    
                totalTime += sw.ElapsedMilliseconds;
            }
    
            averageTime = totalTime / 100;
    
            Console.WriteLine($"Calculation time of auto-implemented property : {averageTime}");
        }
    }
    
    public class Program
    {
        public static int Main(string[] args)
        {
            PerformanceTest tester = new PerformanceTest();
            
            tester.TestPublicField();
            tester.TestAutoImplementedProperty();
    
            return 0;
        }
    }

     

    그 결과 Public Field의 경우 209ms, Auto-Implemented Property의 경우 652ms가 소요됬다. 상대적으로 비교하면 Auto-Implemented Property가 거의 3배 이상의 시간이 소요되지만, 1억회라는 횟수 관점에서는 유의미한 성능 차이가 없다고 판단된다.

    Public field와 Auto-Implemented Property의 Setter, Getter 성능 비교

     

    왜 Property일까?

    하지만, 약간이라도 느린 Auto-Implemented Property를 굳이 사용하는 이유가 무엇일까? 그 해답을 찾아본 결과 Binary Compatiblity 때문이라는 글을 찾을 수 있었다.

     

    Binary Compatiblity란 소프트웨어의 업데이트나 변경을 수행할 때 기존에 컴파일된 코드를 변경하지 않고도 새로운 버전의 코드를 사용할 수 있는 능력을 뜻한다. 예를 들어, 캐릭터의 스텟을 위한 CharacterStat이란 클래스를 내가 개발하고, 이를 활용하여 다른 팀원이 Character 클래스를 개발한다고 가정해보자. 캐릭터의 체력을 위한 Health라는 Public Field를 추가하고, 이를 활용하여 캐릭터가 피격됬을 때, 체력을 감소시키는 로직을 팀원이 개발할 것이다.

    // 내가 개발하는 클래스
    public class CharacterStat
    {
        public int Health;
        
        // .. Other members
    }
    
    // 다른 팀원이 개발하는 클래스
    public class Character
    {
        private CharacterStat statController;
        
        public void OnDamage(int damage)
        {
            statController.Health -= damage;
        }
        
        // .. Other members
    }

     

    지금 당장에는 문제가 없어 보일 수도 있다. 하지만 테스트 중 캐릭터의 체력이 음수가 될 경우 UI가 정상적으로 작동하지 않는 이슈가 발생했다고 하자. 그래서 캐릭터의 체력이 음수가 될 수 없도록 예외처리를 해달라는 추가 요구 사항이 발생하게 된다. 그럼 Health의 값을 변경할 때 예외 처리 로직을 추가하게 된다면, 다른 팀원이 개발하고 있는 Character 클래스에서도 추가로 코드를 수정해야하는 상황이 발생하게 된다.

    // 내가 개발하는 클래스
    public class CharacterStat
    {
        // Public Field를 private으로 변경.
        private int health;
        
        // Health가 음수일 경우 0으로 변경하는 예외 처리 로직 추가.
        public void SetHealth(int value)
        {
            if (value < 0)
            {
                value = 0;
            }
            
            health = value;
        }
        
        // .. Other members
    }
    
    // 다른 팀원이 개발하는 클래스
    public class Character
    {
        private CharacterStat statController;
        
        public void OnDamage(int damage)
        {
            // Error : Health가 public에서 private으로 바뀜에 따라 에러 발생.
            // Health가 아닌 SetHealth라는 메소드를 사용해야 함으로 코드를 수정해야 함.
            statController.Health -= damage;
        }
        
        // .. Other members
    }

     

    지금은 Coupling이 존재하는 클래스가 하나 밖에 없어서 '이 정도야 수정할 수 있지.'라고 생각할 수 있다. 하지만 개발이 진행될수록, Coupling이 존재하는 클래스는 매우 많아질 것이며, 이에 따라 수정해야 하는 코드의 양은 감당할 수 없는 수준이 될 것이다.

     

    그럼 Public Field가 아닌 Auto-Implemented Property로 구현한 상황을 보자. 마찬가지로 캐릭터 체력을 위한 Health라는 Property를 추가하고, 이를 다른 팀원이 캐릭터 피격 로직에 사용할 것이다.

    // 내가 개발하는 클래스
    public class CharacterStat
    {
        // Auto-Implemented Property
        public int Health { set; get; }
            
        // .. Other members
    }
    
    // 다른 팀원이 개발하는 클래스
    public class Character
    {
        private CharacterStat statController;
        
        public void OnDamage(int damage)
        {
            statController.Health -= damage;
        }
        
        // .. Other members
    }

     

    마찬가지로 Health의 구현 로직이 수정된다면? 내가 개발하고 있는 CharacterStat 클래스만 수정한다면, 외부 클래스에서 코드 수정이 발생할 일은 전혀 없다. 이를 통해 Binary Compatiblity를 유지하면서 새로운 기능들을 추가할 수 있게 된다.

    // 내가 개발하는 클래스
    public class CharacterStat
    {
        private int health;
        
        // 추가 요구 사항을 위한 코드 수정
        public int Health 
        { 
            set { health = value >= 0 ? value : 0; }
            get => health;
        }
            
        // .. Other members
    }
    
    // 다른 팀원이 개발하는 클래스
    public class Character
    {
        private CharacterStat statController;
        
        public void OnDamage(int damage)
        {
            // 변경 사항 없음.
            statController.Health -= damage;
        }
        
        // .. Other members
    }

     

    참고 자료

    https://learn.microsoft.com/ko-kr/dotnet/csharp/programming-guide/classes-and-structs/properties

     

    속성 - C# 프로그래밍 가이드 - C#

    C#의 속성은 접근자 메서드를 사용하여 공용 데이터 멤버인 것처럼 private 필드의 값을 읽고, 쓰고, 계산하는 멤버입니다.

    learn.microsoft.com

     

    https://stackoverflow.com/questions/737290/why-prefer-properties-to-public-variables

     

    Why prefer Properties to public variables?

    Other being able to sanity check values in a setter is there a more underlying reason to prefer properties to public variables?

    stackoverflow.com

     

    '프로그래밍 언어' 카테고리의 다른 글

    OOP Design Pattern - Structural Pattern  (0) 2024.01.18
    OOP Design Pattern - Creational Pattern  (0) 2024.01.12
    [C++] 이동 의미론 Move Semantics  (0) 2023.11.20
    입실론 테스트  (0) 2023.11.20
    객체 지향 설계 SOLID  (0) 2023.11.16
Designed by Tistory.