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 구조 설계
위의 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 로 코딩을 하면 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 이름이 서로 다르다.
이를 그림으로 나타내 보았다.
우리는 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 에 정의된 요소들이 서로 충돌을 일으켜서 빌드 시나 실행 시에 문제를 일으킬 수 있다.
위에 사용한 코드는 여기에서 다운로드 받을 수 있다.