클래스, 하나 잘만들기 정말로 어렵다. 무슨 책임을 가지는지(이게 뭐다) 정의하고 만드는 것이 중요하다고 생각합니다.

ToDo

C++ Class

객체 지향 프로그래밍은, 객체들의 협력(collaboration), 책임(responsibility), 역할(role)을 정하여 프로그래밍 하는 것을 의미합니다. C++ 클래스는 이러한 객체 지향을 잘 지원합니다.

객체란 데이터와 코드 데이터를 가지고 있는 형태화된 필드(속성 또는 성질)이며, 절차(메서드)를 가집니다. 이러한 객체는 추상화(abstraction)과정을 통해서 만들어집니다.

  • 일반적으로 추상화는 컴퓨터 과학 및 소프트웨어 개발의 기본 개념입니다. 추상화의 과정은 모델링 이라고도 할 수 있으며 이론 및 디자인의 개념과 밀접한 관련이 있습니다. 모델은 현실 측면의 일반화에 따라 추상화 유형으로 간주하기도 합니다.

객체는 자기 만의 정보를 나타내는 변수들과, 이를 가지고 어떠한 작업을 하는 함수들로 둘러싸고 있다고 보시면 됩니다. 참고로, 이러한 객체의 변수나 함수들을 보통 인스턴스 변수(instance variable) 와 인스턴스 메소드(instance method) 라고 부르게 되는데, 그냥 알고 계시는 변수와 함수와 동일한 것으로 생각하시면 됩니다. 누군가 인스턴스 메소드라고 하면 “아 그냥 객체에 정의되어 있는 함수구나” 라고 생각하시면 됩니다.

  • 실제로 작업을 하다보면, 개인적인 판단이 필요한 부분입니다.
  • “내가 누구한테 일을 시킨다”라는 대상이 명확해야합니다. 클래스 또는 ptr을 가져와 .을 찍는 것은 일을 시키는 것 입니다. 따라서 액터의 각각의 부품이 무슨일을 하는지 명확하게 알아야합니다.
클래스와 인스턴스

클래스는 어떤 문제를 해결하기 위한 데이터를 만들기 위해 추상화를 거쳐 집단에 속하는 속성(attribute)과 행위(behavior)를 변수와 메서드로 정의한 것으로 객체를 만들기 위한 메타정보라고 볼 수 있습니다.

인스턴스(객체)는 클래스에서 정의한 것을 토대로 실제 메모리에 할당된 것으로 실제 프로그램에서 사용되는 데이터

객체지향의 목표는 실세계를 모방하는 것이 아니다. 오히려 새로운 세계를 창조하는 것 입니다. 객체를 스스로 생각하고 스스로 결정하는 현실 세계의 생명체에 비유하는 것은 상태와 행위를 ‘캡슐화’하는 소프트웨어 객체의 ‘자율성’을 설명하는 데 효과적입니다.

현실 세계의 사람들이 암묵적인 약속과 명시적인 계약을 기반으로 목표를 달성해 나가는 과정은 ‘메시지’를 주고받으며 공동의 목표를 달성하기 위해 ‘협력’하는 객체들의 관계를 설명하는 데 적합합니다.

실세계의 모방이라는 객체지향의 개념은 훌륭한 프로그램을 설계하고 구현하는 실무적인 관점에서는 부적합하지만 객체지향이라는 용어에 담긴 기본 사상을 이해하고 학습하는 데는 매우 효과적이다.

  • 메타데이터는 데이터에 대한 데이터입니다. 이렇게 흔히들 간단히 정의하지만 엄격하게는, 캐런 코일에 의하면 ‘어떤 목적을 가지고 만들어진 데이터’라고도 정의합니다. 가령 도서관에서 사용하는 서지기술용으로 만든 것이 그 대표적인 예입니다.
캡슐화가 무엇인가요?

절차 지향 프로그래밍에서도 라이브러리를 통해서 변수와 함수를 재활용할 수는 있었지만, 코드의 수정이 일어났을 때 영향 범위를 예상하기 어려운 문제가 있었다. 그러나 객체 지향 프로그래밍에서는 캡슐화를 통해 객체가 외부에 노출하지 않아야할 정보 또는 기능을 접근제어자를 통해 적절히 제어 권한이 있는 객체에서만 접근하도록 할 수 있기에 코드의 수정이 일어났을 때 책임이 있는 객체만 수정하면 되기에 영향 범위를 예측하는데 수월해졌다.

뿐만 아니라 관련된 기능과 특성을 한 곳에 모으고 분류하기 때문에 객체 재활용이 원활해졌다. 객체 지향 프로그래밍에서 기능과 특성의 모음을 “클래스”라는 “캡슐”에 분류해서 넣는것이 캡슐화다. 객체가 맡은 역할을 수행하기 위한 하나의 목적을 한데 묶는다.

