과제를 통해 배울 수 있는 것

try catch를 이용한 예외 처리인 것 같다. finally까지 쓸 정도의 내용은 아니었고, 단순 try catch문 뿐만 아니라 커스텀 exception 클래스를 만들어 볼 수 있는 과제였다.

근본적으로 00의 내용만 할 줄 알면 그 뒤의 01 ~ 03은 그냥 클래스를 확장시키기만 하면 되는 거라 00만 정리하겠다.

Exercise 00: Mommy, when I grow up, I want to be a bureaucrat!

Bureaucrat 클래스를 만드는 것으로, 여기서 해야할 일을 크게 나누자면 다음과 같았다.

  1. getter, setter 구현
  2. 출력 연산자(<<) 오버로딩
  3. 예외 처리

1. getter setter 구현

Bureaucrat 클래스에는 private으로 _name_grade라는 멤버 변수가 있다. private으로 선언된 멤버 변수는 외부에서 접근이 절대 불가하기 때문에 해당 변수에는 값을 쓸 수도, 읽어올 수도 없다.
그런데 이를 가능하게 해주는 것이 getter setter다. getter와 setter는 멤버 변수에 접근하는 함수로, 당연히 해당 멤버 함수는 public으로 선언되어야 한다.

따라서 아래와 같이 Bureaucrat 클래스를 구성했다.

...

 private:
  const std::string _name;
  int _grade;

 public:
  Bureaucrat(std::string name, int grade);
  Bureaucrat(const Bureaucrat &other);
  Bureaucrat &operator=(const Bureaucrat &other);
  ~Bureaucrat();

  std::string getName() const;
  int getGrade() const;

...

여기서 getName()getGrade()가 각각 _name, _grade를 읽어오는 getter의 역할을 하게 했다.

2. 출력 연산자(<<) 오버로딩

서브젝트에서 Bureaucrat의 인스턴스를 출력할 때, <name>, bureaucrat grade <grade>.의 형식으로 출력하라는 내용이 있었다.
저걸 그냥 깡으로 하드 코딩할 수 있겠지만, 저 내용을 여기저기서 쓸 때 일일이 박아넣기도 싫고 재사용성도 떨어지는 것 같아서 출력 연산자를 오버로딩하기로 했다.

3. 예외 처리

예외 처리는 try catch사용과 커스텀 예외 클래스 파트로 나눌 수 있다.

3.1. try catch 사용

내부에서 뭔가 예외 상황(exception)이 벌어졌을 때, 이를 처리한 것으로 이를 예외 처리라고 한다.

대표적인 예외로는 division by zero(0으로 나누기), out of range(범위를 벗어남. 주로 인덱스로 배열에 접근할 때 발생한다.) 등등이 있다.

예외 처리 방법으로 대표적인 것이 try catch 문인데 구조는 다음과 같다.

try {
  실행 내용...
} catch (std::exception &e) {
  처리 내용...
}

try 문 내에는 예외가 발생할 만한 상황을 넣어두고, catch 문에서 예외가 발생했을 때 처리할 내용을 작성한다. 이 과제에서는 에러 내용을 출력하게 했다.
만약 데이터베이스 작업을 하다가 에러가 발생했다면, catch 문에는 작업 내용을 롤백하는 내용이 들어갈 것이다.

이 내용을 바탕으로 Bureaucrat의 등급을 올릴 때 발생할 예외 처리를 다음과 같이 작성했다.

try {
    std::cout << "* increase 100" << std::endl;
    bureaucrat.increaseGrade(100);
    std::cout << bureaucrat << std::endl;
    std::cout << "* increase 51" << std::endl;
    bureaucrat.increaseGrade(51);
  }
  catch (const std::exception &e) {
    std::cout << F_RED
              << e.what()
              << FB_DEFAULT << std::endl;
  }

increaseGrade() 함수는 brueacrat의 등급(1 ~ 150)을 올리는 함수로, 최대 1까지만 올릴 수 있다. 그런데 이미 100을 올린 상태에서 또 51을 올려버리면 등급은 1보다 작아지며 예외가 발생한다.
이때 발생한 예외를 catch 문에서 std::exception의 객체인 e를 이용하여 에러 메시지를 출력하도록 했다.

