속성 사용하기

  • 52 minutes to read

속성은 필드의 값을 읽거나 쓰고 계산하는 방법을 제공하는 클래스의 속성을 나타내는 멤버입니다. 아주 간단하게 클래스의 속성을 변경하거나 알아보는 기능을 학습하겠습니다.

> // 속성: 필드의 값을 읽거나 쓰거나 계산하는 방법을 제공하는 클래스의 속성을 나타내는 멤버

속성(Property)

클래스의 멤버 중에서 속성(property)은 단어 그대로 클래스의 속성(특징, 성격, 색상, 크기 등)을 나타냅니다. 속성은 괄호가 없는 메서드와 비슷하고 개체의 필드 중에서 외부에 공개하고자 할 때 사용하는 방법입니다. 개체의 성질, 특징, 색상, 크기, 모양 등을 속성으로 외부에 공개할 수 있습니다. 코드에서는 private 성격이 있는 필드public 속성으로 외부에 공개하고자 할 때 사용합니다.

클래스 안에 선언된 필드의 내용을 설정(set)하거나 참조(get)할 때 사용하는 코드 블록을 속성이라 합니다. 자동차 개체로 비유하면 빨간색 스포츠카, 바퀴 4개 등으로 속성을 표현할 수 있습니다.

속성을 클래스에 정의하는 구문은 다음과 같습니다.

문법:

class 클래스이름
{
    public [반환형식] 속성이름 { get; set; }
}

앞과 같은 형태는 가장 기본적인 속성을 정의하는 구문이고 get; set; 부분을 좀 더 다르게 설정해서 여러 가지 속성의 종류를 구현할 수 있습니다.

예를 들어 Car 클래스에 Name 속성을 정의하는 구문은 다음과 같습니다.

> class Car
. {
.     public string Name { get; set; }
. }

이렇게 한 줄로 속성을 정의하는 것을 자동 속성(auto property) 또는 자동 구현 속성이라고 합니다.

Car 클래스의 인스턴스를 생성한 후 Name 속성에 값을 설정하거나 가져다 사용할 수 있습니다.

> Car car = new Car();
> car.Name = "My Car";
> car.Name
"My Car"

속성 정의 구문의 마지막에는 ;(세미콜론)이 붙지 않습니다. 다만, 속성을 선언과 동시에 특정한 값으로 초기화할 때에는 세미콜론이 붙습니다. 속성을 선언하는 구문도 클래스의 다른 멤버들과 마찬가지로 static을 붙여서 정적인 속성을 만들 수 있습니다. 또한, 속성을 정의할 때 public 이외의 다른 액세스 한정자를 붙일 수 있겠지만, 이 책에서는 속성에 public 액세스 한정자만 사용합니다.

속성 만들고 사용하기

속성을 만들고 사용해보겠습니다. 속성에 값을 설정하는 것을 세터(Setter)라고 하고 값을 읽어오는 것을 게터(Getter)라고 부릅니다.

코드: Property.cs

> //[?] 속성 만들고 사용하기 
> class Developer
. {
.     public string Name { get; set; }
. }
> 
> //[1] 클래스의 인스턴스 생성
> Developer developer = new Developer();
> 
> //[2] 속성에 값 설정(set)
> developer.Name = "박용준";
> 
> //[3] 속성의 값 조회(get)
> Console.WriteLine(developer.Name);
박용준

Developer 클래스에는 Name 속성 하나만 정의되어 있습니다. Developer 클래스의 인스턴스 생성 후 Name 속성에 값을 설정(Set)할 수 있고 이 값을 다시 조회(Get)해서 사용할 수 있습니다. 지금까지 .NET API에서 많이 사용해 오던 속성 사용법 그대로입니다.

접근자와 전체 속성

속성에는 접근자를 통해서 속성에 값을 설정하거나 가져오는 기능을 수행할 수 있습니다. 이러한 접근자로는 값을 가져오는 get 접근자와 값을 설정하는 set 접근자를 제공하는데, 하나씩만 사용하거나 모두 사용할 수 있습니다. 일반적으로 get 접근자와 set 접근자를 게터(Getter)와 세터(Setter)로 읽습니다.

접근자가 포함된 Name 속성을 가지는 Car 클래스의 모양은 다음과 같습니다. 이렇게 정의된 속성을 전체 속성이라고 부릅니다.

> class Car
. {
.     private string name;
.     public string Name
.     {
.         get
.         {
.             return name;
.         }
. 
.         set
.         {
.             name = value; 
.         }
.     }
. }

get 접근자는 return 구문을 사용하여 특정 값 또는 특정 필드의 값을 반환합니다. 위 코드에서는 name 필드의 값을 반환합니다. set 접근자는 value 키워드를 사용하여 속성에 지정된 값을 가져오는 역할을 합니다. 가져온 value 값은 계산식에 사용되거나 속성과 관련된 필드에 저장합니다. Name 속성에서 사용할 데이터 저장 공간으로 name 필드를 선언한 것을 볼 수 있습니다. 이처럼, 속성은 내부적으로 필드를 사용합니다. 앞서 살펴본 것처럼 자동 속성을 사용할 때에는 속성에서 사용할 필드를 따로 선언해줄 필요는 없습니다.

전체 속성 사용하기

속성의 가장 기본적인 모양을 가지고 있는 전체 속성을 사용해보겠습니다.

코드: PropertyFull.cs

> //[?] 전체 속성 사용하기 
> class Person
. {
.     // 필드
.     private string name;
.     // 속성
.     public string Name
.     {
.         get { return name; }
.         set { name = value; }
.     }
. }
> 
> var person = new Person();
> person.Name = "Gilbut"; // set
> Console.WriteLine(person.Name); // get
Gilbut

자동 속성이 나오기 전까지는 전체 속성을 사용하였습니다. 하지만, 최근에는 특별한 계산식이 필요한 경우가 아니면 자동 속성을 사용합니다.

노트: 속성 관련 코드 조각

