C++과 같은 OOP(object-oriented programming )언어의 특징 중에 캡슐화라는 것이 있습니다. OOP 이전의 언어에서는 변수와 함수를 같은 몸체로 생각하는 것이 아니라 필요에 따라 만들어 가는 식이었습니다. 즉, 프로그램을 작성하면서 함수를 만들고 함수를 만들어 가면서 필요한 변수를 만들어서 사용하는 것이죠.

  물론 변수와 함수를 동시에 생각하거나 변수를 먼저 생각할 수도 있지만 변수와 함수가 하나의 몸체처럼 묶어 주거나 객체식으로 생각하지는 못했습니다.

광역? 지역? 오토?

  C 언어에서는 변수 선언을 선언하는 장소에 따라 광역, 지역, 오토변수로 나눌 수 있습니다. 같은 이름과 같은 기능의 변수라도 어떤 모습으로 선언되었는지에 따라서 프로그램의 품격이 많이 달라집니다. 그러나 프로그램을 작성하면서 광역변수로 사용할 지, 지역변수로 선언할지에 대한 명확한 기준은 없으며 대신에 많은 프로그램을 작성하면서 얻은 경험에 따르는 경우가 많습니다.

유능한 프로그래머

  제가 생각하는 유능한 프로그래머는 시스템 분석을 명확하게 하고 프로그램을 훌륭하게 작성하는 것도 덕목 중 하나이지만 프로그램 실행 중에 버그나 문제가 발생하면 (1) 얼마만큼 빠르게 문제의 원인을 파악하고 또한 (2) 얼마나 빠르게 문제를 처리할 수 있느냐 하는 능력이 가장 큰 덕목이라고 생각합니다.

  순전히 저 개인적인 생각이기 때문에 틀릴 수 있습니다만, 문제의 원인을 빠르게 파악하기 위해서는 제일 먼저 변수관리가 제대로 되어야 한다고 생각합니다. 광역이나 지역 변수의 편리함 때문에 모두 광역, 지역 변수만을 이용한다면 실행 중에 전혀 엉뚱한 문제가 발생할 수 있는 경우가  변수를 사용한 만큼 발생할 확률이 높아집니다.

  그렇다고 죄다 내부 변수만 사용한다면 프로그램이 매우 복잡해 지고, 복잡해진 코드는 나중에는 나도 몰라라하는 코드가 되고 맙니다.

임베디드 시스템 개발자 분들의 C 언어를 선호

  OOP에 대한 매력 중에 하나가 캡슐화입니다. 이전까지의 언어에서는 변수와 함수가 분리되어 구상하고 작성되어 왔습니다. 그러나 OOP에서는 하나하나의 문제를 객체로 보고 처리하는 단위를 변수와 함수를 묶어서 처리합니다. 이렇게 변수와 함수를 묶어 하나의 객체로 생각하고 처리하다 보니 변수관리가 훨씬 편리하고 그마만큼 안정된 프로그램을 작성할 수 있습니다.

  이외에도 OOP의 특징은 많죠. 상속이니, 은폐니하는 C 보다는 발전된 코딩 방법임에 틀림 없습니다. 그러나 임베디드 개발자분들을 뵈면 C++ 이 좋은 것을 알면서도 대부분 C를 이용하시더군요. 개발자는 개발자와 얘기 나누기를 좋아합니다. 당연히 이 문제에 대해 말씀을 구했는데, 서로 다른 면도 있지만 간단히 말씀 드려 배보다 배꼽이 더 커질 수 있다는 점이었습니다.

C++ 보다는 C가 편리

  윈도우 프로그램 처럼 매우 복잡한 GUI 함수를 이용해야 되거나 큰 프로그램이라면 C++을 사용하는 것이 편리하겠지만 임베디드 시스템에서는 C++ 보다는 C로 작성된 것이 프로그램을 관리하거나 나중에 코드를 보더라도 더 명확하다는 것이죠. 또한 공개된 소스나 라이브러리가 C 로 되어 있는 경우가 많고 서로 주고 받는 것도 C가 더 편리하다는 것입니다.

C에서 캡슐화를 구현

  저와 같이 처음 시작하는 분들은 선배들의 말을 항상 경청할 수 밖에 없습니다. 선배라고 C++의 좋은 점을 모르겠습니까? 모두 해보셨겠죠. 그러다가 점점 C를 선택한 이유가 있을 것이고 이 모든 것을 이해할 수 있도록 설명해 달라는 것은 무리이겠습니다. 해서 저도 C++은 나중에 경험해 보기로 하고, C 를 일단 사용해 보기로 했습니다.

  그러나 앞서 OOP의 캡슐화를 말씀드렸지만 C 로 프로그램을 작성하더라도 C++의 캡슐화 장점을 얻고 싶었습니다. 해서 저 나름대로 완전한 캡슐화는 아니지만 변수와 함수를 한 곳에 모아서 프로그램을 작성하는 코드 방법을 찾게 되었는데, 그것은 struct 를 이용하는 방법입니다. 아래의 예를 봐 주세요.

