Application with Pluggable Database

때로는 Application 이 두 개의 DB 를 골라서 사용하고 싶을 때가 있다.
즉, Application 의 업무 로직은 똑같은데, 기존에 사용하던 DB 를 다른 DB 로 변경하고 싶은 경우 어떻게 해야 할까?

Application 의 모든 소스(업무 로직 포함)를 DB 가 변경되었다고 해서 완전히 별개의 2 벌로 유지해야한다는건 이만저만 큰 일이 아니다.
어느 한쪽의 로직에서 코드에 문제가 발생하여 수정이 발생한 경우, 이 코드 변경 사항은 다른 소스에도 반드시 반영이 되어야 한다.
실수로 반영을 누락했다면 DB 변경 시 반드시 같은 문제가 발생하게 되니까.
게다가 이러한 작업은 십중팔구 개발자가 Manual 로 수행해야 한다.
작업 중 실수의 여지도 배제할 수가 없다.

사실 이러한 문제때문에 ODBC 나 JDBC 같은 DB Interface 표준 Spec 이 생긴 것이다.
ODBC 나 JDBC Spec 을 사용하여 Application 을 개발하게 되면, DB 가 변경되더라도 사용자의 Application 코드는 거의 변경할 일이 없어진다.
단순히 변경된 DB 에서 제공하는 Library(Driver)를 교체하여 사용하면 된다.
즉, DB 에 연결을 하거나 DB 관련 작업을 하기 위해서 표준으로 정해진 Interface 에 따라 코딩을 하였다면, 이 코드는 DB 가 변경되더라도 그대로 유지한 채 DB Driver 만 교체하면 동일한 형태로 동작하게 된다.
(물론 DB Vendor 에서 제공하는 Driver 에 문제가 없다는 가정 하에)
이를 위해서는 당연히 교체하려는 신규 DB Vendor 에서 표준 Interface 에 맞게 개발된 ODBC/JDBC Driver 를 제공해야 한다.

Application 이 ODBC 나 JDBC 같은 표준 Spec 을 이용하여 개발된 상황이 아니라면 어떻게 해야할까?
오늘은 이에 대한 얘기를 하려고 한다.

Embedded SQL 의 경우 DB 작업 시 ODBC 나 JDBC 보다 훨씬 코딩이 쉽고 직관적인 관계로 현업에서 많이 사용한다.
단점이라면 표준이 아니라는 것이다.
Embedded SQL 의 사용법이나 Syntax 는 DB 마다 제각기 다를 수 있다.

이 경우 가장 중요한 것은 Application 의 layer 를 business layer 와 DB layer 로 분리하는 것이다.
DB 로직과 business 로직이 뒤섞여 있을 경우 DB 가 변경되면 전방위로 코드 변경작업을 해야할 뿐 아니라 검증을 위한 시험 과정도 상당히 어려워진다.
따라서, 초기 설계단계부터 개발까지 업무 로직 따로, DB 로직 따로 원칙을 철저하게 준수해야 한다.

  • business layer 와 DB layer 의 분리
  • business layer 에서 DB 작업을 수행하기 위한 Interface 통일
  • DB layer 의 경우 모듈화하여 각 DB 별로 dynamic library(so)로 빌드
    (DB layer 에서는 각 작업 별로 통일된 interface 에 맞게 함수 구현)

Altibase 라는 DB 와 Goldilocks 라는 DB 가 있다.
모두 유명한 국산 In-memory DBMS 이다.
이 두 개의 DB 를 손쉽게 교체 사용할 수 있도록 Application 을 구현해야하는 상황을 가정하자.

Application 구조 설계

Business 와 DB 로직의 분리

