MyBatis

2025. 6. 5. 12:39Spring Framework/스프링 DB접근

1. MyBatis 코드 적용하기  

//MemberMapper.java
@Mapper
public interface MemberMapper {
    void save (Member member);
    void update(@Param("id") Long id, @Param("updateParam") MemberUpdateDTO updateParam);
    Optional<Member> findById(Long id);
    List<Member> findAll(MemberSearchCond cond);
}
<!-- application.properties -->
mybatis.type-aliases-package=com.hnjee.tripnow.member.domain
mybatis.configuration.map-underscore-to-camel-case=true
logging.level.com.hnjee.tripnow.repository.mybatis=trace

<!-- MemberMapper.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="com.hnjee.tripnow.member.mapper.MemberMapper">

    <!-- void save (Member member); -->
    <insert id="save" useGeneratedKeys="true" keyProperty="id">
        INSERT INTO members (
            login_id, password, name, phone_number, email,
            nickname, role, is_deleted, created_at, updated_at
        )
        VALUES (
            #{loginId}, #{password}, #{name}, #{phoneNumber}, #{email},
            #{nickname}, #{role}, #{isDeleted}, #{createdAt}, #{updatedAt}
        )
    </insert>
    
    <!-- void update(@Param("id") Long id, @Param("updateParam") MemberUpdateDTO updateParam); -->
    <update id="update">
        UPDATE members
        SET
            password = #{updateParam.password},
            name = #{updateParam.name},
            phone_number = #{updateParam.phoneNumber},
            email = #{updateParam.email},
            nickname = #{updateParam.nickname}
        WHERE id = #{id}
    </update>
    
    <!-- Optional<Member> findById(Long id); -->
    <select id="findById" resultType="Member">
        SELECT
            id, login_id, password, name, phone_number, email,
            nickname, role, is_deleted, created_at, updated_at
        FROM members
        WHERE id = #{id}
    </select>

    <!-- List<Member> findAll(MemberSearchCond cond); -->
    <select id="findAll" resultType="Member">
        SELECT
            id, login_id, password, name, phone_number, email,
            nickname, role, is_deleted, created_at, updated_at
        FROM members
        <where>
            <if test="loginId != null and loginId != ''">
                and login_id like concat ('%',#{loginId},'%')
            </if>
            <if test="name != null and name != ''">
                and name like concat ('%',#{name},'%')
            </if>
            <if test="nickname != null and nickname != ''">
                and nickname like concat ('%',#{nickname},'%')
            </if>
        </where>
    </select>
</mapper>
public interface MemberRepository {
    Member save(Member member);
    void update(Long memberId, MemberUpdateDTO updateParam);
    Optional<Member> findById(Long id);
    List<Member> findAll(MemberSearchCond cond);
}

@Repository
@RequiredArgsConstructor
public class MybatisMemberRepository implements MemberRepository{
    private final MemberMapper memberMapper;
    
    @Override
    public Member save(Member member) {
        member.setRole(Role.MEMBER);
        member.setIsDeleted("N");
        member.setCreatedAt(LocalDateTime.now());
        memberMapper.save(member);
        return member;
    }
    @Override
    public void update(Long memberId, MemberUpdateDTO updateParam) {
        memberMapper.update(memberId, updateParam);
    }
    @Override
    public Optional<Member> findById(Long id) {
        return memberMapper.findById(id);
    }
    @Override
    public List<Member> findAll(MemberSearchCond cond) {
        return memberMapper.findAll(cond);
    }
}

 

2. Mapper 인터페이스 ↔ Mapper XML 매핑

2-1. 매핑 항목 

  • MyBatis의 Mapper 인터페이스와 XML 파일은 이름과 경로, 그리고 메서드 id가 정확히 맞아야 정상 작동한다. 
  • interface는 자바에서 호출하는 계약서 역할
  • xml은 그 계약에 따른 구현 내용(SQL)
항목 예시 설명
@Mapper @Mapper 붙인 인터페이스 MyBatis가 이 인터페이스를 Mapper로 인식하도록 함
namespace <mapper namespace=
"com.hnjee.tripnow.member
.mapper.MemberMapper">
인터페이스의 전체 경로와 정확히 일치해야 함
id <select id="findById"> 인터페이스의 메서드명과 일치해야 함
parameterType
/ @Param
@Param("id") Long id → #{id} 파라미터를 XML에 매핑하기 위해 필요
resultType
/ resultMap
resultType="Member" 리턴 타입으로 어떤 객체를 만들지 지정

2-2. 파라미터 매핑

상황 interface에서 XML에서 
파라미터 1개 (기본 타입, DTO 등) parameterType 사용 선택적 #{파라미터명 or 필드 or key}
파라미터 2개 이상 @Param("이름") → 필수 #{이름}, #{이름.필드}

1) 단일 파라미터 → parameterType 사용

Member findById(Long id);

<select id="findById" parameterType="long" resultType="Member">
  SELECT * FROM members WHERE id = #{id}
</select>
  • 이때는 파라미터가 1개이기 때문에 parameterType으로 타입 명시 가능하고 XML에서는 #{id}로 직접 접근 가능
  • parameterType은 생략해도 동작은 하지만, 디버깅이나 명시적 코드로는 명시하는 게 좋음.
  • DTO 1개만 전달: #{필드}로 접근 
  • Map 전달: #{key}로 접근

2) 파라미터 2개 이상 or DTO 조합  @Param 어노테이션 사용

void update(@Param("id") Long id, @Param("updateParam") MemberUpdateDTO dto);

<update id="update">
  UPDATE members
  SET name = #{updateParam.name}
  WHERE id = #{id}
</update>
  • 파라미터가 2개 이상이면 MyBatis 내부에서 Map으로 변환되는데
  • 이때 키 이름을 정확히 지정하기 위해 @Param("이름")을 사용

2-3. 리턴타입 매핑 

항목 resultType resultMap
용도 간단한 매핑 (자동 매핑) 복잡한 매핑 (수동 매핑)
필드/컬럼 이름 같거나, camelCase 설정으로 해결 가능 다를 경우 명시적으로 매핑
조인 결과 어렵다 association, collection으로 구조적 매핑 가능
가독성 짧고 간단 길지만 유연함
추천 시점 CRUD 기본 조회 다대일, 일대다, 컬럼명이 다를 때

1) resultType

  • 컬럼명과 자바 필드명이 거의 그대로 일치할 때
  • 간단한 엔티티 (Member, User, Board 등) 매핑에 사용 
