kinggora 2022. 11. 13. 18:57

MyBatis 적용2 - 설정과 실행

MyBatisItemRepository 클래스

@Repository
@RequiredArgsConstructor
public class MyBatisItemRepository implements ItemRepository {

    private final ItemMapper itemMapper;

    @Override
    public Item save(Item item) {
        itemMapper.save(item);
        return item;
    }

    @Override
    public void update(Long itemId, ItemUpdateDto updateParam) {
        itemMapper.update(itemId, updateParam);
    }

    @Override
    public Optional<Item> findById(Long id) {
        return itemMapper.findById(id);
    }

    @Override
    public List<Item> findAll(ItemSearchCond cond) {
        return itemMapper.findAll(cond);
    }
}

 

  • ItemRepository 를 구현해서 MyBatisItemRepository 를 만들자.
  • MyBatisItemRepository 는 단순히 ItemMapper 에 기능을 위임한다. ItemMapper 은 의존관계 주입 받아 사용한다.

MyBatisConfig 설정 파일

@Configuration
@RequiredArgsConstructor
public class MyBatisConfig {
    
    private final ItemMapper itemMapper;

    @Bean
    public ItemService itemService(){
        return new ItemServiceV1(itemRepository());
    }

    @Bean
    public ItemRepository itemRepository(){
        return new MyBatisItemRepository(itemMapper);
    }
}

 

마이바티스 모듈이 데이터 소스나 트랜잭션 매니저 등을 자동으로 연결해준다.

 

ItemServiceApplication - 변경

@Import(MyBatisConfig.class)
@SpringBootApplication(scanBasePackages = "hello.itemservice.web")
public class ItemServiceApplication {}

설정 파일 MyBatisConfig 으로 변경

MyBatis 적용3 - 분석

MyBatisItemRepository 는 의존관계 주입 받은 ItemMapper 인터페이스의 기능을 위임하고 있다.

그러나 그 어디서도 ItemMapper 의 구현체를 생성한 적도, 직접 스프링 빈으로 등록한 적도 없다.

 

MyBatis 스프링 연동 모듈의 @Mapper 처리

 

1. 애플리케이션 로딩 시점에 MyBatis 스프링 연동 모듈은 @Mapper 가 붙어있는 인터페이스를 조사한다.

2. 해당 인터페이스가 발견되면 동적 프록시 기술을 사용해서 ItemMapper 인터페이스의 구현체를 만든다.

3. 생성된 구현체를 스프링 빈으로 등록한다.

 

ItemMapper 인터페이스 기반으로 xml 코드를 호출하는 프록시 객체를 동적으로 생성하여 스프링 빈으로 등록한다.

 

@Slf4j
@Repository
@RequiredArgsConstructor
public class MyBatisItemRepository implements ItemRepository {

    private final ItemMapper itemMapper;
    
    @Override
    public Item save(Item item) {
        log.info("itemMapper class={}", itemMapper.getClass());
        itemMapper.save(item);
        return item;
    }
}

 

실행 결과

itemMapper class=class com.sun.proxy.$Proxy66

출력해보면 JDK 동적 프록시가 적용된 것을 확인할 수 있다.

매퍼 구현체

  • 마이바티스 스프링 연동 모듈이 만들어주는 ItemMapper 의 구현체 덕분에 인터페이스 만으로 편리하게 XML의 데이터를 찾아서 호출할 수 있다.
  • 원래 마이바티스를 사용하려면 더 번잡한 코드를 거쳐야 하는데, 이런 부분을 인터페이스 하나로 매우 깔끔하고 편리하게 사용할 수 있다.
  • 매퍼 구현체는 예외 변환까지 처리해준다. MyBatis에서 발생한 예외를 스프링 예외 추상화인 DataAccessException 에 맞게 변환해서 반환해준다. JdbcTemplate이 제공하는 예외 변환 기능을 여기서도 제공한다고 이해하면 된다.

정리

  • 매퍼 구현체 덕분에 마이바티스를 스프링에 편리하게 통합해서 사용할 수 있다.
  • 매퍼 구현체를 사용하면 스프링 예외 추상화도 함께 적용된다.
  • 마이바티스 스프링 연동 모듈이 많은 부분을 자동으로 설정해주는데, 데이터베이스 커넥션, 트랜잭션과 관련된 기능도 마이바티스와 함께 연동하고, 동기화해준다.

