람다식과 LINQ 기초

람다식과 LINQ 기초

람다식은 C# 3.0에서 도입된 개념입니다. 람다식을 통해 C#은 프로그래밍 언어로써 표현력이 크게 진화되었습니다.  람다식은 생소하지만 익숙해 진다면 생산성과 코드 이해도 면에서 큰 발전을 경험할 수 있습니다.

람다식이란?

람다식은 delegate 키워드에서 발전된 형태로 이해하시면 됩니다. delegate 키워드 대신 =>(람다 연산자)가 사용됩니다. 

람다식은 일종의 메서드라고 생각하셔도 됩니다. 람다식을 통해 인자를 넘길 수 있으며 기존에 사용한 delegate나 익명 메서드를 대신하여 간단한 작성과 코드 이해도를 높일 수 있습니다.

var count = Count(numberList, n => n % 2 == 0);

위와 같이 사용이 가능합니다. 람다식을 처음 대면하면 이해하기가 어렵습니다. 그냥 위와 같이 코드를 주면서 ‘이렇게 사용하면 되! 이해했지?’ 라고 합니다. 우리는 코드를 길게 풀어서 하나씩 이해하도록 하겠습니다.

Step 1

람다식을 가장 길게 풀어쓴 코드입니다.

Predicate<int> even = 
  (int n) => {
    if (n % 2 == 0)
      return true;
    else
      return false;
  };

var count = Count(numberList, even);

Step 2

위 식에서 even과 count를 합쳐보겠습니다.

var count = Count(numberList, 
    (int n) => {
        if (n % 2 == 0)
        return true;
            else
        return false;
        }
    );

Step 3

if문을 없애봅시다. if문을 없앨 수 있는 이유는 아래 두 가지 이유 때문입니다.

1. return의 오른쪽에 식을 쓸 수 있다.

2. ‘n % 2 == 0’은 식이며 bool 형의 값을 가진다.

var count = Count(numberList, (int n) => { return n % 2 == 0; });

Step 3까지 오면 처음에 제시한 식과 비슷하게 바뀝니다.

Step 4

람다식은 인수의 형을 생략할 수 있습니다. 또한 인수가 한 개인 경우에는 괄호()를 생략할 수 있습니다.

var count = Count(numberList, n => { return n % 2 == 0; });

이러한 람다식을 통해 코드를 작성할 때 어떻게 하는가(How)가 아니라 무엇을 하는가(What)를 생각하며 코드를 작성할 수 있다. 이것은 추상도가 높아졌다고 말할 수 있습니다. 추상화가 되면 될수록 확장성이 좋은 코드가 됩니다.

왜 람다식을 사용할까?

람다식을 사용하는 이유는 길고 복잡한 코드를 짧고 간단하게 표현할 수 있도록 해줍니다. 위에서 언급했던 것처럼 생소하게 보이는 문법들이 있지만 있숙해지면 위 Step 처럼 점차 간결한 코드를 짤 수 있습니다. 

또한 곧 나올 LINQ에서 식을 작성하는데 매우 유용합니다. 람다식이 존재하지 않는다면 LINQ를 구현할 때 함수를 하나하나 정의해서 LINQ를 사용했을 것 입니다. 그러면 가독성이 떨어지고, 코드 수가 늘어나며, 사용되지 않는 프로그래밍 방식이 되었을 겁니다.

이처럼 로컬 함수를 만들어 사용할 때 유용하게 사용될 수 있습니다. 또한 LINQ에서는 서로 상호작용을 통해 매우 간결하게 코드를 작성할 수 있도록 도와줍니다. LINQ를 살펴보면서 어떻게 사용되는지 알아보겠습니다.

LINQ 구성요소

1. 시퀀스

시퀀스는 LINQ에서 IEnumerable<T>를 구현하는 모든 것을 의미합니다.

시퀀스는 반복자를 제공한다. 이 반복자는 컬렉션 자식 객체들 속을 순환하면서 컬렉션이 가진 개체의 값을 사용하거나 기능을 사용할 수 있도록 해줍니다. 반복자는 foreach, 컬렉션 객체 등에서 내부를 순환하기 위해 객체를 반환하는 매소드를 가지고 있습니다.

2. 지연된 질의 수행

지연된 질의 수행은 LINQ의 핵심이라고 필자는 생각합니다. 

지연된 질의 수행은 말 그대로 컴파일 할 때 수행하는 것이 아닌 호출 시점에서 질의를 수행하는 것입니다. 

간혹 즉시 질의 수행과 혼동하여, 원하는 값이 나오지 않을 수 있습니다. 지연된 질의 수행을 잘 인지하고 사용해야 합니다.

