본문으로 바로가기

객체는 실존하는 모든 것을 가리킨다. 개발자는 실존하는 객체를 일반화하고 추상화하는 작업을 거쳐 클래스를 만드는데 클래스는 객체를 생성하기 위한 틀이 된다. 예를 들어 자동차라는 실존 객체를 위해 Car라는 이름의 클래스를 생성하고 적절한 변수와 함수를 클래스 안에 추가할 수 있다.

이번 포스트에서는 클래스를 어떻게 설계할 것인지에 대해 다룬다. 단순히 클래스를 정의하는 것을 넘어 캡슐화를 통해 데이터와 알고리즘을 보호하고 상속을 통해 코드의 재사용성을 줄이며 다형성을 통해 하나의 인터페이스로 클래스의 다양한 기능을 제공할 수 있도록 클래스를 설계한다.

캡슐화(Encapsulation)

캡슐화는 클래스 내부에서 여러가지 속성과 함수를 묶어서 외부에 노출하지 않고 데이터와 알고리즘을 보호하기 위해 사용한다. 클래스는 접근 지정자를 통해 멤버 변수와 메소드를 외부에서 접근 가능할게 할 것인지, 불가능하게 할 것인지를 구분할 수 있다.

아래 코드는 이름과 나이를 멤버 변수로 가지는 Person 클래스를 정의한 것이다. _name_age를 외부에서 직접적으로 접근할 수 없게 private 접근 지정자로 두 변수를 보호한다. 대신에 public에 정의된 getter 메소드와 setter 메소드로 멤버 변수에 접근과 수정을 가능하게 한다.

class Person {

public:
  Person(string, int);

  string name() const;
  int age() const;

  void name(string);
  void age(int);

  void info() const;

private:
  string _name;
  int _age;

};
Person::Person(string name, int age): _name(name), _age(age) {
  // constructor
}

string Person::name() const {
  return _name;
}

int Person::age() const {
  return _age;
}

void Person::name(string name) {
  _name = name;
}

void Person::age(int age) {
  _age = age;
}

void Person::info() const {
  cout << "My name is " << name() << " and I am " << age() << " years old\n";
}
int main() {

  Person person("Aaron Ramsey", 28);
  person.info();// My name is Aaron Ramsey and I am 28 years old

  //  person._name = "Jack Wilshere"; not accessible
  //  person._age = 26; not accessible

  person.name("Jack Wilshere");
  person.age(26);
  person.info();// My name is Jack Wilshere and I am 26 years old

  return  0;
}

상속(Inheritance)

이번엔 Professor 클래스와 Student 클래스를 정의해보자. 두 클래스 모두 이름과 나이 그리고 아이디를 멤버 변수로 가진다. 마찬가지로 멤버 변수에 대한 getter 메소드와 setter 메소드를 정의해야 하는데 두 클래스가 공통적으로 가지는 멤버 변수와 메소드를 다시 정의하지 않고 효율적으로 클래스를 설계할 수 없을까?

정답은 상속에 있다. 이미 정의한 Person 클래스를 상속받아 _name_age 멤버 변수를 가질 수 있고 Person 클래스에 정의된 메소드 역시 사용할 수 있다. 다만 private에 정의된 _name_age 변수에 직접적인 접근은 불가능 하더라도 public에 정의된 메소드는 언제든지 접근이 가능하기 때문에 멤버 변수를 조작할 수 있다. 만약 하위 클래스에서 상위 클래스의 멤버 변수를 조작할 수 있게 하려면 상위 클래스의 멤버 변수를 protected 접근 지정자 안에 선언하면 된다.

Professor 클래스와 Student 클래스 모두 _id 멤버 변수를 가지지만 Person 클래스에 _id 멤버 변수를 정의하지 않은 까닭은 상속받은 멤버 변수와 메소드 외에 클래스가 독자적으로 멤버 변수와 메소드를 선언할 수 있음을 보여주기 위해서이다.

class Professor : public Person {

public:
  Professor(int, string, int);

  int ID() const;
  void ID(int);

