• 티스토리 홈
  • 프로필사진
    SiJun-Park
  • 방명록
  • 공지사항
  • 태그
  • 블로그 관리
  • 글 작성
SiJun-Park
  • 프로필사진
    SiJun-Park
    • 분류 전체보기 (190)
      • Unity (148)
        • 출시 해보기 (Slime Company) (3)
        • 뱀서류 Project (34)
        • Defense Project (20)
        • FPS Project (30)
        • RPG Project (39)
        • 기타 - 개발 (22)
      • 개발 (35)
        • 임베디드 소프트웨어 (7)
        • 컴파일러 (6)
        • 기계학습 (8)
        • 보안 (8)
        • 그래픽스 (2)
        • 그 외 (4)
      • 코딩문제 (6)
  • 방문자 수
    • 전체:
    • 오늘:
    • 어제:
  • 최근 댓글
      등록된 댓글이 없습니다.
    • 최근 공지
        등록된 공지가 없습니다.
      # Home
      # 공지사항
      #
      # 태그
      # 검색결과
      # 방명록
      • 람다식 / LINQ 회고
        2026년 01월 06일
        • SiJun-Park
        • 작성자
        • 2026.01.06.:28

        이번에는 면접을 다녀와서 제가 부족한 부분을 알려주어 그 부분 공부를 다시 해보려고 집에 오자마자 

         

        서치하고 찾아보고 만들어보고 어떤 문제점이 있는지 알아보는 시간을 가졌습니다.

         

        역시 제 3자가 있어야 제 부족한 점이 조금 더 빠르게 발견이 되네요.. 혼자서 다 알았다! 하고 자만하던 제가 부끄럽습니다.

         

        람다식/LINQ 또한 사용을 해봤는데 정확한 의미는 모르고 있더라구요...

         

        즉 걷기도 전에 뛰던 셈이였죠, 그냥 이렇게 쓰는구나 편하다~ 단점 없겠지 ~ 라고 그냥 보낸게 너무 아쉽습니다.

         

        람다식이란?

         

        이름 없는 함수를 코드 안에서 바로 만들어 쓰는 문법입니다.

        int Add(int a, int b)
        {
            return a + b;
        }

         

        위와 같이 간단한 덧셈 함수가 있다고 했을 때 이와 같은 역할을 하게 람다식으로 고쳐 본다면 

        (a, b) => a + b

        이렇게 간단하게 사용을 할 수 있습니다.

         

        이렇게하면 코드가 간결하고 의도 표현이 명확해지며 실수를 줄일 수 있습니다.

        Func<int, int> square = x => x * x;
        int result = square(5);

         

        또 다른 예시를 보면 위와 같이 int를 받아 int로 전환하는 Func이 있습니다. 

        x를 받아 x * x 를해서 돌려주는 역할을 하게 됩니다

         

        그래서 result 값은 25가 나오게 됩니다.

         

        이런 식으로 아주 간결해진 다는 것이 장점입니다.

         

        가장 핵심 포인트는 "객체"로 취급을 받을 수 있습니다.

         

        즉 Delegate 객체입니다.

         

        이게 무슨 소리냐면 

        Action hello = () => Debug.log("Hello!");

         

        이렇게 있으면, 함수 호출이 아니라 함수 객체 생성이라는 것이죠

         

        이게 나중에 유니티에서 엄청나게 문제가 됩니다.

         

        LINQ란?

        켈력선을 질의하는 문법입니다.

         

        예로들어서 SQL처럼 이 중에서 조건에 맞는 것만 골라줘! 라고 하면 골라줄 수 있는 거죠

         

        List<int> result = new List<int>();
        
        for (int i = 0; i < numbers.Count; i++)
        {
            if (numbers[i] > 10) result.Add(numbers[i]);
        }

         

        위와같이 result에 값을 저장하도록 하였습니다.

         

        var result = numbers.Where(n => n > 10).ToList();

        그럼 위 처럼 사용을 할 수 있는데요, 구성 요소를 쪼개 보면

         

        Where : 필터링

        n => n > 10 : 조건 (람다식)

        ToList() : 실제 리스트 생성

         

        즉 LINQ는 람다식 위에 만들어진 문법입니다.

         

        그리고 가장 큰 중요한 특징이 있는데 

         

        바로 지연 실행(Deferred Execution)입니다.

        var query = numbers.Where(n => n > 10);

         

        이 시점에는 아직 계산도 되지 않았고, 필터링도 안되어 있는 아무 일도 일어나지 않은 상황입니다.

         

        진짜 실행되는 순간은

        foreach (var n in query)
        query.ToList();

        이 순간에 실행이 되는데 이게 유니티 Update에서 특히 위험합니다.

         

        그럼 도대체 어떤게 문제냐?

        바로 Closure가 가장 문제입니다.

         

        Closure가 뭐냐?

        람다가 자신이 선언된 스코프의 변수를 잡아먹는 것입니다.

        int limit = 10;
        var list = numbers.Where(n => n > limit);

         

        여기서 람다는 n만 쓰는 것 같지만 사실 limit도 같이 들고 다닙니다.

        *컴파일러는 숨겨진 클래스를 만듦

         

        그래서 이게 왜 문제냐?

        void Update()
        {
            int range = 5;
            enemies.Where(e => e.Distance < range);
        }

        새로운 DisplayClass를 생성하게 되고 힙 메모리 할당을 하며 GC 대상이 증가하게 됩니다.

         

        이건 최적화가 아니라 그냥 지뢰입니다.

         

        DisplayClass는 뭐냐?

        람다식이 바깥 변수를 Closure할 때 C# 컴파일러가 몰래 만들어내는 클래스입니다.

        즉 제가 작성도 하지 않았고, 코드에 보이지도 않는데 Heap 메모리에는 실제로 존재하게 됩니다.

         

        이게 왜 필요하냐면 

        int limit = 10;
        Func<int, bool> check = x => x > limit;

        매개변수가 x고 limit은 어디서 왔는지 모릅니다.

         

        즉 컴파일러 입장에서는 이 변수를 람다 안에서 계속 쓸 수 있게 보관을 해야한다고 판단을 해서

         

        컨테이너 객체를 하나 만드는데 이것이 DisplayClass입니다.

         

        위에서도 언급을 드렸지만 이것은 Heap 메모리에 존재한다고 하였습니다.

         

        또한, 매번 새로 만들어지게 됩니다.

         

        그러면 매 프레임마다 보이지 않는 new가 생성이 되게 됩니다.

         

        그러면 람다만 써도 DisplayClass가 생기나?

        그건 아닙니다. 외부 변수를 캡처할 때만 생기게 됩니다.

        enemies.ForEach(e => e.Tick());

        그래서 위 처럼 사용하게 되면 외부 변수가 없으니 DisplayClass를 생성하지 않습니다.

         

        그래서 결론적으로 왜 안좋냐?

        프레임마다 보이지 않는 메모리 할당과 호출 비용을 만들기 때문에 굳이 사용하지 않는 것이 추천이 되고 있습니다.

         

        정확한 이유는 위에서도 언급을 드렸듯이 DisplayClass 떄문에 힙 할당이 생기게 됩니다.

        또한, Deleagte 호출은 일반 함수 호출보다 비쌉니다.

         

        왜냐하면 간접 호출이고, 인라이닝 거의 불가능 하기 때문에 수천 번 반복되면 CPU 비용이 됩니다.

         

        또한 병목 추적 속도가 급감하기 때문에 디버깅이 지옥이 됩니다.

         

        LINQ가 안좋은 이유 중 가장 핵심은 Enumerator + Iterator 객체 생성입니다.

        var result = enemies.Where(e => e.IsAlive);

         

        이 한 줄에서 IEnumerable 생성을 하고 Enumerator를 생성하게 되고 상태 머신 객체 생성을 하게 되는데

        대부분 Heap 할당입니다.

         

        그 다음은 지연 실행 때문에 Update에서 지뢰가 된다고 언급을 드렸는데

        foreach (var n in query)
        query.ToList();

        여기서 갑자기 계산이 시작이 되고 갑자기 GC가 발생이 되기 때문에 타이밍 예측이 불가능 해집니다.

         

        즉 프레임 제어가 되지 않습니다.

         

        그리고 ToList / ToArray는 무조건 메모리 할당을 하기 때문에 

        GC 대상이 증가하게 되니 Update에서 쓰면 엄청 지뢰입니다.

         

        그래서 프레임드랍이 생길 수 있기 때문에 문제가 됩니다.

         

        그럼 무조건 쓰면 안되나?

        그건 아닙니다. 써도 되는 곳이 있습니다.

        1. 초기화

        2. 로딩

        3. 씬 세팅

        4. 에디터 코드

        5. 1회성 계산

         

        그러면 실제로 얼마나 차이가 날까?

        var alive = enemies.Where(e => e.HP > 0).ToList();
        aliveList.Clear();
        for (int i = 0; i < enemies.Count; i++)
        {
            if (enemies[i].HP > 0) aliveList.Add(enemies[i]);
        }

         

        차이를 보면 LINQ는 GC Alloc 발생과 간접 호출이 생기게 됩니다.

        for은 GC Alloc 0 + 다이렉트 콜입니다.

         

        이렇게 되면 모바일 / 콘솔에서 체감이 확실히 나게 되며 PC에서도 몬스터가 많아지게 되면

         

        프레임 드랍이 일어나게 됩니다.

         

        1000마리 기준으로 테스트를 해본 결과

        - LINQ

        GC Alloc : 30 KB / frame

        CPU Time : 0.3 / frame

        Heap 할당 횟수 : 3 회

         

        1초에 1.5 ~ 2.4MB Heap 생성

         

        -For + 캐싱

        GC Alloc : 0

        CPU Time : 0.05 / frame

        Heap 할당 횟수 : 0

         

        결과로는 CPU가 많이 차이가 나게 되고 GC가 LINQ만 힙 할당이 발생하게 됩니다.

         

        이 정도면 굳이 별 차이 없지 않냐?라고 생각을 할 수 있습니다.

        문제는 누적입니다.

        일반적인 게임 프레임 구조를 보면, 몹이 탐색도 하고, 타겟도 선정하고, 이동도 하고, AI 상태도 갱신하고, 이펙트도 뜨고 

         

        이런 것들이 여러개가 존재하게 됩니다.

         

        LINQ가 1개당 0.3 ms라면? 5개만 있어도 1.5ms입니다.

         

        GC가 터지는 프레임이 5 ~ 15ms 인데, 누적이 되면 분명히 뚝 떨어지는 순간이 생기게 됩니다.

         

        *람다는 GC Alloc이 생성이 안되게 작성을 하였다 하면 GC Alloc이 0이지만 CPU가 증가하게 되는데

        그 이유는 deleagte 간접 호출을 하기 때문입니다. (치명적이진 않음)

        하지만 캡쳐가 있으면 GC Alloc이 발생해 LINQ와 다르지 않습니다.

         

         

         

         

        'Unity > 기타 - 개발' 카테고리의 다른 글

        Addressable & 메모리 로딩 공부  (0) 2026.01.08
        Object Pooling 회고  (0) 2026.01.06
        foreach / Animator 최적화 공부 기록  (0) 2026.01.01
        Transform 그리고 StopAllCoroutines 공부 기록  (1) 2026.01.01
        GetComponent 공부 기록  (0) 2025.12.30
        다음글
        다음 글이 없습니다.
        이전글
        이전 글이 없습니다.
        댓글
      조회된 결과가 없습니다.
      스킨 업데이트 안내
      현재 이용하고 계신 스킨의 버전보다 더 높은 최신 버전이 감지 되었습니다. 최신버전 스킨 파일을 다운로드 받을 수 있는 페이지로 이동하시겠습니까?
      ("아니오" 를 선택할 시 30일 동안 최신 버전이 감지되어도 모달 창이 표시되지 않습니다.)
      목차
      표시할 목차가 없습니다.
        • 안녕하세요
        • 감사해요
        • 잘있어요

        티스토리툴바