속성 생성용 Visual Studio의 코드 조각은 여러 개가 있지만 그 중에서 가장 많이 사용되는 코드 조각은 proppropfull이 있습니다. Visual Studio에서 클래스 코드내에서 prop를 입력 후 탭을 두 번 입력하면 자동으로 속성 코드가 만들어집니다. 참고로 자동 구현 속성은 C# 3.0부터 제공하기 시작했습니다.

> public int MyProperty { get; set; }

자동으로 구현된 속성

앞서 속성 소개에서 사용했었지만, 전체 속성을 사용하기 편하게 줄여 놓은 속성이 자동으로 구현된 속성(Automatically Implemented Property)이라고 합니다.

이번에는 자동으로 구현된 속성을 사용해보겠습니다.

코드: AutomaticallyImplementedProperty.cs

> //[?] 자동으로 구현된 속성 사용하기 
> // 자동차 속성: 이름, 색상, ...
. public class Car
. {
.     //[1] 필드와 속성을 함께 사용하는 전체 속성
.     // 필드
.     private string name;
.     // 속성(Property)
.     public string Name
.     {
.         get
.         {
.             return name; // 필드를 외부에 공개
.         }
.         set
.         {
.             name = value; // 외부에서 전달된 값을 필드에 초기화
.         }
.     }
.     //[2] 자동으로 구현된 속성으로 간단하게 생성
.     public string Color { get; set; }
. }
> 
> Car c1 = new Car();
> c1.Name = "남보러가니"; // setter
> Console.WriteLine(c1.Name); // getter
남보러가니
> 
> Car c2 = new Car();
> c2.Name = "제네실수"; c2.Color = "Red";
> Console.WriteLine("{0}, {1}", c2.Name, c2.Color);
제네실수, Red

자동 속성을 사용하여 Car 클래스에 Color 속성을 만들었습니다. Name 속성은 name 필드에 문자열 값을 저장 후 출력하는 기능을 제공합니다.

여러 개의 자동 속성 사용하기

하나 이상의 속성을 사용하는 자동 구현 속성 예제를 사용해보겠습니다.

코드: PropertyAutoImplemented.cs

> // 자동 구현 속성(C# 3.0): prop 코드 조각 사용해서 빠르게 만들기 가능
> class Exam
. {
.     public int Id { get; set; } // Id 속성
.     public string Title { get; set; } // Title 속성
. }
>
> Exam exam = new Exam();
> exam.Id = 1;
> exam.Title = "중간고사";
> $"{exam.Id} - {exam.Title}"
"1 - 중간고사"

정수형 속성 Id와 문자열 속성 Title을 만들고 값을 대입하고 그 값을 다시 사용해보는 예제입니다.

자동 속성 이니셜라이저

자동 속성 이니셜라이저(Auto Property Initializers)를 사용하면 속성을 선언과 동시에 기본 값으로 초기화할 수 있습니다.

코드: AutoPropertyInitializers.cs

using System;

class AutoPropertyInitializers
{
    public static string Name { get; set; } = "길벗";
    static void Main()
    {
        Console.WriteLine(Name);
    }
}
길벗

위 코드의 Name 속성은 선언과 동시에 "길벗" 문자열로 초기화됩니다.

정적인 자동 속성을 선언과 동시에 초기화하기

앞서 이미 살펴봤지만, C# 6.0 버전부터 사용 가능한 자동 속성(Auto Property)은 속성을 선언과 동시에 특정 값으로 자동으로 초기화할 때 사용하는 개념입니다. ***코드: ***

// AutoProperty.cs
> class UserService
. {
.     // 자동 속성: 속성 선언과 동시에 초기화 가능
.     public static int UserId { get; set; } = 1234;
. }
> Console.WriteLine(UserService.UserId); // 1234
1234

UserService 클래스에 정적인 속성인 UserId를 선언과 동시에 기본값으로 1234를 초기화하였습니다. Main() 메서드에서 UserService.UserId 출력해보면 기본값인 1234가 출력됩니다.

자동 속성 여러 개를 선언과 동시에 초기화하기

이번에는 자동 속성으로 하나 이상의 속성을 자동으로 초기화한 후 사용하는 예제를 살펴보겠습니다. ***코드: ***

// AutoPropertyDemo.cs
> class Person
. {
.     public Guid Id { get; set; } = Guid.NewGuid();
.     public string Name { get; set; } = "홍길동";
. }
> Person p = new Person();
> $"{p.Id}, {p.Name}"
"1a1d68e7-fa02-4ae0-9296-a40522773fac, 홍길동"

Person 클래스의 Id 속성은 유일한 값을 나타내는 Guid 값을 Guid.NewGuid() 메서드를 통해서 자동으로 초기화하고 Name 속성은 "홍길동"으로 초기화한 후 사용하는 예를 보여줍니다.

읽기 전용(Read Only) 속성과 쓰기 전용(Write Only) 속성

속성의 get과 set 구문을 하나만 사용하여 읽기 전용 속성과 쓰기 전용 속성을 구현할 수 있습니다. 속성을 선언과 동시에 값을 초기화하고 이때 private set 절을 사용하면 설정(set)이 불가능한 읽기 전용 속성을 만들 수 있습니다. ***코드: ***

// PropertyPrivateSet.cs
> public class Page
. {
.     public string Message { get; private set; } = "읽기 전용 속성";
. }
> Page page = new Page();
> page.Message = "외부에서 쓰기 불가능";
(1,1): error CS0272: The property or indexer 'Page.Message' cannot be used in this context because the set accessor is inaccessible
> Console.WriteLine(page.Message); // 읽기 전용 속성
읽기 전용 속성

Message 속성은 읽기 전용 속성으로 만들어져 쓰기가 불가능하고 읽기만 가능합니다.

속성의 여러 가지 유형 살펴보기

이번에는 속성의 여러 가지 사용법을 살펴보겠습니다. ***코드: ***

// PropertyAll.cs
using System;

