잊지 않겠습니다.

gradle이 major update를 할 때마다 querydsl Q-class들의 생성방법이 계속해서 바뀌고 있습니다. 이제는 좀 더 자연스러운 방법으로 처리가 되네요. 더 이상 추가 task를 만들 필요없이 지원이 가능합니다.

변경된 build.gradle입니다. lombok querydsl을 모두 적용한 상태입니다.

dependencies {
    annotationProcessor(
            "com.querydsl:querydsl-apt:${rootProject.ext.querydslVersion}:jpa",
            "org.hibernate.javax.persistence:hibernate-jpa-2.1-api:1.0.2.Final",
            "javax.annotation:javax.annotation-api:1.3.2",
            "org.projectlombok:lombok"
    )
}

sourceSets {
    main.java.srcDirs += [ generated ]
}

tasks.withType(JavaCompile) {
    options.annotationProcessorGeneratedSourcesDirectory = file(generated)
}

clean.doLast {
    file(generated).deleteDir()
}

간단한 설명입니다. annotationProcessor는 compile시의 annotationProcessor를 gradle에서 자동으로 추가해줍니다. 이때 사용되는 각각의 jar를 추가해주면 됩니다. 기존의 generateQueryDsl을 전혀 할 필요가 없습니다. 다음 JavaCompile환경에 생성될 Q-class들의 위치를 지정해주고 그 위치를 sourceSets에 추가합니다. 마지막으로 clean으로 생성된 Q-class들을 제거해주는 코드를 넣어줍니다.

gradle에서 annotationProcessor를 지원함으로서 더욱 편하게 QueryDsl을 지원할 수 있게 되었습니다.

Posted by Y2K
,

기본적인 CRUD를 지원하는 GenericDAO를 구성하는 코드의 비교를 해볼려고 합니다.

일단 기술 배경으로는 Spring을 사용하고, Spring내에서의 Hibernate, JPA, 그리고 Hibernate를 이용한 queryDSL 코드간의 GenericDao를 비교해보고자 합니다.

기본적으로 Entity는 BaseEntity를 상속하는 것을 원칙으로 갖습니다. DB에서 생성되는 단일키를 사용하는 것으로 기본 구상을 잡고 들어가면 GenericDao의 interface는 다음과 같이 구성될 수 있습니다.

public interface EntityDao<T extends BaseEntity> {
    List<T> getAll();
    void deleteAll();
    T getById(int id);
    void add(T entity);
    void update(T entity);
    int countAll();
}

이제 이 interface를 구현한 AbstractDao class를 각 3개의 기술로 따로 구현해보고자 합니다.

Hibernate

Hibernate는 SessionFactory를 통해 생성되는 Session을 기반으로 구성합니다. Spring을 이용하기 때문에 SessionFactory.getCurrentSession() method를 이용해서 Session을 구해와서 처리합니다.

약간의 특징이라고 할 수 있는 것이 Criteria를 이용하는 코드의 경우, Class<?>를 반드시 각 EntityDao 생성 객체에서 넘겨주는 것이 필요합니다.

public abstract class AbstractSessionFactoryDao<T extends BaseEntity> implements EntityDao<T> {

    @Autowired
    protected SessionFactory sessionFactory;
    private final Class<?> clazz;

    protected AbstractSessionFactoryDao(Class<?> clazz) {
        this.clazz = clazz;
    }


    @Override
    public List<T> getAll() {
        Session session = sessionFactory.getCurrentSession();
        return session.createCriteria(clazz).list();
    }

    @Override
    public void deleteAll() {
        List<T> items = getAll();
        Session session = sessionFactory.getCurrentSession();
        for (T item : items) {
            session.delete(item);
        }
    }

    @Override
    public T getById(final int id) {
        Session session = sessionFactory.getCurrentSession();
        return (T)session.get(clazz, id);
    }

    @Override
    public void add(final T entity) {
        Session session = sessionFactory.getCurrentSession();
        session.save(entity);
    }

    @Override
    public void update(final T entity) {
        Session session = sessionFactory.getCurrentSession();
        session.update(entity);
    }

    @Override
    public int countAll() {
        Session session = sessionFactory.getCurrentSession();
        Long count = (Long) session.createCriteria(clazz)
                .setProjection(Projections.rowCount())
                .uniqueResult();

        if(count == null) {
            return 0;
        } else {
            return count.intValue();
        }
    }
}
JPA

