OOP는 죽었다. DOD를 적용해보자

OOP는 죽었다. DOD를 적용해보자
Photo by Mathew MacQuarrie / Unsplash

본 포스트는 "OOP Is Dead, Long Live Data-oriented Design" 을 보고 정리한 것입니다.


[목차]

  1. Data-oriented design의 접근법
  2. OOP로 짠 코드의 예시
  3. DOD로 짠 코드의 예시
  4. DOD로 짰을 때 성능 개선
  5. DOD의 단점
  6. 개인적인 느낀점

Data-oriented design의 접근법

OOP(Object-oriented programming)는 많은 사람들이 애용하는 프로그래밍 패러다임이다. 하지만 OOP는 잘 사용하지 못했을 때 여러 문제점들이 있다.

(1) 관계 없는 데이터들이 묶인다

초기 구현 단계에서 데이터와 Operation이 몇 개 없으면 OOP는 아주 잘 동작한다. 하지만 시간이 지나면서 프로그램을 계속 유지 보수하면 클래스에 점점 Operation과 데이터들이 계속 추가되고 결합이 심해지게 된다. 그러면서 관계 없는 Operation과 데이터들이 묶이기 시작한다.

예를 들어 게임에서 적을 구현하려고 하면 물리, AI, 렌더링 데이터 등이 필요하다. "적" 클래스에 이러한 데이터를 전부 집어넣으면 적을 게임에서 그릴 때 클래스의 모든 데이터를 메모리에 올리게 된다. 당장 사용하지 않는 데이터들도 메모리에 올라가기 때문에 자주 쓰이는 데이터가 캐시에서 나가게되고 전체적인 성능이 떨어진다.

(2) 상태를 숨기는 feature들을 가진다

메소드 안에 여러 if 문과 boolean flag를 사용해 코드의 복잡도를 올린다. 이는 성능뿐만 아니라 scalability, modifiability, testability에 모두 영향을 준다. 이에 대한 자세한 내용은 뒤에서 설명하겠다.

Data-oriented design의 접근법

https://www.youtube.com/watch?v=yy8jQgmhbAU

Data-oriented design은 OOP와 다른 접근을 취한다.

OOP는 logical한 entity로 데이터를 묶지만 Data-oriented design은 사용되는 시스템으로 묶는다. 위 예시를 보면 Data A는 시스템 알파에 사용되는 데이터들로 묶여져 있다.

Data-oriented design은 앞으로 이 글에서 DOD라고 칭하겠다.

DOD의 요지

(1) Logic에서 데이터를 분리한다

DOD에서 struct와 function은 독립된 수명을 가지고, 데이터는 변환될 수 있는 정보로 취급된다.

(2) Logic은 데이터를 숨기지 않는다

또한 함수가 배열에서 동작할 수 있도록 구현해야 한다.

(3) 데이터를 사용처에 따라 조직해야 한다

(4) 숨겨진 상태를 피하고 virtual call을 피해야 한다(C++)

OOP로 짠 코드의 예시

영상에서는 Chromium 엔진을 예시로 들었다. Chromium 엔진에서 css 애니메이션을 호출하는 코드는 OOP로 짜져있다.

https://youtu.be/yy8jQgmhbAU?si=SUxqMlY83OJ0sSPW

위 클래스는 6개의 클래스를 상속한다. 그리고 상속하는 클래스들은 또 각각 여러 개의 클래스를 상속한다. 이미 여기서부터 개발자는 높은 복잡도를 마주한다.

https://youtu.be/yy8jQgmhbAU?si=SUxqMlY83OJ0sSPW

해당 클래스의 메소드 ServiceAnimation은 컬렉션에 있는 모든 애니메이션을 업데이트한다.

그런데 구현을 보면 업데이트가 필요한 애니메이션만 순회하는 것이 아니라 모든 애니메이션 포인터들을 복사한 뒤 업데이트하는 것을 볼 수 있다. 그 이유는 원본 벡터를 삭제하기 때문이다.

이 때문에 lifetime이 명확하지 않고 언제 애니메이션이 삭제되는지 이해하기 어렵다.

https://youtu.be/yy8jQgmhbAU?si=SUxqMlY83OJ0sSPW

애니메이션 객체의 업데이트 메소드를 보면 첫번째 라인에 숨겨진 상태가 있는 것(timeline)을 확인할 수 있다. 즉 특정 상황에서 이 메소드는 아무것도 하지 않는다.

하지만 우리는 이 if문을 보기 위해서 이미 비용을 지불했다. 모든 애니메이션은 힙에 이미 올라가있고, 이 필드를 보기 위해 캐시 미스를 대가로 지불했기 때문이다.

더군다나 아까 반복문에서 애니메이션 객체들을 순회하는 과정에서 Branch prediction도 많이 일어날텐데, active 애니메이션과 inactive 애니메이션이 섞여있다면 branch predictor가 제대로 동작하지 못할 것이다.

https://youtu.be/yy8jQgmhbAU?si=SUxqMlY83OJ0sSPW

특정 상황에서 애니메이션은 이벤트를 호출해야된다. event_delgate_ 플래그가 true일 때 OnEventCondition 메소드를 호출하는데 이로 인해 애니메이션 시스템과 이벤트 시스템의 커플링이 일어난다.

