하위 태스크 1

MyBatis 의존성 확인

build.gradle 에서 MyBatis 및 관련 의존성 확인

프로젝트의 build.gradle은 스프링 부트와 MyBatis 의존성을 포함한다.

build.gradle:

dependencies {
	implementation 'org.mybatis.spring.boot:mybatis-spring-boot-starter:3.0.5'
	runtimeOnly 'com.h2database:h2'
	testImplementation 'org.springframework.boot:spring-boot-starter-test'
	testImplementation 'org.mybatis.spring.boot:mybatis-spring-boot-starter-test:3.0.5'
	// ...
}

하위 태스크 2

데이터소스 설정 확인

application.properties 의 DB 연결 설정 확인

MyBatis와 데이터베이스를 매핑하기 위해 데이터베이스 연결을 설정한다. 현재 프로젝트는 내장 데이터베이스 H2를 사용하여 데이터베이스를 연결하기 위한 URL, 사용자명, 비밀번호, 드라이버 이름 등을 명시하지 않는다.

application.properties:

# 외부 데이터베이스를 사용하는 경우에도 애플리케이션을 실행할 때 초기화한다.
spring.sql.init.mode=always
# 외부 시스템에서 복사한 schema.sql, data.sql의 인코딩이 UTF-8이 아닌 경우에 지정한다.
spring.sql.init.encoding=utf-8
 
# 언더스코어를 카멜 표기법으로 매핑한다.
mybatis.configuration.map-underscore-to-camel-case=true
# SQL 매핑을 담당하는 XML 파일의 위치를 지정한다.
mybatis.mapper-locations=classpath:mapper/**/*.xml
 
# ...

하위 태스크 3

매퍼 구조 파악

매퍼 XML과 Mapper 인터페이스의 매핑 구조 이해

mapper/MemberMapper.xmlmapper/MemberMapper.java는 매핑 관계에 있다. MemberMapper 인터페이스에 정의된 메서드에 대한 구체적인 동작을 MemberMapper.xml에서 표현한다. 예를 들어, MemberMapper 인터페이스의 selectAll 메서드와 MemberMapper.xml에서 나타낸 구체적인 동작의 모습은 다음과 같다.

MemberMapper.java:

@Mapper
public interface MemberMapper {
    List<Member> selectAll();
    // ...
}

MemberMapper.xml:

<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE mapper
        PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
        "https://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="com.example.demo.mapper.MemberMapper">
	<select id="selectAll">
        SELECT * FROM member
    </select>
    <!-- ... -->
</mapper>

하위 태스크 4

XML 기반 CRUD 구현

XML 매퍼에 INSERT/SELECT/UPDATE/DELETE 구현

XML 파일에서 CRUD 기능을 구현하기 위한 <select>, <insert>, <update>, <delete> 태그 요소를 사용한다.

