힘내라 일처리

C++ Pimpl 알아보기 본문

알아보기

C++ Pimpl 알아보기

일처리 2022. 7. 1. 19:17
반응형

개요

C++를 이용해서 개발을 공부하거나 개발을 진행하다 보면 Pimpl이라는 것을 마주하게 됩니다.
이 것이 무었이고 또 왜 사용하는지 이번 기회에 알아봅시다.

Pimpl 이란?

Pimpl은 Pointer to IMPLement의 약자로 C++에서 구현된 객체를 동일한 형태의 인터페이스용 객체에 포인터만 전달하여 실제 기능 구현을 사용자에게서 감추는 프로그래밍 기법입니다.

실제 코드 예시

.
├── libcar
│   ├── car.cpp
│   ├── car.h
│   ├── carimpl.cpp
│   ├── carimpl.h
│   └── CMakeLists.txt
└── user_dev
    ├── CMakeLists.txt
    ├── include
    │   ├── car.h
    │   └── libcar.a
    └── main.cpp

이런 식의 코드 구성이 있다고 합시다.
libcar 은 저희가 만든 라이브러리이고, user_dev 는 저희가 만든 라이브러리를 이용하는 사용자의 코드입니다.

user_dev 하위에는 저희가 이미 빌드해서 재공 하는 libcar.a 라이브러리와 car.h 라는 라이브러리가 있습니다. 즉, libcar의 빌드 결과물과 필요한 헤더만 공유한 것이지요.

이제 각각의 코드를 살펴봅시다.

libcar/car.h

#ifndef CAR_H_
#define CAR_H_
#include <memory>

class CarImpl;
class Car {
  public:
    Car();
    ~Car();

    void ride();
  private:
    CarImpl *carImpl = nullptr;
};

car.h는 기본적인 인터페이스 형태만 나타냅니다.

libcar/car.cpp

#include "car.h"
#include "carimpl.h"
#include <iostream>
#include <memory>


Car::Car()
{
  carImpl = new CarImpl();
}

Car::~Car()
{
  delete carImpl;
}

void
Car::ride()
{
  carImpl->ride();
}

car.cpp 에서는 CarImpl에서 구현된 기능을 호출해서 사용합니다.

libcar/carimpl.h

#ifndef CAR_IMPL_H_
#define CAR_IMPL_H_
#include <string>


class CarImpl {
  public:
    CarImpl() = default;
    ~CarImpl() = default;

    void ride();
  private:
    void playEngineSound();
    std::string engineSound = "부와아아앙";
};



#endif // CAR_IMPL_H_

carimpl.h에서는 실제 코드 구현에 사용되는 프로퍼티들이 명시되어있습니다. 라이브러리 사용자에게 보여주고 싶지 않은 부와아아앙같은 값이 들어있네요 제 라이브러리를 사용하는 고객이 부와아아앙같은 핵심 기술을 혹시라도 훔쳐갈까 걱정이 됩니다.

libcar/carimpl.cpp

#include "carimpl.h"
#include <iostream>
void
CarImpl::ride()
{
  playEngineSound();
}

void
CarImpl::playEngineSound()
{
  std::cout << this->engineSound << std::endl;
}

carimpl.cpp 에서는 코드가 동작하는 기능이 구현되어 있습니다.

libcar/CMakeLists.txt

project (libcar)

add_library(car car.cpp carimpl.cpp)

제가 make를 잘 못써서 cmake로 빌드되도록 작성했습니다.
이걸 빌드하면 libcar.a파일이 만들어지는 거지요.

user_dev/main.cpp

#include <iostream>
#include "include/car.h"

int main()
{

  Car car;

  car.ride();

  return 0;
}

main.cpp 는 고객이 제가 제공한 라이브러리를 이용해서 코드를 구현한 내용입니다.

user_dev/CMakeLists.txt

project (user_dev_car)


add_executable(pimpl_user_dev_test main.cpp)
target_link_libraries(pimpl_user_dev_test ${CMAKE_SOURCE_DIR}/include/libcar.a)

마찬가지로 cmake로 빌드를 하게 되면 pimpl_user_dev_test 라는 바이너리가 나옵니다.

실행 결과

orange@32thread-server:~/project/pimpl/user_dev/build$ cmake ..
-- The C compiler identification is GNU 9.4.0
-- The CXX compiler identification is GNU 9.4.0
-- Check for working C compiler: /usr/bin/cc
-- Check for working C compiler: /usr/bin/cc -- works
-- Detecting C compiler ABI info
-- Detecting C compiler ABI info - done
-- Detecting C compile features
-- Detecting C compile features - done
-- Check for working CXX compiler: /usr/bin/c++
-- Check for working CXX compiler: /usr/bin/c++ -- works
-- Detecting CXX compiler ABI info
-- Detecting CXX compiler ABI info - done
-- Detecting CXX compile features
-- Detecting CXX compile features - done
-- Configuring done
-- Generating done
-- Build files have been written to: /home/orange/project/pimpl/user_dev/build
orange@32thread-server:~/project/pimpl/user_dev/build$ make
Scanning dependencies of target pimpl_user_dev_test
[ 50%] Building CXX object CMakeFiles/pimpl_user_dev_test.dir/main.cpp.o
[100%] Linking CXX executable pimpl_user_dev_test
[100%] Built target pimpl_user_dev_test
orange@32thread-server:~/project/pimpl/user_dev/build$ ./pimpl_user_dev_test 
부와아아앙

설명

user_dev 의 코드 속 내용을 보면 알 수 있듯이 인터페이스용 객체의 헤더 파일만 공유하게 되면 저희가 구현한 기능들을 사용할 수 있게 됩니다.
이때 사용자는 저희가 구현한 객체가 가진 내부 변수 같은 것들을 알고 싶어도 car.h 파일에서는 저희가 구현한 객체의 헤더가 담겨 있지 않기 때문에 결국 사용자는 그 내부 변수들을 확인할 방법이 없어집니다.

물론, 정보를 숨기기만 하는 것은 아니고 아주 큰 대규모 프로젝트에서 소프트웨어를 컴파일할 때 컴파일 성능을 올려주는 용도로도 사용한다고 합니다.

여기까지 c++의 pimpl에 대해 알아보았습니다.

 

 

추가로... 

추가로 공부하다보니 Pimple의 또다른 목적이 있었는데 바로 바이너리 호환성에 대한 문제 또한 한가지 원인 이었습니다.

예를 들어서 

class MyWorks {
public:
	int func();
private:
	int secret1(); // \
	int secret2(); //  | 자주 변경 될 수 있음.
	int secret3(); // /
}



로 되에있는 인터페이스가 있고.

이 인터페이스의 헤더파일을 바탕으로 만들어진 프로젝트가 10개 씩이나 있다면.

실제 사용하는 프로젝트에서 사용하는 func() 의 사용법은 변하지 않은 상황에서, 내부 구현의 최적하등의 과정으로 private 영역의 함수들이 서너개 더 추가가 되었다면

10개의 프로젝트들은 새로운 헤더 파일을 기반으로 새로 빌드를 해야  합니다.

하지만 pimple을 통해서 정말 필요한 인터페이스만 남기게 되었다면, 헤더 파일을 업데이트 하지 않고, DLL이나 so 파일만 업데이트 하면 충분히 업데이트가 되게 할 수 있습니다.

 

pimple이 사용되는 데에는 다양한 이유들이 있었네요.

반응형
Comments