클래스 기타

  • 16 minutes to read

지금까지 클래스부터 이벤트까지 클래스의 주요 구성 요소를 살펴보았습니다. 이번 강의에서는 클래스의 또 다른 기능들을 몇 가지 정리해보겠습니다.

> // 부분과 정적: 부분 클래스는 클래스를 나눠서 관리하고 정적 클래스는 정적 멤버로 구성됨

부분(Partial) 클래스

부분(Partial) 또는 분할 클래스는 C# 2.0부터 제공된 기능입니다. 기존의 클래스는 하나의 CS 파일에서 모든 멤버를 구현해야 했습니다. 그러나 클래스의 기능이 복잡해질수록 한 개의 파일에 모든 구현을 담는 것이 어려워졌습니다. 이를 해결하기 위해 .NET 2.0부터 partial 키워드를 사용하여 클래스를 여러 개의 CS 파일로 분할할 수 있도록 지원합니다. 이렇게 분할된 클래스는 컴파일 시 하나의 클래스로 병합됩니다.

부분 클래스를 사용하여 다른 파일에 멤버를 따로 관리

이번에는 동일한 이름의 클래스를 여러 CS 파일로 나누어 관리하는 방법을 살펴보겠습니다.
PartialClassDemo 프로젝트에서 FirstDeveloper.cs, SecondDeveloper.cs, PartialClassDemo.cs 파일을 생성하고 아래와 같이 코드를 작성합니다.

코드: FirstDeveloper.cs

//[1] Hello 클래스의 첫 번째 파일
using System;

namespace PartialClassDemo
{
    public partial class Hello
    {
        public void Hi() => Console.WriteLine("FirstDeveloper.cs");
    }
}

코드: SecondDeveloper.cs

//[2] Hello 클래스의 두 번째 파일
using System;

namespace PartialClassDemo
{
    public partial class Hello
    {
        public void Bye() => Console.WriteLine("SecondDeveloper.cs");
    }
}

코드: PartialClassDemo.cs

//[?] 부분 클래스를 사용하여 하나의 프로젝트 또는 CS 파일에 
//    동일한 이름의 클래스를 하나 이상 두고 개발할 때 partial 키워드 사용 
namespace PartialClassDemo
{
    class PartialClassDemo
    {
        static void Main()
        {
            //[A] Hello 클래스의 개체로 서로 다른 파일의 멤버들 호출 가능
            var hello = new Hello();
            hello.Hi(); // FirstDeveloper.cs
            hello.Bye(); // SecondDeveloper.cs
        }
    }
}
FirstDeveloper.cs
SecondDeveloper.cs

[1]번 코드에서는 Hello 클래스에 Hi() 메서드를 정의하고, [2]번 코드에서는 Bye() 메서드를 정의하였습니다. [A]번 코드에서는 Hello 클래스의 인스턴스를 생성한 후, Hi()Bye() 메서드에 접근하는 모습을 확인할 수 있습니다. 이처럼 부분 클래스를 활용하면 하나 이상의 CS 파일에 동일한 이름의 클래스를 나누어 정의하고 효과적으로 관리할 수 있습니다.

부분 클래스를 사용하여 속성과 메서드 멤버를 나눠서 관리

부분(Partial) 클래스를 사용하면 클래스의 여러 멤버를 동일한 이름의 클래스에 나눠서 관리할 수 있는 편리함을 제공합니다.

코드: PartialClass.cs

// [?] 부분 클래스(Partial Class): 하나 이상의 동일한 클래스에 멤버를 나눠서 관리 
using System;

namespace PartialClass
{
    // [1] 클래스에 partial 키워드를 붙여 부분 클래스로 설정하고 멤버 제공
    public partial class Person
    {
        public string Name { get; set; }
        public int Age { get; set; }
    }

    // [2] 부분 클래스의 다른 클래스/파일에 정의된 멤버 사용 가능 
    public partial class Person
    {
        public void Print() => Console.WriteLine($"{Name}: {Age}");
    }

    class PartialClass
    {
        static void Main()
        {
            // [A] 부분 클래스인 Person 클래스의 인스턴스 생성
            Person person = new Person();

            // [B] 멤버가 함께 노출되는 것 확인
            person.Name = "C#";
            person.Age = 20;

            // [C] 출력
            person.Print();
        }
    }
}
C#: 20

PartialClass 네임스페이스에는 [1]번과 [2]번과 같이 동일한 이름을 가지는 Person 클래스가 있습니다. 동일한 이름의 클래스를 가질 수 있는 이유는 클래스 시그니처에 partial 키워드가 제공되기 때문입니다. [1]Person 클래스에는 속성 멤버들만 제공하고 [2]Person 클래스에는 메서드 멤버만 제공해 보았습니다. 프로그램을 실행하는 과정에서 [B]번과 [C]번 내용처럼 서로 다른 곳에 정의된 멤버들이 모두 하나의 인스턴스 개체인 person으로 접근해서 사용할 수 있습니다.  

부분 속성(Partial Property)

