15 3월 2024
C#의 접근 방식과 언어 구조 중 어떤 것이 좋은지, 어떤 것이 좋지 않은지에 대해 이야기해 보겠습니다. 물론 좋은지 좋지 않은지 판단하는 기준은 C#에서 번역한 후 결과물인 Java 코드가 얼마나 가독성과 유지보수가 가능한지를 고려합니다.
C#에는 숨겨진 작업을 많이 수행하는 간결한 언어 구조가 몇 가지 있습니다. 이러한 구성을 다른 언어로 번역할 때는 숨겨진 부분을 암시적으로 재현해야 하며, 대부분의 경우 코드가 원래의 디자인을 잃고 매우 달라집니다.
자동 프로퍼티는 C# 프로그래머들 사이에서 매우 널리 사용되고 있습니다. 이러한 종류의 프로퍼티를 사용하면 프로그래머는 get 및/또는 set 메서드를 통해 숨겨진 필드와 상호 작용할 수 있습니다. C#에서는 자동 프로퍼티의 실제 구현에서 벗어나 매우 간결한 구문을 사용하여 프로퍼티를 선언할 수 있습니다. 하지만 Java에는 이러한 언어 구조가 없기 때문에 프로퍼티를 필드 및 액세스 제어 메서드로 명시적으로 선언해야 합니다:
public int Value { get; set; }
Java에서는 다음과 같습니다:
private int auto_Value;
public int get_Value()
{
return auto_Value;
}
public void set_Value(int value)
{
auto_Value = value;
}
이제 하나의 솔리드 멤버가 아니라 세 개의 개별 멤버가 있습니다. 각 자동 프로퍼티에 대해 동일한 코드가 반복된다고 상상해보면 어떤 모습일까요? 그런 불쾌한 경험을 피합시다. 하지만 어떻게 할까요?
자동 프로퍼티를 비공개 필드에 대한 액세스 제어 기능을 제공하는 객체로 대체해 보세요. 이러한 객체의 해시 맵이 있을 수 있습니다. 일부 데이터에 대한 액세스를 제한하는 설계를 따르고 있다면 좋은 방법이 될 것입니다. 자동 프로퍼티는 멋져 보일 수 있지만 반드시 사용할 필요는 없습니다.
C#에는 구조체(값 유형)를 위한 전용 메모리 로직이 있습니다. 이러한 구조체의 수명은 스택 프레임 또는 포함 개체의 수명에 의해 제한되며, 함수 인수로 전달하거나 함수에서 반환하거나 일부 필드에 할당할 때 참조가 아닌 값으로 작업하기 때문에 자주 복사됩니다. 복사본을 변경할 때 원본은 변경하지 않습니다. Java 클래스는 항상 참조 유형임에도 불구하고 값 유형을 Java로 변환하면 동일한 로직을 다시 만들어야 합니다. 각 복사본을 저장하기 위해 힙에서 메모리를 할당하고 가비지 컬렉터에 과부하가 걸리므로 이제 잦은 복사가 문제가 됩니다. 성능을 관심사 중 하나로 고려한다면 메모리 관리 세부 사항에서 C# 코드를 추상화해야 합니다. 하지만 어떻게 할까요?
가장 쉬운 방법은 값 유형을 변경 불가능하게 만드는 것입니다. 변경 가능한 상태가 없으면 해당 상태를 복사할 필요가 없으므로 결정되지 않은 동작을 방지할 수 있습니다.
이제 코드의 시각적 특성만 바꾸고 행동은 바꾸지 않는 언어 구성 요소에 대해 이야기할 시간입니다. 예를 들어:
public class Item
{
string name;
string price;
public Item(string name, int price) => (this.name, this.price) = (name, price);
public string ToString() => $"Name = {name}, Price = {price}";
public string Name => name;
public int Price => price;
}
여기 튜플 분해(이 표현식은 실제로 튜플을 만들지 않습니다), 보간 문자열 리터럴, 표현식 본문 메서드와 속성을 볼 수 있습니다.
대리자는 메서드 선언의 짧은 형태이기 때문에 사용하기 좋습니다. 예를 살펴보겠습니다:
using System;
using System.Linq;
class Program
{
delegate int ChangeNumber(int arg);
static void Main()
{
Console.WriteLine("Input some numbers");
int[] numbers = Console.ReadLine().Split(" ").Select(int.Parse).ToArray();
Console.WriteLine("Input addition");
int addition = int.Parse(Console.ReadLine());
ChangeNumbers(n => n + addition, numbers);
Console.WriteLine("Result :");
Console.WriteLine(string.Join(" ", numbers.Select(n => n.ToString())));
}
static void ChangeNumbers(ChangeNumber change, int[] numbers)
{
for(int i = 0; i < numbers.Length; i++)
{
numbers[i] = change(numbers[i]);
}
}
}
n => n + addition
표현식에 대해 우리는 Java 익명 클래스 표현식을 생성할 수 있습니다:
// translated to Java code
interface ChangeNumber
{
int invoke(int arg);
}
// ...static void main(String[] args)...
// anonymous class expression for Java 7 or older version
changeNumbers(new ChangeNumber()
{
public int invoke(int n)
{
return n + addition;
}
}, numbers);
// or lambda expression for higher Java 8 or newer version
changeNumbers(n -> n + addition, numbers);
C#에는 구문 설탕 뒤에 큰 구현을 숨기는 코드를 간단하게 보이게 하는 많은 언어 구성 요소가 있습니다. 이러한 구성 요소 중 일부는 의심스럽고 지원하기 어렵고, 다른 일부는 유연하고 쉽게 재현할 수 있습니다. 설계 측면에서는 C# 전용 언어 구성 요소가 아닌 객체로 추상화를 구성하는 것이 좋습니다. 성능 측면에서는 메모리 관리로부터 추상화를 유지하고, 이를 두 배의 비용으로 에뮬레이션하는 것으로부터 자유롭게 해야 합니다.