참고: 마이바티스 스프링 연동 모듈이 자동으로 등록해주는 부분은 MybatisAutoConfiguration 클래스를 참고하자.

MyBatis 기능 정리1 - 동적 쿼리

MyBatis 공식 메뉴얼: https://mybatis.org/mybatis-3/ko/index.html

MyBatis 스프링 공식 메뉴얼: https://mybatis.org/spring/ko/index.html

 

마이바티스가 제공하는 최고의 기능이자 마이바티스를 사용하는 이유는 바로 동적 SQL 기능 때문이다.

동적 쿼리를 위해 제공되는 기능은 다음과 같다.

  • if
  • choose (when, otherwise)
  • trim (where, set)
  • foreach

공식 메뉴얼에서 제공하는 예제를 통해 동적 SQL을 알아보자.

if

해당 조건에 따라 값을 추가할지 말지 판단한다.

if 조건이 만족하면 AND 조건을 추가하고, 만족하지 않으면 AND 조건을 버린다.

내부의 문법은 OGNL을 사용한다. 자세한 내용은 OGNL을 검색해보자.

 

<select id="findActiveBlogWithTitleLike" resultType="Blog">
   SELECT * FROM BLOG
   WHERE state = ‘ACTIVE’
   <if test="title != null">
      AND title like #{title}
   </if>
</select>

choose, when, otherwise

자바의 switch 구문과 유사한 구문도 사용할 수 있다.

when 을 하나도 만족하지 않으면 otherwise

<select id="findActiveBlogLike" resultType="Blog">
   SELECT * FROM BLOG WHERE state = ‘ACTIVE’
   <choose>
      <when test="title != null">
         AND title like #{title}
      </when>
      <when test="author != null and author.name != null">
         AND author_name like #{author.name}
      </when>
      <otherwise>
         AND featured = 1
      </otherwise>
   </choose>
</select>

trim, where, set

<select id="findActiveBlogLike" resultType="Blog">
   SELECT * FROM BLOG
   WHERE
   <if test="state != null">
      state = #{state}
   </if>
   <if test="title != null">
      AND title like #{title}
   </if>
   <if test="author != null and author.name != null">
      AND author_name like #{author.name}
   </if>
</select>

 

이 예제의 문제점은 문장을 모두 만족하지 않을 때 발생한다.

SELECT * FROM BLOG

WHERE

 

title 만 만족할 때도 문제가 발생한다.

SELECT * FROM BLOG

WHERE AND title like ‘someTitle’

 

결국 WHERE 문을 언제 넣어야 할지 상황에 따라서 동적으로 달라지는 문제가 있다.

<where>를 사용하면 이런 문제를 해결할 수 있다.

 

<where> 사용

<where>는 뒤에 문장이 없으면 where 를 추가하지 않는다.

문장이 있으면 where 를 추가한다.

만약 and 가 먼저 시작된다면 and 를 지운다. ex) WHERE AND -> WHERE

 

<select id="findActiveBlogLike" resultType="Blog">
   SELECT * FROM BLOG
   <where>
       <if test="state != null">
          state = #{state}
       </if>
       <if test="title != null">
          AND title like #{title}
       </if>
       <if test="author != null and author.name != null">
          AND author_name like #{author.name}
       </if>
   </where>
</select>

 

참고로 다음과 같이 trim 이라는 기능으로 사용해도 된다. 이렇게 정의하면 <where>와 같은 기능을 수행한다.

<trim prefix="WHERE" prefixOverrides="AND |OR ">
 ...
</trim>

 

foreach

컬렉션을 반복 처리할 때 사용한다. where in (1,2,3,4,5,6) 와 같은 문장을 쉽게 완성할 수 있다.

파라미터로 List 를 전달하면 된다.

<select id="selectPostIn" resultType="domain.blog.Post">
   SELECT *
   FROM POST P
   <where>
      <foreach item="item" index="index" collection="list"
         open="ID in (" separator="," close=")" nullable="true">
            #{item}
      </foreach>
   </where>
</select>

 

참고: 동적 쿼리에 대한 자세한 내용은 다음을 참고하자.