C# 13에서는 partial 속성을 지원하여 속성의 선언구현을 분리할 수 있습니다. 이를 활용하면 코드의 유지보수성가독성이 향상되며, 자동 코드 생성 도구와의 통합도 용이해집니다. 아래 예제에서는 partial 속성을 사용하여 두 개의 partial class 정의로 속성을 나누고, 이를 Main 메서드에서 테스트합니다.

코드: PartialPropertyDemo.cs

using System;

// 첫 번째 파일: 선언 부분
public partial class EmployeePartial
{
    public partial string FullName { get; set; }
}

// 두 번째 파일: 구현 부분
public partial class EmployeePartial
{
    private string _fullName = "";
    public partial string FullName
    {
        get => _fullName;
        set => _fullName = value.ToUpper(); // 값을 대문자로 변환
    }
}

class PartialPropertyDemo
{
    static void Main()
    {
        EmployeePartial employee = new()
        {
            FullName = "john doe"
        };
        Console.WriteLine(employee.FullName); // JOHN DOE
    }
}
JOHN DOE

C# 13의 partial 속성을 사용하면 속성의 선언과 구현을 다른 파일에서 정의할 수 있습니다. 위 코드에서는 EmployeePartial 클래스의 FullName 속성을 partial로 선언한 후, 별도의 partial class 정의에서 getset 접근자를 구현했습니다.

특히 set 접근자에서는 값을 대문자로 변환하여 저장하는 동작을 추가하였습니다. 이처럼 부분 속성을 활용하면 코드 생성 도구와의 통합이 쉬워지고, 여러 개발자가 협업할 때도 유용하게 사용할 수 있습니다.

정적 클래스(Static Class)

C#에서는 클래스 이름 앞에 static 키워드를 붙여 정적 클래스(Static Class)를 만들 수 있습니다.

정적 클래스는 다음과 같은 특징들이 있습니다.

  • static 키워드를 붙여 선언
  • 정적 멤버만 포함 가능
  • 인스턴스화 불가능
  • 유틸리티 클래스 용도로 사용
  • 팩터리 클래스로 활용 가능

참고: 싱글톤(Singleton) 패턴

싱글톤(Singleton) 패턴은 하나의 프로그램에서 단 하나의 인스턴스만 존재하도록 제한하는 기법입니다.
정적 클래스(Static Class)는 인스턴스화가 불가능하지만, 싱글톤 패턴을 적용하면 인스턴스화 가능한 단일 개체를 생성할 수 있습니다.

필드에 public 붙여 외부 클래스에 공개하기

박용준 강사의 강의에서는 필드를 무조건 private으로 선언하지만, 필드를 public으로 선언해도 문제는 없습니다.

코드: PointImperative.cs

> class Point
. {
.     // 필드: public 필드
.     public int x;
.     public int y;
.     // 생성자
.     public Point(int x, int y)
.     {
.         this.x = x;
.         this.y = y;
.     }
.     // 메서드
.     public void MoveBy(int dx, int dy)
.     {
.         x += dx;
.         y += dy;
.     }
. }
> 
> Point point = new Point(0, 0); // 좌표 기본값 설정
> point.MoveBy(100, 200); // 100, 200으로 이동
> Console.WriteLine($"X: {point.x}, Y: {point.y}"); // 100, 200
X: 100, Y: 200

필드를 public으로 선언해도 프로그램에 문제가 발생하지는 않습니다.

하지만 필드는 클래스의 부품 역할을 하기에 이왕이면 꽁꽁 숨기는 것을 권장하기에 private으로 선언하는 것이 더 바람직합니다.

함수형 프로그래밍 스타일: 메서드 체이닝

메서드의 반환값을 자신의 클래스 형식으로 지정하면 메서드를 계속 반복해서 호출하는 함수형 프로그래밍 스타일을 제공할 수 있습니다.

코드: PointFunctional.cs

> class Point
. {
.     // readonly 필드
.     public readonly int x;
.     public readonly int y;
.     public Point(int x, int y)
.     {
.         this.x = x; // readonly 필드는 반드시 생성자로 초기화 필요
.         this.y = y;
.     }
.     //[1] 메서드의 반환값을 나 자신(Point)으로 지정 
.     public Point MoveBy(int dx, int dy)
.     {
.         return new Point(x + dx, y + dy);
.     }
. }
> 
> //[A] 함수형 프로그래밍 스타일: 메서드 체이닝
> var p = (new Point(0, 0)).MoveBy(10, 10).MoveBy(20, 20).MoveBy(30, 30);
> $"X: {p.x}, Y: {p.y}"
"X: 60, Y: 60"

이미 우리는 LINQ에서 메서드 체이닝 개념을 학습했습니다. 이러한 메서드 체이닝을 구현하려면 반환 값으로 나 자신의 개체를 반환하면 됩니다. 메서드 체이닝이 사용할 때에는 편리하지만 구현할 때에는 코드의 복잡도가 증가합니다. 나중에 우리는 확장 메서드를 만드는 방법을 배웁니다. 확장 메서드를 사용하면 이번 예제에서 만드는 방법보다 훨씬 간단하게 메서드 체이닝을 구현할 수 있습니다.