namespace PropertyAll
{
    public class Car
    {
        // 필드
        private string color; 

        // 생성자
        public Car()
        {
            this.color = "Black";
        }

        // 메서드로 외부에 공개
        public void SetColor(string color)
        {
            this.color = color; // this.필드 = 매개 변수;
        }

        public string GetColor()
        {
            return color;
        }

        // 속성(프로퍼티;Property)
        public string Color
        {
            get
            {
                return color;
            }
            set
            {
                color = value;
            }
        }

        // 읽기전용 속성
        public string Make
        {
            get
            {
                return "한국자동차";
            }
        }

        // 쓰기전용 속성
        private string _Type;
        public string Type
        {
            set
            {
                _Type = value;
            }
        }

        // 축약형 속성
        public string Name { get; set; }
    }

    class PropertyAll
    {
        static void Main()
        {
            //[1] Set과 Get 메서드 사용
            Car car1 = new Car();
            car1.SetColor("Red");
            Console.WriteLine(car1.GetColor()); // Red

            //[2] 속성을 사용
            Car whiteCar = new Car();
            whiteCar.Color = "White"; // set {}
            Console.WriteLine(whiteCar.Color); // get {}

            //[3] 읽기전용 속성
            Car k = new Car();
            // k.Make = ""; // <- 이 코드는 에러
            Console.WriteLine(k.Make); // 읽기만 가능

            //[4] 쓰기전용 속성
            Car car = new Car();
            car.Type = "중형"; // 쓰기만 가능
            // Console.WriteLine(car.Type); // <- 이 코드는 에러: 읽기는 불가

            //[5] 축약형 속성
            Car myCar = new Car();
            myCar.Name = "좋은차";
            Console.WriteLine(myCar.Name);
        }
    }
}
Red
White
한국자동차
좋은차

속성의 개념이 없는 프로그래밍 언어는 [1]번 코드와 같이 Set 또는 Get으로 시작하는 메서드를 사용하여 클래스의 필드의 값을 설정하거나 가져다 사용했습니다. C#은 [2]번 코드와 같이 괄호가 없는 속성을 사용하여 데이터를 저장 및 사용을 손쉽게할 수 있으며 이러한 속성은 [3], [4]번과 같이 읽이 전용 또는 쓰기 전용 속성을 만들 수 있습니다.

[실습] 속성을 사용한 클래스의 멤버 설정 및 참조하기

소개

속성의 여러 가지 기능을 3개의 C# 파일을 생성해서 연습해 보도록 하겠습니다.

따라하기

(1) 새로운 C# 콘솔 프로젝트를 다음과 같이 생성합니다.

프로젝트 형식 템플릿 이름 위치
Visual C# 콘솔 응용 프로그램 PropertyNote C:\C#

(2) 솔루션 탐색기에서 프로젝트에 마우스 오른쪽 버튼을 클릭 <추가 > 새 항목>을 클릭하여 다음과 같이 2개의 클래스를 프로젝트에 추가합니다.

Visual Studio에 설치되어 있는 템플릿 이름
클래스 PropertyNote.Car.cs
클래스 PropertyNote.Person.cs

(3) 추가된 PropertyNote.Car.cs 파일을 열고 다음과 같이 프로그램을 작성합니다.

코드: PropertyNote.Car.cs

namespace PropertyNote
{
    public class Car
    {
        //[1] public 필드로 속성처럼 사용
        public static string Color;

        //[2] _(언더스코어) 문자로 속성에 대한 필드명 정의
        private static string _Type;

        //[3] public한 속성 정의 : 읽고 쓰기 가능한 속성
        public static string Type
        {
            get
            {
                return _Type;
            }
            set
            {
                _Type = value;
            }
        }

        //[4] 정적 개체를 생성하는 생성자
        static Car()
        {
            Color = "Red";
            _Type = "스포츠카";
        }
    }
}

(4) 추가된 PropertyNote.Person.cs 파일을 열고 다음과 같이 프로그램을 작성합니다.

코드: PropertyNote.Person.cs

using System;

namespace PropertyNote
{
    public class Person
    {
        private int _BirthYear; // 생년월일

        public string Name { get; set; } // 이름 

        // 쓰기 전용: 계산식 사용
        public int BirthYear
        {
            set
            {
                if (value >= 1900)
                {
                    _BirthYear = value;
                }
                else
                {
                    _BirthYear = 0;
                }
            }
        }

        // 읽기 전용: 계산식 사용
        public int Age
        {
            get
            {
                return (DateTime.Now.Year - _BirthYear);
            }
        }

        public Person(string name)
        {
            Name = name; // Name 속성에 넘겨온 name 매개 변수 값 저장
        }
    }
}

속성에는 getset 접근자를 사용하여 계산식을 넣을 수 있습니다.

(5) 솔루션 탐색기에서 Program.cs 파일을 PropertyNote.PropertyNote.cs 파일로 이름을 변경한 후 이미 만들어져 있는 모든 코드를 삭제한 후 다음과 같이 프로그램을 작성합니다.

코드: PropertyNote.PropertyNote.cs

using System;

namespace PropertyNote
{
    class PropertyNote
    {
        static void Main(string[] args)
        {
            //[1] Car 클래스(정적) 사용
            Car.Color = "Black"; // 필드 사용
            Car.Type = "세단"; // 속성 사용
            Console.WriteLine($"차종: {Car.Type}, 색상 : {Car.Color}");

            //[2] Person 클래스(인스턴스) 사용
            Person person = new Person("박용준");
            person.BirthYear = (DateTime.Now.Year - 21); // 21살로 고정
            Console.WriteLine($"이름: {person.Name}, 나이: {person.Age}");
        }
    }
}

(6) 소스 코드를 다 입력한 후 Ctrl+F5를 눌러 프로그램을 실행하면 명령 프롬프트 창에 다음과 같이 출력됩니다.

차종: 세단, 색상 : Black
이름: 박용준, 나이: 21