jpa를 이용하는 경우, @PersistenceContext 를 이용해서 EntityManager를 등록해서 사용해주면 됩니다. Spring은 @PersistenceContext를 확인하면, 등록된 EntityManagerFactoryBean은 EntityManager를 사용할때마다 EntityManagerFactory에서 얻어와서 사용하게 됩니다. 이는 Spring에서 핸들링시켜줍니다. 이에 대한 코드 분석은 다음 Post에서 한번 소개해보도록 하겠습니다.

JPA를 이용한 GenericDao class의 코드는 다음과 같습니다.

public abstract class AbstractJpaDao<T extends BaseEntity> implements EntityDao<T> {

    protected AbstractJpaDao(String entityName) {
        this.entityName = entityName;
    }

    @PersistenceContext
    protected EntityManager em;

    @Autowired
    protected JpaExecutor executor;
    protected final String entityName;

    @Override
    public List<T> getAll() {
        return em.createQuery("from " + entityName).getResultList();
    }

    @Override
    public void deleteAll() {
        System.out.println(em);
        em.createQuery("delete from " + entityName).executeUpdate();
    }

    @Override
    public T getById(final int id) {
        Query query = em.createQuery("from " + entityName + " where id = :id");
        query.setParameter("id", id);
        return (T) query.getSingleResult();
    }

    @Override
    public void add(final T entity) {
        em.merge(entity);
    }

    @Override
    public void update(final T entity) {
        em.merge(entity);
    }

    @Override
    public int countAll() {
        return em.createQuery("from " + entityName).getResultList().size();
    }
}
Hibernate기반의 queryDSL

GenericDAO class를 만들기 위해서 QueryDsl에서 가장 문제가 되는 것은 QClass들의 존재입니다. Generator에서 생성되는 Q Class들은 EntityPathBase class에서 상속받는 객체들입니다. 따라서 다른 GenericDAO class 들과는 다르게 2개의 Generic Parameter를 받아야지 됩니다.

다음은 queryDSL을 이용한 GenericDAO code입니다.

public abstract class AbstractQueryDslDao<T extends BaseEntity, Q extends EntityPathBase<T>> implements EntityDao<T> {

    @Autowired
    protected SessionFactory sessionFactory;
    protected final Q q;
    protected final Class<?> clazz;

    protected AbstractQueryDslDao(Class<?> clazz, Q q) {
        this.clazz = clazz;
        this.q = q;
    }

    protected HibernateQuery getSelectQuery() {
        return new HibernateQuery(sessionFactory.getCurrentSession()).from(q);
    }

    protected HibernateDeleteClause getDeleteQuery() {
        return new HibernateDeleteClause(sessionFactory.getCurrentSession(), q);
    }

    protected HibernateUpdateClause getUpdateQuery() {
        return new HibernateUpdateClause(sessionFactory.getCurrentSession(), q);
    }


    @Override
    public List<T> getAll() {
        HibernateQuery query = getSelectQuery();
        return query.list(q);
    }

    @Override
    public void deleteAll() {
        getDeleteQuery().execute();
    }

    @Override
    public T getById(int id) {
        return (T)sessionFactory.getCurrentSession().get(clazz, id);
    }

    @Override
    public void add(T entity) {
        sessionFactory.getCurrentSession().saveOrUpdate(entity);
    }

    @Override
    public void update(T entity) {
        sessionFactory.getCurrentSession().saveOrUpdate(entity);
    }

    @Override
    public int countAll() {
        Long count = getSelectQuery().count();
        return count.intValue();
    }
}

개인적으로는 JPA에서 사용하는 JPQL의 문법이 조금 맘에 안듭니다. 거의 SQL query문을 객체를 이용해서 사용한다는 느낌만 들고 있기 때문에, 오타 등의 문제에서 자유롭지가 않습니다. 그런 면에서는 queryDSL을 이용하는 것이 가장 좋은 것 같지만 queryDSL의 경우에는 처음 초기 설정이 조금 까다롭습니다. 같이 일하는 팀원중 이런 설정을 잘 잡는 사람이 있으면 괜찮지만…

다음에는 글 내용중 언급한것처럼 @PersistenceContext Autowiring에 대해서 코드 분석을 해볼까합니다. ^^ 아무도 안오는 Blog지만요. ^^

Posted by Y2K
,

* 사내 강의용으로 사용한 자료를 Blog에 공유합니다. Spring을 이용한 Web 개발에 대한 전반적인 내용에 대해서 다루고 있습니다.