위의 Application 구조를 보면, 최상단에 business 로직이 존재하고 이곳에서는 DB 작업을 위해 Connect() 라는 함수와 select() 라는 함수를 사용한다.
Connect() 와 select()는 다수의 DB 를 자유롭게 사용할 수 있도록 사용자가 정한 interface 이다.
즉, 어떤 DB 를 사용하든지 DB 에 접속할 때는 Connect() 함수를, 조회를 하고자할 경우에는 select() 함수를 일관되게 사용하겠다는 것이다.
이 부분은 DB 가 변경된다고 코드가 변경되는 것은 아니다.
아주 간략하게 business layer 의 코드를 아래와 같이 구현해 보았다.
(코드는 아주 단순하고 허접하니 코드 자체보다는 방법론에 대한 이해에 집중하자.)

Business 로직

// main.cpp
#include <cstdio>
#include <cstdlib>
#include <string>

using namespace std;

int Connect( const string& aUsr,
             const string& aPwd,
             const string& aDsn );
int select();

int main(int argc, char **argv)
{
    int ret;

    ret = Connect("test", "test", "DSN=GOLDILOCKS");
    printf("connect : %d\n", ret);

    ret = select();
    printf("select : %d\n", ret);

    return 0;
}

위의 코드만으로는 binary 로 빌드할 수 없다.
Connect() 함수와 select() 함수에 대한 선언만 있지 정의가 없기 때문이다.
이 함수에 대한 정의는 Altibase 와 Goldilocks 에 대해 각각 구현한 후 so 파일로 만들어야 한다.
다음은 각각 Altibase 와 Goldilocks DB 를 위한 Connect()함수와 select() 함수의 구현이다.

DB 로직

// sel.sc (Altibase 용 Embedded SQL 코드)

#include <cstdio>
#include <cstring>
#include <cstdlib>
#include <string>

using namespace std;

int Connect( const string& aUsr,
             const string& aPwd,
             const string& aDsn )
{
    EXEC SQL BEGIN DECLARE SECTION;
        char usr[10];
        char pwd[10];
        char dsn[32];
    EXEC SQL END DECLARE SECTION;

    std::strcpy(usr, aUsr.c_str());
    std::strcpy(pwd, aPwd.c_str());
    std::strcpy(dsn, aDsn.c_str());

    EXEC SQL CONNECT :usr IDENTIFIED BY :pwd USING :dsn;
    if(sqlca.sqlcode) goto fail;
    printf("Succeeded to Connect Altibase\n");

    return 0;

fail:
    printf("Failed to Connect Altibase [ %s / %s using (%s) ] : [%s]\n",
            usr, pwd, dsn, sqlca.sqlerrm.sqlerrmc);
    return -1;
}

int select()
{
    EXEC SQL BEGIN DECLARE SECTION;
    int  sum;
    EXEC SQL END DECLARE SECTION;

    EXEC SQL SELECT SUM(AMOUNT) INTO :sum FROM T1;
    if(sqlca.sqlcode) goto fail;

    return 0;

fail:
    printf("Error : [%d] %s\n\n", SQLCODE, sqlca.sqlerrm.sqlerrmc);
    return -1;
}
// sel.gc (Goldilocks 용 Embedded SQL 코드)

#include <cstdio>
#include <cstring>
#include <cstdlib>
#include <string>
#include <goldilocks.h>

using namespace std;

EXEC SQL INCLUDE SQLCA;

int Connect( const string& aUsr,
             const string& aPwd,
             const string& aDsn )
{
    EXEC SQL BEGIN DECLARE SECTION;
        char usr[10];
        char pwd[10];
        char dsn[32];
    EXEC SQL END DECLARE SECTION;

    std::strcpy(usr, aUsr.c_str());
    std::strcpy(pwd, aPwd.c_str());
    std::strcpy(dsn, aDsn.c_str());

    EXEC SQL CONNECT :usr IDENTIFIED BY :pwd USING :dsn;
    if(sqlca.sqlcode) goto fail;
    printf("Succeeded to Connect Goldilocks\n");

    return 0;

fail:
    printf("Failed to Connect Goldilocks [ %s / %s using (%s) ] : [%s]\n",
            usr, pwd, dsn, sqlca.sqlerrm.sqlerrmc);

    return -1;
}

