하위 태스크 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.xml과 mapper/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을 사용해서 복잡한 매핑을 구현할 수 있다. 다음은 id가 memberResult인 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