소개
SimpleCriteria는 P of EAA의
Query Object 패턴의 미니멀한 구현입니다. 비즈니스 로직이 DAO 객체에 복잡한 검색 룰을 전달 할 때나 DAO에서 실행시에 복잡한 조건의 SQL문을 만들어야 할 때 사용 합니다. 오랫동안 제가 업무에서 사용하던 방식을 재설계해서 만들어 봤습니다.
Spring JDBC Template 과 함께 사용할 생각으로 만들었지만 직접 JDBC를 다루거나 다른 JDBC 유틸리티를 쓸 때에도 사용 할 수 있습니다. 비즈니스 로직에 Hibernate 디펜던시를 주고 싶지 않다면 Hibernate HQL을 만들 때에도 쓸 수 있지 않을지... ㅎㅎ
Query Object 패턴Query Object 패턴 설명은 먼저 마틴 파울러의 글을 번역하는 것으로 대신하겠습니다. 직접 들어가서 보시면 클래스 다이어그램도 있습니다.
SQL은 언어의 하나라고 할 수 있다. 그리고 많은 개발자들이 특히 이것에 익숙하지 않다. 더욱이 쿼리를 만들려면데이터베이스 스키마에 대해서도 알아야 한다. 특별한 검색용 메소드를 제작하고 페러미터를 갖는 메소드들 안에 SQL을 숨기면 이런 것들을 피할 수 있지만 이렇게 하면 보다 특별한 쿼리들은 만들기 힘들어진다. 또한, SQL 구문 안의 중복으로 이어져 데이터베이스 스키마를 변경해야 하게 된다.
Query Object는 ‘인터프리터’[Gof]인데 나중에 SQL 쿼리로 변환 될 수 있는 객체들의 구조이다. 테이블이나 컬럼이 아닌 클래스와 필드를 사용해서 이 쿼리를 만들 수 있다. 이 방법에서 쿼리를 만드는 사람은 데이터베이스 스키마와 스키마의 변경에 대해 독립적일 수 있다. 스키마는 언제 어떤 곳에서 로컬라이즈 될지 모르는 일이다.
배경- 비즈니스 로직에서 DAO에 복잡한 검색 조건을 전달 할 때에 SQL 구문을 만들어 전달하는 안티패턴이 자주 발생
- 매개변수를 통한 검색 조건 전달로는 복잡한 조건을 전달하기 힘들고 변경이 어렵다. 또한 변경이 반복되면서 매개변수가 난잡하게 추가되고 메소드 호출이 애매모호해진다.
- 마틴 파울러의 Query Object 패턴에 첨부된 클래스 다이어그램의 방식은 물리적인 테이블명이나 컬럼명을 추상화 해주기는 하지만 구조를 추상화 해주지는 않는다. 어플리케이션 로직과 스키마 구조는 서로 분리 되어야 한다.
- 기존 Query Object들은 별도의 설정이 필요하거나 복잡하다.
목표- 설정이 필요 없게 한다.
- 어플리케이션 로직에 근거한 검색 조건 표현과 SQL 및 DB 스키마는 분리되어야 한다.
- 사용하기 편해야 한다.
- 디펜던시를 최소화 한다.
맛보기
소스 코드 받기
요것을 다운 받아서 이클립스에서 여십시오.
simple_criteria.zip
SimpleCriteria를 사용하는 방법은 총 세 단계로 나눌 수 있습니다.
- QueryMapper 제작 : 조건 규칙과 쿼리문간의 맵핑을 수행하는 객체
- Criteria 구성 : DAO에 전달할 조건식 작성
- Query 얻기 : Criteria을 질의문으로 변환해서 DB 질의에 사용
단계 1 : QueryMapper 만들기
먼저 검색 조건과 쿼리를 맵핑할 맵퍼를 생성합니다. 맵퍼를 만드는 가장 원시적인 방법은 QueryMapper 인터페이스를 직접 임플리먼트하고 map() 메소드를 구현하는 것입니다. 나중에 설명할 SimpleQueryMapper와 MethodQueryMapper를 사용하면 더 단순하게 작업할 수 있습니다.
package my.simplecriteria;
import com.gen128.simplecriteria.*;
public class MyQueryMapper implements QueryMapper
{
public Query map(Condition condition)
{
Query query = new Query();
String rule = condition.getRule(); // 조건 규칙
Object[] params = condition.getParams(); // 쿼리에 전달될 매개변수
if("BBS_ID_IS".equals(rule)) // BBS_ID_IS 룰에 해당하는 쿼리 맵핑
query.append("bbs_id = ?", params[0]);
else
throw new UnknownRuleException("Invalid condition rule ("+rule+")");
return query;
}
}
코드가 길어보이지만 정작 주의해서 볼 코드는 굵은 글씨로 표시한 두줄입니다.
맵퍼는 조건이 담긴 객체 Condition를 받아 그 안에 있는 조건 규칙과 매개변수로 질의문(Query 객체)을 만들어서 반환해야 합니다. Query 객체는 SQL에 사용될 조건식 문자열과 이에 사용될 매개변수로 구성되어 있습니다.
위 코드를 보면 "BBS_ID_IS"라는 조건 규칙을 SQL 조건식 "bbs_id = ?"으로 맵핑하고 있습니다.
맵핑하지 못하는 조건 규칙일 경우는 UnknownRuleException 예외를 던지면 됩니다. RuntimeException이라서 catch 하지 않아도 됩니다.
단계 2 : 조건식(Criteria) 만들기
이 맵퍼를 사용해서 검색을 요청하는 비즈니스 로직은 다음과 같습니다.
Criteria criteria = new Criteria(new MyQueryMapper());
criteria.and("BBS_ID_IS", new Long(1234));
Collection data = bbsDao.getData(criteria);
비즈니스 로직에서는 앞서 만든 MyQueryMapper를 사용해서 Criteria를 만듭니다. Criteria는 조건식을 담는 객체입니다. 이 Criteria는 여러 조건항들과 조건절들을 담을 수 있습니다.
단계 3 : 조건식에서 질의문(Query) 얻기
DAO 쪽에서는 다음과 같이 이 검색 조건을 받아서 처리하게 됩니다. Spring JdbcTemplate을 사용해서 DB 질의를 실행합니다.
public Collection getData(Criteria criteria)
{
String sql = new StringBuilder().append("SELECT * FROM bbs WHERE ")
.append(criteria.getQuery().getQueryString()).toString();
return this.jdbcTemplate.query(sql, criteria.getQuery().getPrams(),
new ColumnMapRowMapper());
}
CriteriaCriteria는 검색 조건을 구조적으로 표현하는 수단으로 다음과 같이 구성되어 있습니다.
Criteria는 조건 절을 표현합니다. 여러 Criterion과 하위 Criteria를 갖고 있을 수 있고 각 Criterion과 Criteria 간의 논리적 관계를 AND 또는 OR의 연산자로 맺도록 되어 있습니다.
Criterion은 조건 항 하나를 표현합니다. Condition 인터페이스를 구현하여 특정 조건을 표현 할 수 있습니다.
Condition은 특정 검색 조건 하나를 표현하는 나타내는 인터페이스 입니다. 조건 규칙 이름을 반환하는 getRule()과 매개변수값을 반환하는 getParams() 메소드를 가지고 있습니다. 조건 규칙은 로직상 의미 있는 조건의 이름인 문자열이고 이 조건 규칙이 구체적인 매개변수값과 함께 결합되어 비로서 의미 있는 조건이 됩니다. 굳이 비유를 하자면 개념상 조건 규칙은 객체지향에서의 클래스를 조건은 인스턴스가 같다고 할 수 있습니다.
Criteria 생성위의 샘플에서처럼 생성자 매개변수로 쿼리 맵퍼를 사용해서 Criteria를 생성 합니다.
Criteria criteria = new Criteria(SomeQuerymapper);조건 항 추가쿼리 맵퍼에 등록되어 있는 조건 규칙 중 하나를 사용해서 조건항을 추가 할 수 있습니다.
criteria.and("USER_ID_IS", userId);criteria에 조건을 두 개 이상 넣게 되면 다항식이 됩니다.
criteria.and("USER_ID_IS", userId);criteria.and("BALANCE_OVER", 10000);두 조건항을 더하는 메소드는 and()를 포함해서 4가지가 있습니다.
public void and(String rule, Object... params) : A 와 B 관계로 새 조건을 추가
public void or(String rule, Object... params) : A 또는 B 관계로 새 조건을 추가
public void andNot(String rule, Object... params) : A 와 B가 아닌 관계로 새 조건을 추가
public void orNot(String rule, Object... params) : A 또는 B가 아닌 관계로 새 조건을 추가
복합 조건다음과 같은 복잡한 논리식을 쉽게 표현하도록 하는 것이 Simple Criteria를 만든 목적입니다.
(A OR B) AND (A OR C) 위와 같이 괄호 안의 조건식 두 개의 복합식을 SimpleCriteria로 표현하려면 sub criteria를 사용해야 합니다.
Criteria criteria = new Criteria(MyQueryMapper);// 첫번째 괄호안 조건식. A OR BCriteria subCriteria1 = criteria.subCriteria(); subCriteria1.and("A");subCriteria1.or("B");// 두번째 괄호안 조건식. A OR CCriteria subCriteria2 = criteria.subCriteria(); subCriteria2.and("A");subCriteria2.or("C");// 두 조건식을 AND 연산자로 결합. (A OR B) AND (A OR C)criteria.and(subCriteria1);criteria.and(subCriteria2);위 코드의 마지막 코드 두 줄을 보면 and() 메소드의 매개변수가 조건이 아니라 Criteria 입니다. 이와 같이 조건 대신 criteria나 criterion 조건을 직접 매개변수로 받을 수 있는 메소드는 다음과 같습니다. 각 역할은 위에 설명한 메소드들과 동일합니다.
public void and(QueryMappable mappable) public void or(QueryMappable mappable)public void andNot(QueryMappable mappable)public void orNot(QueryMappable mappable)QueryMapperQueryMapper는 위에 예제에서 본 것 처럼 criteria로 구성된 조건을 SQL 조건문으로 변환하는 mapper가 구현할 인터페이스입니다. 예제에서처럼 직접 인터페이스를 사용해서 구현해도 되지만 편의를 위해 두 가지 기본 구현 객체를 가지고 있습니다.
SimpleQueryMapperSimpleQueryMapper는 조건 규칙과 SQL 질의문 사이의 맵핑이 단순할 경우에 사용 할 수 있습니다. 위의 MyQueryMapper를 SimpleQueryMapper를 사용해서 표현하면 다음과 같습니다.
QueryMapper MyQueryMapper= new SimpleQueryMapper("BBS_ID_IS", "bbs_id = ?", Integer.class);위에서 봤던 코드 보다 훨씬 간결하게 되었습니다. 맵핑 정보를 추가하려면 필요한 만큼 append() 메소드를 사용하시면 됩니다.
new SimpleQueryMapper()
.append("BBS_ID_IS", "bbs_id = ?", Integer.class)
.append("NAME_HAS", "name like ?", String.class)
.append("DATE_BETWEEN", "date >= ? AND date <= ? ", Date.class, Date.class);append() 메소드의 프로토타입은 다음과 같습니다.
public SimpleQueryMapper append(String rule, String query, Class<?>... types)첫번째 매개변수는 조건 규칙명, 두번째 매개변수는 이에 대응하는 SQL 질의문, 세번째 매개변수부터는 질의문에 사용될 매개변수값의 타입들입니다.
SimpleQueryMapper는 간단하지만 조건 규칙과 질의문이 단순 맵핑 될 때에만 사용 가능 합니다. 질의문을 맵핑하는 과정에서 로직이 필요하거나 매개변수값을 그대로 질의문 매개변수로 전달하지 않고 조작을 해야 할 경우 등에는 사용 할 수 없습니다.
MethodQueryMapperMethodQueryMapper는 좀 더 복잡한 처리를 할 수 있는 QueryMapper 구현체입니다. 이 QueryMapper는 맵핑을 메소드를 사용해서 처리 합니다. 조건명으로 메소드를 찾아서 그 메소드를 실행해줍니다.
일단 MyQueryMapper를 MethodQueryMapper로 구현해보겠습니다.
public class MyQueryMapper extends MethodQueryMapper{ public void mapBbsIdIs(Query query, int bbsId) { query.append("bbs_id = ?", bbsId); // 조건 추가 }}코드를 보면 BBS_ID_IS 조건 규칙의 맵핑을 메소드 mapBbsIdIs()가 처리하기 하고 있습니다.이와 같이 이름을 접두사 + 조건 규칙명으로 만들고 첫번째 매개변수로 Query를 받도록 메소드를 만들면 자동으로 그 메소드와 조건을 맵핑시켜줍니다.
조건 규칙명으로 메소드를 찾는 순서는 다음과 같습니다.
- 접두사 + 조건 규칙명의 메소드를 찾는다.
- 접두사 + 첫자를 대문자로 만든 조건 규칙명으로 찾는다.
- 접두사 + 조건 규칙명을 '_'와 화이트 스페이스로 토큰화 해 첫단어를 대문자로 만든 글자로 찾는다.
즉 MethodQueryMapper를 쓴다면 BbsIdIs, bbsIdIs, BBS_ID_IS(대소문자 구별 안함) 등 세가지 서로 다른 조건 규칙명이 다 getBbsIdIs() 메소드에 대응하게 됩니다.
맵핑을 처리하는 메소드명의 접두사를 기본 "map" 외의 다른 것으로 변경하고 싶다면 생성자에 매개변수로 전달 하거나 setMethodPrefix() 메소드를 사용하시면 됩니다.
new MyQueryMapper("convert");mapper.setMethodPrefix("convert");CompositeQueryMapperSimpleQueryMapper와 MethodQueryMapper를 보시면 아시겠지만 하나는 편한 반면 너무 단순하고 또 하나는 여러가지 복잡한 조작을 할 수 있지만 살짝 귀찮습니다. 단순한 맵핑은 SimpleQueryMapper로 처리하고 한두가지 복잡한 것만 따로 MethodQueryMapper로 맵핑하면 더 효율적일 것입니다. CompositeQueryMapper는 이렇게 여러 QueryMapper를 한 맵핑에 사용하고 싶을 때 쓰기 위해 만들었습니다.
CompositeQueryMapper 자체는 아무 맵핑 능력이 없습니다. 대신 CompositeQueryMapper는 다른 QueryMapper를 하나 이상 가지고 있으면서 이 QueryMapper들에게 맴핑을 위임합니다. CompositeQueryMapper는 등록된 QueryMapper들을 등록된 순으로 맵핑 시도합니다. 하나씩 시도하면서 UnkownRuleException이 발생하면 다음 QueryMapper로 넘어가고 성공하면 결과를 반환하면서 작업을 끝냅니다.
사용할 QueryMapper를 CompositeQueryMapper에 등록하려면 add() 메소드를 사용하십시오.
CompositeQueryMapper mapper = new CompositeQueryMapper();mapper.add(new MySimpleQueryMapper());mapper.add(new MyMethodQueryMapper());mapper.add(new MyCustomQueryMapper());다음 처럼 생성자를 사용해서 여려 QueryMapper를 등록할 수도 있습니다.
new CompositeQueryMapper(new MySimpleQueryMapper(), new MyMethodQueryMapper(), new MyCustomQueryMapper());조건 규칙명 명명법저건 규칙명을 명명하는 방법은 자유이지만 제가 하는 몇가지 관례가 있습니다.
XXX_IS : ~와 같다 조건을 표현. "XXX = ?"
XXX_OVER : ~보다 크다 조건을 표현."XXX > ?"
XXX_UNDER : ~보다 작다 조건을 표현. "XXX < ? "
XXX_AFTER : 날짜 비교에서 ~ 이후 조건을 표현. "XXX > ?"
XXX_BEFORE: 날짜 비교에서 ~ 이전 조건을 표현. "XXX < ?"
XXX_BETWEEN : ~와 ~ 사이 조건을 표현. "XXX BETWEEN ? AND ?"
XXX_~NOT~ : 반대 조건을 표현. ex) XXX_IS_NOT, XXX_NOT_AFTER
XXX_HAS : 패턴 매칭. "XXX like ?"
IS_XXX 또는 형용사 : 비즈니스 룰의 의미를 갖는 조건. ex) IS_NOTICE, REMOVABLE
조건 규칙 만들기와 관련된 조언ORM... 그러니까 도메인 모델을 쓰지 않고 JDBC를 사용해서 어플리케이션을 개발 할 때 가장 문제되는 것은 비지니스 룰이 Java 코드와 SQL에 분산된다는 것입니다. 분산 될 뿐 아니라 Java 코드에 있는 로직은 한곳으로 모는 것이 쉽지만 SQL로 표현된 로직은 여기저기 흩어지기 마련입니다. 스토어드 프로시듀어를 쓰면 어느 정도 해결이 되기는 해도 완전히 해결되지는 않죠.
이 SimpleCriteria를 쓸 때에도 가능하면 SQL의 where 절에 사용하는 구조를 그대로 조건 규칙에 반영하지 않으시는게 좋습니다. 조건 규칙은 가능한 비즈니스 룰이 되게 하는게 하자는거죠.
예를 들어 "삭제 가능" 이라는 비즈니스 룰이 "작성자가 같고 답글이 없는"이라고 정의된다면 아마도 쿼리가 이렇게 되겠죠.
writer = ? AND num_reply <= 0이것을 비즈니스 로직에서 Criteria를 만들 때에 다음 같이 1:1로 표현하는 것 보다는
criteria.and("WRITER_IS", user_id);criteria.andNot("NUM_REPLY_OVER", 0);Mapper에 해당 비즈니스 룰을 정의하고 아래 처럼 표현하는 것이 좋다는 것입니다.
criteria.and("REMOVABLE", user_id);나중에 "삭제 가능"이라는 비즈니스 룰의 조건이 바뀌어도 변경하기 좋기도 하구요.
----------------
역시 설명하는 것은 코딩보다 힘드네요. 헥헥
부족하지만 테시트 코드를 봐주시구요. 소스가 간단하니 그냥 소스를 보시면 금방 파악하실겁니다.
혹시 이해 안되시거나 좋은 아이디어 있으신 분은 리플 달아주세요. 버그 발견하신 분도... *-_-*
2008/1/3 갱신 - MethodQueryMapper가 상수 네이밍 방식의 조건 규칙명을 메소스 네이밍 방식으로 변환 할 수 있게 수정 - 테스트 코드 정리 - 명명법 설명 추가
2008/1/4 갱신
- 테스트 코드에 TestRuleSet을 추가해서 비즈니스 로직의 TestQueryMapper 종속성 제거
- CompositeQueryMapper 추가
- UnknownRuleException 오타 수정
- '조건 규칙 만들기와 관련된 조언' 추가
2008/1/7 갱신
- 예제에서 parameter를 사용하지 않는 오류 수정
- ColumnMapRowMapper를 사용해서 예제를 단순화