int select()
{
    EXEC SQL BEGIN DECLARE SECTION;
    int  sum;
    EXEC SQL END DECLARE SECTION;

    EXEC SQL SELECT SUM(AMOUNT) INTO :sum FROM T1;
    if(sqlca.sqlcode) goto fail;

    return 0;

fail:
    printf("Error : [%d] %s\n\n", SQLCODE, sqlca.sqlerrm.sqlerrmc);
    return -1;
}

DB Shard Library 빌드

위의 2 개 파일을 각각 libalti.so 와 libgold.so 라는 shared library 로 만든다.

// Altibase 용 shared library(libalti.so) 빌드

include $(ALTIBASE_HOME)/install/altibase_env.mk

alti_sel.o : sel.sc
  apre -t cpp sel.sc
  g++ -c -fPIC ${CFLAGS}  ${INCLUDES} -o sel.o sel.cpp
  gcc -shared sel.o -o libalti.so

clean : 
  rm -rf *.o sel.cpp *.o libalti.so
// Goldilocks 용 shared library(libgold.so) 빌드

INCLUDES+=-I${GOLDILOCKS_HOME}/include -I.
LIB_PATH=-L${GOLDILOCKS_HOME}/lib

CFLAGS=-g -W -Wall 

gold_sel.o : sel.sc
  gpec --include-path=. --output=sel.cpp  sel.gc
  g++ -c -fPIC ${CFLAGS}  ${INCLUDES} -o sel.o sel.cpp
  gcc -shared sel.o -o libgold.so

clean : 
  rm -rf *.o sel.cpp *.o libgold.so

위의 makefile 들을 이용하여 아래와 같이 libalti.so 와 libgold.so 파일을 만들었다.

% make -f makefile.alti 
apre -t cpp sel.sc
-----------------------------------------------------------------
     Altibase C/C++ Precompiler. 
     Release Version 7.1.0.2.7
     Copyright 2000, ALTIBASE Corporation or its subsidiaries.
     All Rights Reserved.
-----------------------------------------------------------------
g++ -c -fPIC -D_GNU_SOURCE -W -Wall -pipe -D_POSIX_PTHREAD_SEMANTICS -D_POSIX_THREADS -D_POSIX_THREAD_SAFE_FUNCTIONS -D_REENTRANT -DPDL_HAS_AIO_CALLS -m64 -mtune=k8 -O3 -funroll-loops -fno-strict-aliasing -fno-omit-frame-pointer -DPDL_NDEBUG -D_GNU_SOURCE -DACP_CFG_COMPILE_64BIT -DACP_CFG_COMPILE_BIT=64 -DACP_CFG_COMPILE_BIT_STR=64  -I/home/altibase/altibase-server-7.1.0/include -I. -o sel.o sel.cpp
gcc -shared sel.o -o libalti.so
[cluster00] altibase: ~/app
% make -f makefile.gold 
gpec --include-path=. --output=sel.cpp  sel.gc

 Copyright © 2010 SUNJESOFT Inc. All rights reserved.
 Release Trunk.3.2.0 revision(26903)

FileName: sel.gc
Pre-compile sel.gc -> sel.cpp
g++ -c -fPIC -g -W -Wall   -I/home/altibase/work/product/Gliese/home/include -I. -I/home/altibase/altibase-server-7.1.0/include -o sel.o sel.cpp
gcc -shared sel.o -o libgold.so
[cluster00] altibase: ~/app
% ls -al *.so
-rwxr-xr-x 1 altibase sunje  7922 2019-09-02 18:48 libalti.so
-rwxr-xr-x 1 altibase sunje 34611 2019-09-02 18:48 libgold.so

하위의 DB layer 의 shared library 들을 빌드했으니, 이제 business layer(main.cpp)를 빌드할 차례다.
(Connect() 함수와 select() 함수의 구현이 library 로 준비가 되었으니까.)
아래 makefile 은 main.cpp 파일을 빌드하기 위한 것이다.

Business 로직 빌드

CXX      = g++ 
SRCS     = main.cpp
OBJS     = $(SRCS:.cpp=.o)
TARGET   = sel 
LIBS     = -ldb -lodbc -lpre -lpthread -ldl -lm -lrt
LIB_DIRS = -L .
INC      = -I ./include
 
