
C#에서 여러 개의 결과를 한 번에 돌려주고 싶을 때 여러분은 어떻게 해결하셨나요?
굳이 새로운 class나 struct를 정의하기는 귀찮고 번거롭습니다.
이럴 때 '간단한 종이봉투' 역할을 해주는 튜플(Tuple)이라는 기능이 있습니다.
새로운 class나 struct를 따로 정의하지 않고, 여러 값을 하나로 묶는 방법입니다.
튜플(Tuple)은 ()소괄호를 사용하여 간단하게 만들 수 있으며,
각 요소에는 이름을 붙일 수도 있어 가독성이 매우 뛰어납니다.
// (int, string, bool) 타입의 튜플 생성
var myTuple = (20, "C#", true);
// 요소에 접근할 때는 Item1, Item2... 순서로 접근
Console.WriteLine(myTuple.Item1);
Console.WriteLine(myTuple.Item2);
[실행 결과]
20
C#
Item1, Item2, Item3... 와 같이 순서에 기반한 이름을 사용합니다.
어딘가 좀 아쉽지 않나요? 각 항목이 무엇을 의미하는지 알기 어렵습니다.
// 각 요소에 Id, Name 이라는 이름을 부여
var user = (Id: 1, Name: "홍길동");
// 이제 의미있는 이름으로 접근 가능! (가독성 UP!)
Console.WriteLine($"ID: {user.Id}");
Console.WriteLine($"이름: {user.Name}");
[실행 결과]
ID: 1
이름: 홍길동
명명된 튜플(Named Tuple)은 만들 때 각 요소에 이름을 붙여줄 수 있습니다.
이제 user.Item1대신 user.Id로 접근할 수 있으니 코드가 훨씬 명확해졌습니다!
튜플의 진가는 메서드의 반환 값으로 사용될 때 제대로 발휘됩니다.
class Program
{
static void Main()
{
int[] data = { 3, 2, 8, 1, 9 };
int minResult, maxResult; // 변수를 미리 선언해야 함
FindMinMax(data, out minResult, out maxResult);
Console.WriteLine($"Min: {minResult}, Max: {maxResult}");
}
// 전달된 모든 숫자의 최솟값과 최댓값을 구하는 메서드
static void FindMinMax(int[] numbers, out int min, out int max)
{
min = numbers.Min();
max = numbers.Max();
}
}
[실행 결과]
Min: 1, Max: 9
코드가 장황하고 직관적이지 않습니다.
class Program
{
static void Main()
{
int[] data = { 3, 2, 8, 1, 9 };
var result = FindMinMax(data); // 하나의 변수로 결과를 받음
Console.WriteLine($"Min: {result.Min}, Max: {result.Max}");
}
// 반환 타입을 (int Min, int Max) 튜플로 지정
static(int Min, int Max) FindMinMax(int[] numbers)
{
int min = numbers.Min();
int max = numbers.Max();
return (min, max); // 튜플을 생성하여 반환
}
}
[실행 결과]
Min: 1, Max: 9
코드가 간결해지고, "최솟값과 최댓값을 반환하는구나!" 하고 바로 이해할 수 있습니다.
튜플의 또 다른 강력한 기능은 바로 분해(Deconstruction)입니다.
반환받은 튜플 꾸러미를 풀어서 각각의 값을 개별 변수에 바로 할당할 수 있습니다.
[사용법1]
class Program
{
static void Main()
{
int[] data = { 5, 2, 8, 1, 9 };
// FindMinMax가 반환한 튜플을 (min, max) 두 변수로 바로 분해하여 할당
(int min, int max) = FindMinMax(data);
Console.WriteLine($"최솟값은 {min} 입니다.");
Console.WriteLine($"최댓값은 {max} 입니다.");
}
// 반환 타입을 (int Min, int Max) 튜플로 지정
static(int Min, int Max) FindMinMax(int[] numbers)
{
int min = numbers.Min();
int max = numbers.Max();
return (min, max); // 튜플을 생성하여 반환
}
}
[실행 결과]
최솟값은 1 입니다.
최댓값은 9 입니다.
[사용법2]
class Program
{
static void Main()
{
int[] data = { 5, 2, 8, 1, 9 };
// 만약 최솟값만 필요하고 최댓값은 버리고 싶다면?
// 언더스코어(_)를 사용하여 무시합니다.
(int min, _) = FindMinMax(data); // 최댓값은 무시
Console.WriteLine($"최솟값만 필요해: {min}");
}
// 반환 타입을 (int Min, int Max) 튜플로 지정
static(int Min, int Max) FindMinMax(int[] numbers)
{
int min = numbers.Min();
int max = numbers.Max();
return (min, max); // 튜플을 생성하여 반환
}
}
[실행 결과]
최솟값만 필요해: 1
C#에서는 두 가지 종류의 튜플이 존재합니다.
하나는 클래스 기반의 튜플, 다른 하나는 구조체 기반의 튜플입니다.
우리가 앞에서 사용했던 var user = (Id: 1, Name: "홍길동");의 경우
내부적으로 구조체 기반의 튜플(ValueTuple)을 사용합니다.
이 두 튜플을 '강철 자동차', '알루미늄 스쿠터'에 비유해서 설명하겠습니다.
[클래스 기반의 튜플]
C# 4.0부터 있었던 고전적인 방식의 튜플입니다.
Tuple이라는 클래스를 직접 사용해서 만듭니다.
// 1. 클래스 기반의 튜플 생성 시 Tuple.Create 메서드를 사용합니다.
Tuple<int, string, int> legacyPerson = Tuple.Create(15, "홍길동", 30);
// 2. Item1, Item2, Item3, ... 로 고정됩니다.
Console.WriteLine($"아이디: {legacyPerson.Item1}");
Console.WriteLine($"이름: {legacyPerson.Item2}");
Console.WriteLine($"나이: {legacyPerson.Item3}");
// 3. 한 번 생성되면 내부 값을 변경할 수 없습니다.
// legacyPerson.Item3 = 41; // 이 코드는 컴파일 오류가 발생합니다.
튜플은 '서울에서 부산까지 가는 장거리 여행'을 위한 것이 아니라,
'집 앞에서 편의점까지 잠깐 다녀오는' 용도로 태어났습니다.
이런 용도에 '강철 자동차(클래스 튜플)'를 쓰는 것은 어떨까요?
시동을 걸고, 주차장에서 차를 빼고, 다녀와서 다시 주차하고...
너무 번거롭고 비효율적입니다. (클래스는 메모리 할당/해제에 비용이 듭니다.)
잠깐 쓰고 버릴 데이터 묶음인데도, 클래스이기 때문에
메모리 할당(Heap)과 정리(Garbage Collection)에 대한 부담이 있었습니다.
C# 개발팀은 이러한 문제점을 해결하기 위해 C# 7.0부터는 튜플의 본질에 맞는
'알루미늄 스쿠터(구조체 튜플)'로 새로 설계했습니다. 이것이 바로 ValueTuple입니다.
그냥 올라타고 바로 출발했다가, 돌아와서 아무 데나 세워두면 그만입니다.
이처럼 튜플의 '가볍게, 임시로'라는 본질적인 용도가,
구조체의 '빠르고 효율적인' 특성과 찰떡같이 맞아떨어지는 것입니다.
최신 C# 문법에서는 구조체 기반의 튜플(ValueTuple)을 권장합니다.
더 이상 여러 값을 반환하기 위해 out매개변수를 만들지 마세요.
여러 개의 결과를 한 번에 돌려주고 싶을 때 튜플이 편리합니다.
이 글에서 C# 튜플을 명확히 이해하는 데 도움이 되었기를 바랍니다.