마무리

속성은 C# 응용프로그래밍에서 자주 사용될 아주 중요한 클래스의 구성요소입니다. 하나의 클래스의 여러 가지 필드 값을 설정하거나 읽어오고자 할 때는 private 형식의 필드를 사용하고, private 형식의 필드를 외부에 노출할 때에는 public 형식의 속성을 사용하여야 함을 기억하겠습니다.

화살표 연산자로 속성과 메서드를 줄여서 표현하기

화살표(=>) 연산자를 사용하면 속성과 메서드를 표현할 때 축약해서 표현할 수 있습니다. 속성을 줄여 표현하는 방법을 식 본문 속성(Expression-Bodied Property)이라고 합니다.

코드: ArrowDemo.cs

using System;

class Counter
{
    // 필드
    private int count;

    // 속성
    public int Count
    {
        get => count;
        set => count = value;
    }

    // 메서드 
    public void IncreaseCount() => Count++;
}

class ArrowDemo
{
    static Counter counter;
    static void Main()
    {
        counter = new Counter();
        counter.IncreaseCount();
        Console.WriteLine($"카운트: {counter.Count}");
    }
}
카운트: 1

위 코드처럼 속성 또는 메서드에서 간단한 모양일 경우에는 화살표 연산자인 => 연산자를 사용하여 코드를 줄여서 표현할 수 있습니다.

게터와 세터에 화살표 연산자 사용하기

게터와 세터와 함께 화살표 연산자를 사용하면 속성을 계산식으로 쉽게 활용할 수 있습니다.

코드: GetterSetter.cs

using System;

class GetterSetter
{
    public string Name { get; set; }
    public DateTime BirthDate { get; set; }
    public int Age
    {
        get => (DateTime.Now - BirthDate).Days / 365 + 1; 
    }

    static void Main()
    {
        GetterSetter user = new GetterSetter();
        user.Name = "마이크로소프트";
        user.BirthDate = new DateTime(1975, 01, 01);

        Console.WriteLine($"{user.Name} 나이 - {user.Age}");
    }
}
마이크로소프트 나이 - 45

속성의 getset은 메서드와 비슷하고 화살표 연산자를 사용하여 중괄호 표현식을 줄여서 사용할 수 있습니다. 이러한 게터와 세터를 사용하면 단순하게 필드의 값을 주고 받는게 아닌 특정 계산식을 추가할 수 있습니다.

개체 이니셜라이저

