새소식

Database

데이터 접근 기술 (3) - MyBatis

 

 

개요

 

이번 글은 데이터 접근 기술 시리즈의 세 번째로, MyBatis에 대한 글이다.

김영한 님의 스프링 DB 강의와 개인 공부를 통해 학습한 내용을 정리하고자 한다.

 

Spring Boot 3.2.2, JDK 17 버전이며 H2를 데이터베이스로 사용하니 참고하자.

 

 

 

 

 

MyBatis란?

 

 

데이터베이스 접근과 상호작용을 단순화하는 오픈 소스 프레임워크로, xml 기반의 sql 매핑을 지원한다.

특히 동적으로 sql을 생성할 수 있기 때문에 복잡한 쿼리의 작성이나 필요에 따라 동적으로 변경할 수 있는 유연성이 장점이다.

 

이전 글에서 설명한 JdbcTemplate보다 더 많은 기능을 지원하지만 오픈 소스이기 때문에 MyBatis를 사용하려면 좀 더 많은 설정이 필요하다.

 

MyBatis의 주요 특징은 다음과 같다.

 

  • sql 매핑 파일(xml)을 사용하여 데이터베이스 쿼리 정의
  • 동적 sql 생성 가능
  • 데이터베이스 테이블과 자바 객체 간의 필드 매핑 설정 가능
  • 자동 트랜잭션, 커넥션 등 관리

 

 

 

기본 설정

 

1. build.gradle

dependencies {
    //MyBatis 사용
    implementation 'org.mybatis.spring.boot:mybatis-spring-boot-starter:3.0.3'
    
    //H2 데이터베이스 사용
    runtimeOnly 'com.h2database:h2'
}

 

MyBatis를 사용하기 위한 설정을 추가한다. 이 때 버전은 스프링 부트의 버전과 맞춰야 한다.

나는 앞서 언급했듯 스프링 부트 3.2.2를 사용하기 때문에 3.0.3을 추가했다.

 

https://start.spring.io/에서 프로젝트를 생성한다면 Dependencies에 MyBatis를 추가하여 버전에 맞는 라이브러리를 자동으로 등록할 수 있다.

 

 

📌 참고

H2 데이터베이스 설치 및 실행 방법은 아래의 글을 참고하자.

https://hyunrian.tistory.com/88

 

Mac에서 H2 설치 및 실행하기

H2 Database는 간단한 개발이나 테스트 용도로 사용하기 좋은 DB이다. 이번 글에서는 맥에서 H2를 설치하고 실행하는 과정을 진행해보려 한다. H2 설치하기 아래의 링크로 들어가면 최신 버전을 OS에

hyunrian.tistory.com

 

 

2. application.properties

#DB 연결
spring.datasource.url=jdbc:h2:tcp://localhost/~/test
spring.datasource.username=sa

#MyBatis 관련 설정(편의)
mybatis.type-aliases-package=review.data.domain
mybatis.mapper-locations=classpath:mapper/**/*.xml
mybatis.configuration.map-underscore-to-camel-case=true
logging.level.review.data.repository.mybatis=trace

 

DB 연결 설정은 필수지만 MyBatis 관련 설정은 편의를 위한 것이니 선택적으로 작성하면 된다.

 

  • `mybatis.type-aliases-package`
    • xml 파일에서 resultType의 패키지명을 생략하여 작성할 수 있음
    • 지정한 패키지를 포함하여 그 하위 패키지까지 자동으로 인식
  • `mybatis.mapper-locations`
    • sql을 작성할 xml 파일의 경로는 기본적으로 아래에서 나올 Mapper 인터페이스와 동일한 경로에 위치해야 하며, 이 경우 별도의 설정이 필요하지 않음
    • 인터페이스와 다른 경로에 위치할 경우 해당 디렉토리 경로를 작성
    • `classpath:mapper/**/*.xml` : resources/mapper를 포함하여 그 하위 디렉토리 내 xml 파일 전체를 인식
  • `mybatis.configuration.map-underscore-to-camel-case=true`
    • 데이터베이스 컬럼명이 `user_name`이고 객체의 필드명이 `userName`인 경우 자동으로 변경됨
  • `logging.level.hello.itemservice.repository.mybatis=trace`
    • MyBatis에서 실행되는 쿼리 로그 확인 가능

 

 

 

