본문 바로가기

개발/쉽게 이해하고 사용하는 JPA

JPA 환경에서 MyBatis 를 사용하여 데이타를 가지고 오기

앞서 JPA 관련된 이야기를 할때 JPA 와 Mybatis 를 비교하면서 서로의 장점과 단점에 대한 이야기를 나눈 적이 있습니다. 그때 JPA 를 사용함으로써 얻는 가장 큰 이점은 드러나지 않아 파악하기 어려운 Query 구문이 로직상으로 도출이 됨으로 인해 향후 유지보수가 용이해지며 테스트를 쉽게 할 수 있는 유연한 구조를 얻을 수 있다는 점 이였지만 복잡한 데이타를 집계 한다거나 여러 Entity 간의 관계를 통한 결과를 도출하려고 하는 용도로는 적합하지 않으며 결국은 Query 를 사용해 데이타를 가지고 올 수 밖에는 없는데 이를 위해 Mybatis 와 JPA 가 같이 사용해야 된다는 점을 이야기 드렸습니다.

이번 글에는 이전 JPA 가 설정된 동일한 소스 코드에 어떻게 MyBatis 를 사용 하는지에 대한 이야기를 해보려 합니다.

먼저 MyBatis 는 Framework 이 아니라 SQL Query Template Library 라는 것을 기억해야 합니다. 개발자가 동적 Query 를 쉽게 작성할 수 있게 관리를 도와주는 Library 이며 MyBatis 자체로는 Query 작성에 아무런 도움을 주진 않습니다. 결국 개발자가 데이타베이스의 특징에 맞는 SQL Query 를 정확하게 이해하고 있어야만 Query 를 작성할 수 있는데 이 때문에 역설적으로 데이타베이스 특성에 맞춘 속도 최적화 같은 작업을 할 수 있게 되는 것 입니다. 물론 그렇기 때문에 발생되는 코드의 비효율은 감안해야 합니다.

물론 JPA 로 동적 쿼리를 작성할 수 없는 것은 아닙니다. JPA 표준으로는 Creteria 라는 문법이 존재하며 보다 명확한 문법을 제시하는 QueryDSL 같은 Library 도 존재합니다. 하지만 복잡한 데이타를 가지고 올때 어디까지 JPA 로 개발 해야 하며 어디부터 MyBatis 로 개발 해야 되는지에 대한 기준은 매우 모호하며 이것은 개발자의 경험에 의존 할 수 밖에는 없다고 봅니다. 개인적인 기준을 이야기 드리자면 리팩토링 법칙 중 2번 이상 반복이 된다면 공통 기능으로 도출해야 된다는 법칙에 의해 2개 이상의 테이블과 조인이 되어야 한다면 MyBatis 로 Query 를 작성해라 고 이야기 드릴 것 같습니다.

PostViewMapper.xml

먼저 MySQL 데이타베이스의 특성에 맞는 SQL Query 구문을 이용해 Post Entity 의 조회 Mapper xml 를 작성해 보겠습니다. 원래 명칭은 PostMapper 로 지정하는것이 맞겠지만 JPA 와 구분을 위해 PostViewMapper 로 작성 하였습니다. 구문은 단순 조회용 Query 이며 post 와 연관이 있는 category, post_detail, post_tag 테이블과 함께 JOIN 으로 데이타를 질의해 데이타를 가지고 오는 속도를 향상 시켰습니다.

<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd">

<mapper namespace="net.jogeum.hellojpa.repository.PostViewMapper">
    <sql id="select">
        select
            a.id,
            b.name as category_name,
            a.title,
            a.content,
            c.description,
            d.tag,
            a.created_by,
            a.created_date
    </sql>

    <sql id="from">
        from
            post a
            inner join
                category b
                on a.category_id = b.id
            left join
                post_detail c
                on a.id = c.post_id
            left join
                (
                    select
                        e.post_id,
                        group_concat(e.tag_name) as tag
                    from
                        post_tag e
                    group by
                        e.post_id
                ) d
                on a.id = d.post_id
    </sql>

    <sql id="where">
        <where>
            <if test="type != null">
                <if test="type.name() == 'title'">
                    and a.title like concat('%', #{value}, '%')
                </if>
                <if test="type.name() == 'category'">
                    and b.name = #{value}
                </if>
                <if test="type.name() == 'date'">
                    and to_char(a.created_date, 'yyyymmdd') &gt;= #{value}
                </if>
                <if test="type.name() == 'tag'">
                    and d.tag like concat('%', #{value}, '%')
                </if>
            </if>
        </where>
    </sql>

    <sql id="order">
        order by
            a.created_date DESC
    </sql>

    <select id="getList" parameterType="map" resultType="net.jogeum.hellojpa.domain.PostView">
        <include refid="select"/>
        <include refid="from"/>
        <include refid="where"/>
        <include refid="order"/>
    </select>

    <select id="getCount" parameterType="map" resultType="int">
        select count(*)
        <include refid="from"/>
        <include refid="where"/>
    </select>
</mapper>

 

PostViewMapper

JPA 의 Repository interface 에 대응되는 Mapper interface 를 정의합니다. 이때 interface 상단에 @Mapper 어노테이션을 정의하면 Mybatis 의 SqlSessionFactory 가 해당 어노테이션을 찾아와 Mapper 의 구현체를 정의하고 관리해 줍니다.

@Mapper
@Repository
public interface PostViewMapper {
    List<PostView> getList(Map<String, ?> params);

    Integer getCount(Map<String, ?> params);
}

 

PostViewService

그런 다음 마지막으로 Mapper 를 사용하는 Service 를 작성합니다. Mapper 의 사용은 별다른 구현 없이 Mybatis 의 SqlSessionFactory 가 @Mapper 가 정의되어 있는 객체를 찾아와 정의한 구현체를 그대로 사용하면 됩니다.

@Slf4j
@Service
public class PostViewService {
    @Autowired
    PostViewMapper postViewMapper;

    @Transactional(rollbackFor = Exception.class)
    public List<PostViewDTO> getList(PostSearchType type, String value) {

        Map<String, Object> params = new HashMap<>();

        if (!StringUtils.isEmpty(type) && !StringUtils.isEmpty(value)) {
            params.put("type", type);
            params.put("value", value);
        }

        return postViewMapper
                .getList(params).stream()
                .map(this::dto)
                .collect(Collectors.toList());
    }

    private PostViewDTO dto(PostView post) {
        DateTimeFormatter formatter = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss");

        return new PostViewDTO(
                post.getId(),
                post.getCategoryName(),
                post.getTitle(),
                post.getContent(),
                post.getDescription(),
                post.getTag(),
                post.getCreatedBy(),
                post.getCreatedDate().format(formatter)
        );
    }
}

소스 상에 데이타를 가지고 오는 방법 중 JPA 로 구현된 PostController 와 MyBatis 로 구현된 PostViewController 를 서로 비교해 보면서 확인 하시는 것도 좋을 것 같습니다.

 

소스코드

앞서 설명한 JPA 환경 구성과 MyBatis 의 사용 예제에 대한 소스 코드는 아래 주소에서 확인 하실 수 있습니다.

https://github.com/jogeum/hellojpa