불변 형식

영어 단어로 Immutable은 '변경 불가능한'의 의미를 가집니다. 프로그래밍에서 불변 형식(Immutable Type)은 개체가 만들어지고 값이 변경되지 않음을 의미합니다. 개체가 생성된 후 변경되지 않아야 프로그래밍 부작용을 줄일 수 있는 경우에 사용합니다.

코드: ImmutableTypeDemo.cs

> //[?] 불변 형식: 개체의 상태는 생성 후 변경되지 않아야 프로그래밍 부작용을 줄임
> public class Circle
. {
.     public int Radius { get; private set; } = 0;
.     public Circle(int radius) => Radius = radius;
.     public Circle MakeNew(int radius) => new Circle(radius);
. }
> 
> //[1] 생성자를 통해서 반지름이 10인 Circle 개체 생성
> Circle circle = new Circle(10);
> $"Radius: {circle.Radius} - {circle.GetHashCode()}"
"Radius: 10 - 62301924"
> 
> //[2] 메서드를 통해서 반지름이 20인 Circle 개체 새롭게 생성
> circle = circle.MakeNew(20);
> $"Radius: {circle.Radius} - {circle.GetHashCode()}"
"Radius: 20 - 37804102"

처음에 생성자를 통해서 생성된 circle 개체는 반지름이 10으로 설정되어 더 이상 변경되지 않습니다. 이렇게 특정 개체가 가지는 속성은 생성 후 변경되지 않도록 설정하면 중간에 속성을 통한 값이 변경되어 잘못된 개체가 되는 걸 방지할 수 있습니다. 만약, 이 개체의 속성을 변경하려면 새로운 메서드를 통해서 새로운 개체를 생성하여 사용하면 됩니다.

레코드(Record)

C# 9.0부터 도입된 레코드 형식은 불변 개체를 쉽게 정의할 수 있는 참조 타입입니다.

record는 동일한 값을 가지면 같은 것으로 간주되는 값 비교(Value Equality)를 기본으로 하며, 생성된 이후 변경할 수 없는 불변성(Immutability)을 가지며, with 식을 활용하여 새로운 값을 가진 개체를 생성할 수 있고, 일반적인 class보다 짧고 간결한 코드로 정의할 수 있습니다.

다음 예제는 record를 사용하여 불변 개체를 생성하고 값을 변경하는 방법을 보여줍니다.

코드: RecordDemo.cs

using System;

// 구독자 정보를 나타내는 레코드
record Subscriber(string Title, int Duration, bool IsAvailable);

class RecordDemo
{
    static void Main()
    {
        // 구독자 개체 생성
        var subscriber = new Subscriber("Visual", 3, true);

        // 새로운 제목을 가진 개체 생성
        var vip = subscriber with { Title = "VIP" };

        Console.WriteLine(vip);

        // 개체 값 분해
        var (title, duration, isAvailable) = subscriber;

        Console.WriteLine($"{title} - {duration} - {isAvailable}");
    }
}
Subscriber { Title = VIP, Duration = 3, IsAvailable = True }
Visual - 3 - True

이 코드는 Subscriber 개체를 생성하고, with 식을 사용하여 새로운 개체를 만들고, 값을 분해하는 방법을 보여줍니다.

record class는 참조 타입이며, record struct는 값 타입으로 동작합니다.

코드: RecordStructDemo.cs

using System;

// 직원 정보를 나타내는 레코드 클래스
public record class EmployeeClass(string Name, int Age);

// 직원 정보를 나타내는 레코드 구조체
public record struct EmployeeStruct(string Name, int Age);

class RecordStructDemo
{
    static void Main()
    {
        // 직원 개체 생성
        var employee1 = new EmployeeClass("홍길동", 21);

        // 새로운 나이를 가진 개체 생성
        var employee2 = employee1 with { Age = 31 };

        Console.WriteLine(employee2);
    }
}
EmployeeClass { Name = 홍길동, Age = 31 }

이 코드는 record classrecord struct의 차이를 보여주며, with 식을 사용하여 개체를 변경하는 방법을 설명합니다.

레코드는 클래스보다 간결한 코드로 사용될 수 있으나, 이 강의 전체에서는 클래스를 첫 번째 기준으로 사용합니다.

장 요약

C#에서는 클래스는 여러 모양을 가질 수 있습니다. 그 중에서 이번 강의는 여러 파일에 클래스를 나눠서 관리할 수 있는 부분 클래스와 정적인 멤버로만 구성할 수 있는 정적 클래스를 다루어 보았습니다.

VisualAcademy Docs의 모든 콘텐츠, 이미지, 동영상의 저작권은 박용준에게 있습니다. 저작권법에 의해 보호를 받는 저작물이므로 무단 전재와 복제를 금합니다. 사이트의 콘텐츠를 복제하여 블로그, 웹사이트 등에 게시할 수 없습니다. 단, 링크와 SNS 공유, Youtube 동영상 공유는 허용합니다. www.VisualAcademy.com
박용준 강사의 모든 동영상 강의는 데브렉에서 독점으로 제공됩니다. www.devlec.com