이번엔 increaseGrade() 함수에서 어떻게 예외를 발생시키는지를 보겠다.

void Bureaucrat::increaseGrade(int grade) {
  if (this->_grade - grade < 1)
    throw Bureaucrat::GradeTooHighException();
  this->_grade -= grade;
}

increaseGrade() 함수를 보면 객체의 등급이 grade만큼 높아질 때 1보다 작아진다면 throw를 이용하여 Bureaucrat::GradeTooHighException()를 반환하고 있다. throw를 이용하면 예외를 강제로 발생시키기 때문에 내가 예상한 예외 상황을 throw로 처리할 수 있다.

여기서 눈에 띄는 게 Bureaucrat::GradeTooHighException()인데 이는 Bureaucrat 클래스에서 선언한 GradeTooHighException이라는 예외 클래스이다.

3.2. 커스텀 예외 클래스

서브젝트에서 아예 Bureaucrat::GradeTooHighException()이라고 표기해 두어서, 일단 이 예외 클래스는 Bureaucrat 클래스의 내부 클래스겠거니 했다. 그리고 이 예외 클래스에서 할 일이 있었는데, 에러 메시지를 출력하는 것이었다.

보통 c++에서 std::exception 클래스에서 에러 메시지를 출력할 때 what()라는 함수를 사용하는 것 같았다. 문서에 따르면 이 함수는 예외와 관련된 문자열을 반환하는 함수로, 사용 예시도 아래와 같이 친절하게 적혀 있었다.

// exception::what
#include <iostream>       // std::cout
#include <exception>      // std::exception

struct ooops : std::exception {
  const char* what() const noexcept {return "Ooops!\n";}
};

int main () {
  try {
      throw ooops();
  } catch (std::exception& ex) {
      std::cout << ex.what();
  }
  return 0;
}

ooops 구조체를 보면 what 함수를 오버로딩하는 방법을 적어놓았고 이를 바탕으로 에러 메시지를 커스텀하여 다음과 같이 작성했다.

class GradeTooHighException: public std::exception {
  public:
  /**
    * @override std::exception
    * @return const char *errorMessage
    */
  virtual const char *what() const throw() {
    return "Error: too high grade";
  }
};

번외

예외 VS 에러

참고: Java의 Error와 Exception 그리고 예외처리 전략

  • 예외: 프로그래머가 예상하여 대응할 수 있는 것
  • 에러: 프로그래머가 예상하지 못해 대응할 수 없는 것

함수 오버라이딩 시, 자식 클래스의 virtual 키워드는 필수인가?

평가를 받는데, 깜빡하고 한 멤버 함수에 virtual 키워드를 작성하지 않고 그냥 제출했다. A 평가자는 자식 클래스의 멤버 함수에 virtual 키워드가 없다면 오버라이딩이 안 된다는 입장이었다.
그리고 그 다음 평가자는 virtual 키워드 없어도 오버라이딩이 잘 된다는 입장이었다. 그 근거로 부모 클래스에 그 멤버 함수는 순수 가상 함수였기 때문에 구현이 안 되어있는데 자식 클래스에서 오버라이딩이 이루어지지 않았다면 프로그램이 제대로 동작할 리가 없다는 것이었다.

속으로 아 디펜스 적극적으로 해볼 걸 싶기도 했는데, 뭐 잘 몰라서 디펜스도 못 한 거라 억울하지는 않았다. 그리고 위의 내용에 대해 누구 말이 맞는지 구글링을 해봤는데, virtual 키워드를 이용한 오버라이딩 예시를 보니 누구는 virtual을 쓰고 누구는 안 쓰는 등 딱히 이렇다 할 답이 없었다. 그래서 G교수님께 여쭤봤다.

virtual_keyword1 virtual_keyword2

결론은 써도 되고 안 써도 되지만, 명시적으로 virtual을 사용하는 게 좋다고 한다.

댓글남기기