포인터 vs 참조형 타입

|

보통 C++ 개발자는 C 언어를 먼저 배우고 나서 C++을 배우는 경우가 많습니다.

C 언어에서 데이터 주소를 넘기는 방법은 포인터(Pointer)밖에 없습니다. 그러다보니 C++에서도 포인터로 데이터 주소를 넘기는 경우가 종종 있는데, 대부분의 경우는 포인터가 아닌 참조형(Reference) 타입으로 대체할 수 있습니다.

포인터대신 참조형 타입을 사용하면 좋은 점들은 다음과 같습니다.

참조형 타입의 장점

참조형 타입은 포인터보다 안전합니다.

왜냐하면 메모리 주소를 직접 다루지 않기 때문에 nullptr 같은 경우가 발생할 수가 없습니다.


코드 스타일이 좋아집니다.

‘*’ 이나 ‘&’와 같은 심볼을 사용하지 않아도 됩니다.

대신 함수 원형을 보지 않고 호출부만 봤을 경우는 매개변수가 값이 복사되서 넘어가는지 주소값만 넘어가는지 알 수 없는 단점이 있습니다. 항상 함수의 선언부를 봐야 알 수 있습니다.


메모리의 오너십(Ownership)이 어디에 있는지 명확하게 해준다.

누군가가 만든 함수에 다른 프로그래머가 참조형으로 객체를 넘겨줄 경우, 그 함수 안에서는 그 객체를 메모리에서 해제할 수 없습니다. 만약 포인터로 넘겨받았을 경우는 메모리 해제의 책임이 변수를 생성한 사람에게 있는지, 그 함수를 작성한 사람에게 있는지 명확하지 않습니다.


미세하지만 참조형 타입이 포인터보다 성능이 더 좋다.

큰 차이는 없습니다. 다만 참조형 타입은 포인터처럼 주소값 복사의 과정이 없기 때문에 성능면에서는 조금 더 좋습니다.

Inheritance vs Composition

|

개발을 하다보면 남발하는 것 중 하나가 ‘상속(Inheritance)’입니다. 여러 클래스가 있을 때 공통되는 부분을 부모 클래스로 만들어서 상속으로 구현하면 보기에도 좋아보이고 코드양도 줄어들고 뿌듯할 때가 있습니다.

하지만, 상속보다는 이양을 사용하는 편이 설계 측면에서는 훨씬 더 유리합니다. ‘이양’이란 부모/자식간의 관계처럼 서로 상속하는 관계가 아니라 인스턴스안에 다른 인스턴스를 품어서 그 역할을 전달하는 방법입니다.

상속과 이양은 ‘Inheritance vs Composition’으로 표현하기도 하며, ‘is-A vs has-A’로 표현하기도 합니다.

상속 관계는 부모와 자식간의 관계가 아주 밀접하게 결합합니다. 부모 클래스가 수정이 되면 자식 클래스에도 영향을 미칩니다. 상속의 예는 다음과 같습니다.


Inheritance 예제 코드

class Robot {
 public:
  void update() {
    move();
  }
  // ...
  
 private:
  virtual void move() = 0;
  // ...
};

class CleanerRobot : public Robot {
 private:
  virtual void move() override {
    clean();
    moveForward();
    // ...
  }
};

class CombatRobot : public Robot {
 private:
  virtual void move() override {
    attack();
    rotateLeft();
    // ...
  }
};


Composition 예제 코드

위 코드를 이양 관계로 바꾸면 아래와 같습니다.

class RobotBehavior {
 public:
  virtual ~RobotBehavior() {}

  virtual void move() = 0;
};

class Robot {
 public:
  Robot(RobotBehavior* behavior) {
    mBehavior(behavior);
  }

  ~Robot() {
    delete mBehavior;
  }

  void update() {
    mBehavior->move();
  }

 private:
  RobotBehavior* mBehavior;
};

class CleanerRobot : public RobotBehavior {
 public:
  virtual void move() override {
    clean();
    moveForward();
    // ...
  }
};

class CombatRobot : public RobotBehavior {
 private:
  virtual void move() override {
    attack();
    rotateLeft();
    // ...
  }
};

코드가 길어지고 조금 더 복잡해졌습니다. 하지만 RobotBehavior이라는 행동을 하는 추상 클래스가 생기면서 Robot 클래스와 나머지 다른 클래스들인 CleanerRobot, CompatRobot 클래스와의 결합이 끊어졌습니다. 따라서 Robot 클래스의 내용이 변경되더라도 다른 로봇 클래스들에게는 영향을 주지 않습니다.

물론, CleanerRobot, CompatRobot 클래스들은 RobotBehavior 클래스를 상속받기 때문에 RobotBehavior 클래스가 수정이 되면 상속받은 클래스들은 모두 변경이 됩니다. 하지만, RobotBehavior 클래스는 아주 추상적인 레벨의 행동 인터페이스만 정의되어 있기 때문에 향후 변경될 가능성은 많이 낮습니다. 만약 변수나 구현부가 포함이 된 경우에는 향후 변경될 가능성이 아주 높습니다. 따라서 RobotBehavior 클래스에는 최소한의 인터페이스만 정의되어 있어야 합니다.