<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE mapper
        PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
        "https://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="com.example.demo.mapper.MemberMapper">
    <select id="selectAll">
        SELECT * FROM member
    </select>
    <select id="selectById">
        SELECT * FROM member WHERE id=#{id}
    </select>
    <select id="selectByEmail">
        SELECT * FROM member WHERE email=#{email}
    </select>
    <select id="selectByNameLike">
        SELECT * FROM member WHERE name LIK #{name}
    </select>
    <select id="selectAllOrderByAgeAsk">
        SELECT * FROM member ORDER BY age ASC
    </select>
    <select id="selectAllOrderBy">
        SELECT * FROM member ORDER BY ${order} ${dir}
    </select>
    <select id="selectAllCount">
        SELECT COUNT(*) FROM member
    </select>
    <insert id="insert" useGeneratedKeys="true" keyProperty="member.id" keyColumn="id">
        INSERT INTO member (name, email, age) VALUES (#{member.name}, #{member.email}, #{member.age})
    </insert>
    <update id="update">
        UPDATE member SET name=#{member.name}, email=#{member.email}, age=#{member.age} WHERE id=#{member.id}
    </update>
    <delete id="delete">
        DELETE FROM member WHERE id=#{member.id}
    </delete>
    <delete id="deleteById">
        DELETE FROM member WHERE id=#{id}
    </delete>
    <delete id="deleteAll">
        DELETE FROM member
    </delete>
</mapper>

하위 태스크 5

Mapper 인터페이스 정의

Mapper 인터페이스 메서드 시그니처 정의

MemberMapper.xml과 대응하는 MemberMapper.java의 인터페이스를 완성한다.

@Mapper
public interface MemberMapper {
    List<Member> selectAll();
    Optional<Member> selectByID(@Param("id") Long id);
    Optional<Member> selectByEmail(@Param("email") String email);
    List<Member> selectAllOrderByAgeAsc();
    List<Member> selectAllOrderBy(@Param("order") String order, @Param("dir") String dir);
    List<Member> selectByNameLike(@Param("name") String name);
    int selectAllCount();
    int insert(@Param("member") Member member);
    int update(@Param("member") Member member);
    int delete(@Param("member") Member member);
    int deleteById(@Param("id") Long id);
    int deleteAll();
}

하위 태스크 6

Service/Controller 연동

Mapper를 주입받아 비즈니스 로직/엔드포인트 구현

ApplicationRunner 인터페이스를 구현하는 MyBatisApplication 클래스를 생성한다. 해당 인터페이스를 구현하여 셸에서 실행 가능한 애플리케이션을 작성한다.

@Component
@RequiredArgsConstructor
@Slf4j
public class MyBatisApplication implements ApplicationRunner {
    private final MemberMapper memberMapper;
    private final ArticleMapper articleMapper;
 
    @Override
    public void run(ApplicationArguments args) throws Exception {
        int count = memberMapper.selectAllCount();
        log.info("Member count: {}", count);
 
        Member member = memberMapper
                .selectByEmail("[email protected]")
                .orElseThrow();
        log.info("Member: {}", member);
 
        Article article = Article.builder()
                .title("Hello, MyBatis")
                .description("MyBatis is an SQL Mapper framework")
                .memberId(member.getId())
                .build();
        int inserted = articleMapper.insert(article);
        log.info("Inserted: {}", inserted);
    }
}

스프링 부트 실행 결과는 다음과 같다.

Member count: 4
Member: Member(id=1, name=윤서준, [email protected], age=10)
Inserted: 1

하위 태스크 7

파라미터 바인딩 실습

#{} 와 ${} 를 사용한 파라미터 바인딩 예제 구현

MyBatis는 매핑할 SQL 문장을 사용해 PreparedStatement로 만들고, 이때 #{}는 파라미터를 치환하는 자리에 사용하므로 치환이 가능한 컬럼에만 사용할 수 있다. ORDER BY는 PreparedStatement의 파라미터 치환으로 적용할 수 없으므로 직접 SQL을 작성할 때 동적으로 적용하기 위해 ${}를 사용해야 한다.

MemberMapper.xml에서 <select> 태그와 <insert> 태그를 이용할 때 #{}과 ${}을 활용했다.

<select id="selectAllOrderBy">  
    SELECT * FROM member ORDER BY ${order} ${dir}  
</select>
<insert id="insert" useGeneratedKeys="true" keyProperty="member.id" keyColumn="id">  
    INSERT INTO member (name, email, age) VALUES (#{member.name}, #{member.email}, #{member.age})  
</insert>
<!-- ... -->

하위 태스크 8

ResultMap 작성

ResultMap으로 복잡한 매핑 구현

테이블의 컬럼명과 모델의 프로퍼티명이 서로 다를 때, ResultMap을 사용해서 복잡한 매핑을 구현할 수 있다. 다음은 idmemberResult인 ResultMap을 정의하고 selectAll 메서드에 적용하는 XML이다.

<resultMap id="memberResult" type="com.example.demo.model.Member">
	<result column="display_name" property="name"/>
	<result column="primary_contact" property="email"/>
	<result column="age" property="age"/>
</resultMap>
<select id="selectAll" resultMap="memberResult">
	SELECT * FROM member
</select>

하위 태스크 9

동적 SQL 작성

<if>, <where>, <choose>를 활용한 조건부 쿼리 구현

요청에 따라 SQL이 유동적으로 변해야 할 때, <if>, <where>, <choose> 태그를 사용할 수 있다. 세 태그를 모두 사용한 searchMembers 메서드 정의 예제는 다음과 같다.

MemberMapper.java 일부:

@Mapper  
public interface MemberMapper {
	// ...
	List<Member> searchMembers(@Param("name") String name, @Param("email") String email, @Param("sortBy") String sortBy);
}

MemberMapper.xml 일부:

// ...
<select id="searchMembers">
    SELECT * FROM member
    <where>
        <if test="name != null and name != ''">
            AND name LIKE CONCAT('%', #{name}, '%')
        </if>
        <if test="email != null and email != ''">
            AND email=#{email}
        </if>
        <choose>
            <when test="sortBy == 'name'">
                ORDER BY name
            </when>
            <when test="sortBy == 'email'">
                ORDER BY email
            </when>
            <when test="sortBy == 'age'">
                ORDER BY age
            </when>
        </choose>
    </where>
</select>

하위 태스크 10

CRUD 및 검색 테스트

Postman 또는 테스트 코드로 CRUD/검색 기능 검증

MemberMapper를 사용한 CRUD 기능 및 검색 기능을 검증한다.

@Component
@RequiredArgsConstructor
@Slf4j
public class MyBatisApplication implements ApplicationRunner {
    private final MemberMapper memberMapper;
    private final ArticleMapper articleMapper;
 
    @Override
    public void run(ApplicationArguments args) throws Exception {
        log.info("이름에 '서준'이 포함된 회원을 검색한다.");
        memberMapper
                .searchMembers("서준", null, null)
                .forEach(m -> log.info("검색 결과: {}", m));
 
        log.info("새로운 멤버 '장서윤'을 등록한다.");
        Member newMember = Member.builder().name("장서윤").email("[email protected]").age(21).build();
        memberMapper.insert(newMember);
        log.info("삽입된 멤버 ID: {}", newMember.getId());
 
        log.info("삽입한 멤버 '장서윤'의 이름을 '장지현'으로 수정한다.");
        newMember.setName("장지현");
        memberMapper.update(newMember);
        log.info("수정된 멤버: {}", memberMapper.selectByEmail("[email protected]"));
 
		log.info("수정한 멤버 '장지현'을 삭제한다.");
        memberMapper.deleteById(newMember.getId());
        boolean exists = memberMapper.selectByEmail("[email protected]").isPresent();
        log.info("멤버의 존재 여부: {}", exists);
    }
}

스프링 부트 실행 결과는 다음과 같다.

이름에 '서준'이 포함된 회원을 검색한다.
검색 결과: Member(id=1, name=윤서준, [email protected], age=10)
새로운 멤버 '장서윤'을 등록한다.
삽입된 멤버 ID: 5
삽입한 멤버 '장서윤'의 이름을 '장지현'으로 수정한다.
수정된 멤버: Optional[Member(id=5, name=장지현, [email protected], age=21)]
수정한 멤버 '장지현'을 삭제한다.
멤버의 존재 여부: false