개체 이니셜라이저(Object Initializer)(C# 3.0 특징)는 속성을 사용하여 개체의 값을 설정하는 쉬운 방법을 제공합니다. 다만, 개체 이니셜라이저가 생성자를 완전히 대체하지 않습니다.

코드: ObjectInitializer.cs

> //[?] 개체 이니셜라이저(C# 3.0 특징) 
> class Course
. {
.     public int Id { get; set; } // Id 속성
.     public string Title { get; set; } // Title 속성
. }
> 
> //[1] 속성을 사용하여 개체 초기화
> Course csharp = new Course(); csharp.Id = 1; csharp.Title = "C#";
> Console.WriteLine($"{csharp.Id} - {csharp.Title}");
1 - C#
> 
> //[2] 개체 이니셜라이저를 사용하여 개체 초기화
> Course aspnet = new Course() { Id = 2, Title = "ASP.NET" };
> Console.WriteLine($"{aspnet.Id} - {aspnet.Title}");
2 - ASP.NET

Course 개체의 Id, Title 속성을 초기화하는 방법은 위 코드에서처럼 속성을 사용하거나 개체 이니셜라이저를 사용할 수 있습니다. 이 중에서 개체 이니셜라이저를 사용하면 좀 더 쉽게 속성을 초기화할 수 있습니다.

개체 이니셜라이저를 사용하여 개체의 속성을 초기화

C#은 클래스의 인스턴스를 만들 때 바로 속성을 특정 값으로 초기화할 수 있습니다. 이러한 기능을 개체 이니셜라이저(Object Initializer)라고 합니다. 개체 이니셜라이저를 사용하면 개체를 생성할 때 생성자를 의미하는 괄호 기호를 생략할 수 있습니다.

코드: ObjectInitializerPractice.cs

> //[1] Person 클래스 선언
> class Person
. {
.     public string Name { get; set; }
.     public string City { get; set; }
. }
> //[2] 개체 이니셜라이저로 개체의 속성 초기화하기 
> Person person = new Person { Name = "C#", City = "Seoul" };
> $"{person.Name} : {person.City}"
"C# : Seoul"

Person 클래스의 인스턴스 생성시 개체 이니셜라이저를 사용하여 Name 속성과 City 속성을 초기화할 수 있습니다. 이렇게 개체 이니셜라이저를 사용하면 개체 생성 후 따로 속성 초기화를 하지 않고 선언과 동시에 초기화해서 사용할 수 있는 장점을 가집니다.

개체를 초기화하는 3가지 방법

개체 생성시 속성을 초기화하는 방법에는 개체 이니셜라이저 이외에 지금까지 사용해오던 생성자와 속성 자체를 사용할 수 있습니다. 이러한 3가지 방법을 통해서 속성을 초기화하는 예제를 살펴보겠습니다.

코드: ObjectInitializers.cs

//[?] 개체 이니셜라이저를 사용하여 개체의 속성을 초기화
using System;

public class Person
{
    // 속성
    public string Name { get; set; }
    public int Age { get; set; }

    // 생성자
    public Person()
    {

    }
    public Person(string name, int age)
    {
        Name = name; Age = age;
    }
}

class ObjectInitializers
{
    static void Main()
    {
        //[1] 속성 사용
        Person pp = new Person();
        pp.Name = "백두산";
        pp.Age = 100;

        //[2] 생성자 사용
        Person pc = new Person("홍길동", 21);

        //[3] 개체 이니셜라이저 사용
        Person pi = new Person { Name = "임꺽정", Age = 30 };
        Console.WriteLine($"{pi.Name}, {pi.Age}");
    }
}
임꺽정, 30

개체의 인스턴스 생성시 Name 속성과 Age 속성을 초기화하려면 직접 속성에 값을 지정하거나, 생성자의 매개 변수로 전달된 값을 속성에 초기화하거나 개체 이니셜라이저를 사용하여 속성을 초기화할 수 있습니다.

이번에는 개체 이니셜라이저의 또 다른 사용법을 알아보겠습니다. C# 파일은 ObjectInitializerNote 프로젝트에 ObjectInitializerNote.Person.cs 파일과 ObjectInitializerNote.cs 파일 2개를 생성합니다.

코드: ObjectInitializerNote.Person.cs

namespace ObjectInitializerNote
{
    public class Person
    {
        // propfull: 전체 속성
        private string _Name;
        public string Name
        {
            get { return _Name; }
            set { _Name = value; }
        }

        // prop: 자동으로 구현된 속성
        public int Age { get; set; }

        // 자동 속성: 속성을 자동으로 초기화(C# 6.0)
        public string Type { get; set; } = "사람";
    }
}

코드: ObjectInitializerNote.cs

// Object Initializer: 개체 이니셜라이저
using System;

namespace ObjectInitializerNote
{
    class ObjectInitializerNote
    {
        static void Main()
        {
            //[1] Person 클래스의 인스턴스 생성
            Person p1 = new Person();
            p1.Name = "홍길동";
            p1.Age = 21;
            Console.WriteLine($"이름: {p1.Name}, 나이: {p1.Age}, 타입: {p1.Type}");

            //[2] 개체 이니셜라이저를 사용하여 개체를 초기화
            Person p2 = new Person() { Name = "백두산", Age = 99 };
            Console.WriteLine($"이름: {p2.Name}, 나이: {p2.Age}, 타입: {p2.Type}");
        }
    }
}
이름: 홍길동, 나이: 21, 타입: 사람
이름: 백두산, 나이: 99, 타입: 사람

위 코드의 [1]번 영역은 일반적인 방식으로 개체를 초기화하는 방식인데, 속성이 많을 경우 코드가 늘어나는데 이를 [2]번 영역처럼 중괄호({})를 사용하여 선언과 동시에 특정 속성을 바로 초기화하는 방식이 바로 개체 이니셜라이저(Object Initializer)입니다.

자동 속성을 사용하여 레코드 클래스 구현하기

자동 속성을 사용하면 표 형태의 데이터, 즉 레코드 단위로 데이터를 저장할 때 유용하게 사용됩니다. 배열을 사용하던 전통적인 프로그래밍 방식에서 속성을 사용하여 레코드 형태로 데이터를 관리하는 클래스를 모델 클래스라고 합니다.

코드: DataClass.cs

> //[?] 데이터 클래스(레코드 클래스, 레코드 형식, 모델 클래스): 자동 속성을 사용하여 구현
> public class Customer
. {
.     public int Id { get; set; } // 번호
.     public string Name { get; set; } // 이름
.     public string City { get; set; } // 도시 
. }
. 
> // 개체 리터럴을 사용하여 개체 초기화 
> var customer = new Customer { Id = 1, Name = "박용준", City = "인천" };
> $"{customer.Id}, {customer.Name}, {customer.City}"
"1, 박용준, 인천"

속성으로만 이루어진 데이터 클래스인 Customer 클래스를 만들고 사용하였습니다. 단순히 하나의 레코드만을 저장했지만, Customer 클래스 배열 또는 컬렉션 클래스를 사용하여 여러 개의 Customer 개체를 저장하고 사용할 수 있습니다.

nameof 연산자

C# 6.0 버전부터 제공하는 nameof 연산자를 사용하면 속성 이름 자체를 문자열로 가져올 수 있습니다. nameof 연산자는 식별자 또는 변수 이름에 대한 리팩터링 및 이름 변경을 할 때 유용하게 사용됩니다. 이를 사용하면 이름 바꾸기 등의 IDE 기능을 사용할 수 있어 프로그래밍할 때 도움이 됩니다.

throw new ArgumentNullException("Product");

대신에

throw new ArgumentNullException(nameof(product));

식으로 해당 타입의 이름을 직접 불러올 수 있습니다.

13.1. 속성 이름을 nameof 연산자로 가져오기

이번에는 nameof 연산자를 사용하여 속성 자체를 문자열로 출력해보겠습니다.

코드: NameOfDemo.cs

> // `nameof` 연산자: C# 6.0의 새로운 기능: 개체 이름 문자열 제공
> class Car
. {
.     public string Maker { get; set; }
. }
> Car car = new Car();
> Console.WriteLine(nameof(car.Maker));
Maker

Car 클래스의 Maker 속성이 문자열인 "Maker"와 같이 속성 이름을 문자열로 가져올 때에는 nameof 연산자를 사용합니다.

메서드 이름을 nameof 연산자로 가져오기

속성 이외에 메서드 이름 자체도 nameof 연산자를 사용하여 문자열로 가져올 수 있습니다.

코드: NameOfOperator.cs

using System;

class NameOfOperator
{
    static void Main()
    {
        Console.WriteLine("NameToString");          // "NameToString"
        Console.WriteLine(nameof(NameToString));    // "NameToString"
    }

    static void NameToString()
    {
        // 프로그래밍할 때 함수명 또는 속성명을 다른 곳에 문자열로 넘겨주어야할 때가 있다. 
    }
}
NameToString
NameToString

위 예제에서는 단순히 화면에 NameToString 메서드의 이름을 출력하였지만, nameof 연산자를 사용하면 특정 함수명 또는 속성명을 문자열로 전달해야 하는 경우에 유용하게 사용할 수 있습니다. 직접 문자열로 작성해도 되지만, nameof()로 묶은 상태에서 개체 이름은 자동으로 인텔리센스의 도움을 받을 수 있고 문자열로 묶는 방식은 잘못 타이핑할 가능성이 높습니다. C# 기초 문법이 아닌 실제 현업 프로그램을 작성하다 보면 nameof 연산자가 굉장히 유용하게 사용됩니다.

익명 형식(Anonymous Type)

이름이 없이 직접 개체를 생성하는 문법이 익명 형식입니다. 익명 형식을 사용하면 특정 클래스로 형식을 만들 필요없이 간단히 사용할 개체를 만들어 낼 수 있습니다. 다음과 같은 간단한 코드로 NameAge 속성을 가지는 person 개체를 만들어 낼 수 있습니다. new 키워드 뒤에 지정하는 속성들은 타입 추론을 통해서 자동으로 stringint 형으로 형식이 결정됩니다.

> var person = new { Name = "홍길동", Age = 21 };
> person.Name
"홍길동"
> person.Age
21

익명 형식 사용하기

이번에는 익명 형식을 사용해보겠습니다.

코드: AnonymousType.cs

using System;

class AnonymousType
{
    static void Main()
    {
        var data = new { Id = 1, Name = "익명 형식" };
        Console.WriteLine($"{data.Id} - {data.Name}");
    }
}
1 - 익명 형식

개체를 만들 때에는 new 키워드 다음에 특정 데이터 형식을 지정하는데 이를 지정하지 않고 바로 중괄호 안에 직접 원하는 속성 이름과 속성 값을 지정하여 이름이 없는 익명 형식을 만들 수 있습니다.

무명 형식 사용하기

익명 형식은 다른 말로 무명 형식으로도 불립니다. 무명 형식에 관한 예제를 한번 더 살펴보겠습니다.

코드: AnonymousClass.cs

// 익명 형식(Anonymous Type): 익명(무명) 클래스(Anonymous Class)
using System;

class AnonymousClass
{
    static void Main()
    {
        // 익명 형식으로 개체를 생성하고 3개의 속성을 초기화
        var presenter = new { Name = "박용준", Age = 48, Topic = "C#" };

        // 개체 출력
        Console.WriteLine(
            $"{presenter.Name}, {presenter.Age}, {presenter.Topic}");
    }
}
박용준, 48, C#

익명 형식은 프로그램내에서 간단히 묶어서 사용하고자 하는 개체에 대해서 새로운 클래스를 만들지 않고 바로 개체로 만들어 사용할 때 유용합니다.

배열 모양의 익명 형식

익명 형식은 다음 샘플 코드와 같이 배열 형식으로도 사용이 가능합니다.

> var developers = new[] {
.     new { Name = "RedPlus", Age = 48 },
.     new { Name = "Taeyo", Age = 53 }
. };
> developers[0].Name
"RedPlus"
> developers[1].Age
53

익명 형식과 덕 타이핑

프로그래밍에서는 덕 타이핑(Duck Typing)이라는 개념이 있습니다. 덕 타이핑(Duck Typing)은 다음 문장의 의미를 가집니다.

"새가 오리처럼 생기고, 오리처럼 수영하며, 오리처럼 꽥꽥 거린다면, 나는 그 새를 오리라고 부를 것입니다."

덕 타이핑에 대한 내용을 코드로 살펴보겠습니다. 다음 코드는 DuckTyping.cs 파일에서 살펴볼 수 있습니다.

IdName의 속성을 갖는 익명 형식을 만들어 duck 개체에 할당합니다.

> var duck = new { Id = 1, Name = "Duck 1" };

duck 개체의 값을 출력하면 정수와 문자열이 출력됩니다.

> $"{duck.Id} - {duck.Name}"
"1 - Duck 1"

duck 개체에 또 다른 익명 형식을 대입합니다. 대신 앞서 선언한 모양과 동일한 속성과 형식을 갖는 개체를 대입해야 합니다.

> duck = new { Id = 2, Name = "Duck 2" };

동일한 모양의 개체이기에 값이 정상적으로 저장되고 출력됩니다.

> $"{duck.Id} - {duck.Name}"
"2 - Duck 2"

이번에는 처음에 선언한 모양이 아닌 Id에 실수 데이터를 넣어보겠습니다. 처음 선언된 duck 개체와 다른 개체가 대입되기에 다음과 같이 예외가 발생합니다.

> duck = new { Id = 3.14, Name = "Duck 3" };
(1,8): error CS0029: 암시적으로 '<anonymous type: double Id, string Name>' 형식을 '<anonymous type: int Id, string Name>' 형식으로 변환할 수 없습니다.

이번에는 Name 속성을 제외한 채 개체를 할당해보겠습니다. 이때에도 예외가 발생합니다.

> duck = new { Id = 3 };
(1,8): error CS0029: 암시적으로 '<anonymous type: int Id>' 형식을 '<anonymous type: int Id, string Name>' 형식으로 변환할 수 없습니다.

처음에 없던 Email 속성을 추가해서 할당해도 예외가 발생됩니다.

> duck = new { Id = 3, Name = "Duck 3", Email = "Email 3" };
(1,8): error CS0029: 암시적으로 '<anonymous type: int Id, string Name, string Email>' 형식을 '<anonymous type: int Id, string Name>' 형식으로 변환할 수 없습니다.

처음에 선언할 때의 개체 형태로 값을 할당하면 정상적으로 할당 및 출력이됩니다.

> duck = new { Id = 3, Name = "Duck 3" };
> $"{duck.Id} - {duck.Name}"
"3 - Duck 3"

처음 개체가 만들어지면 그 형식과 동일한 모양으로만 다시 할당이 됩니다. 이러한 내용을 덕 타이핑이라고 합니다.

속성에 대한 유효성 검사를 생성자를 통해서 구현

프로그램을 작성하다 보면 특정 속성은 반드시 특정 값으로 초기화되어야 하는 경우가 있습니다. 즉, null 또는 빈 값이 들어오면 안되는 경우가 있는데, 이러한 경우에는 생성자를 통해서 반드시 특정 문자열을 넘겨주도록 강제할 수 있습니다. 만약, 넘어온 값이 null 또는 빈 값이면 에러를 강제로 발생시킴으로써 좀 더 견고한 클래스를 만들 수 있습니다.

코드: PropertyValidation.cs

//[?] 특정 속성은 null 또는 빈 값이 들어오면 안되는 경우 => 생성자를 사용하여 초기화
using System;

namespace PropertyValidation
{
    class Car
    {
        public string Name { get; private set; }
        public Car(string name)
        {
            if (string.IsNullOrEmpty(name))
            {
                // 빈 값이면 강제로 ArgumentException 예외 발생
                throw new ArgumentException(); 
            }
            this.Name = name;
        }
    }

    class PropertyValidation
    {
        static void Main()
        {
            //[1] 정상 실행
            Car car = new Car("자동차");
            Console.WriteLine(car.Name); // 자동차

            //[2] 예외 발생
            //Console.WriteLine((new Car("")).Name); // 예외 발생
        }
    }
}
자동차

Main() 메서드의 [1]번 코드는 생성자의 매개 변수로 전달된 값으로만 속성을 초기화하는 예제입니다. 만약, [2]번 코드처럼 생성자에 빈 값이 전달되면 throw 구문에 의해서 강제로 예외를 발생시킬 수 있습니다.

메서드로 속성의 값 초기화하기

속성과 메서드를 함께 사용하는 예제를 만들어보겠습니다. 클래스와 속성을 만들고 /// (슬래시 3개)를 타이핑하면 자동으로 XML 주석인 <summary> 코드 주석이 만들어 집니다. 이곳에 클래스와 속성 그리고 메서드에 대한 설명을 입력하면 좋습니다.

***코드: PetDemo.cs ***

using System;

/// <summary>
/// 애완동물
/// </summary>
class Pet
{
    /// <summary>
    /// 몸무게
    /// </summary>
    public int Weight { get; set; }

    /// <summary>
    /// 먹이를 주면 몸무게 증가 
    /// </summary>
    /// <param name="weight">몸무게</param>
    public void Feed(int weight)
    {
        Weight += weight;
    }
}

class PetDemo
{
    static void Main()
    {
        Pet pet = new Pet();
        pet.Weight = 50; // 속성으로 값 초기화
        pet.Feed(10); // 메서드로 값 증가 
        Console.WriteLine(pet.Weight); // 60
    }
}
60

애완동물을 나타내는 Pet 클래스에 몸무게를 의미하는 Weight 속성을 정의하였습니다. Feed() 메서드를 통해서 전달된 값을 속성에 더하여 몸무게가 증가되는 내용을 다루어 보았습니다. 자동으로 구현된 속성을 사용하면 Weight와 같이 클래스에서 사용되는 데이터를 위해 따로 필드를 사용하지 않고 속성으로만 처리할 수 있습니다.

속성 선언과 동시에 초기화

자동 속성과 화살표 연산자를 사용하여 앞서 살펴본 예제를 좀 더 간결하게 꾸며보도록 하겠습니다. 속성을 선언과 동시에 초기화하여 사용하는 예제를 만들어 보겠습니다.

코드: PropertyPractice.cs

using System;

class Fish
{
    public int Weight { get; set; } = 50; 
    public void Feed(int weight) => Weight += weight;
}

class PropertyPractice
{
    static void Main()
    {
        var fish = new Fish();
        fish.Weight = 10;
        fish.Feed(15);
        Console.WriteLine(fish.Weight); // 25
    }
}
25

Fish 클래스의 Weight 속성은 선언과 동시에 50으로 초기화됩니다. 그리고 Feed 메서드는 매개 변수로 넘어온 소문자 weight 매개 변수의 값을 대문자 Weight 속성에 누적시키는 역할을 합니다. 내용은 앞선 예제와 동일한데 코드는 좀 더 간단합니다.

클래스, 속성, 메서드 함께 사용하기

다음 샘플 코드는 클래스와 속성 그리고 메서드를 함께 사용한 내용을 보여줍니다.

코드: ClassPropertyMethod.cs

> // 클래스
. class Point
. {
.     // 속성
.     public int X { get; set; } = 100;
.     public int Y { get; set; } = 200; 
.     // 메서드(함수)
.     public void Draw() => Console.WriteLine($"X: {this.X}, Y: {this.Y}");
. }
> (new Point()).Draw()
X: 100, Y: 200

속성에 대해서 ?.?? 연산자를 함께 사용하기

개체에 들어있는 속성의 값이 null일 때에는 전에 살펴봤던 ?. 연산자와 ?? 연산자를 사용하여 널 값에 대한 처리를 편하게 할 수 있습니다. 다음 코드는 약간 복잡할 수 있으니, 한 번 정도 작성 및 실행 후 넘어가도 됩니다.

코드: NullWithObject.cs

using System.Collections.Generic;
using static System.Console;

namespace NullWithObject
{
    class Person
    {
        public string Name { get; set; }
        public Address Address { get; set; }
    }

    class Address
    {
        public string Street { get; set; } = "알 수 없음";
    }

    class NullWithObject
    {
        static void Main()
        {
            var people = new Person[] { new Person { Name = "RedPlus" }, null };

            ProcessPeople(people);

            void ProcessPeople(IEnumerable<Person> peopleArray)
            {
                foreach (var person in peopleArray)
                {
                    // [1] ?.로 널 확인 후 널이면 ?? 이후의 문자열로 초기화 
                    WriteLine($"{person?.Name ?? "아무개"}은(는) " +
                        $"{person?.Address?.Street ?? "아무곳"}에 삽니다.");
                }
            }

            var otherPeople = null as Person[];

            // [2] ?[0] 형태로 인덱서에 대해서 널 값 확인 가능
            WriteLine($"첫 번째 사람: {otherPeople?[0]?.Name ?? "없음" }");
        }
    }
}
RedPlus은(는) 아무곳에 삽니다.
아무개은(는) 아무곳에 삽니다.
첫 번째 사람: 없음

[1]번 코드처럼 person?.Name 형태는 Name 속성이 null이 아니면 해당 Name 속성을 사용하고 그렇지 않으면 null을 반환하여 ?? 연산자를 추가하여 null 대신 "아무개"를 반환하는 형태로 null을 처리합니다. [2]번 코드처럼 otherPeople?[0]?.Name 형태로 [0]번째 인덱스의 배열 값이 null인지를 확인하는 조금 복잡하지만, null에 대한 처리를 효과적으로 하는 연산자를 제공합니다.

NOTE

이번 예제의 내용처럼 ?.?? 연산자를 함께 사용하는 것은 오히려 더 복잡한 코드를 만들 수도 있습니다. 하지만, 이 두 연산자에 대한 확실한 이해가 생기고나면 이보다 더 간결한 코드가 없을 정도로 깔끔한 코드를 만들어줍니다.

init 키워드

C# 9에서 도입된 init 키워드는 개체 초기화 시에만 속성 값을 설정할 수 있도록 제한하는 기능을 제공합니다. 이를 사용하면 개체가 생성된 후 속성이 변경되지 않도록 보장할 수 있습니다.

코드: InitDemo.cs

using static System.Console;

public class PersonInit
{
    public string? Name { get; init; }  // Name은 개체 초기화 시에만 설정 가능
    public int Age { get; init; }  // Age는 개체 초기화 시에만 설정 가능
    public string? Address { get; set; } // Address는 개체 생성 후에도 변경 가능
}

class InitDemo
{
    static void Main()
    {
        // 개체 초기화 시 속성 값 설정 가능
        var person = new PersonInit { Name = "홍길동", Age = 30, Address = "서울" };
        WriteLine($"이름: {person.Name}, 나이: {person.Age}, 주소: {person.Address}");

        // 개체가 생성된 후에도 Address 속성 값 변경 가능
        person.Address = "부산";
        WriteLine($"변경된 주소: {person.Address}");

        // 개체가 생성된 후에는 init 속성 값 변경 불가능 (컴파일 오류 발생)
        // person.Age = 35;  // 오류: 'init' 속성은 초기화 이후 변경할 수 없음
    }
}
이름: 홍길동, 나이: 30, 주소: 서울
변경된 주소: 부산

이 예제에서 PersonInit 클래스는 NameAge 속성의 set 접근자를 init으로 설정하여 개체가 생성될 때만 값을 설정할 수 있도록 하였습니다. 반면 Address 속성은 set 접근자를 사용하여 개체가 생성된 이후에도 변경할 수 있습니다. InitDemo 클래스의 Main 메서드에서 PersonInit 개체를 초기화한 후, Address 속성은 변경할 수 있지만 NameAge 속성은 변경할 수 없습니다. 이를 통해 불변 속성과 변경 가능한 속성을 분리하여 안전한 데이터 모델을 구축할 수 있습니다.

장 요약

속성은 클래스의 필드의 값을 설정하거나 읽는 기능을 편리하게 해줍니다. 이러한 속성은 개체 초기화와 익명 형식 등에서 자주 사용됩니다. C#의 속성은 속성 개념이 없는 Java와 같은 다른 언어 비해 코드의 간결함을 유지해주는 장점으로 강조됩니다.

정보처리기사 실기 시험 기출 문제 - 참조 변수와 메서드 호출

문제

다음 C# 프로그램이 실행되었을 때의 동작을 설명하고, 출력 결과를 예측하시오.

소스 코드 파일명: ReferenceVariableDemo.cs

using System;

class MyClass {
    public int A { get; set; }
    public int B { get; set; }
}

public class ReferenceVariableDemo {
    static void ModifyFieldA(MyClass m) {
        m.A *= 10;
    }
    static void ModifyFieldAB(MyClass m) {
        m.A += m.B;
    }
    public static void Main(string[] args) {
        MyClass m = new MyClass();
        m.A = 100;
        ModifyFieldA(m);
        m.B = m.A;
        ModifyFieldAB(m);
        Console.WriteLine(m.A);
    }
}

입력 예시

이 프로그램은 사용자로부터 입력을 받지 않습니다.

출력 예시

1100

해설

이 프로그램은 C#에서 자동 속성을 사용하여 개체의 상태를 간결하게 수정하는 방법과 메서드 호출을 통한 값의 변경 과정을 보여줍니다.

  1. MyClass라는 이름의 클래스는 AB라는 두 개의 자동 속성을 가집니다. 이 속성들은 간결한 구문을 사용하여 정의되며, 내부적으로는 각각의 백킹 필드를 자동으로 생성하고 관리합니다.
  2. ReferenceVariableDemo 클래스의 Main 메서드에서는 MyClass 타입의 개체 m을 생성하고, A 속성에 100을 할당합니다.
  3. ModifyFieldA 메서드는 개체 m의 참조를 전달받아, m.A 속성의 값을 10배 증가시킵니다. 이 때, m.A의 값은 1000이 됩니다.
  4. m.Bm.A의 값을 할당한 후, ModifyFieldAB 메서드를 호출하여 m.A 속성의 값에 m.B 속성의 값을 더합니다. 이 때, m.Am.B 모두 1000이므로, 최종적으로 m.A 속성의 값은 1100이 됩니다.
  5. 최종적으로, m.A 속성의 값을 출력합니다.
VisualAcademy Docs의 모든 콘텐츠, 이미지, 동영상의 저작권은 박용준에게 있습니다. 저작권법에 의해 보호를 받는 저작물이므로 무단 전재와 복제를 금합니다. 사이트의 콘텐츠를 복제하여 블로그, 웹사이트 등에 게시할 수 없습니다. 단, 링크와 SNS 공유, Youtube 동영상 공유는 허용합니다. www.VisualAcademy.com
박용준 강사의 모든 동영상 강의는 데브렉에서 독점으로 제공됩니다. www.devlec.com