all : $(TARGET)
  $(CXX) -o $(TARGET) $(OBJS) $(INC) $(LIB_DIRS) $(LIBS)
 
$(TARGET) :
  $(CXX) -c $(SRCS) $(INC) $(LIB_DIRS) $(LIBS)
 
clean :
  rm -f $(TARGET)
  rm -f *.o

위의 makefile 은 main.cpp 파일을 이용하여 sel 이라는 실행가능한 binary 를 만든다.
그런데, LIBS 항목을 보면 여러 개의 library 를 link 하는 것을 볼 수 있다.
이 중에서 pthread, dl, m, rt 는 system 에서 제공하는 library 이니 생각할 필요 없고, -ldb -lodbc -lpre 는 왜 필요한지 생각해보자.

Link DB library

이 library 들은 대체 무엇인가?
DB 가 제공하는 Embedded SQL 을 사용하기 위해서는, DB 에서 제공하는 2 개의 library 를 기본적으로 link 해야 한다.
하나는 Embedded SQL 의 자체 기능을 위한 library(ESQL 을 위한 메모리 관리 등의 처리)이고, 나머지 하나는 Embedded SQL 내부에서 호출하게 되는 odbc library 이다.

Embedded SQL 은 내부적으로 odbc driver 를 이용한다.

우리가 Embedded SQL 로 코딩을 하면 precompiler 라는 유틸리티를 통해 해당 소스를 c/cpp 파일로 변환해야 하는 단계를 거친다.
가독성 좋은 ESQL 코드를 c/cpp 파일로 변환(parsing & conversion)하는 것이다.
변환된 c/cpp 코드는 내부적으로 odbc driver 에서 제공하는 API(SQLConnect, SQLExecute, SQLPrepare,…)를 이용하여 DB 에 작업을 하게 된다.

현재 우리는 business 로직을 빌드해야 하는데, libalti.so 나 libgold.so 내부의 Connect() 와 select() 함수를 사용해야 하는 상황이다.
또한, 각 library 의 Connect()와 select()함수는 내부적으로 각자의 DB 에서 제공하는 Embedded SQL library 와 odbc library 를 사용해야 한다.
이는 Altibase 와 Goldilocks 에서 제공하는 library 이름이 서로 다르다.

이를 그림으로 나타내 보았다.

공통 Interface 를 제공하는 DB library

우리는 business layer 를 한번 빌드하고 나면 하위의 DB layer 의 구현이 변경되어도 변함없이 사용하고 싶다.
이러한 경우에는 DB 별로 빌드해둔 so 파일만 살짝 교체하면 되도록 해야 한다.
즉, business layer 가 바라보는 DB layer 의 so 파일에 대한 symbolic link 만 살짝 바꾼 후 실행하면 아무 변경없이 해당 DB 로의 작업을 수행하는 것이다.

Goldilocks DB 쪽으로 작업을 수행하고 싶으면 아래와 같이 Goldilocks 용 so library 들에 대해 symbolic link 를 걸어두면 된다.

% cat goldso.sh 
rm libdb.so libodbc.so libpre.so

ln -s libgold.so libdb.so
ln -s ${GOLDILOCKS_HOME}/lib/libgoldilockss.so libodbc.so
ln -s ${GOLDILOCKS_HOME}/lib/libgoldilocksesqls.so libpre.so

반대로 Altibase DB 쪽으로 작업을 수행하고 싶으면 Altibase 용 so library 들에 대해 공통 이름(libdb.so, libpre.so, libodbc.so)으로 symbolic link 를 걸도록 한다.

% cat altiso.sh 
rm libdb.so libodbc.so libpre.so

ln -s libalti.so libdb.so
ln -s ${ALTIBASE_HOME}/lib/libodbccli_sl.so libodbc.so
ln -s ${ALTIBASE_HOME}/lib/libapre_sl.so libpre.so

