Fox Query Overview¶
Fox Query 는 MyBatis 와 유사한 쿼리 매퍼(혹은 SQL 매퍼) 기능을 제공합니다. 즉, SQL 문장과 DB 매개변수 정보를 외부 XML 파일에 기록하여 관리하며 쿼리 수행 결과를 객체 인스턴스에 매핑하는 기능을 제공합니다. 하지만 MyBatis 는 쿼리 수행 결과를 객체에 매핑하는 것에 중점을 두는 반면 Fox Query 는 수행 결과를 객체 뿐만 아니라 DataSet
과 같은 일반적인 데이터 컨테이너에 기록하는 것에도 포커스를 둔다는 점입니다.
Information
2000 년대 초반에 .Net framework 가 처음 등장했을 때부터 사용된 DataSet
을 2020 년대 중반인 현 시점까지 지원하는 이유에 대해서는 논란이 있을 수 있습니다. 엔티티는 DataSet
에 비해 가독성이 높고 컴파일 타임에 오류를 찾기도 쉬우며 성능면에서도 더 우수합니다. 하지만 NeoDEEX 가 주로 사용되는 대한민국의 소위 SI 개발 상황에서는 엔티티를 사용하기란 쉽지 않습니다. NeoDEEX 는 높은 개발 생산성 하에서 SI 프로젝트를 성공적으로 완수하기 위해 사용되는 어플리케이션 프레임워크이기 때문에 여전히 NeoDEEX 는 DataSet
을 지원하고 있습니다.
Fox Query 는 Entity Framework (이하 EF)와 같은 ORM 이 아닙니다. ORM 은 SQL 문장을 직접 작성하지 않고 객체를 다루는 코드들 작성하면 SQL 문장이 자동으로 생성되는 반면 Fox Query 는 SQL 문장을 개발자가 직접 작성해야 합니다. SQL 문장을 개발자가 작성해야 하는 MyBatis 와 유사하며 Fox Query 를 쿼리 매퍼로 소개하는 이유 입니다.
이 문서와 관련된 예제 코드는 다음 예제를 참조 하십시요.
Basic concept¶
Fox Query 는 외부 XML 파일에 SQL 문장과 DB 매개변수 정보를 기록해 두고 필요할 때 이 정보들을 읽습니다. Fox Query 가 사용하는 외부 XML 파일을 Foxml 파일이라 부르며 확장자는 .foxml
입니다. .foxml
파일은 복수 개가 존재할 수 있으며 각 .foxml
파일에는 여러 개의 SQL 문장과 각 SQL 문장에 대한 DB 매개변수 정보가 포함되어 있습니다. 다음은 Foxml 파일의 예로서 northwind.foxml
파일을 보여줍니다.
각 SQL 문장들은 <statement>
태그 내에 포함되며 SQL 문장은 <text>
태그에 기록하고 DB 매개변수 정보는 <parameters>
태그 내에 <parameter>
태그로 기록합니다. 각 SQL 문장은 고유하게 구별할 수 있도록 <statement>
태그의 id
속성에 고유 값을 사용하며 Fox Query 를 수행할 때 .foxml
파일 이름과 id
값을 조합하여 수행하고자 하는 SQL 문장을 불러오게 됩니다.
위 예제에서 ExecuteQueryList
메서드가 내부적으로 수행되는 작업은 다음과 유사합니다.
먼저 .foxml
파일로부터 FoxQuery 정보를 읽어 옵니다(GetQuery
메서드). 읽어온 FoxQuery 정보에 포함된 SQL 문장과 DB 매개변수 정보를 사용하여 Command
객체를 생성합니다(CreateCommand
메서드). 이제 생성된 Command
객체를 사용하여 쿼리를 수행하고 그 결과를 반환합니다.
FoxQueryMapper¶
.foxml
파일들을 검색하고 XML 을 파싱하여 SQL 문장과 DB 매개변수 정보를 추출해 내는 작업은 FoxQueryMapper
에 의해 수행됩니다.
FoxDbAccess
클래스는 ExecuteQuery-
메서드들이 호출되면 FoxQueryMapper
를 호출하여 FoxQuery 정보를 요청하고 FoxQueryMapper
는 .foxml
파일 목록에서 요청된 .foxml
파일을 찾아 XML 을 파싱하여 Fox Query 캐시에 FoxQuery 들을 추가하며 요청된 FoxQuery 정보를 반환합니다. 이제 FoxDbAccess
는 FoxQueryMapper
가 반환한 FoxQuery 정보(SQL 문장 및 DB 매개변수 정보)를 사용하여 Command
객체를 생성하고 생성한 Command
객체를 수행하고 결과를 반환합니다.
개발자는 직접 FoxQueryMapper
에 접근할 수 없지만 neodeex.config.json
구성 설정을 통해 .foxml
파일들을 검색할 위치(디렉터리) 등의 설정을 수행할 수 있습니다.
다음 구성 설정 예제는 queryMappers
섹션에 FoxQueryMapper
설정을 추가하여 .foxml
파일을 검색할 위치로 ./oracle/foxml
디렉터리 및 그 하위 디렉터리를 지정하고 있습니다. 그리고 이 FoxQueryMapper
설정에 MyOracleMapper
라는 이름을 사용합니다. 이 이름은 데이터베이스 연결 문자열의 queryMapper
속성에 사용하여 이 연결 문자열을 사용하는 FoxDbAccess
객체(이 경우 FoxOracleDbAccess
객체)는 Fox Query 를 수행할 때 MyOracleMapper
라는 이름의 FoxQueryMapper
설정을 사용하도록 지시합니다.
연결 문자열에 FoxQueryMapper
설정이 존재하지 않는 경우, 즉 연결 문자열 설정에 queryMapper
속성이 없는 경우에는 디폴트 FoxQueryMapper
설정이 사용됩니다. 디폴트 설정은 ./foxml
디렉터리(하위 디렉터리 제외)에서 .foxml
파일을 검색합니다.
Database-Independent Foxml¶
Foxml 에 쿼리를 기록하고 관리하는 장점으로 쿼리가 변경되더라도 코드를 수정할 필요가 없다는 점입니다(적어도 최소한의 수정만으로도 가능합니다). 이 외에도 Foxml 을 사용하면 데이터베이스에 독립적인 쿼리 관리가 가능합니다.
다음은 PostgreSQL 의 함수(저장 프로시저)를 호출하는 Foxml 의 예제 입니다. 이 예제에서 호출하는 get_categories
함수는 매개변수가 없으며 결과값으로 ref cursor 를 반환합니다.
위 Fox Query 를 호출하는 코드는 다음과 같습니다.
만약 이 멋지고 거대한 앱이 PostgreSQL 과 Oracle 을 모두 지원해야 한다면 어떻게 해야 할까요? 다양한 답이 있을 수 있겠지만 Fox Query 를 사용하는 경우 Oracle 을 위한 .foxml
파일을 별도로 작성하면 됩니다.
Oracle 의 경우 일반적으로 저장 프로시저를 많이 사용하며 저장 프로시저는 OUT
매개변수를 사용하여 ref cursor 를 반환하기 때문에 Output
매개변수를 사용해야 합니다. 따라서 Oracle 을 위한 Foxml 을 다음과 같이 작성하면 됩니다.
데이터베이스가 PostgreSQL 에서 Oracle 로 변경된다 할지라도 저장 프로시저를 호출하는 코드 변경이나 다시 빌드할 필요없이 구성 설정의 연결 문자열 변경과 .foxml
파일을 교체하기만 하면 됩니다.
좀 더 멋진 부분은 구성 설정에서 다양한 데이터베이스 지원에 대한 대비를 모두 할 수 있다는 점입니다. 다음 구성 설정은 PostgreSQL 과 Oracle 을 위해 2개의 연결 문자열을 정의합니다. 그리고 각 연결 문자열은 데이터베이스 액세스에 사용할 FoxQueryMapper
설정을 지정하고 있습니다. FoxQueryMapper
설정은 두 데이터베이스에 공통적으로 사용할 수 있는 쿼리들을 담는 .foxml
디렉터리와 호환되지 않는 쿼리들을 담는 디렉터리를 설정합니다.
이제 우리의 멋진 앱은 데이터베이스에 맞추어 구성 설정의 defaultConnectionString
속성값으로 적절한 연결 문자열만 지정하면 됩니다. defaultConnectionString
속성의 값을 PostgreSQL
로 지정하면 컴파일된 우리의 코드는 PostgreSQL 데이터베이스에 접속하여 PostgreSQL 에 적합한 쿼리들을 수행할 것입니다. 만약 데이터베이스로 Oracle 을 사용한다면 defaultConnectionString
속성 값을 Oracle
로 바꾸기만 하면 됩니다. 코드 수정이나 컴파일, 재배포 없이도 우리의 앱은 Oracle 에 접속하고 그에 맞는 쿼리를 수행할 것입니다.
Reloading Foxml¶
FoxQueryMapper
는 설정된 Foxml 파일 디렉터리에 대해 .foxml
파일들에 대한 변경 사항을 모니터 하고 변경을 탐지하면 .foxml
파일을 다시 로드 합니다. 즉, 설정된 Foxml 디렉터리에서 .foxml
파일이 추가/수정/삭제되면 Fox Query 들을 다시 로드 한다는 것입니다. Foxml 재로드 기능은 운영 중인 서비스를 중지하지 않고 Fox Query 를 추가/수정/삭제해야 하거나 개발 환경에서 .foxml
파일을 개발하고 테스트 할 때 유용합니다. Foxml 재로드 기능은 기본적으로 활성화 되어 있습니다.
한편, Foxml 파일 재로드 기능이 의미가 없는 상황도 존재합니다. Azure 와 같은 클라우드 환경은 Foxml 파일 변경은 필연적으로 배포 과정을 거치게 되며 배포 과정에서 서비스는 중지되고 다시 배포가 됩니다. Docker 컨테이너 환경에서도 비슷하게 적용될 수 있습니다. 이 경우 .foxml
파일 변경 모니터링은 FileSysemWatcher
리소스를 낭비하게 됩니다.
디폴트 FoxQueryMapper
구성과 명시적인 구성 설정에서 디렉터리/파일 이름 앞에 !
문자로 시작하는 경우 디렉터리/파일 모니터링을 활성화하지 않습니다. 예를 들어, 다음 구성 설정에서 PostgreSQLMapper
설정은 .foxml
파일들에 대한 변경 사항을 모니터링하고 재로드를 수행하지만 OracleMapper
설정은 모니터링을 수행하지 않습니다.
내부 작동 방식
새로운 .foxml
파일이 생성되거나 기존 .foxml
파일이 제거 혹은 변경되면 FoxQueryMapper
는 .foxml
파일 테이블을 업데이트하고 이미 읽어 놓은 캐시를 무효화 합니다. 그 결과로 이 이후에 호출되는 ExecuteQuery-
메서드는 .foxml
파일을 다시 읽어 FoxQuery
캐시를 채우게 됩니다.
주의 사항
.foxml
파일이 변경 되더라도 재로드 작업은 즉시 발생하지 않습니다. 여러 .foxml
파일을 복사하여 업데이트 하는 상황에서 FileSystemWatcher
가 여러 이벤트를 발생하고 이로 인해 파일 테이블 변경이 여러 차례 발생할 수 있기 때문입니다. FoxQueryMapper
는 .foxml
파일 변경 이벤트 발생 후 300msec 내에 다른 파일 변경이 없는 경우에 파일 테이블 및 Fox Query 캐시 무요화를 수행합니다. 이러한 작동 방식은 짧은 시간동안 여러 차례 발생하는 FileSystemWatcher
변경 이벤트에 대해서도 한 번의 재재로드만을 수행하도록 해 줍니다.
Dynamic Query¶
동적 쿼리(Dynamic Query)는 여러 조건에 의해 SQL 문장을 동적으로 구성하는 것을 말합니다. 동적 쿼리를 통해 개발자는 다양한 조건을 갖는 복잡한 쿼리를 효율적으로 작성할 수 있으며 쿼리의 재사용성을 크게 높일 수 있는 등 쿼리의 유연성과 효율성을 높일 수 있습니다. 한편 동적 쿼리는 SQL 인젝션(injection)과 같은 보안 취약점을 가질 수 있습니다.
MyBatis 의 동적 쿼리 기능은 SQL 문장 내에 <If>
태그와 같은 조건절을 삽입하여 동적으로 쿼리를 구성합니다. 한편 Fox Query 는 매크로 기반의 동적 쿼리를 지원합니다. Fox Query 의 매크로 기능은 SQL 문장 안에 매크로 함수 호출문이 존재하고 매크로 함수는 C# 스크립트 코드로 쿼리 문을 반환하는 방식으로 수행됩니다.
다음 Foxml 은 매크로를 사용한 동적 쿼리의 예를 보여 줍니다. 이 예제에서 $$SEARCH_CONDITION()$$
문자열은 SEARCH_CONDITION
매크로를 호출하는 수식입니다. <macro>
태그를 사용하여 SEARCH_CONDITION
매크로에 대한 매크로 스크립트를 정의할 수 있습니다.
매크로 스크립트는 C# 스크립트로 표현되며 env
객체는 Fox Query 에 대한 정보를 제공합니다. env.Args
속성은 코드에서 전달된 매개변수 인자(argument)를 나타냅니다. 위 예제에서는 product_name
이라는 이름을 가진 인자가 null
이 아닌 경우에 AND product_name LIKE #product_name#
문자열을 반환하여 WHERE
조건을 추가합니다.
매크로 스트립트에서 env
객체는 매개변수 인자 뿐만 아니라 <parameter>
태그에 의해 정의된 DB 매개변수 정보에 접근할 수도 있으며 디버깅을 위한 로깅 기능도 포함합니다. 또한 매크로 스크립트는 C# 스크립트이므로 for
, foreach
, while
과 같은 반복문 뿐만 아니라 List<T>
, Dictionary<K,V>
타입들도 사용할 수 있어서 매우 강력한 매크로를 작성할 수 있습니다.
이처럼 Fox Query의 동적 쿼리는 SQL 문장 작성에 있어서 유연성과 효율성을 증대 시키지만 가독성을 떨어뜨리고 보안 위험을 가진 양날의 검입니다. 동적 쿼리를 사용하기 위해서는 세심한 주의가 필요하며 무분별하게 동적 쿼리를 사용하지 않도록 주의해야 합니다.
보안 코딩 주의 사항
동적 쿼리를 사용할 때에는 항상 SQL Injection 공격에 주의해야 합니다. 위 예제에서 LIKE
연산자의 뒤에 다음과 같이 문자열 연산으로 매개변수 값을 추가하면 SQL Injection 공격을 받을 수 있습니다.
사용자로부터 전달받은 값은 반드시 매개변수화(parameterized)하여 SQL Injection 에 대비해야 합니다.
Summary¶
Fox Query 는 MyBatis와 비슷한 SQL 매퍼 기능을 제공하여 SQL 문장과 DB 매개변수를 외부 XML 파일(Foxml 파일)에 기록하여 관리하고 쿼리 결과를 객체나 데이터 컨테이너에 매핑해 줍니다. FoxQueryMapper
는 구성 설정 혹은 디폴트 설정을 통해 주어진 디렉터리에서 .foxml
파일들을 검색하고 파싱하여 쿼리들을 효율적으로 관리합니다. 또한 FoxQueryMapper
는 .foxml
파일들의 변경 사항을 감시하며 변경이 발생했을 때 다시 로드하는 기능을 사용할 수도 있습니다.
Fox Query 는 SQL 문장과 DB 매개변수 들을 외부 .foxml
파일에 기록해 둠으로써 데이터베이스에 독립적인 코드를 작성할 수 있으며 동적 쿼리와 같은 기능을 활용하여 동적으로 SQL 문장, DB 매개변수들을 유연하고 효율적으로 관리할 수도 있습니다.
Fox Query는 높은 개발 생산성을 제공하지만, 동적 쿼리 사용 시 보안 취약점에 대한 주의가 필요하며, 이러한 장점과 한계를 균형 있게 고려하여 활용해야 합니다.