상속과 컴포지션. 무엇을 선택할지 어떻게 판단해야 하는가?

OO에서 재사용성을 중요시하는 이유

Don't reinvent the wheel!

  • 바퀴는 이미 동작과 상태가 명확한 물체
    • 설계, 구현, 테스팅까지 모두 마친 물체
  • 이걸 가지고 다른 유용한 물체를 만들자.
    • 시간 낭비하지 말자
    • 자동차, 자전거

클래스를 재사용하면 좋은 점

설계와 코딩에 드는 시간을 절약

  • OO외의 프로그래밍에도 적용되는 올바른 원칙
  • 하지만 실전에서 100% 적용 불가
  • 프로그램이 미래에 어떻게 변할지 완전히 예측 불가
  • 재사용성에 눈이 멀어 잘못된 바퀴를 장착 시킬 수 있음
    • 원시시대에 사용했던 돌덩이 바퀴를 자동차에 사용
  • 지금 굳이 사용안하는데 재사용 생각하고 가독성 떨어트리는 코드를 생산할 수도 있음
  • 즉, 주관적임, 상식의 문제임

테스트에 걸리는 시간을 절약

  • 이미 테스트까지 끝낸 클래스를 다시 테스트할 필요가 없음
  • 상속시 부모 클래스는 이미 테스트가 끝난 상황
  • 부모 클래스는 테스트 할 필요가 없다고 주장도 하는 편
  • 하지만 실제로는 그렇지 않은 경우가 빈번
    • 새로운 방법으로 부모 클래스를 사용할 수도 있음
    • 부모 클래스를 변경할 수도 있음

관리 비용을 절약

  • 코드 중복이 없음
    • 한 곳만 고치고 다른 곳을 실수로 안 고칠 가능성이 없음
  • 관련된 코드가 모두 한 파일에 있음
    • 해당 파일을 열면 모든 로직 및 데이터 파악 가능
    • 다만, 재사용성을 위해 클래스를 잘게 나누다보면 파일 수가 많아짐

OO 모델링은 많은 연습이 필요

  • Best practice로 커버 불가
    • 주관적이기 때문
    • 너무나 다양한 프로그램이 있기 때문
    • 즉, 다양한 문제 타입에 따라 맞는 해결책은 다름
  • 지름길은 없다.
    • 남들보다 많이 해보고, 많이 고민해보면 빠르게 늘 수 있다.
  • 도움
    • 다른 사람의 코드 사용
      • 다만, 보고 안다고 착각하지는 마라.
      • 괜찮은 프레임워크
    • 코드 리뷰

상속 vs 컴포지션

  • 둘 다 재사용성이 목적
  • 둘 중 하나를 고른 원칙이 필요

성능적 측면

  • 메모리 문제: 상속을 사용하는 것이 보다 좋다.
  • 개체를 생성하면, 메모리에 하나의 덩어리로 들어가게 됨
  • 그렇기 때문에 어떤 방식을 사용하느냐에 따라 메모리에 들어가는 방식이 변하게 된다.
  • Rectangle의 경우 가로 세로를 표현하고, Box는 깊이까지 표현하는 코드를 짠다고 생각해보자.

// Inheritance
Box box = new Box(10, 20, 30);

// Composition
Rectangle rectangle = new Rectangle(10, 20);
Box box = new Box(rectangle, 30);
  • 상속같은 경우 새로운 개체를 만들면(new), 내부에서 다른 개체를 만들지 않고 한 덩어리로 메모리 할당을 한다.
  • 그와 반대로 컴포지션은 메모리가 여러 덩어리로 생성이 될 수 있다.
  • 다른 개체를 생성해서 넘겨주어야 하기 때문이다. 참조한 주소만을 가지고 있기 때문에, 결국은 다른 곳에 메모리 덩어리가 생성된다.
  • 즉, 여기서 알 수 있는 점은 상속과 컴포지션에서 메모리 상에 차이가 발생한다는 것이다.
  • 그리고 이는 실행 성능에 영향을 미친다.
  • 그렇기에 성능을 주요시 하는 작업의 경우에는 고려해야하는 대상으로 변모한다.

첫번째 병목점 (캐시 최적화)