코드 작성

 

간단한 CRUD 기능을 구현할 것이며 작성한 클래스 및 파일은 아래와 같다.

 

  • `User` : 도메인 객체. 아이디(pk), 유저명, 핸드폰 번호를 필드로 가짐
  • `UserMapper` : xml을 호출하는 매퍼 인터페이스
  • `UserRepository` : 리포지토리 인터페이스
  • `MyBatisUserRepository` : 리포지토리 구현체
  • `UserSearchCondition` : 데이터 조회시의 검색어를 필드로 갖고 있는 검색 조건 객체
  • `UserUpdateDto` : update에서 사용할 데이터 객체. `User` 대신 사용할 것
  • `UserMapper.xml` : 실행할 sql이 있는 xml 파일

 

이 중에서 중요하지 않은 몇가지 클래스는 코드를 생략하겠다.

 

📌 참고

데이터베이스(H2)에 테이블은 아래와 같이 생성해 두었다.

create table users (
    id        bigint generated by default as identity,
    user_name varchar(10),
    phone_num varchar(25),
    primary key (id)
);

 

 

1. `User`

@Data
@NoArgsConstructor
public class User {
    private Long id;
    private String userName;
    private String phoneNum;

    public User(String userName, String phoneNum) {
        this.userName = userName;
        this.phoneNum = phoneNum;
    }
}

 

 

 

2. `UserMapper`

@Mapper
public interface UserMapper {
	
    //회원 저장
    void save(User user);

    //회원 수정
    void update(@Param("id") Long id, @Param("userDto") UserUpdateDto userDto);

    //회원 아이디로 찾기
    Optional<User> findById(Long id);

    //회원 전체 조회. 검색어가 있는 경우 검색 결과에 반영
    List<User> findAll(UserSearchCondition searchCondition);

    //회원 삭제
    void delete(Long id);

}

 

MyBatis Mapping xml을 호출해주는 인터페이스로, `@Mapper`를 반드시 붙여야 MyBatis가 인식할 수 있다.

xml로 넘길 파라미터가 두개 이상인 경우 `@Param`으로 파라미터명을 지정하도록 한다.

 

 

3. `UserMapper.xml`