typedef struct rs232_t_ rs232_t;
struct rs232_t_
{
  int   poll_ndx;
  int   fd;
  int   type;                                     // tcp/udp/uds/serial/
  int   tag;
  void *private;
  void  (*on_poll_in )( rs232_t *);
  void  (*on_poll_out)( rs232_t *);
  void  (*on_poll_err)( rs232_t *);
  void  (*on_poll_hup)( rs232_t *);

  char  devname[20];                                                  // 장치명을 담을 버퍼
  int   baud;
  int   databit;
  int   stopbit;
  int   parity;

  void  (*on_read    )( rs232_t *);
  void  (*on_writable)( rs232_t *);
  void  (*on_error   )( rs232_t *, int);
};

  현재 제가 사용하고 있는 통신 라이브러리 중 rs232 통신을 위한 레코드입니다. 이렇게 변수와 함께 함수를 묶기 위해서 이전 시간에 소개해 드린 함수 포인터를 이용하고 있습니다. 이번에는 TCP/IP 통신을 위한 struct 입니다.

typedef struct tcp_t_ tcp_t;
struct tcp_t_
{
  int   poll_ndx;
  int   fd;
  int   type;                                     // tcp/udp/uds/serial/
  int   tag;
  void *private;
  void  (*on_poll_in )( tcp_t *);
  void  (*on_poll_out)( tcp_t *);
  void  (*on_poll_err)( tcp_t *);
  void  (*on_poll_hup)( tcp_t *);

  int                   port;                                         // 통신을 위한 포트 번호
  struct sockaddr_in    addr;                                         //
  char                  ip[IP_ADDR_SIZE];
  void  (*on_accept    )( tcp_t *, int *);
  void  (*on_connect   )( tcp_t *);
  void  (*on_disconnect)( tcp_t *);
  void  (*on_read      )( tcp_t *);
  void  (*on_writable  )( tcp_t *);
};

  많이 비슷하죠? 보시면 C++의 멤버함수 처럼 포인터 변수가 선언된 것을 보실 수 있으며, rs232나 TCP/IP 나 모두 공통된 변수명을 가지고 있고, 같은 이름의 함수, on_read(), on_writable 가 존재합니다.

  각각의 가상 함수는 작성하려는 프로젝트에 따라 함수를 구현하고 이 가상함수에 포인터만 지정해 주면, 다른 함수에서는 이 struct 를 이용해서 변수 뿐만 아니라 함수를 사용할 수 있게 됩니다.

  예로 아래와 같이 말이죠.

int main()
{
   rs232_t   rs232;
   tcp_t      tcp;

   rs232.read( rs232, ptr_buf, BUF_SIZE);
   tcp.read  ( tcp  , ptr_buf, BUF_SIZE);
}

  뿐만 아니라 다른 함수에서도 레코드를 전송해 주면 rs232와 tcp/ip 처럼 다른 레코드 임에도 같은 함수에서 처리할 수 있습니다. 이를 위해 공통 분모만 뽑아서 아래와 같이 struct 를 선언해 줍니다.

typedef struct net_obj_t_ net_obj_t;
struct net_obj_t_
{
  int   poll_ndx;
  int   fd;
  int   type;                                     // tcp/udp/uds/serial/
  int   tag;
  void *private;
  void  (*on_poll_in )( net_obj_t *);
  void  (*on_poll_out)( net_obj_t *);
  void  (*on_poll_err)( net_obj_t *);
  void  (*on_poll_hup)( net_obj_t *);
};

  다른 함수에서는 이 struct 를 이용하여 rs232 또는 tcp/ip, udp/ip, uds 레코드 자료를 인수로 받아 들일 수 있습니다. 그리고 아래와 같이 사용할 수 있게 됩니다.

int poll_loop( net_obj_t *obj)
{
     obj->read( obj, ptr_buf, BUF_SIZE);
}

void abc(void)
{
   rs232_t   rs232;
   tcp_t      tcp;

   poll_loop( rs232);
   tcp.read ( tcp  );
}

  이와 같이 하면 변수와 변수를 처리하는 함수가 같이 이동되고 사용되기 때문에 변수관리에 편리함도 있지만 비록 통신 방식이 다른 코드에 대해서도 같은 방법으로 처리할 수 있다는 점이 장점이 되겠습니다. 물론 이것은 저 개인적인 방법이며, 이 보다 더 훨씬 능률적이고 편리한 코드 방법이 있겠습니다만 앞으로 rs232, tcp/ip, udp/ip, uds 라이브러리를 이와 같은 방법으로 정리하여 올리도록 하겠습니다.