강좌와 팁

C# Aggregate 확장 메서드 날짜:2020-3-16 3:24:48 조회수:92
작성자 : 소엔
포인트 : 1368
가입일 : 2020-02-02 00:09:14
방문횟수 : 72
글 186개, 댓글 51개
소개 : SoEn 운영자입니다.
작성글 보기
쪽지 보내기
C#의 System.Linq 네임스페이스에 컬렉션을 제어하는 여러 가지 확장 함수가 포함되어 있다. Sum, Max, First, Count 같은건 이름만 봐도 뭐하는건지 쉽게 알 수 있고 사용 방법도 직관적이다. 그 중에 난이도가 높은게 Aggregate이다. 이 메서드를 통해 확장 함수와 람다식을 활용하는 방법에 대해 연구해 보자. 다음 3 개의 원형이 있다.

Aggregate<TSource,TAccumulate,TResult>(IEnumerable<TSource>, TAccumulate, Func<TAccumulate,TSource,TAccumulate>, Func<TAccumulate,TResult>)

Aggregate<TSource,TAccumulate>(IEnumerable<TSource>, TAccumulate, Func<TAccumulate,TSource,TAccumulate>)

Aggregate<TSource>(IEnumerable<TSource>, Func<TSource,TSource,TSource>)

초기값을 받는 버전이 있고 결과값을 선택하는 함수를 제공하는 버전이 있다. 가장 간단한 세번째 원형을 사용해 보자. 첫번째 인수는 IEnumerable<TSource>이며 TSource 타입을 저장하는 임의의 컬렉션이다. 확장 함수이니 실제 인수로 전달할 필요 없이 컬렉션.Aggregate() 형식으로 호출한다. 배열이나 List 같은 열거 가능한 모든 타입에 대해 다 쓸 수 있다는 얘기다.

두번째 인수는 TSource 타입의 인수 둘을 받아 TSource 타입을 리턴하는 함수 Func이다. 이 함수는 컬렉션의 요소를 순서대로 전달하여 두 값을 연산한 결과 하나를 리턴한다. 요소에 대해 임의의 동작이 가능하므로 순회가 필요한 거의 모든 연산을 다 처리할 수 있다. 첫 번째 활용 예제를 보자.

using System;
using System.Collections.Generic;
using System.Linq;

class CSTest {
 static int value = 48;

 static void Main() {
  int[] pos = { 15, 90, 34, 82, 52, 43, 69, 27, 75, 79 };

  int nearest = pos.Aggregate((last, next) => Math.Abs(value - last) < Math.Abs(value - next) ? last : next);
  Console.WriteLine("근접값 = " + nearest);
 }
}

정수형 배열 pos에서 value와 가장 근접한 값을 찾는 예제이다. 육안으로 대충 검색해 봐도 52라는 것을 알 수 있으며 결과값은 물론 52로 출력된다.
Aggregate의 인수로 람다 함수를 전달했다. 이 함수는 last와 next 두 개의 인수를 전달받아 차의 절대값을 비교하여 둘 중 어떤 값이 value에 가까운지 결과를 리턴한다. Aggregate 메서드는 pos의 첫 요소와 두 번째 요소를 람다 함수로 전달하고 비교 결과를 last에 대입하여 다시 다음 요소와 함께 전달하기를 모든 요소에 대해 반복한다. 실행 순서는 다음과 같다.

람다(15, 90) => 15 리턴
람다(15, 34) => 34 리턴
람다(34, 82) => 34 리턴
람다(34, 52) => 52 리턴
람다(52, 43) => 52 리턴
....

이 과정을 컬렉션 끝까지 반복하여 최종값 52를 리턴한다. 이 복잡한 동작을 Aggregate 메서드 호출문 하나로 처리할 수 있는 이유는 인수로 람다식을 전달하여 내부에서 원하는 연산을 할 수 있기 때문이다.
익숙해지면 이런 식을 금방 작성할 수 있지만 처음 보는 사람은 도대체 이게 뭔 코드인지 헷갈릴 수밖에 없다. 그래서 좀 더 쉽게 풀어 보았다.

class CSTest {
 static int value = 48;

 static void Main()
 {
  int[] pos = { 15, 90, 34, 82, 52, 43, 69, 27, 75, 79 };

  int nearest = pos.Aggregate(findNear);
  Console.WriteLine("근접값 = " + nearest);
 }

 static int findNear(int last, int next)
 {
  return Math.Abs(value - last) < Math.Abs(value - next) ? last : next;
 }
}