지연된 질의 수행의 장점은 호출 할 때만 수행되어 자원이 절약됩니다. 질의를 한번 정의하고 나면 필요할 때만 별도로 호출 할 수 있습니다. 결과를 선언 시 저장하지 않고 질의를 호출할 때 결과 값을 계산 후 반환 합니다.

물론 모든 LINQ 메서드가 지연된 질의를 수행하는 건 아니기 때문에 즉시 실행이 필요할 때는 특정 메소드(ToList() 등)를 선택하여 사용하면 됩니다. 

3. 질의 연산자

LINQ 질의들의 문맥을 이어주고 동작을 수행하는 확정 메서드의 집합을 질의 연산자라 합니다. 

특징으로는 열거형에 대해서 동작을 합니다. 지연된 수행에 의존하며, 파이프 라인화 된 데이터 처리를 가능하게 합니다. 

이러한 질의 연산자는 표준 질의 연산자를 통해 질의를 만들 수 있습니다. 표준 질의 연산자는 Where, Skip, Take, Select 등 다양한 메서드를 제공합니다. 그리고 실행 형태를 지연 실행을 가집니다. First, Last, ElementAt, Count, ToList 같은 메서드는 실행 형태를 즉시 실행을 가집니다. 표준 질의 연산자 간의 차이점을 알고 있어야 합니다. 

IEnumerable<int> linq = list.Where(n => n.Length <= 10); 
Console.Write(linq.ToStirng());

linq 선언 시 해당 질의가 실행되지 않고 그대로 식만 가지고 있습니다. 그 다음 Console.Write에서 호출 할 때 해당 질의가 실행이 되고 결과 값을 반환해 줍니다. 

위 코드를 참고하여 지연 실행에 대한 개념을 인지하고 가는 것이 좋습니다. 

4. 질의 표현식

질의 연산자에서 사용한 코드를 보면 Where 메소드가 있습니다. 이것을 질의 표현식이라고 합니다. 질의 연산자를 SQL문으로 표현한 식이라고 보시면 됩니다. 이러한 질의 표현식의 장점은 코드 가독성 향상과 유지보수가 쉽다는 점입니다. SQL 문과 비슷하게 표현되기 때문에 친숙하게 사용이 가능합니다. 

질의 표현식은 컴파일러가 자동으로 질의 연산자로 변환해 줍니다. 이를 통해 제대로 식이 처리 될 수 있도록 합니다. (.) 점을 이용하기 때문에 visual studio 자동 완성 기능을 사용할 수 있습니다. 이를 통해 기억나지 않는 메소드를 바로 찾아 사용이 가능합니다. 또한 visual studio가 가진 인텔리센스 기능을 활용할 수 있습니다.

5. 표현식 트리

표현식 트리는 람다 표현식에 대해 수행하는 해석 절차의 결과물 입니다. 표현식 트리로 변환하면서 자동화 된 도구들이 최적화 처리를 대신해줍니다. 트리 생성은 컴파일러가 동적으로 진행합니다. 표현식 트리의 특징 때문에 지연된 질의 수행을 달성하는 한 가지 방법이 됩니다. 

질의를 데이터로 표현하기에 높은 확장성을 제공합니다. 

IQueryable 인터페이스는 표현식 트리를 가지고 있습니다. 표현식 트리를 사용함으로써 지연된 질의 수행을 제공합니다. 그리고 IEnumerable<T>과 파이프 라인 패턴이 유연하지 못할 때 사용 됩니다. 즉, 좀 더 확장성 있는 식을 작성하도록 도와줍니다. 

LINQ to Objects

LINQ는 ‘언어로 통합된 쿼리’ 입니다. 람다식과 마찬가지로 C# 3.0 버전에 추가된 기능입니다. 

LINQ를 사용하면 객체, 데이터, XML 등 다양한 데이터를 표준화하여 처리가 가능합니다. 

LINQ 사용 방법

LINQ를 사용하기 위해서는 가장 먼저 using 지시자를 사용해 System.Linq 네임스페이스를 지정해야 합니다. 

using 지시자를 사용했다면 아래와 같이 LINQ를 사용할 수 있습니다.

IEnumerable<int> linq = list.Where(n => n.Length <= 10); 

여기서 사용된 Where 메서드는 조건을 만족하는 것만 추출하는 메서드입니다. 

람다식과 다른점은 IEnumerable<T> 인터페이스를 구현하고 있는 형이기만 하면 Where 메서드를 사용할 수 있다는 점입니다.

즉, 람다식보다 제약이 적으며, 더 많은 타입에서 사용이 가능합니다.

LINQ를 사용하면 서로 다른 형의 컬렉션이어도 IEnumerable<T> 인터페이스를 구현한다면 같은 메서드를 이용할 수 있습니다. 