또 OnEventCondition 메소드가 정확히 무엇을 하는지 알기 어렵다. 아마 자바스크립트를 호출하면서 캐시에 있던 데이터를 전부 버리고 자바스크립트의 call data로 채울 것이고 이것이 끝나면 다시 우리는 데이터들을 reload해야한다.

이때 데이터와 instruction 캐시 미스가 둘 다 발생한다.

https://youtu.be/yy8jQgmhbAU?si=SUxqMlY83OJ0sSPW

(C++) Virtual function은 애니메이션에 사용될 property의 타입에 의존하기 때문에 데이터와 instruction 캐시 미스를 발생시킨다.

DOD로 짠 코드의 예시

위 Chromium 엔진 코드를 DOD로 다시 짜보자.

https://youtu.be/yy8jQgmhbAU?si=SUxqMlY83OJ0sSPW

위 구조에서 element 포인터들은 DOM 포인터들이고 DOM을 직접 호출하지 않는다. 이러한 데이터들을 output으로 내놓을 뿐이고 이들을 다음 데이터 파이프라인으로 넘길 뿐이다.

https://youtu.be/yy8jQgmhbAU?si=SUxqMlY83OJ0sSPW

struct는 런타임에 필요한 모든 데이터를 가지고 있게 flat하게 정의한다.

https://youtu.be/yy8jQgmhbAU?si=SUxqMlY83OJ0sSPW

type을 동적으로 정하는 abstract class를 없애는 대신 static하게 구현하기 위해 template을 사용한다.

https://youtu.be/yy8jQgmhbAU?si=SUxqMlY83OJ0sSPW

이때 타입의 크기가 달라져서 생기는 캐시 미스를 막기 위해 벡터를 순회할 때 같은 크기의 타입을 가진 벡터들을 모아서 순회한다.

같은 크기의 타입으로 이루어진 벡터들을 순회하는 경우 메모리 위치 및 타입의 변경으로 인한 CPU의 prefetcher가 발생시키는 캐시 미스를 줄일 수 있다. Element를 접근할 때 element의 크기가 같아야 CPU가 미리 prefetch해오기 쉽기 때문이다.

OOP에서는 한 타입당 한 번의 캐시미스가 생기지만, 이렇게 하면 타입이 변경될 때만 캐시 미스가 생기게 된다.

https://youtu.be/yy8jQgmhbAU?si=SUxqMlY83OJ0sSPW

Branch prediction을 최적화하기 위해 Active 애니메이션과 Inactive 애니메이션을 분리한다. 이런 식으로 최대한 if문을 없애려고 노력한다.

DOD로 짰을 때 성능 개선

DoD 적용 이후 알고리즘을 변경하지 않고도 성능이 6배가 증가하였다.

Scalability

OOP는 dependency들이 data race가 있는지 체크했어야 했다. 또한 hidden state를 갖고 있어 런타임에 어떤 상태를 가지고 있는지 확인하기 힘들었다. DoD를 사용하면 이런 점에서 단순하다는 이점이 있다.

Testability

DoD를 사용하면 animation 시스템을 다른 시스템과 완전히 분리시킴으로써 mocking할 의존성을 줄일 수 있다.

Modifiability

OOP는 base 클래스를 수정하기 까다롭다. DoD는 각각 데이터를 온전히 소유하므로 비교적 수정하기 편하다. 그러나 OOP가 수정하기 쉬울 때도 존재하는데, 새로운 상태를 추가하거나 메소드를 추가하기는 훨씬 편하다. DoD에서는 데이터를 재구조화해야할 수도 있다.

DOD의 단점

https://youtu.be/yy8jQgmhbAU?si=SUxqMlY83OJ0sSPW

DoD도 OOP처럼 하나의 도구기 때문에 단점이 존재한다.

일단 OOP보다 적용하기 훨씬 어렵다. Data 분리 및 existence base prediction을 제대로 구현하기 어렵다.

또한 위에서 보았듯이 OOP에서 수정하기 쉬운 것들이 DoD에서는 어려울 때도 있다.

개인적인 느낀점

영상에서 마지막에 OOP와 DOD는 둘 다 silver bullet이 아니라고 말한다. 중요한 것은 프로그래밍 패러다임을 잘 이해하고 적용하는 것이다.

이 영상에서 언급한 OOP의 단점은 OOP를 제대로 사용하지 못했을 때 발생하는 문제(bad engineering)들이다. 하지만 chromium 엔진 개발자들이 탑급 개발자들인 것을 고려해볼 때 이들조차 앞서 본 문제점들을 만든다는 것은 OOP를 쓰는 거의 모든 개발자들이 이러한 문제점을 발생시키고 있다는 것을 암시하기도 한다. 그래서 마지막 질의응답에서는 강연자가 이러한 문제들을 bad engineering이 아닌 OOP의 문제라고 생각한다고 말한다.

그렇다고 DOD를 적용하는 것은 현실적으로 어렵다고 생각한다. 프로그램을 한번 개발하면 긴 시간동안 유지 보수해야 한다는 것을 감안할 때, 기능 추가시 데이터를 재구조화해야하는 DOD를 도입하는 것은 OOP 대비 몇 배의 리소스를 더 소모시킬 것이다.

OOP를 사용하고 있는 개발자라면 이 영상에서 지적한 문제점들을 항상 기억하고 최대한 방지하는 것이 현실적으로 좋은 코드를 짜는 방법이지 않을까 싶다.


Reference

"OOP Is Dead, Long Live Data-oriented Design"