queryDSL과 Spring Data JPA 모두 DAO에 대한 접근 방법을 제어하고, 코드양을 줄일 수 있는 좋은 방법입니다. 그렇지만, Spring Data JPA의 경우에는 규격대로 되어 있는 select 구문, update 구문 이외에는 사용할 수 없는 단점을 가지고 있습니다. 이러한 단점을 극복하는 좋은 방법은 Spring Data JPA에 queryDSL을 결합해서 사용하는 것입니다. 이렇게 되면, Spring Data JPA와 queryDSL의 모든 장점을 사용할 수 있습니다. 

queryDSL과 Spring Data JPA를 연동해서 사용하기 위해서는 maven repository에 다음과 같은 설정을 해야지 됩니다. 

    <repository>
      <id>spring-snapshot</id>
      <name>Spring Maven SNAPSHOT Repository</name>
      <url>http://repo.springsource.org/libs-snapshot</url>
    </repository>

그리고 버젼에 유의해서 Spring Data JPA와 queryDSL을 설정해줘야지 됩니다. queryDSL이 2.x에서 3.x대로 넘어가면서 객체의 이름이 많이 변경이 되었기 때문에 상호간의 호환에 문제가 있습니다. queryDSL 버젼은 3.1로, Spring Data JPA의 버젼은 1.4.0.BUILD.SNAPSHOT으로 설정해줍니다. 지금 queryDSL을 지원하는 Spring Data JPA의 경우에는 beta version이지만, 곧 정식버젼이 나온다고 하니 잠시 기다릴수 있을 것 같습니다.

다음은 설정될 maven pom 파일의 properties입니다. 

  <properties>
    <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
    <project.build.resourceEncoding>UTF-8</project.build.resourceEncoding>
    <hibernate.version>4.1.10.Final</hibernate.version>
    <hibernate.validator.version>4.3.1.Final</hibernate.validator.version>
    <spring.version>3.2.2.RELEASE</spring.version>
    <querydsl.version>3.1.0</querydsl.version>
    <spring.jpa.version>1.4.0.BUILD-SNAPSHOT</spring.jpa.version>
  </properties>

이렇게 설정을 모두 마치고 코드를 한번 다시 살펴보도록 하겠습니다.

QueryDslPredicateExecutor

select 구문의 핵심입니다. QueryDslPredicateExecute는 JpaRespository interface와 같이 사용할 수 있습니다. interface는 다음과 같은 method를 포함합니다. 

public interface QueryDslPredicateExecutor<T> {
    T findOne(Predicate predicate);
    Iterable<T> findAll(Predicate predicate);
    Iterable<T> findAll(Predicate predicate, OrderSpecifier<?>... orders);
    Page<T> findAll(Predicate predicate, Pageable pageable);
    long count(Predicate predicate);
}


Predicate는 queryDSL의 query입니다. 이 interface를 상속하는 경우 BookRepository의 코드는 다음과 같이 구성될 수 있습니다. 

@Repositorypublic interface BookDao extends JpaRepository<Book, Integer>, QueryDslPredicateExecutor<Book> {

}

다른 코드는 전혀 필요없습니다. 이제 query를 한번 구성해보도록 하겠습니다. 

search에 대한 코드를 구성해보도록 하겠습니다. Spring Data JPA는 like 구문을 지원하지만, 모든 전/후에 대한 Like만 지원되기 때문에, 기존의 코드로는 구성이 불가능했습니다. 
다음과 같은 코드로 구성이 가능합니다. 

    @Test
    public void testProdicate() {
        String bookName = "책이름";

        QBook qBook = QBook.book;
        Predicate predicate = qBook.name.like(bookName + "%").and(qBook.status.eq(BookStatus.CanRent));
        Iterable<Book> books = bookDao.findAll(predicate);

        for(Book book : books) {
            assertThat(book.getName().contains(bookName), is(true));
        }
    }

Type에 safe 하고, 간단한 query 구문으로 code를 구성하는 것이 가능합니다. findAll method가 queryDSL의 Predicate를 지원하기 때문에 이러한 코드를 구성할 수 있습니다.
findAll을 이용하는 경우, 이제 select에 대한 이슈는 거의 해소가 가능합니다. 그래도 아직 문제가 조금 더 남아있습니다. 그 부분은 바로 각 항목에 대한 max, min 값을 구하는 Predicate query와 다양한 update,delete를 하는 query들을 만들어주는 것이 아직은 불가능합니다. 이러한 문제를 해결하고, queryDSL의 모든 기능을 지원하기 위해서는 QueryDslRepositorySupport를 사용해야지 됩니다. 

QueryDslRepositorySupport

