MyBatis
2025. 6. 5. 12:39ㆍSpring 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 인터페이스의 구현체를 만든다
→ 생성된 구현체를 스프링 빈으로 등록
- 마이바티스 스프링 연동 모듈의 매퍼 구현체
- 매퍼 구현체 덕분에 마이바티스를 스프링에 편리하게 통합해서 사용할 수 있다.
- 매퍼 구현체를 사용하면 스프링 예외 추상화도 함께 적용된다.
- 마이바티스 스프링 연동 모듈이 많은 부분을 자동으로 설정해주는데,
데이터베이스 커넥션, 트랜잭션과 관련된 기능도 마이바이스와 함께 연동하고, 동기화해준다.