https://mybatis.org/mybatis-3/ko/dynamic-sql.htm

MyBatis 기능 정리2 - 기타 기능

애노테이션으로 SQL 작성

다음과 같이 XML 대신에 애노테이션에 SQL을 작성할 수 있다.

@Select("select id, item_name, price, quantity from item where id=#{id}")
Optional<Item> findById(Long id);

 

  • @Insert , @Update , @Delete , @Select 기능이 제공된다.
  • 이 경우 XML에 정의한 SQL 은 제거해야 한다.
  • 동적 SQL이 해결되지 않으므로 간단한 경우에만 사용한다.

 

애노테이션으로 SQL 작성에 대한 더 자세한 내용은 다음을 참고하자.

https://mybatis.org/mybatis-3/ko/java-api.html

문자열 대체(String Substitution)

#{} 문법은 ?를 넣고 파라미터를 바인딩하는 PreparedStatement 를 사용한다.

때로는 파라미터 바인딩이 아니라 문자 그대로를 처리하고 싶은 경우도 있다. 이때는 ${} 를 사용하면 된다.

@Select("select * from user where ${column} = #{value}")
User findByColumn(@Param("column") String column, @Param("value") String value);

 

  • ${column} 의 위치는 파라미터 바인딩이 불가능한 위치이기 때문에 ${} 를 사용했다.
  • user 테이블의 컬럼명을 파라미터로 받아서 sql 을 완성했다.

주의

${} 를 사용하면 SQL 인젝션 공격을 당할 수 있다. 따라서 가급적 사용하면 안된다.

사용하더라도 매우 주의깊게 사용해야 한다. 

재사용 가능한 SQL 조각

<sql> 을 사용하면 SQL 코드를 재사용 할 수 있다.

<sql id="userColumns"> ${alias}.id,${alias}.username,${alias}.password </sql>
<select id="selectUsers" resultType="map">
   select
      <include refid="userColumns"><property name="alias" value="t1"/></include>,
      <include refid="userColumns"><property name="alias" value="t2"/></include>
   from some_table t1
      cross join some_table t2
</select>

 

  • <include> 를 통해 <sql> 조각을 찾아서 사용할 수 있다.
<sql id="sometable">
   ${prefix}Table
</sql>

<sql id="someinclude">
   from
      <include refid="${include_target}"/>
</sql>

<select id="select" resultType="map">
   select
      field1, field2, field3
   <include refid="someinclude">
      <property name="prefix" value="Some"/>
      <property name="include_target" value="sometable"/>
   </include>
</select>

 

  • 프로퍼티 값을 전달할 수 있고, 해당 값은 내부에서 사용할 수 있다.

Result Maps

결과를 매핑할 때 테이블은 user_id 이지만 객체는 id 이다.

이 경우 컬럼명과 객체의 프로퍼티 명이 다르다. 그러면 다음과 같이 별칭( as )을 사용하면 된다.

<select id="selectUsers" resultType="User">
   select
      user_id 		as "id",
      user_name 	as "userName",
      hashed_password 	as "hashedPassword"
   from some_table
   where id = #{id}
</select>

 

별칭을 사용하지 않고도 문제를 해결할 수 있는데, 다음과 같이 resultMap 을 선언해서 사용하면 된다.

select 쿼리에서 resultMap 을 지정해주면 쿼리 결과가 자동으로 매핑된다.

<resultMap id="userResultMap" type="User">
   <id property="id" column="user_id" />
   <result property="username" column="username"/>
   <result property="password" column="password"/>
</resultMap>

<select id="selectUsers" resultMap="userResultMap">
   select user_id, user_name, hashed_password
   from some_table
   where id = #{id}
</select>

 

복잡한 결과 매핑

MyBatis도 매우 복잡한 결과에 객체 연관관계를 고려해서 데이터를 조회하는 것이 가능하다. (객체에 포함된 객체)

이때는 <association> , <collection> 등을 사용한다.

이 부분은 성능과 실효성에서 측면에서 많은 고민이 필요하다.

JPA는 객체와 관계형 데이터베이스를 ORM 개념으로 매핑하기 때문에 이런 부분이 자연스럽지만, MyBatis에서는 들어가는 공수도 많고, 성능을 최적화하기도 어렵다.

