[2025_10_17]Dirty Flag패턴과 지연평가(Lazy Evaluation)

2025. 10. 17. 21:11TIL

오늘 배운 것

스탯 시스템을 구현하면서 예전에 사용해봤던 Dirty Flag 패턴을 다시 떠올리고 적용했다. 이것이 지연 평가(Lazy Evaluation)의 일종이라는 것을 명확히 이해하게 되었다.


Dirty Flag 패턴이란?

"값의 변경이 일어났을 때 플래그만 설정하고, 실제 계산은 꼭 필요한 순간까지 미루는 패턴"

핵심 개념

  • 변경이 일어나면 "더러워졌다(dirty)"는 플래그만 표시
  • 실제 값이 필요할 때만 계산 수행
  • 변경이 없으면 캐시된 값을 재사용

왜 필요한가?

매 프레임마다 또는 매번 값을 재계산하면 불필요한 연산으로 인해 성능 저하가 발생한다. 특히 게임에서는 많은 객체들이 존재하고, 계층 구조를 가진 경우가 많아 더욱 중요하다.


게임 렌더링에서의 실제 사용 예시

문제 상황

 
 
몸통 (부모)
  ├─ 팔 (자식)
  └─ 다리 (자식)

- 몸통이 움직이면 팔, 다리도 같이 움직임
- 렌더링하려면 월드 좌표가 필요
- 모든 객체를 매 프레임마다 계산하면? → CPU 과부하

 Dirty Flag로 해결

1. 각 객체의 로컬 변환 값과 월드 변환 값을 캐시
2. 객체가 움직이지 않으면? → 캐시된 값 사용
3. 객체가 움직였다면? → dirty 플래그 설정 → 필요할 때 재계산

움직이는 객체만 렌더링, 나머지는 캐시 활용!

내가 작성한 코드 분석

구현 코드

 
 
csharp
public void IsChangeStat()  // Dirty Flag 설정
{
    isChangeStat = true;  // "스탯이 변경되었다"는 표시만
}

public void RefreshStat()  // 실제 계산은 여기서
{
    if (isChangeStat)  // 변경이 있었을 때만
    {
        UpdateStats();  // 계산 수행
        isChangeStat = false;  // 플래그 초기화
    }
}

public void UpdateStats()  // 무거운 계산 작업
{
    atk = baseAtk;
    def = baseDef;
    critRate = baseCritRate;
    evadeRate = baseEvadeRate;
    
    if (equippedWeapon != null)
        atk += equippedWeapon.atk;
    if (equippedArmor != null)
        def += equippedArmor.def;
}

동작 흐름

 
 
csharp
// 시나리오: 한 프레임에 여러 장비 교체
EquipWeapon(newWeapon);
IsChangeStat();  // 플래그만 설정

EquipArmor(newArmor);
IsChangeStat();  // 플래그만 설정

ApplyBuff();
IsChangeStat();  // 플래그만 설정 

// UI 업데이트 직전
RefreshStat();  // 여기서 한 번만 계산! 
UpdateUI();

// 만약 Dirty Flag 없이 즉시 계산했다면
// UpdateStats() 3번 실행 -> 불필요한 연산 낭비

지연 평가 (Lazy Evaluation)

Dirty Flag는 지연 평가의 일종이다!

지연 평가란?

필요할 때까지 계산을 미루는 방식

장점

  1. 성능 최적화: 불필요한 계산 회피
  2. 배치 처리: 여러 변경사항을 한 번에 처리
  3. 자원 절약: CPU/메모리 효율적 사용

다른 지연 평가 예시

C#의 Lazy<T>

 
 
csharp
private Lazy<ExpensiveObject> lazyObject = new Lazy<ExpensiveObject>(() => 
{
    return new ExpensiveObject();  // 처음 접근할 때만 생성
});

var obj = lazyObject.Value;  // 여기서 처음 생성됨

 

LINQ의 지연 실행: 쿼리를 정의할 때는 실행하지 않고, 실제로 데이터가 필요할 때 실행되는 방식

csharp
var query = list.Where(x => x > 10).Select(x => x * 2);
// 여기까지는 실행 안 됨!

foreach (var item in query)  // 여기서 실제 실행
{
    Console.WriteLine(item);
}

🎯 배운 점

  1. 패턴의 본질 이해
    • 예전에 사용해봤던 Dirty Flag 패턴이 지연 평가의 일종이라는 것을 알았다
    • 단순히 "최적화 기법"으로만 생각했는데, 더 넓은 개념의 일부였다
  2. 성능 최적화의 기본 원칙
    • "필요하지 않은 일은 하지 마라"
    • 특히 게임처럼 매 프레임 반복되는 작업에서는 더욱 중요
  3. 캐싱의 중요성
    • 계산 결과를 저장해두고 재사용하는 것이 핵심
    • 변경이 있을 때만 다시 계산
  4. 계층 구조에서의 효과
    • 부모-자식 관계가 있는 객체에서 특히 유용
    • 상위 객체 변경이 하위 객체에 재귀적으로 영향을 미칠 때 성능 차이가 크다
  5. 지연 평가의 본질
    • 단순히 계산을 늦추는 게 아니라, 불필요한 계산을 아예 제거하는 것
    • Lazy<T>, LINQ, Dirty Flag 모두 같은 철학을 공유
 
 

실무에서 Dirty Flag가 사용되는 곳

  • 게임 엔진: Unity Transform의 계층 구조 계산
  • UI 프레임워크: 레이아웃 재계산 (React의 Virtual DOM)
  • 3D 렌더링: 행렬 변환 캐싱
  • 데이터베이스: ORM의 변경 추적 (Entity Framework)

결론

예전에 사용해봤던 Dirty Flag 패턴을 다시 구현하면서, 이것이 지연 평가(Lazy Evaluation)라는 더 큰 개념의 일부라는 것을 깨달았다.

패턴을 사용하는 것도 중요하지만, 그 패턴이 어떤 의미와 개념을 담고 있는지 이해하는 것이 더 중요하다는 것을 배웠다. 앞으로는 "왜 이렇게 동작하는가?"를 더 깊이 생각하며 코드를 작성해야겠다!

다음엔 프로그램의 기본이자 모두가 쓰고 있지만 정확한 용어는 모르는 expression과 evaluation에 대해 정리해볼려고 생각 중이다.