CPU와 메모리 사이의 데이터 전송

  • 메모리 -> 버스(CPU쪽으로 데이터를 이동해주는 녀석) -> CPU
  • 여기서 CPU 처리 속도, 데이터 이동 속도가 다르게 되어 병목이 생긴다.
  • 이런 문제를 해결하고자 캐시 메모리가 있는 것.
  • 맨날 계산하고 필요할 때마다 메모리한테 요청, 버스타고 이동, 연산 이렇게 하다보면 너무 오래걸리니,
  • 어차피 4kb 계산위해 읽어올 거면 한번에 읽어오고, 캐시쪽에 저장해두는게 더 빠르다는 것
  • 이런 걸 CPU 최적화라고 한다. 요즘은 캐시 메모리가 계속 증가하는 추세. CPU 성능에 영향을 많이 준다는 반증이겠지?
  • 이러한 점에서 상속 모델로 만든 개체는 한번에 캐시 메모리에 들어갈 가능성이 높다.
  • 컴포지션 모델로 만든 개체의 경우 개체 내 부품 수만큼 캐시 메모리로 로딩할 가능성이 높다.

두번째 병목점 (메모리 할당/해제)

  • 메모리 할당(new)과 해제(delete, release)
  • 프로그래밍 언어에 따라 둘 중 특히 느린 것이 "반드시" 있다. (이부분 확인 필요)
  • 상속은 메모리 할당과 해체가 딱 한번씩 생긴다.
  • 컴포지션은 기본적으로 바깥쪽 만들때 한번, 부품 수만큼씩 추가로 필요하다.

다형성

  • 다형성을 사용하여 모든 하위 개체에 동시에 명령을 내리고 싶은 경우: 상속 사용

유지보수

  • 관리의 효율성을 위해 특정 방식으로 가는 경우가 있다.

상속이 나은 경우

  • 컴포지션으로 가게 되었을 때, 오히려 코드가 많이 발생하는 경우
  • 결국 조립해서 사용하는 개체에서 부품의 메서드를 호출해서 값을 가져와야 한다면, 필요없는 signature를 만들어서 처리해야 한다.
  • 릴레이 함수 (하위에서 값만 가져와서 넘겨주는)가 생기게 된다.

컴포지션이 나은 경우

  • 깊은 상속관계
    • 중간의 클래스를 바꾸면 아래 클래스도 모두 바뀐다.
    • 자식 클래스에서 문제 없는지 모두 확인해야 한다.
    • 컴포지션도 비슷한 문제가 있긴 하나, 조립의 측면이 더 강하기 때문에 덜하다.

그외 일반적인 상황

  • 상식적으로 생각할 것
  • 이 프로그램을 작성하는 이해관계자들 간의 공통된 합의를 생각하자.
  • has-a: 컴포지션 - 개념이 분리되는 것이 맞다면
  • is-a: 상속 - 개념이 포함되는 것이 맞다면

Entity Component System(ECS)

  • 컴포지션을 선호하는 또 다른 예
  • 코드 변경 없이 자유롭게 개체를 만들 수 있도록 하는 것이 목적
  • 아키텍쳐 패턴 중 하나다.
  • 게임 업게에서 많이 사용한다. (Unity3D)
  • 게임에서 개체 설계를 하려면 깊은 상속을 사용해야 하는 경우가 많다.
    • GameObject <- PhysicsObject <- Character <- Player...
  • 그런데 그러다 보니 기획에서 변경이 들어왔을 때, 이를 바꾸는게 상당히 귀찮은 작업이 될 수 있다.
  • 개발자: 왜 기획에서 초창기에 확인을 안하지?
  • 기획자: 안해보고 어떻게 판단?
  • 업무 효율성이 매우 떨어짐.

문제: 재컴파일 없이 기획자가 원하는 대로 개체를 조립할 수 없을까?

  • Entitiy Component System
  • GameObjectComponent를 List로 들고 있다.
  • 각각은 update()하는 방법을 override하여 상황에 맞게 구현한다.
  • 여기서 생각해야 할 것은, NPC, Player, Effect와 같은 클래스가 사라졌다는 것이다.
    • 보다 일반화된 개념으로 자리잡아두고 처리하고 있음
    • 이 Component를 조립한 결과로, NPC, Player와 같은 개체를 만들 수 있음

Reference

profile
Goal, Plan, Execute.

0개의 댓글