(그래서 보통은 테이블을 조인해서 각각 조회하거나, 한 테이블에 몰아넣고 플랫 데이터를 따로 매핑한다.)

따라서 해당기능을 사용할 때는 신중하게 사용해야 한다.

 

결과 매핑에 대한 자세한 내용은 다음을 참고하자.

https://mybatis.org/mybatis-3/ko/sqlmap-xml.html#Result_Maps

5. 데이터 접근 기술 - JPA

JPA 시작

스프링과 JPA는 자바 엔터프라이즈(기업) 시장의 주력 기술이다.

스프링이 DI 컨테이너를 포함한 애플리케이션 전반의 다양한 기능을 제공한다면, JPA는 ORM 데이터 접근 기술을 제공한다.

 

스프링 + 데이터 접근기술의 조합을 구글 트랜드로 비교했을 때

글로벌에서는 스프링+JPA 조합을 80%이상 사용한다.

국내에서도 스프링 + JPA 조합을 50%정도 사용하고, 2015년 부터 점점 그 추세가 증가하고 있다.

 

JPA는 스프링 만큼이나 방대하고, 학습해야할 분량도 많다. 하지만 한번 배워두면 데이터 접근 기술에서 매우 큰 생산성 향상을 얻을 수 있다. 대표적으로 JdbcTemplate이나 MyBatis 같은 SQL 매퍼 기술은 SQL을 개발자가 직접 작성해야 하지만, JPA를 사용하면 SQL도 JPA가 대신 작성하고 처리해준다.

 

실무에서는 JPA를 더욱 편리하게 사용하기 위해 스프링 데이터 JPA와 Querydsl이라는 기술을 함께 사용한다.

중요한 것은 JPA이다. 스프링 데이터 JPA, Querydsl은 JPA를 편리하게 사용하도록 도와주는 도구라 생각하면 된다.

 

  • JPA - 자바 ORM 표준 JPA 프로그래밍 - 기본편
  • 스프링 데이터 JPA - 실전! 스프링 데이터 JPA
  • Querydsl - 실전! Querydsl

ORM 개념1 - SQL 중심적인 개발의 문제점

22.05.26 :: 자바 ORM 표준 JPA 프로그래밍 - 기본편 (tistory.com)

ORM 개념2 - JPA 소개

22.05.28 (tistory.com)

JPA 설정

spring-boot-starter-data-jpa 라이브러리를 사용하면 JPA와 스프링 데이터 JPA를 스프링 부트와 통합하고, 설정도 아주 간단히 할 수 있다.

 

build.gradle - 의존 관계 추가

//JPA, 스프링 데이터 JPA 추가
implementation 'org.springframework.boot:spring-boot-starter-data-jpa'

//JdbcTemplate 의존 관계 제거
//implementation 'org.springframework.boot:spring-boot-starter-jdbc'

spring-boot-starter-data-jpa 는 spring-boot-starter-jdbc 도 함께 포함(의존)한다. 따라서 해당 라이브러리 의존관계를 제거해도 된다.

참고로 mybatis-spring-boot-starter 도 spring-boot-starter-jdbc 를 포함하기 때문에 제거해도 된다.

 

다음과 같은 라이브러리가 추가된다.

  • hibernate-core : JPA 구현체인 하이버네이트 라이브러리
  • jakarta.persistence-api : JPA 인터페이스
  • spring-data-jpa : 스프링 데이터 JPA 라이브러리

application.properties - 로그 옵션

main 과 test 모두 추가해준다.

#JPA log
logging.level.org.hibernate.SQL=DEBUG
logging.level.org.hibernate.type.descriptor.sql.BasicBinder=TRACE

 

  • org.hibernate.SQL=DEBUG : 하이버네이트가 생성하고 실행하는 SQL을 확인할 수 있다.
  • org.hibernate.type.descriptor.sql.BasicBinder=TRACE : SQL에 바인딩 되는 파라미터를 확인할 수 있다.

참고: spring.jpa.show-sql=true

이 설정은 System.out 콘솔을 통해서 SQL이 출력된다. 따라서 권장하지는 않는다.

(둘다 켜면 logger , System.out 둘다 로그가 출력되어서 같은 로그가 중복해서 출력된다.)