1. 소개
드라마 볼 때 모든 등장인물에 대한 모든 정보를 다 알 필요는 없잖아? 스토리에 필요한 것만 알면 돼. 프로그래밍에서도 비슷해: 전체 객체가 아니라 필요한 필드, 값, 혹은 그걸로 뭔가 계산한 결과만 필요할 때가 많아.
프로젝션은 LINQ에서 원본 시퀀스의 요소를 새로운 형태로 바꾸는 거야. 보통 Select 메서드로 하지. Select는 마치 마법 기계처럼 각 요소에 함수를 적용해서 결과를 반환해줘. 그게 다야, 별거 없어.
언제 프로젝션이 필요할까?
- 유저의 이름만 뽑아서 보여주고 싶을 때, 전체 데이터를 다 쓸 필요 없지.
- 이메일 발송할 때: 고객 객체에서 주소랑 이름만 뽑아와.
- 계산할 때: 상품 가격에 세금 더해서 새로운 컬렉션 만들기.
- UI에선: 데이터 일부만 보여주고, 화면을 복잡하게 만들지 않으려고.
2. Select 메서드: 문법과 기본 사용법
기본 문법
LINQ는 두 가지 스타일을 지원해: Method Syntax랑 Query Syntax. Select는 둘 다 결과가 똑같아.
메서드 문법
var query = myCollection.Select(x => x.뭘_반환할지);
여기서 x는 컬렉션의 각 요소고, 오른쪽엔 그걸 어떻게 바꿀지 쓰는 거야.
쿼리 문법
var query = from x in myCollection
select x.뭘_반환할지;
이건 좀 더 SQL 느낌이지.
간단한 예시: 숫자 리스트
var numbers = new List<int> { 1, 2, 3, 4, 5 };
// 제곱값을 얻고 싶어
var squares = numbers.Select(n => n * n);
foreach (var s in squares)
{
Console.WriteLine(s); // 1, 4, 9, 16, 25
}
이렇게 숫자 컬렉션을 제곱값 컬렉션으로 바로 바꿨어 — 직접 for문 돌릴 필요도 없어!
예시 2
이전 예제에서 Product 클래스가 있었지:
public class Product
{
public string Name { get; set; }
public double Price { get; set; }
}
리스트가 있다고 해보자:
var products = new List<Product>
{
new Product { Name = "초콜릿", Price = 120 },
new Product { Name = "치즈", Price = 250 },
new Product { Name = "빵", Price = 55 }
};
모든 상품의 이름만 뽑고 싶으면 전체 객체가 필요 없지? 이름 리스트만 있으면 돼:
var names = products.Select(p => p.Name);
foreach (var name in names)
{
Console.WriteLine(name); // 초콜릿, 치즈, 빵
}
3. 반환되는 컬렉션 — 어떤 게 있을까?
Select는 항상 컬렉션을 반환해 (정확히는 IEnumerable<TOutput>), 여기서 TOutput은 함수 결과 타입이야. 네가 직접 정하는 거지: 문자열, 숫자, 익명 타입, 새 객체 등등.
새 타입으로 프로젝션하기
예를 들어, 원본 객체가 아니라 "프로젝션"만 받고 싶을 때:
var projections = products.Select(p => new { p.Name, 부가세포함가격 = p.Price * 1.2 });
foreach (var item in projections)
{
Console.WriteLine($"{item.Name}: {item.부가세포함가격}");
}
이렇게 이름이랑 부가세 포함 가격만 있는 익명 객체 컬렉션을 만든 거야!
4. 새 클래스나 익명 타입 반환하기
결과를 위한 클래스를 따로 만들 수도 있고, 구조가 "지금만" 필요하다면 익명 타입으로도 충분해.
익명 타입
var result = products.Select(p => new { 상품명 = p.Name, 이전가격 = p.Price, 새가격 = p.Price + 40 });
foreach (var p in result)
{
Console.WriteLine($"{p.상품명}: 이전 {p.이전가격}, 새 {p.새가격}");
}
익명 타입은 결과를 빠르게 만들 때 좋아, 굳이 클래스 만들 필요 없을 때!
직접 만든 클래스로 프로젝션하기
새 클래스를 만들어보자:
public class ProductDto
{
public string Name { get; set; }
public double PriceWithTax { get; set; }
}
이제 LINQ 쿼리 안에서:
var productsWithTax = products.Select(p => new ProductDto
{
Name = p.Name,
PriceWithTax = p.Price * 1.2
});
foreach (var p in productsWithTax)
{
Console.WriteLine($"{p.Name}: {p.PriceWithTax}");
}
5. 내부 컬렉션과 속성 접근하기
객체 안에 컬렉션이 있으면 어떡하지? 예를 들어, 각 유저마다 주문 리스트가 있다고 해보자.
public class User
{
public string Name { get; set; }
public List<Product> Purchases { get; set; }
}
모든 유저의 구매 리스트만 뽑고 싶어? 문제 없어!
var allPurchases = users.Select(u => u.Purchases);
foreach (var purchaseList in allPurchases)
{
foreach (var product in purchaseList)
{
Console.WriteLine(product.Name);
}
}
중요! 이 경우엔 "평평한" 상품 리스트가 아니라 컬렉션의 컬렉션이야. 모든 유저의 상품을 하나의 리스트로 만들고 싶으면? 그건 다음 강의에서 다룰 거야 — SelectMany를 배울 거거든.
6. 유용한 팁
컬렉션 타입 변환: ToList() 등
Select는 IEnumerable<T>를 반환해. 만약 일반 리스트(List<T>)로 바꾸고 싶으면 .ToList()만 붙이면 돼:
var nameList = products.Select(p => p.Name).ToList();
이제 익숙하게 쓸 수 있는 리스트가 생긴 거지.
요소의 인덱스 사용하기
가끔 컬렉션에서 몇 번째 요소인지 알아야 할 때가 있어. Select엔 이런 오버로드도 있어:
var indexed = products.Select((product, index) => new { 인덱스 = index, 이름 = product.Name });
foreach (var item in indexed)
{
Console.WriteLine($"{item.인덱스}: {item.이름}");
}
이렇게 하면 번호 매긴 리스트 같은 것도 쉽게 만들 수 있어.
프로그래밍에서의 프로젝션
Select 잘 쓰는 건 면접에서도 자주 나오는 질문이야. 이거 없이 요즘 앱은 상상하기 힘들지:
- DB나 외부 서비스에서 필요한 데이터만 뽑아오기.
- 앱 계층 간 데이터 전달 준비.
- 화면에 보여주거나 네트워크로 보낼 수 있게 데이터 변환.
- 메모리 절약 (컬렉션 전체 객체가 아니라 필요한 필드만 가져옴).
데이터 많이 다루는 회사들은 이걸로 비즈니스 로직을 짜는 경우가 많아.
7. 흔한 실수와 특징
초보 개발자들이 짜증나는 버그를 만나는 경우가 있는데 — 보통 LINQ의 지연 실행(deferred execution)을 잘 몰라서 그래. 예를 들어, Select를 호출해도 바로 쿼리가 실행되는 게 아니라, 실제로 컬렉션을 순회할 때(foreach, ToList() 등) 실행돼. 이 덕분에 "파이프라인"을 만들 수 있지만, 원본 데이터가 바뀌면 결과도 달라질 수 있다는 점을 주의해야 해.
그리고 Select는 원본 컬렉션을 바꾸지 않아 — 새로운 걸 만드는 거지. 그래서 products.Select(...) 한 뒤에 products에 새 필드가 생기길 기대하면 안 돼.
null 처리: 만약 프로젝션에서 null일 수 있는 필드를 접근한다면 꼭 신경 써야 해. C# 8+에선 NullReferenceException이 날 수 있다고 컴파일러가 경고해줘.
GO TO FULL VERSION