<select id="findById" resultType="Member">
  SELECT id, name, email FROM members WHERE id = #{id}
</select>
  • resultType="Member" → MyBatis가 Member 객체를 만들어줌
  • 컬럼명필드명이 일치하거나, map-underscore-to-camel-case=true 설정이 있으면 자동으로 매핑됨

2) resultMap

  • resultType보다 더 복잡한 매핑이 필요할 때 사용
  • 컬럼명과 필드명이 다를 때
  • 조인해서 중첩 객체나 리스트를 만들 때
  • 생성자를 통해 값을 주입할 때
<resultMap id="memberResultMap" type="Member">
  <id property="id" column="member_id"/>
  <result property="name" column="member_name"/>
  <result property="email" column="email_address"/>
</resultMap>

<select id="findById" resultMap="memberResultMap">
  SELECT member_id, member_name, email_address FROM members WHERE member_id = #{id}
</select>
class Member {
    Long id;
    String name;
    Address address;
}

class Address {
    String city;
    String street;
}

<resultMap id="memberMap" type="Member">
  <id property="id" column="id"/>
  <result property="name" column="name"/>
  <association property="address" javaType="Address">
    <result property="city" column="addr_city"/>
    <result property="street" column="addr_street"/>
  </association>
</resultMap>

 

3. application.properties 설정

//1.type에 객체 작성할때 패키지명을 생략하고 객체명만 작성할 수 있게 해줌 
// com.hnjee.tripnow.member.domain.Member → Member
mybatis.type-aliases-package=com.hnjee.tripnow.member.domain

//2.컬럼명과 필드명이 변수 작성 방식에 따라 달라지는 경우 자동으로 매핑해줌
// phone_number → phoneNumber (resultMap 없이도 돼서 편리) 
mybatis.configuration.map-underscore-to-camel-case=true

//3. SQL 로그 찍히게 해서 디버깅 가능하게 해줌 
logging.level.com.hnjee.tripnow.repository.mybatis=trace

 

4. 동적 SQL 활용

1) if: 해당 조건에 따라 값을 추가할지 말지 판단

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

2) choose (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>

3) 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>
  • <where> 는 문장이 없으면 where를 추가하지 않는다.
  • 만약 and가 먼저 시작 된다면 and를 지운다.

4) foreach

<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>

 

5. MyBatis 스프링 연동 모듈의 매퍼 구현체

  • Mapper 인터페이스의 구현체 없이 어플리케이션이 동작할 수 있을까?
애플리케이션 로딩 시점에 MyBatis 스프링 연동 모듈은 @Mapper 가 붙어있는 인터페이스를 조사
→ 해당 인터페이스가 발견되면 동적 프록시 기술을 사용해서 Mapper 인터페이스의 구현체를 만든다
→ 생성된 구현체를 스프링 빈으로 등록

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