<?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="review.data.repository.mybatis.UserMapper">

    <insert id="save" useGeneratedKeys="true" keyProperty="id">
        insert into users (user_name, phone_num)
        values (#{userName}, #{phoneNum})
    </insert>

    <update id="update">
        update users
        set user_name = #{userDto.userName},
            phone_num = #{userDto.phoneNum}
        where id = #{id}
    </update>

    <select id="findById" resultType="User">
        select * from users
        where id = #{id}
    </select>

    <select id="findAll" resultType="User">
        select * from users
        <if test="userName != null || userName != ''">
            where user_name like concat('%', #{userName}, '%')
        </if>
    </select>

    <delete id="delete">
        delete from users
        where id = #{id}
    </delete>
    
</mapper>

 

  • application.properties에서 `mybatis.mapper-locations`를 따로 지정하지 않았다면 위에서 작성한 `UserMapper` 인터페이스와 동일한 경로를 resources 디렉토리 하위에 생성하여 xml 파일을 위치하도록 한다.
  • `<mapper></mapper>` 위의 내용은 기본 설정이니 반드시 작성해야 한다.
  • `<mapper>`의 `namespace`는  인터페이스의 전체 경로를 지정한다.

 

전체 파일의 구조도

 

  • id는 Mapper 인터페이스에서 설정한 메서드명을 지정한다.
    • `void save(User user);` 👉🏻 `<insert id="save">`
  • UserMapper 인터페이스에서 넘어온 파라미터가 객체인 경우 #{필드명}으로 작성한다.
    • 👉🏻 `#{userName}`
  • UserMapper 인터페이스에서 넘어온 파라미터가 2개 이상이며 그 중 객체가 있을 경우 `@Param()`으로 지정한 파라미터명을 넣어 #{파라미터명.필드명}으로 작성한다.
    • 👉🏻 `#{userDto.userName}`
  • `userGeneratedKeys`는 DB가 키를 생성해주는 IDENTITY 전략일 때 사용하며 `keyProperty`는 해당 키의 컬럼명을 지정한다. insert가 완료되면 User 객체에 id로 생성된 값이 입력된다.
  • `<select>`처럼 반환할 데이터가 있는 경우 resultType에 반환할 데이터의 타입을 명시해야 한다.
    • application.properties에서 `mybatis.type-aliases-package`을 지정하지 않았다면 `resultType="review.data.domain.User"`와 같이 전체 경로를 포함하여 작성해야 한다.
  • `<where>`, `<if>`와 같은 동적 쿼리 문법을 사용할 수 있다.
    • `<if>` : 해당 조건에 따라 구문을 추가함

 

 

4. `MyBatisUserRepository`

@Repository
public class MyBatisUserRepository implements UserRepository {

    @Autowired
    UserMapper userMapper;

    @Override
    public User save(User user) {
        userMapper.save(user);
        return user;
    }

    @Override
    public void update(Long id, UserUpdateDto userDto) {
        userMapper.update(id, userDto);
    }

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

    @Override
    public List<User> findAll(UserSearchCondition searchCondition) {
        return userMapper.findAll(searchCondition);
    }

    @Override
    public void delete(Long id) {
        userMapper.delete(id);
    }
}

 

`UserRepository` 인터페이스를 구현한 클래스로 `UserMapper` 인터페이스를 주입하여 로직을 수행한다.

`findById()`처럼 `UserMapper.xml`에서 반환하는 객체가 한개일 때는 `User`나 `Optional<User>`를 사용하고,

`findAll()`처럼 여러개일 때는 `List<User>`를 사용하면 된다.

 

 

모든 코드를 작성하고 테스트를 수행하면 모두 정상적으로 동작하는 것을 확인할 수 있다.

만약 오류가 발생한다면 설정이 잘못되었거나 sql 쿼리에 오타가 있을 확률이 높다.

 

 

 

MyBatis의 동적 쿼리

 

MyBatis가 동적 쿼리 작성을 위해 제공하는 기능으로는

 

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

 

등이 있는데, `<where>`과 `<if>`를 함께 사용하는 경우에 대해 잠깐 살펴보겠다.

 

<select id="findAll" resultType="Book">
    select * from books
    <where>
        <if test="state != null"> <!-- 1번 -->
                state = #{state}
        </if>
        <if test="title != null"> <!-- 2번 -->
                and title like #{title}
        </if>
        <if test="author != null and author.name != null">  <!-- 3번 -->
                and author_name like #{author.name}
        </if>
    </where>
</select>

 

쿼리를 보면 `<if>`의 조건에 따라 문장을 추가하거나 추가하지 않거나 결정이 된다. 

 

만약 여기서 1번 조건을 제외하고 2번 조건만 해당된다면

`select * from books where and title like ?`와 같은 쿼리가 생성될 것 같지만

`<where>` 내 문장이 and로 시작한다면 and를 지우고 쿼리를 생성하며, 문장이 없으면 where을 추가하지 않는 기능이 있다.

따라서 위 예시의 쿼리는 정상적으로 동작하게 될 것이다.

 

 

더 많고 자세한 내용은 MyBatis의 공식 문서를 참고하도록 하자.

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

 

mybatis – 마이바티스 3 | 동적 SQL

동적 SQL 마이바티스의 가장 강력한 기능 중 하나는 동적 SQL을 처리하는 방법이다. JDBC나 다른 유사한 프레임워크를 사용해본 경험이 있다면 동적으로 SQL 을 구성하는 것이 얼마나 힘든 작업인지

mybatis.org

 

 

 

참고

Contents

Copied URL!

Liked this Posting!