  void info() const;

private:
  int _id;

};
Professor::Professor(int id, string name, int age): _id(id), Person(name, age) {
  // constructor
}

int Professor::ID() const {
  return _id;
}

void Professor::ID(int id) {
  _id = id;
}

void Professor::info() const {
  cout << "I am professor. ";
  Person::info();
}
class Student : public Person {

public:
  Student(int, string, int);

  int ID() const;
  void ID(int);

  void info() const;

private:
  int _id;

};
Student::Student(int id, string name, int age): _id(id), Person(name, age) {
  // constructor
}

int Student::ID() const {
  return _id;
}

void Student::ID(int id) {
  _id = id;
}

void Student::info() const {
  cout << "I am student. ";
  Person::info();
}
int main() {

  Professor professor(1111, "Aresene Wenger", 68);
  professor.info();// I am professor. My name is Aresene Wenger and I am 68 years old

  Student student(2222, "Alex Iwobi", 21);
  student.info();// I am student. My name is Alex Iwobi and I am 21 years old

  return 0;
}

다형성(Polymorphism)

다형성은 하나의 인터페이스를 통해 클래스의 다양한 기능을 제공하는 것의 의미한다. 다형성을 이해하기 위해 activity() 메소드를 예로 들어보자. Professor 클래스와 Student 클래스 모두 activity() 메소드를 추가하고 아래와 같이 정의하자.

// ...

void info() const;
void activity() const;

// ...
void Professor::activity() const {
  cout << "Give a lecture\n";
}
void Student::activity() const {
  cout << "Take a lecture\n";
}

activity() 메소드는 클래스로 생성될 객체의 주요 활동을 가리킨다. 교수는 강의를 하는 것이 주요 활동이 되겠고 학생은 강의를 듣는 것이 주요 활동이 되겠다. Person 객체로부터 상속받은 직업을 가지게 되는 객체는 모두 주요 활동이라는 함수를 사용하게 될 것이고 따라서 Person 객체로부터 activity() 메소드를 상속받는 것이 효율적으로 클래스를 설계하는 방법이 된다.

다만 상속받은 클래스에서 activity() 메소드를 재정의하는 경우 해당 클래스로 생성된 객체가 activity() 메소드를 호출하였을 때 부모의 메소드를 호출해야하는지 자식의 메소드를 호출해야하는지 명확하지 않은 경우가 생긴다. 이를 방지하기 위해 virtual 키워드를 사용하여 가상 함수를 사용한다. virtual 키워드로 정의된 메소드는 가상 함수로 해당 메소드를 호출할 때 자신을 상속하고 구현한 하위 클래스의 함수를 호출하는 기능을 제공한다.

class Person {

public:
  // ...

  virtual void activity() const {
    cout << "Do something\n";
  }

  // ...
};

만약 메소드가 상위 클래스에서는 사용하지 않고 단순히 자신을 상속한 클래스에게 상속할 목적으로 만들어진 것이라면 어떻게 해야할까? 추상 함수를 사용하면 된다. 추상 함수는 virtual 키워드로 생성된 함수를 0으로 할당시킴으로써 정의할 수 있다.

virtual 키워드로 정의된 함수에 0을 할당시킨 함수를 추상 함수라 하고 추상 함수를 하나 이상 가지고 있는 클래스를 추상 클래스라고 한다. 추상 클래스는 단순히 하위 클래스에게 멤버 변수와 메소드를 상속할 목적으로 만들어졌기 때문에 절대 추상 클래스로 객체를 생성할 수 없다.

class Person {

public:
  // ...

  virtual void activity() const = 0;// abstract function

  // ...
};
int main() {

  Professor professor(1111, "Aresene Wenger", 68);
  professor.info();// I am professor. My name is Aresene Wenger and I am 68 years old
  professor.activity();// Give a lecture

  Student student(2222, "Alex Iwobi", 21);
  student.info();// I am student. My name is Alex Iwobi and I am 21 years old
  student.activity();// Take a lecture

  // Person person("Aaron Ramsey", 28); not allowed for abstract class

  return 0;
}

댓글을 달아 주세요