때로는 변수나 유틸리티 함수를 protected로 선언해 테스트 코드에 접근을 허용하기도 한다. 같은 패키지 안에서 테스트 코드가 함수를 호출하거나 변수를 사용해야 한다면 그 함수나 변수를 protected로 선언하거나 패키지 전체로 공개한다.

하지만 그 전에 비공개 상태를 유지할 온갖 방법을 강구한다. 캡슐화를 풀어주는 결정은 언제나 최후의 수단이다.

상속은 무엇인가요?

상속은 부모클래스의 속성과 기능을 그대로 이어받아 사용할 수 있게하고 기능의 일부분을 변경해야 할 경우 상속받은 자식클래스에서 해당 기능만 다시 수정(정의)하여 사용할 수 있게 하는 것이다.

  • 다중상속은 불가하다. (클래스의 상속 관계에서 혼란을 줄 수 있기 때문에 상속은 반드시 하나만 가능하고 필요에 따라 인터페이스를 사용할 수 있게 했다. 자세한 내용은 이전 포스트에 있음.)
Is a - Has a

is-a는 추상화(형식이나 클래스와 같은)들 사이의 포함 관계를 의미하며, 한 클래스 A가 다른 클래스 B의 서브클래스(파생클래스)임을 이야기합니다. 다른 말로, 타입 A는 타입 B의 명세(specification)를 암시한다는 점에서 타입 B의 서브타입이라고도 할 수 있습니다.

is-a 관계는 타입 또는 클래스 간의 has-a 관계와는 대조됩니다. has-a 및 is-a 관계들 간의 혼동은 실세계 관계 모델에 대한 설계에 있어 자주 발견되는 에러입니다. is-a 관계는 또한 객체 또는 타입 간의 instance-of 관계와도 대조됩니다.

has-a는 구성 관계를 의미하며 한 오브젝트(구성된 객체, 또는 부분/멤버 객체라고도 부릅니다)가 다른 오브젝트(composite type이라고 부릅니다)에 “속한다(belongs to)”를 말합니다. 단순히 말해, has-a 관계는 객체의 멤버 필드라고 불리는 객체를 말하며, Multiple has-a 관계는 소유 계층구조를 형성하기 위해 결합하는 경우를 말합니다.

is-a 관계를 통해 생성된 클래스 및 객체는 상속 관계에서 둘은 밀접하게 결합되므로 부모 또는 기저 클래스의 명세에 변경이 발생하면 코드가 손상될 위험이 있습니다. 대신에 이러한 밀접한 관계는 클래스 계층구조에서 좀 더 안정적인 기반을 마련한다는 의미이기도 합니다. 게다가 상위 클래스의 기능을 하위 클래스가 물려받아 사용할 수 있는 장점도 있습니다.

반면 has-a 방식으로 생성된 클래스 및 객체는 느슨하게 결합됩니다. 이는 상속에 비해서 명세에 변경이 발생하더라도 구성 요소를 쉽게 변경할 수 있다는 의미입니다(코드의 손상이 적거나 없다는 의미입니다). 이러한 점에서 더 많은 유연성을 제공합니다. 하지만, has-a 방식은 상속보다 항상 낫다고 말할 정도로 단순한 문제가 아니며 실상은 더 복잡합니다.