람다식을 findNear라는 함수로 따로 선언하고 이 함수를 Aggreagte인수로 전달했다. findNear가 원형의 Func<TSource,TSource,TSource>에 해당한다. 여기서 TSource는 int이며 findNear는 int 인수 last와 next를 받아 다시 int를 리턴하는 함수이다. 본체에서는 last와 next 중 누가 value와 가까운지 비교하여 가까운 값을 리턴한다.
첫 예제를 풀어 쓴 것이므로 실행 결과는 같다. 그렇다면 Aggregate 메서드는 어떻게 작성되어 있을까? 이 함수 내부도 풀어 써 보자. 소스까지 볼 필요도 없이 내부 동작을 쉽게 유추할 수 있다.

class CSTest {
 static int value = 48;

 static void Main()
 {
  int[] pos = { 15, 90, 34, 82, 52, 43, 69, 27, 75, 79 };

  int last = pos[0];
  for (int i = 1;i < pos.Length;i++)
  {
   last = findNear(last, pos[i]);
  }
  int nearest = last;

  Console.WriteLine("근접값 = " + nearest);
 }

 static int findNear(int last, int next)
 {
  return Math.Abs(value - last) < Math.Abs(value - next) ? last : next;
 }
}

last에 첫 요소를 대입하고 1번째 요소부터 끝까지 last와 함께 findNear 함수로 전달한다. 이 함수가 리턴하는 근접값이 다시 last에 대입되어 다음 요소와 비교하기를 컬렉션 끝까지 반복하는 것이다. 평이하게 풀어 썼으니 이해하기는 어렵지 않다. 이것을 한줄로 압축하면 첫번째 예제가 된다.

확장 함수와 람다식이 들어가 복잡해 보일 뿐 쓰기에는 오히려 더 간편하다. 압축적인 코드가 잘 이해되지 않으면 이런 식으로 풀어서 생각해 보면 된다. 굳이 코드로 짜 볼 필요 없이 머리속에서 바로 해체 가능해야 람다식을 자유 자재로 쓸 수 있다.

풀어쓴 것에 비해 좀 복잡해 보여서 그렇지 코드는 확실히 짧다. 그렇다면 람다식을 쓴 것과 평이하게 풀어쓴 것의 속도차는 어떨까 테스트해 보자. 천만개의 난수 배열을 생성하고 이 배열을 검색한다.

class CSTest {
 static int value = 48;

 static void Main()
 {
  List<int> pos = new List<int>();
  Random r = new Random();
  for (int i = 0; i < 10000000; i++)
  {
   pos.Add(r.Next(1, 100));
  }

  System.Diagnostics.Stopwatch sw = new System.Diagnostics.Stopwatch();
  sw.Start();
  int last = pos[0];
  for (int i = 1; i < pos.Count; i++)
  {
   last = findNear(last, pos[i]);
  }
  int nearest = last;
  sw.Stop();

  Console.WriteLine("시간 = " + sw.Elapsed);

  sw.Reset();
  sw.Start();
  nearest = pos.Aggregate((last, next) => Math.Abs(value - last) < Math.Abs(value - next) ? last : next);
  sw.Stop();
  Console.WriteLine("시간 = " + sw.Elapsed);

 }

 static int findNear(int last, int next)
 {
  return Math.Abs(value - last) < Math.Abs(value - next) ? last : next;
 }
}

C#에서 시간 측정은 Stopwatch로 한다. 각 방식으로 검색해 보고 소요 시간을 측정하여 출력했다.

시간 = 00:00:00.1356860
시간 = 00:00:00.1249265

보다시피 대동소이하다. 천만번 순차 검색을 0.1초만에 해 치우니 이 정도면 준수하다. 람다식을 사용하는 것이 근소하게 약간 더 빠르지만 유의미한 차이는 아니다. 확장 함수도 어차피 C# 코드로 작성되어 있으니 별반 차이날 리가 없다. 다만 10줄 코드를 단 한줄로 쓸 수 있어 편리하다.

이 강좌를 통해 람다식도 별거 아니라는 것을 알았으면 좋겠다. 딱 한번만 쓸 함수를 이름은 빼고 간략하게 정의한 것 뿐이다. 닷넷 프레임워크에는 잘 만들어 놓은 확장 함수가 많으니 시간날 때마다 틈틈이 찾아서 연구해 보자. 익숙해지면 시간을 많이 절약할 수 있고 이미 검증된 코드로 생산성을 향상시킬 수 있다.

 



개발자의 천국 SoEn

목록보기 삭제 수정 신고 스크랩


로그인하셔야 댓글을 달 수 있습니다.