또한, 상속은 컴파일시 생성이 됩니다. 하지만 이양의 경우는 실행중에도 동적으로 변경이 가능한 장점도 있습니다.

따라서 상속과 이양이 둘 다 가능한 경우에는 가급적 상속보다는 이양을 우선하는게 좋습니다.

함수의 기본 원칙

|

함수화를 잘 하는 것은 유지 보수성이 높은 코드를 만드는 첫 번째 단계라고 볼 수 있습니다. 경험이 중요하지만 기본적인 원칙을 따르며 함수를 작성해가면 새로운 언어 등에서도 유연하게 개발할 수 있습니다.


함수의 기본 원칙

  • 하나의 함수는 하나의 역할만 담당한다.
  • 함수를 두 종류로 분류한다.

하나의 함수가 하나의 역할만 담당하는 것은 상당히 쉽지만 많은 사람들이 실수하는 부분입니다. 함수가 여러 가지 기능을 담당하게되면, 함수의 이름도 복잡해지고 매개변수의 처리나 코드의 복잡성도 높아지게 됩니다.

함수는 크게 두 종류로 분류할 수 있습니다. 각종 연산이나 알고리즘 수행을 행하는 함수와 이런 함수들을 조합해서 관리하는 함수들로 나눌 수 있습니다.

예를 들어 다음의 함수는 게임 등에서 자주 쓰이는 update 함수 예제입니다.

void update(long msec) {
  for (iter i = actor.begint(); i != actor.end(); i++) {
    (*i)->move(msec);
  }

  collide();
}

사물의 이동과 충돌 판정 계산이 같이 포함되어 있는데, 어딘가 부자연스럽습니다. 사물의 이동은 실제 연산 부분이 구현되어 있고, 충돌 판정 부분은 함수 호출로 되어 있어 서로 코드의 레벨이 다르기 때문입니다.

따라서 다음과 같이 코드를 수정할 수 있습니다.

void move(long msec) {
  for (iter i = actors.begint(); i != actors.end(); i++) {
    (*i)->move(msec);
  }
}

void update(long msec) {
  move(msec);
  
  collide();
}


함수화 패턴

함수화를 하는데에는 보통 다음과 같은 패턴이 있습니다. 일일이 따를 필요는 없지만 대략 다음과 같은 패턴으로 함수화를 하면 유지 보수성이 높은 코드를 쉽게 작성할 수 있습니다.

  • 조건식 함수화
  • 계산식 함수화
  • 분기문의 블록 내부 함수화
  • 반복문 함수화
  • 데이터 변환 함수화
  • 데이터 확인 함수화
  • 배열 접근 함수화


조건식 함수화

if 조건문의 조건식을 함수화하는 방법입니다.

if((speed > 100) && (posY > 250)) {
  // ...
}

위 코드는 다음과 같은 형태로 바꿀 수 있습니다.

if(isJump()) {
  // ...
}


계산식 함수화

계산식을 함수화하면 그 함수에 이름을 붙이기 쉽습니다. 그 의미를 명확하게 할 뿐 아니라 그 함수를 호출하는 쪽의 코드 분량이 줄어들어 가독성또한 좋아집니다.

double getDistance(double x1, double y1, double x2, double y2) {
  return std::sqrt((x2 - x1) * (x2 - x1) + (y2 - y1) * (y2 - y1));
}

예를 들어, 위와 같은 함수를 작성하면 호출하는 쪽은 단지

double distance = getDistance(x1, y1, x2, y2);

와 같은 간단한 호출만으로 원하는 값을 얻을 수 있습니다.


반복문 함수화

다음 코드는 2개의 반복문을 갖고 있습니다.

void update(long msec) {
  for (iter i = actors.begin(); i != actors.end(); i++) {
    (*i)->update(msec);
  }

  for (iter i = actors.begin(); i != actors.end(); i++) {
    for (iter j = std::next(i); j != actors.end(); j++) {
      (*i)->collide(*j);
    }
  }
}

각각의 반복문들을 다음과 같이 함수화합니다.

void move(long msec) {
  for (iter i = actors.begin(); i != actors.end(); i++) {
    (*i)->update(msec);
  }
}

void collide() {
  for (iter i = actors.begin(); i != actors.end(); i++) {
    for (iter j = std::next(i); j != actors.end(); j++) {
      (*i)->collide(*j);
    }
  }
}

void update(long msec) {
  move(msec);

  collide();
}

이렇게 각각을 함수로 분리함으로써 각 함수는 역할이 분명해지고 가독성도 훨씬 더 좋아졌습니다.

이렇게 분리한 반복문들은 STL을 이용해 더 간략화할 수 있습니다.