Is a Has a 어떻게 사용할 것인가?
  • 상속의 경우 is 관계가 확실하면 사용하는 것이 좋습니다.

  • (구성(composition)의 경우, 하나의 객체가 다른 객체를 “(부분으로써) 갖거나” 하는 경우에 사용할 수 있습니다.

다형성은 무엇인가요?

하나의 변수명, 함수명 등이 상황에 따라 다른 의미로 해석될 수 있는 것이다. 즉 오버라이딩(Overriding), 오버로딩(Overloading)이 가능하다는 얘기다. 오버라이딩 : 부모클래스의 메서드와 같은 이름, 매개변수를 재정의 하는것. 오버로딩 : 같은 이름의 함수를 여러개 정의하고, 매개변수의 타입과 개수를 다르게 하여 매개변수에 따라 다르게 호출할 수 있게 하는 것.

getter, setter 를 사용하는 이유

멤버변수에 직접접근하지 못하게 private으로 접근지정자를 설정하고 public으로 getter, setter 메서드를 만드는 것을 많이 해왔다. 그러면서 이럴꺼면 어차피 아무나 접근가능한데 왜 private을 할까? 라고 생각했었다. 결론부터 말하면 getter, setter를 사용하면 메서드를 통해서 접근하기 때문에, 메서드 안에서 매개변수같이 어떤 올바르지 않은 입력에 대해 사전에 처리할 수 있게 제한하거나 조절할 수 있기 때문이다. 예를들면 setter에서 유효범위를 넘은 정수가 들어왔을 때의 처리를 하고나서 set하거나 예외처리를 해버릴 수 있는 것이다. getter도 마찬가지로 굳이 예를들자면 자료에 무언가 더하거나 빼고 준다든지가 가능하다.

클래스를 어떻게 만들어야 하는지에 대해서… 무엇을 어떻게 선택할 것인가… 고민이 많습니다.

클래스는 작아야 한다

클래스를 만들 때 첫 번째 규칙은 ‘작아야 한다’는 것이고, 두 번째 규칙은 ‘더 작아야 한다’는 것이다. 클래스를 설계할 때에도 함수와 마찬가지로 ‘작게’가 기본 규칙이며, 행 수로 크기를 측정하는 함수와 달리 클래스는 맡은 책임을 척도로 한다. 또한 클래스 이름은 해당 클래스 책임을 기술해야 한다. 간결한 이름이 떠오르지 않는다면 클래스 크기가 너무 크기 때문일 것이다. 가령 클래스 이름에 Processor, Manager, Super 등과 같이 모호한 단어가 있다면 클래스에 여러 책임을 떠안겼다는 증거다.

클래스는 ‘만일’, ‘그리고’, ‘~며’, ‘하지만’을 사용하지 않고서 25단어 내외로 설명이 가능해야 한다.

단일 책임 원칙 (Single Responsibility Principle)

클래스나 모듈을 변경할 이유가 단 하나뿐이어야 한다는 원칙이다. 클래스는 책임, 즉 변경할 이유가 하나여야 한다. 책임, 즉 변경할 이유를 파악하려 애쓰다보면 코드를 추상화하기도 쉬워진다.

단일 책임 원칙은 객체 지향 설계에서 더욱 중요한 개념이다.

소프트웨어를 돌아가게 만드는 활동과 소프트웨어를 깨끗하게 만드는 활동은 완전히 별개다. 큰 클래스 몇 개가 아니라 작은 클래스 여럿으로 이뤄진 시스템이 더 바람직하다. 작은 클래스는 각자 맡은 책임이 하나이고, 변경할 이유가 하나이며, 다른 작은 클래스와 협력해 시스템에 필요한 동작을 수행한다.

응집력 (Cohesion)

‘함수를 작게, 매개 변수 목륵을 짧게’라는 전략을 따르다 보면 때때로 몇몇 메소드만이 사용하는 인스턴스 변수가 아주 많아진다. 이는 십중팔구 새로운 클래스로 쪼개야 한다는 신호다.

응집도를 유지하면 작은 클래스 여럿이 나온다. 클래스가 응집력을 잃는다면 쪼개라!

변경하기 쉬운 클래스

깨끗한 시스템은 클래스를 체계적으로 정리해 변경에 수반하는 위험을 낮춘다.

경험에 의한 클래스 일부에서만 사용되는 비공개 메서드는 코드를 개선할 여지를 시사한다. 하지만 실제로 개선에 뛰어드는 계기는 시스템의 변화때문이어야 한다.

각 클래스는 극도로 단순하고 코드는 순식간에 이해될 수 있어야 한다.

새 기능을 수정하거나 기존 기능을 변경할 때 건드릴 코드가 최소인 시스템 구조가 바람직하다. 이상적인 시스템이라면 새 기능을 추가할 때 시스템을 확장할 뿐 기존 코드를 변경하지는 않는다.

변경으로부터의 격리

요구사항은 변하고, 따라서 코드도 변한다. 구체적인 클래스는 상세한 구현(코드)을 포함하며 추상 클래스는 개념만 포함하므로, 상세한 구현에 의존하는 클라이언트 클래스는 구현이 바뀌면 위험에 빠진다.

따라서 우리는 인터페이스와 추상 클래스를 사용해 구현에 미치는 영향을 격리한다. 테스트가 가능할 정도로 시스템의 결합도를 낮추면 유연성과 재사용성도 높아진다. 결합도가 낮다는 것은 각 시스템 요소가 다른 요소로부터 그리고 변경으로부터 잘 격리되어 있다는 의미이다. 시스템 요소가 서로 잘 격리되어 있으면 각 요소를 이해하기도 더 쉬워진다.

이렇게 결합도를 최소로 줄이면, 또 다른 설계 원칙인 DIP(Dependency Inversion Principle)를 따르는 클래스가 나온다. 이것은 클래스가 상세한 구현이 아니라 추상화에 의존해야 한다는 원칙이다.

Unreal UClass

클래스 지정자

클래스 지정자

엔진과 에디터의 다양한 측면에서 클래스가 어떻게 동작하는지 지정하기 위해 UClass를 선언할 때 사용되는 키워드입니다.

주요 참고자료 : 씹어먹는 C++, 위키피디아

C++에 “추상”에 대한 키워드가 있습니까?

c-abstract-keyword

아니요, C++는 추상 클래스를 설명하기 위해 abstract 키워드를 사용하지 않습니다. 그러나 C++에서 abstract 키워드를 사용하지 않는다고 해서 추상 클래스가 없는 것은 아닙니다. C++에서 추상 클래스는 순수 가상 함수 를 사용하여 생성됩니다.