QueryDslPredicateExecutor의 경우에는 select를 이용해서 객체를 얻어올 때 주로 사용됩니다. 그렇지만, 이에 대한 다른 접근이 필요합니다. 객체에 대한 update, delete의 경우에는 좀더 다양하게 처리하는 것이 필요합니다. 

예를 들어 다음 query를 처리할 때, hibernate의 경우에는 HQL 또는 Native query를 이용해서 처리하는 방법밖에는 없습니다. 

update users set name = 'ykyoon' where name='abc';

delete from users where name='abc';

이러한 일괄 변경 및 업데이트, 또는 특정 값의 sum, min, max, average 값을 도출하기 위해서 사용하는 것이 QueryDslRepositorySupport입니다. QueryDslRepositorySupport의 code를 한번 알아보도록 하겠습니다. 

@Repository
public abstract class QueryDslRepositorySupport {

    private final PathBuilder<?> builder;

    private EntityManager entityManager;
    private Querydsl querydsl;

    public QueryDslRepositorySupport(Class<?> domainClass) {
        Assert.notNull(domainClass);
        this.builder = new PathBuilderFactory().create(domainClass);
    }

    @PersistenceContext
    public void setEntityManager(EntityManager entityManager) {

        Assert.notNull(entityManager);
        this.querydsl = new Querydsl(entityManager, builder);
        this.entityManager = entityManager;
    }

    @PostConstruct
    public void validate() {
        Assert.notNull(entityManager, "EntityManager must not be null!");
        Assert.notNull(querydsl, "Querydsl must not be null!");
    }

    protected EntityManager getEntityManager() {
        return entityManager;
    }

    protected JPQLQuery from(EntityPath<?>... paths) {
        return querydsl.createQuery(paths);
    }

    protected DeleteClause<JPADeleteClause> delete(EntityPath<?> path) {
        return new JPADeleteClause(entityManager, path);
    }

    protected UpdateClause<JPAUpdateClause> update(EntityPath<?> path) {
        return new JPAUpdateClause(entityManager, path);
    }

    @SuppressWarnings("unchecked")
    protected <T> PathBuilder<T> getBuilder() {
        return (PathBuilder<T>) builder;
    }

    protected Querydsl getQuerydsl() {
        return this.querydsl;
    }
}

abstract class 이기 때문에, 상속을 받아서 구현해야지 됩니다. 먼저, User Id 값중 max 값을 얻어오는 code를 한번 알아보도록 하겠습니다.

    public int sumUserIds() {
        QUser qUser = QUser.user;
        return from(qUser).uniqueResult(qUser.id.max());
    }


다음은 Update입니다. 

    public Long updateNameByName() {
        QUser qUser = QUser.user;
        update(qUser).where(qUser.name.eq("abcde")).set(qUser.name, "가나다").execute();
        return 0L;
    }


마지막으로 Delete입니다. 

    public void deleteByName(String name) {
        QUser qUser = QUser.user;
        delete(qUser).where(qUser.name.eq(name)).execute();
    }

매우 간단하게 구현하는 것이 가능합니다. 우리가 지금까지 만든 JpaRepository와 QueryDslPredictExecutor와 같이 사용하는 경우에는 거의 모든 query들을 처리하는 것이 가능하게 됩니다. 전체 코드는 다음과 같습니다. 

@Repository
public class CalDao extends QueryDslRepositorySupport {
    public CalDao() {
        super(User.class);
    }

    public int sumUserIds() {
        QUser qUser = QUser.user;
        return from(qUser).uniqueResult(qUser.id.max());
    }

    public Long updateNameByName() {
        QUser qUser = QUser.user;
        update(qUser).where(qUser.name.eq("abcde")).set(qUser.name, "가나다").execute();
        return 0L;
    }

    public void deleteByName(String name) {
        QUser qUser = QUser.user;
        delete(qUser).where(qUser.name.eq(name)).execute();
    }
}

queryDSL + Spring Data JPA에서의 Hibernate 이용

사용하다보면 Hibernate의 기능을 사용해야지 될 때가 발생할 수 있습니다. 이는 기존의 coding이 Hibernate 기준으로 된 경우도 해당될 수 있을 것이고, Hibernate의 다양한 Criteria를 이용해보고 싶은 생각도 생길 수 있습니다. 
먼저, 계속해서 이야기드리는 것은 JPA라는 규격자체가 Hibernate라는 구현체 위에서 동작하는 것이기 때문에 Hibernate의 Session을 얻어내는것도 가능합니다. 다음과 같은 코드로 Session을 얻어서 Hibernate와 동일하게 처리하는 것도 가능합니다. 

    public Session getSession() {
        return (Session) getEntityManager().getDelegate();
    }