추가로 Where 메서드의 반환값은 IEnumerable<T> 입니다. 이 말은 다른 LINQ 메서드를 붙여서 사용이 가능합니다. 

LINQ to Objects – 디자인 패턴

1. 함수형 생성 패턴

var games = from line in.File.ReadAllLines("game.csv")
            where !lines.StartsWith("#") let parts = line.Split(',')
            select new { IsPublish = parts[0], Title = parts[1], Name = parts[2],
                Authors = from authorFullName is parts[2].splits(';') let authorNameParts = authorFullName.Split(' ')
                select new {
                  FirstName = authorNameParts[0], LastName = authorNameParts[1]
                }
            };

ObjectDumper.Write(games, 1);

위 코드를 보면 함수형 언어의 코드 구조와 비슷합니다. 그래서 함수형 생성 패턴이라고 합니다. 

익명을 이용해서 선언적인 접근방법을 사용합니다. 이 방법은 명령형보다 깔끔한 문법을 제공합니다. 

장점으로는 코드가 결과와 매우 유사한 형태를 가진다는 점입니다. 

다만 쿼리 구문이기 때문에 LINQ의 모든 기능을 이용할 수는 없습니다.

2. ForEach

var query = Data.Games.Where(game => game.count > 100)
          .foreach(game > { 
            game.Title += "(3N)";
            Console.WriteLine(game.Title);
          });

질의가 순환문으로 이어질 때 사용할 수 있는 패턴입니다. 위 코드는 하나의 ForEach 내에서 여러 개의 질의문을 사용한 코드입니다.

이를 통해 질의 문법의 통일성을 유지한다.

성능상 고려야해야 할 부분

1. 스트리밍 방식에 대한 선호

스트리밍 방식을 사용하면 메모리나 CPU와 같은 연산 자원을 절약할 수 있다.

예시) StreamRead의 Lines 메소드 => 작은 메모리 사용량을 유지, 매우 큰 파일을 제어 할 수 있습니다.

핵심은 게으른 질의 평가입니다. 객체들은 순환할 때 생성된다.

지연된 질의 수행의 장점을 잘 이용하면 메모리 부하가 적고 자원 낭비가 덜한 방법을 선택할 수 있습니다.

2. 즉시 수행에 대해 조심하기

질의 연산자를 살펴보면 누적연산자(Avarage, Count등)와 몇몇 연산자(orderBy, Reverse 등)들은 원본 시퀀스를 완전히 반복합니다. 이러한 연산자들은 기존 LINQ의 장점인 메모리 효율을 저하됩니다. 사용시 이러한 문제점을 인지하고 사용해야 합니다. 결론은 연산자들의 동작 방식을 잘 이해해야 한다는 점입니다. 

3. LINQ to Objects가 코드 성능을 저하시킬까?

LINQ to Objects를 사용할 때 처리 속도는 어떨까?

속도를 비교해보면 아래와 같은 결과가 나온다.

부분 질의 > OrderBy + First > 두 개의 질의 > 사용자정의 연산자 > foreach

오른쪽으로 갈수록 빠르다. 부분 질의가 가장 느리다. 

위 결론을 도출하기 위한 시나리오는 객체 컬렉션을 가지고 특정 프로퍼티의 최대 값을 갖고 있는 객체를 찾아야 한다는 것으로 잡았습니다. 

결론을 하나씩 보겠습니다. 

부분 질의는 순환을 두번 돌기 때문에 가장 오래 걸리게 됩니다. 부분 질의 사례를 통해 LINQ를 잘못 사용하면 코드 성능이 나빠진다는 점을 기억해야 합니다. 

사용자정의 연산자는 직접 연산자를 새로 만들기 때문에 개발시간이 늘어나게 됩니다. 사용자정의 연산자를 만들고 자주 사용하게 된다면 해볼만한 방법입니다. 가장 빠른 foreach 다음으로 가장 코드 성능이 좋기 때문에 자주 사용되면 만들어 사용하는 것이 좋습니다.

LINQ to Objects는 현명하게 사용해야 한다.

성능에 민감한 어플리케이션을 개발한다면 이런 경루 LINQ보단 foreach나 for문을 통해 빠른 처리를 하는 것이 좋습니다.

스스로 질의 연산자를 만드는게 최적화 된 성능을 구현할 수 있습니다. 

수행 속도가 중요하지 않다면 코드의 명료함과 유지보수성이 가져다 주는 장점을 가진 LINQ를 사용하면 좋습니다.

함께보면 좋은 글

.NET Core와 .NET Framework 비교 분석

참고

https://learn.microsoft.com/ko-kr/dotnet/csharp/programming-guide/concepts/linq/introduction-to-linq-queries

Leave a Comment