main.cpp(business layer)에서 바라보는 library 는 libdb.so(libalti.so 또는 libgold.so 에 대한 symbolic link)인데, 해당 library 는 ESQL 또는 odbc 용 so 파일을 알아서 찾게 된다.
아래와 같이 실행 binary 의 library 의존성을 확인해보자.

% ldd sel
  linux-vdso.so.1 =>  (0x00007ffe815fc000)
  libdb.so (0x00007ff4b6609000)
  libgoldilocksesqls.so => /home/altibase/work/product/Gliese/home/lib/libgoldilocksesqls.so (0x00007ff4b63f4000)
  libgoldilockss.so => /home/altibase/work/product/Gliese/home/lib/libgoldilockss.so (0x00007ff4b547e000)
  libpthread.so.0 => /lib64/libpthread.so.0 (0x0000003e0bc00000)
  libdl.so.2 => /lib64/libdl.so.2 (0x0000003e0c000000)
  librt.so.1 => /lib64/librt.so.1 (0x0000003e0c400000)
  libstdc++.so.6 => /usr/lib64/libstdc++.so.6 (0x0000003e15800000)
  libm.so.6 => /lib64/libm.so.6 (0x0000003e0b800000)
  libgcc_s.so.1 => /lib64/libgcc_s.so.1 (0x0000003e15400000)
  libc.so.6 => /lib64/libc.so.6 (0x0000003e0b400000)
  /lib64/ld-linux-x86-64.so.2 (0x0000003e0b000000)

libdb.so -> libgoldilocksesqls.so -> libgoldilockss.so 의 의존성이 생기는 것을 알 수 있다.
이는 libdb.so 를 libgold.so 로 symbolic link 를 걸어둔 상태이기 때문에 자연스럽게 호출구조가 Goldilocks 의 library 들로 연결되는 것이다.

정리하면,

Altibase 를 사용하고 싶을 때는 libdb.so, libpre.so, libodbc.so 를 각각 아래의 Altibase 용 3 개의 so 파일에 각각 symbolic link 를 걸어주고 실행하면 되고,

  • libalti.so (사용자 구현)
  • libapre_sl.so (DB 제공)
  • libodbccli_sl.so (DB 제공)

Goldilocks 를 사용하고 싶을 때는 아래의 Goldilocks 용 3 개의 so 파일에 각각 symbolic link 를 걸어주고 실행하면 된다.

  • libgold.so (사용자 구현)
  • libgoldilocksesqls.so (DB 제공)
  • libgoldilockss.so (DB 제공)

이제는 business 로직이 구현된 binary 하나로 so 파일만 살짝 교체하면서 DB 를 손쉽게 변경할 수 있다.
사실 위와 같이 symbolic link 로 교체를 할 수도 있지만, Application 에서 직접 dlopen 같은 함수를 사용하여 필요한 so 를 실시간으로 로딩하게 할 수도 있다.
이렇게 처리하면 사용자가 설정 파일 등을 통해 어떤 DB 를 사용할지 선택해주기만 하면, Application 이 알아서 관련 라이브러리를 로딩하도록 할 수 있으니 더 좋은 설계라고 할 수 있겠다.
흔히 MySQL 등 많은 DBMS 나 다양한 솔루션들이 Pluggable 하게 다른 솔루션들을 붙여서 사용할 수가 있는데, 이 때도 내부적으로는 이와 같은 방식으로 처리하는 것이다.

참고로, 각 DB 별로 굳이 libalti.so 나 libgold.so 와 같이 so 파일을 별도로 만들어두는 이유는,
첫번째, 이미 말했듯이 교체가 용이하기 때문이다.
두번째, 각 DB 별로 완벽하게 분리하여 빌드할 수 있기 때문이다. 만약 static 으로 모두 묶어서 빌드를 하게 되면 각 DB 의 header file 에 정의된 요소들이 서로 충돌을 일으켜서 빌드 시나 실행 시에 문제를 일으킬 수 있다.

위에 사용한 코드는 여기에서 다운로드 받을 수 있다.

You may also like...