Summary

지금까지 queryDSL과 Spring Data JPA를 이용한 Model 구성에 대해서 알아봤습니다. 약 2개월간 계속해서 장/단을 뽑아보면서 제일 우리에게 좋은 Model Framework조합이 무엇인가를 고민했던 결과입니다. 이러한 개발 방법은 다음과 같은 장점을 가지고 있습니다. 

1. type-safe 한 query를 만들 수 있습니다.
2. 객체지향적인 코드 구성이 가능합니다.
3. Hibernate Criteria의 단점인 일괄 update / delete 문의 처리가 가능합니다.
4. Repository의 코딩양을 줄일 수 있습니다.
5. eclipse의 intellisense의 지원을 받을 수 있습니다.

최종적으로 구성되는 package입니다. 이 구성으로 대부분의 Model에 관련된 Project가 구성이 될 예정입니다. 




# config : ApplicationConfiguration class가 위치할 package입니다.
# entities : entity 객체들이 위치할 package입니다.
# repositories : JpaRepository, QueryDslPredictExecutor를 상속받은 Repository interface가 위치할 package입니다.
# repositories.support : QueryDslRepositorySupport를 상속받은 객체 또는 repositories에서 사용할 Predict를 지원하는 객체들이 위치할 package입니다.
# services : BL에 관련된 서비스가 위치할 package입니다.
# utils : 데이터의 변환 등 다양한 경우에 사용되는 utility class가 위치하는 package입니다.

여기서 지금까지 이야기하던 Dao와 Repository에 대한 정의를 다시 할 필요성이 있습니다. 

이 둘에 대한 정의는 다음과 같습니다. 

"DAO는 데이터베이스에서 값을 꺼내와 도메인 오브젝트로 반환해주거나 적절한 값으로 반환해주는 계층을 일컫는다. Repository는 한 도메인 오브젝트에 대해서 객체의 값을 보증해주기 위해 도메인 내부에서 데이터베이스와 소통하는 객체을 일컫는다."

말이 어렵습니다. 조금 더 단순하게 말하면....

# DAO는 DB과 연결되어, Domain Object(=Entity)로 변환하는 것을 의미합니다.
# Repository는 DB와 연결되어, Domain Object(=Entity)로 변환합니다. 단, Domain 내부에서만 사용됩니다.

뒤의 조건이 하나가 더 붙어있으면 Repository이고, DB에 접속하는 객체를 어느곳에서나 사용가능해야지 되면 DAO입니다. Repository pattern이란, DB에 접근하는 영역을 Domain Layer, 즉 Model 영역에서만 사용하게 되는 것을 의미하고, 이것은 n-tier system에 적합한 책임영역의 분리가 되는 코드를 의미합니다. 
만약에 Controller Layer에서 DB에 접근하게 되고, 사용하게 된다면, 그것은 더 이상 Repository가 될 수 없습니다. 그렇지만, Model/Domain Layer에서만 접근하게 된다면 이것은 DAO가 아닌 Repository가 되게 됩니다. 너무나 유사한 개념이지만, 영역을 나누는 의미에서 사용하는 용어의 차이라고 생각하시면 될 것 같습니다. 
우리가 개발하는 것은 DAO가 아닌 Repository가 되게 됩니다. (이것은 저도 잘못 개발하고 있던 내용중 하나입니다.) 이 둘간의 영역 차이는 매우 자주 나오는 문제입니다. 만약에 서비스가 아닌 다른 영역(BL이 아닌 다른 영역)에서 DB에 접근하게 되는 객체를 만든다면.. 이것은 DAO를 만들어주게 되는 것이 맞습니다. 
이건 pattern입니다. 무엇이 옳고 그른 문제는 아닙니다. Layer에 의한 명백한 의미를 나누기 위해서 Repository로 사용하는 것이 좀 더 나을 것 같습니다. 

정리입니다. 
queryDSL + Spring Data JPA를 이용한 Domain Layer의 개발을 주로 하게 될 것입니다. 이 부분은 지금 Open Source 측에서도 밀고 있는 추세이기도 하고, 계속해서 발전이 되어가고 있는 분야이기도 합니다. 
이제 다음시간에는 만약에 우리가 외부 SI를 나가게 된다면, 주로 사용하고 있는 myBatis(iBatis)에 대해서 간략하게 알아보도록 하겠습니다. 


Posted by Y2K
,