void move(long msec) {
  std::for_each(actors.begin(), actors.end(),
                [&](Actor* actor) { actor->update(msec); });
}


작은 함수의 필요성

함수화 패턴에 따라 코드를 함수화해나가면 함수 단위가 아주 작아집니다. 작은 함수는 다음과 같은 점에서 유리한 점을 가집니다.

  • 함수의 이름만으로 설명이 쉽게 된다.
  • 함수 개별 테스트(Unit Test)가 쉬워진다.
  • 함수 재활용성이 강화된다.

함수를 많이 쪼갤수록 성능 저하가 발생할 수 있습니다. 실제로도 성능 저하가 발생할 수는 있지만, 요즘의 대부분의 컴파일러들은 최적화 기능을 통해 이러한 문제점들을 많이 극복하고 있습니다.

예를 들어 Release 모드에서는 아주 작은 함수들은 인라인(inline) 함수로 대체해버려서 함수 호출로 인한 오버헤드(Overhead)가 전혀 없어지기도 합니다. 비주얼 스튜디오 같은 경우는 ‘링크시 코드 생성(LTCG)’ 기능이 있어서 컴파일(Compile) 단계가 아닌 링크(Link) 단계에서 함수를 인라인화하는 옵션도 있습니다. 이는 프로그램 전체의 함수가 최적화되기 때문에 다른 파일에 있는 함수들까지 인라인화되는 옵션입니다.

assert 활용하기

|

assert를 잘 쓰면 개발에 꽤 유용할 때가 많습니다.

assert는 디버그 모드(Debug Mode)에서만 작동하고 릴리즈 모드(Release Mode)에서는 컴파일 단계에서 무효화되어 걸러지기 때문에 디버깅 용도로 쓰기에 참 좋습니다. (보통 디버깅 환경에서는 로그 메세지를 남발하며 뿌리다가 릴리즈 모드에서는 그 로그 메세지들을 일일이 막는 것도 번거로운 작업들입니다. 그래서 Log 함수들을 별도의 클래스로 Wrapper하는 경우도 많이 있습니다.)

assert는 괄호안에 들어가는 조건식이 false인 경우 오류 메세지를 출력하면서 프로그램을 강제 종료시킵니다. 프로그램을 강제 종료함으로 인해, 나중에 발생할지도 모르는 논리적인 오류들을 사전에 막아주는 역할을 해서 디버깅 환경에 큰 도움이 됩니다.

assert 활용 예제는 다음과 같습니다.


assert 활용 예제

static const char* weeks[] = {
    "Sunday", "Monday", "Tuesday", "Wednesday", "Thursday",
    "Friday", "Saturday",
};
void displayWeek(int day) {
  assert((day >= 0) && (day <=6));
  cout<<weeks[day];
}

위 코드에서 displayWeek 함수의 인자로 0보다 작거나 6보다 큰 값을 입력하게 되면 다음과 같은 오류 메세지를 출력합니다.

assertion "(day >= 0) && (day <=6)" failed: file "/cygdrive/c/Workspace/Clion/SnowDeer/main.cpp", line 11, function: void displayWeek(int)


주의할 점

assert의 매개변수에는 조건식만 들어가야 합니다. 만약 조건식이 아닌 실행 가능한 구문이 들어가게 되면 큰 오류를 범할 수 있습니다. assert는 디버그 모드에서만 활성화되며, 릴리즈 모드에서는 활성화되지 않기 때문에 다음과 같은 코드는 문제가 발생할 수 있습니다.

void openFile() {
  FILE* fp;
  assert((fp = fopen(file_name, "r")) != nullptr));
}

`assert’는 잘 쓰면 좋은 효과를 가져올 수 있지만, 입력해야 하는 매개변수 또한 주의하지 않으면 또다른 문제를 발생하기도 합니다. 따라서 매개변수에 들어가는 조건문을 최대한 간결하게해서 필요한 부분에만 적절히 사용한다면 개발에 큰 도움이 될 수 있을 것 같습니다.

Fragment의 라이프사이클

|

Fragment의 라이프사이클

Fragment의 라이프사이클은 Activity의 라이프사이클과 비슷하지만 약간 더 복잡합니다. 다음 이미지와 같은 모습의 라이프사이클을 갖고 있습니다.

image -fullwidth

onAttach()onDetach() 이벤트가 Fragment가 실제로 Activity와 연결되거나 끊어지는 부분입니다.

단계 설명
onAttach Activity와 연결될 때 호출. 이 시점에서 getActivity()null 리턴함
onCreate Fragment 생성시 호출
onCreateView View 생성
onActivityCreated 뷰 생성(setContentView 호출 등)
onStart Fragment 시작될 때 호출(화면에 보이기 직전)
onResume 화면에 표시될 때 호출
onPause 화면에서 가려질 때 호출
onStop Fragment가 종료될 때 호출
onDestoryView View가 해제될 때 호출
onDestory Fragment가 해제될 때 호출
onDetach Activity와 연결이 해제될 때 호출. 가장 마지막 단계