이번에 앱의 UI를 리뉴얼하면서 당장의 개발에 치여 공부를 하지 못했었다. UI 리뉴얼을 어느정도 마무리하고 예전부터 공부해야지 했던 UniRx를 찾아보게 되었다. Unity를 사용하다보면 시간이나 이벤트에 관련된 처리를 하는 것이 번거롭거나 구조가 엉켜버리는 경우가 많았다. (사실 Update() 함수를 사용하기 싫다.)

UniRx

2174B63B595720042A.png

일본에서 개발한 Unity(C#) 에서 사용할 수 있게 만든 가볍고 빠른 유니티 전용 Rx이고, Unity 전용으로 사용할 수 있는 Rx 스트림들을 제공한다. .NET용 Rx는 무겁고 큰데다가 Unity의 Mono에서는 사용이 불가한 문제가 있었다고한다.

Reactive Programming

reactive-programming-cover-1-1.jpg

객체지향 프로그래밍과는 다르게 Reactive Programing은 기본적으로 모든 것을 스트림(stream)으로 본다. 모든 데이터의 흐름을 시간 순서에의해 전달되어지는 스트림으로 처리한다. 각각의 스트림은 새로 만들어져서 새로운 스트림이 될 수도 있고 여러개의 스트림이 합쳐질수도 있다. 스트림의 데이터가 필요한 구독자는 스트림을 구독(Subscribe)하여 데이터의 결과값을 얻는다.


Unity에서 UniRx 맛보기

플레이어가 마우스를 클릭하면 총알을 발사하는데 일정시간 동안에는 총알이 발사되지 않도록 하고 싶다면 다음과 같이 프로그래밍 할 것이다.

public class Player : MonoBehaviour {

    // 무기
    private Iweapon weapon;
    // 발사속도
    private float fireRate = 0.25f;
    // 마지막 발사된 시간
    private float lastFired;

    // 매 프레임마다 호출
    void Update () {
        if (Input.GetMouseButtonDown(0)) {
            if (Time.time > lastFired + fireRate) {
                weapon.Fire();
                lastFired = Time.time;
            }
        }
    }
}

그런데 UniRx를 사용하면 위 코드를 다음과 같이 프로그래밍 할 수 있다.

public class Player : MonoBehaviour {

    // 무기
    private Iweapon weapon;
    // 발사속도
    private float fireRate = 0.25f;
    // 마지막 발사된 시간
    private DateTimeOffset lastFired;

    private void Start() {
        this.UpdateAsObserverable()
            .Where(_ => Input.GetMouseButtonDown(0))
            .TimeStamp()
            .Where(x => x.Timestamp > lastFired.AddSeconds(fireRate))
            .Subscribe(x => {
                weapon.Fire();
                lastFired = x.Timestamp;
            });
    }
}

Update() 함수 안에서 이벤트를 확인하진 않지만 UniRx를 제대로 사용하기 위해선 좀 더 공부가 필요하다고 느끼고 자료를 찾아보다가 박민근님의 UniRx 소개 및 활용 을 보게 되었다. 글을 읽어보다가 C# LINQ는 사용할 수 있는 레벨이 되어야 한다는 뼈를 맞고 예전에 읽고있던 C# 6.0 완벽가이드를 다시 집어들었다.


LINQ 질의

LINQ(Language Integrated Query; 언어에 통합된 질의)는 지역 객체 컬렉션과 원격 자료 저장소에 대한 형식에 안전한 구조적 질의(질의문)를 작성하는 데 사용하는 C# 언어 기능들과 .NET Framework 기능들을 통칭하는 용어이다. LINQ를 이용하면 IEnumerable<T> 를 구현하는 임의의 컬렉션(목록, 배열)과 XML DOM에 대해 질의를 수행할 수 있으며, SQL Server 데이터데이스의 테이블 같은 원격 자료 저장소에 대한 질의도 수행할 수 있다.

LINQ의 모든 핵심 형식은 System.Linq 이름공간과 System.Linq.Expressions 이름공간에 정의되어 있다.

LINQ의 기본적인 자료 단위는 순차열(sequence; 서열)과 요소(element)이다. 순차열은 IEnumerable<T>를 구현하는 임의의 객체이고 요소는 그 순차열에 들어가있는 항목이다. 다음 예에서 names는 순차열이고 "Tom", "Dick", "Harry"는 요소들이다.

string[] names = { "Tom", "Dick", "Harry" };

첫걸음

질의연산자(query operator)는 순차열에 어떠한 변환 연산을 적용하는 메서드이다.
예를들어, Where 연산자를 이용하면 이름들을 담은 배열에서 길이가 영문자 네 개 이상인 이름만 추출할 수 있다.

string[] names = { "Tom", "Dick", "Harry" };

IEnumerable<string> filteredNames = names.Where(n => n.Length >=4);

foreach (string name in filteredNames) {
    Console.WriteLine(name);
}


//Dick
//Harry

유창한 구문

질의 표현식에 질의 연산자들을 추가해서 일종의 '사슬'을 형성함으로써 좀 더 복잡한 질의를 만들수 있다. 다음 예제는 문자열 배열에서 영문자 'a'가 있는 모든 문자열을 추출해서 길이순으로 정렬한 후 그 결과를 대문자로 변환한다.

string[] names = { "Tom", "Dick", "Harry", "Mary", "Jay" };

IEnumerable<string> query = names
    .Where (n => n.Contains("a"))
    .OrderBy (n => n.Length)
    .Select (n => n.ToUpper());

foreach (string name in query) {
    Console.WriteLine(name);
}


//JAY
//MARY
//HARRY

WhereOrderBy, Select는 표준 질의 연산자이다. 자료는 연산자들의 사슬을 따라 왼쪽에서 오른쪽으로 흐르므로, 이 예의 경우 자료는 먼저 선별되고, 정렬되고, 투영된다.

  • Where 연산자는 입력 순차열에서 특정 요소들만 필터로 선별해서 출력 연산자에 포함시킨다.
  • OrderBy 연산자는 입력 순차열을 정렬한 버전을 산출한다.
  • Select 메서드는 입력 순차열의 각 요소를 주어진 람다식으로 변환 또는 투영한 결과를 산출한다.

모든 질의 연산자는 다음에 상세하게 다룰 것이다.

질의 표현식

C#은 LINQ 질의를 좀 더 간결하게 표기할 수 있는 단축 구문을 지원하는데, 이를 질의 표현식(query expression) 구문이라고 부른다.

앞 절에서, 영문자 'a'가 있는 문자열들을 길이순으로 정렬한 후 대문자로 변환하는 유창한 구문 질의의 예를 질의 표현식으로 다시 작성하면 다음과 같다.

string[] names = { "Tom", "Dick", "Harry", "Mary", "Jay" };

IEnumerable<string> query =
    from    n in names
    where   n.Contains("a")
    orderby n.Length
    select  n.ToUpper();

foreach (string name in query) {
    Console.WriteLine(name);
}

//JAY
//MARY
//HARRY

질의 표현식은 항상 from 절로 시작하고 select 절이나 group 절로 끝난다. 컴파일러는 질의 표현식을 유창한 구문의 코드로 바꾸어서 컴파일한다. 이는, 질의 표현식으로 작성할 수 있는 모든 것을 유창한 구문으로도 작성할 수 있다는 뜻이기도 하다.

혼합 구문 질의

질의 구문과 유창한 구문을 섞어 쓸 수 있다. 이때 제약은 각각의 질의 구문 구성요소가 완결적이어야 한다는 것이다. (즉, 하나의 from 절로 시작해서 select나 group 절로 끝나야 한다.)

다음은 영문자 'a'가 있는 이름들의 개수를 세는 혼합 구문 질의이다.

string[] names = { "Tom", "Dick", "Harry", "Mary", "Jay" };

int matches = (from n in names where n.Contains("a") select n).Count();
// 3

지연된 실행

대부분의 질의 연산자들의 한 가지 중요한 특징은, 질의 연산자는 질의를 구축(생성)할 때가 아니라 열거할 때(다시 말해 해당 열거자에 대해 MoveNext가 호출될 때) 실행된다는 점이다. 다음 질의를 생각해 보자.

var numbers = new List<int>();
numbers.Add(1);

IEnumerable<int> query = numbers.Select(n => n * 10);  // 질의를 구축한다.

numbers.Add(2);  // 또 다른 요소를 추가한다.    

foreach (int n in query) {
    Console.Write(n + "|");
}

// 10|20|

결과를 보면 질의를 구축한 다음에 목록에 집어넣은 요소가 포함되어 있다. 이는 foreach 문이 실행될 때 비로소 질의 연산자가 실행되었음을 보여주는 증거이다. 이런 방식을 지연된 실행 또는 게으른 실행이라고 부른다. 모든 표준 질의 연산자는 이러한 실행 지연 기능을 제공한다. 단, 다음은 예외이다.

  • 하나의 요소나 스칼라값을 돌려주는 집계 연산자(First 나 Count 등).
  • 다음과 같은 형식 변환 연산자들: ToArray, ToList, ToDictionary, ToLookup

이런 연산자들이 포함된 질의는 구축 즉시 실행된다. 이런 연산자의 결과 형식에는 실행 지연 기능을 제공하는 메커니즘이 없기 때문이다.

재평가

지연된 실행의 또 다른 효과는, 지연 실행 질의를 다시 열거하면 질의가 다시 평가된다는 점이다. 다음은 이 점을 보여주는 예이다.

var numbers = new List<int>() { 1, 2 };

IEnumerable<int> query = numbers.Select(n => n * 10);

foreach (int n in query) {
    Console.Write(n + "|");
}

// 10|20|

numbers.Clear();

foreach (int n in query) {
    Console.Write(n + "|");
}

// <출력없음>

재평가를 피하는 한 가지 방법은 질의 끝에서 ToArrayToList 같은 변환 연산자를 호출하는 것이다.

var numbers = new List<int>() { 1, 2 };

List<int> timesTen = numbers
    .Select(n => n * 10)
    .ToList();                // 질의를 즉시 실행해서 결과를 List<int>에 복사한다.

numbers.Clear();

Console.Write(timesTen.Count);

// 여전히 2

마치며...

삼X에 다니고 있는 친구가 요즘 회사에서 C#을 배우라고 했다고 한다. 그런 기업에서 C#을? 뭔가 이유가 있겠지라고 생각하며 C# 공부 의지를 다시 다졌다. 예전에 공부할때 딱 LINQ 질의 전까지 공부했었는데 LINQ를 공부하면서 느낀건 왜 이제야 공부했을까. 개발했던 앱에 적용할 수 있었던, 그리고 고민했던 부분들이 주마등처럼 지나갔다. C#공부를 충분히 하고, UniRx도 익힌 다음 버전 개발이나 사이드 프로젝트 개발할 때 UniRx를 적극 채용해볼 생각이다.