반응형
Spring MyBatis 실습 가이드
📚 핵심 개념 정리
1. MyBatis 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="패키지경로.인터페이스명">
<!-- SQL 쿼리 작성 -->
</mapper>
핵심 포인트:
namespace: Mapper 인터페이스의 전체 경로id: 인터페이스의 메소드명과 일치해야 함parameterType: 입력 파라미터 타입 (생략 가능)resultType: 반환 타입 (select문에서 필수)
2. XML 기본 태그
INSERT (등록)
<insert id="save" parameterType="BookDto"
useGeneratedKeys="true" keyProperty="id">
INSERT INTO books(title, stock)
VALUES(#{title}, #{stock})
</insert>
useGeneratedKeys="true": 자동생성 키 반환keyProperty="id": DTO의 어떤 필드에 저장할지
SELECT (조회)
<select id="findAll" resultType="BookDto">
SELECT * FROM books
</select>
UPDATE (수정)
<update id="update" parameterType="BookDto">
UPDATE books SET stock = #{stock} WHERE id = #{id}
</update>
DELETE (삭제)
<delete id="delete" parameterType="int">
DELETE FROM books WHERE id = #{id}
</delete>
3. 동적 쿼리 (Dynamic Query)
IF 조건문
<select id="search" resultType="BookDto">
SELECT * FROM rentals
<where>
<if test="member != null">
AND member LIKE CONCAT('%', #{member}, '%')
</if>
<if test="title != null">
AND title LIKE CONCAT('%', #{title}, '%')
</if>
</where>
</select>
언제 사용?
- 검색 조건이 선택적일 때
- 조건에 따라 SQL이 달라져야 할 때
주의사항:
<where>태그는 자동으로 WHERE를 추가하고, 불필요한 AND/OR를 제거test속성에는 파라미터 조건 작성
FOREACH (일괄 처리)
<insert id="saveAll" parameterType="list"
useGeneratedKeys="true" keyProperty="id">
INSERT INTO books(title, stock) VALUES
<foreach collection="list" item="book" separator=",">
(#{book.title}, #{book.stock})
</foreach>
</insert>
속성 설명:
collection="list": 리스트 파라미터item="book": 반복할 때 사용할 변수명separator=",": 각 항목 사이 구분자
4. 트랜잭션 (@Transactional)
@Service
public class BookService {
@Transactional // 이 메소드는 트랜잭션으로 관리됨
public void rentBook(RentDto rentDto) {
// 1. 재고 감소
bookMapper.decreaseStock(rentDto.getBookId());
// 2. 대출 기록 추가
bookMapper.insertRental(rentDto);
// 예외 발생 시 자동으로 롤백됨
}
}
트랜잭션이란?
- 여러 작업을 하나의 단위로 묶어서 처리
- 모든 작업이 성공해야 완료(commit)
- 하나라도 실패하면 전체 취소(rollback)
언제 사용?
- 도서 대출: 재고 감소 + 대출 기록 추가
- 도서 반납: 재고 증가 + 반납일 업데이트
5. SQL 서브쿼리
평균보다 큰 값 찾기
SELECT * FROM books
WHERE stock > (SELECT AVG(stock) FROM books);
가장 많이 대출된 도서
SELECT b.* FROM books b
WHERE b.id = (
SELECT book_id
FROM rentals
GROUP BY book_id
ORDER BY COUNT(*) DESC
LIMIT 1
);
XML에서 작성 시:
<select id="findAboveAverage" resultType="BookDto">
SELECT * FROM books
WHERE stock > (SELECT AVG(stock) FROM books)
</select>
6. ALTER (테이블 수정)
필드 추가
ALTER TABLE books ADD COLUMN price INT DEFAULT 0;
필드 타입 수정
ALTER TABLE books MODIFY COLUMN title LONGTEXT;
XML에서 작성:
<update id="addPriceColumn">
ALTER TABLE books ADD COLUMN price INT DEFAULT 0
</update>
🎯 실습 3: 도서 대출/반납 트랜잭션
필요한 파일 구조
src/main/java/
├── AppStart.java
├── controller/
│ └── BookController.java
├── service/
│ └── BookService.java
├── mapper/
│ └── BookMapper.java (인터페이스)
└── dto/
└── RentDto.java
src/main/resources/
└── mapper/
└── BookMapper.xmlStep 1: DTO 작성
public class RentDto {
private int bookId;
private String member;
// getter, setter
}
Step 2: Mapper 인터페이스
@Mapper
public interface BookMapper {
// 1. 재고 조회
int getStock(int bookId);
// 2. 재고 감소
void decreaseStock(int bookId);
// 3. 대출 기록 추가
void insertRental(RentDto rentDto);
// 4. 재고 증가
void increaseStock(int bookId);
// 5. 반납일 업데이트
int updateReturnDate(@Param("bookId") int bookId,
@Param("member") String member);
}
Step 3: XML 작성 (BookMapper.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="패키지경로.BookMapper">
<!-- 재고 조회 -->
<select id="getStock" parameterType="int" resultType="int">
SELECT stock FROM books WHERE id = #{bookId}
</select>
<!-- 재고 감소 -->
<update id="decreaseStock" parameterType="int">
UPDATE books SET stock = stock - 1 WHERE id = #{bookId}
</update>
<!-- 대출 기록 추가 -->
<insert id="insertRental" parameterType="RentDto">
INSERT INTO rentals(book_id, member, rent_date)
VALUES(#{bookId}, #{member}, NOW())
</insert>
<!-- 재고 증가 -->
<update id="increaseStock" parameterType="int">
UPDATE books SET stock = stock + 1 WHERE id = #{bookId}
</update>
<!-- 반납일 업데이트 -->
<update id="updateReturnDate">
UPDATE rentals
SET return_date = NOW()
WHERE book_id = #{bookId}
AND member = #{member}
AND return_date IS NULL
</update>
</mapper>
Step 4: Service 작성 (트랜잭션 적용)
@Service
public class BookService {
@Autowired
private BookMapper bookMapper;
@Transactional // 핵심!
public void rentBook(RentDto rentDto) {
// 1. 재고 확인
int stock = bookMapper.getStock(rentDto.getBookId());
if (stock <= 0) {
throw new RuntimeException("재고가 없습니다");
}
// 2. 재고 감소
bookMapper.decreaseStock(rentDto.getBookId());
// 3. 대출 기록 추가
bookMapper.insertRental(rentDto);
}
@Transactional
public void returnBook(RentDto rentDto) {
// 1. 재고 증가
bookMapper.increaseStock(rentDto.getBookId());
// 2. 반납일 업데이트
int result = bookMapper.updateReturnDate(
rentDto.getBookId(),
rentDto.getMember()
);
if (result == 0) {
throw new RuntimeException("대출 기록이 없습니다");
}
}
}
Step 5: Controller 작성
@RestController
@RequestMapping("/books")
public class BookController {
@Autowired
private BookService bookService;
@PostMapping("/rent")
public String rentBook(@RequestBody RentDto rentDto) {
bookService.rentBook(rentDto);
return "대출 완료";
}
@PostMapping("/return")
public String returnBook(@RequestBody RentDto rentDto) {
bookService.returnBook(rentDto);
return "반납 완료";
}
}
🎯 실습 4: 도서 등록 및 검색 (동적쿼리)
Step 1: DTO 추가
public class SearchDto {
private String member; // 대출자 이름
private String title; // 도서명
// getter, setter
}
Step 2: Mapper 메소드 추가
@Mapper
public interface BookMapper {
// 기존 메소드들...
// 책 단일 등록
int insertBook(BookDto bookDto);
// 대출 기록 검색 (동적쿼리)
List<RentalDto> searchRentals(SearchDto searchDto);
// 책 일괄 등록
int insertBooks(List<BookDto> books);
}
Step 3: XML 작성
<!-- 책 단일 등록 -->
<insert id="insertBook" parameterType="BookDto"
useGeneratedKeys="true" keyProperty="id">
INSERT INTO books(title, stock)
VALUES(#{title}, #{stock})
</insert>
<!-- 대출 기록 검색 (동적쿼리) -->
<select id="searchRentals" resultType="RentalDto">
SELECT r.*, b.title
FROM rentals r
INNER JOIN books b ON r.book_id = b.id
<where>
<if test="member != null and member != ''">
AND r.member LIKE CONCAT('%', #{member}, '%')
</if>
<if test="title != null and title != ''">
AND b.title LIKE CONCAT('%', #{title}, '%')
</if>
</where>
</select>
<!-- 책 일괄 등록 (동적쿼리) -->
<insert id="insertBooks" parameterType="list"
useGeneratedKeys="true" keyProperty="id">
INSERT INTO books(title, stock) VALUES
<foreach collection="list" item="book" separator=",">
(#{book.title}, #{book.stock})
</foreach>
</insert>
Step 4: Service 작성
@Service
public class BookService {
public int addBook(BookDto bookDto) {
bookMapper.insertBook(bookDto);
return bookDto.getId(); // 자동생성된 ID 반환
}
public List<RentalDto> search(SearchDto searchDto) {
return bookMapper.searchRentals(searchDto);
}
@Transactional
public List<Integer> addBooks(List<BookDto> books) {
bookMapper.insertBooks(books);
return books.stream()
.map(BookDto::getId)
.collect(Collectors.toList());
}
}
🎯 실습 5: 테이블 수정 및 서브쿼리
Step 1: Mapper 메소드 추가
@Mapper
public interface BookMapper {
// 기존 메소드들...
// ALTER 테이블 수정
void addPriceColumn();
void modifyTitleColumn();
// 서브쿼리 조회
List<BookDto> findAboveAverageStock();
BookDto findMostRentedBook();
}
Step 2: XML 작성
<!-- 1. price 필드 추가 -->
<update id="addPriceColumn">
ALTER TABLE books ADD COLUMN price INT DEFAULT 0
</update>
<!-- 2. title 타입 수정 -->
<update id="modifyTitleColumn">
ALTER TABLE books MODIFY COLUMN title LONGTEXT
</update>
<!-- 3. 평균 재고보다 많은 도서 조회 (서브쿼리) -->
<select id="findAboveAverageStock" resultType="BookDto">
SELECT * FROM books
WHERE stock > (
SELECT AVG(stock) FROM books
)
</select>
<!-- 4. 가장 많이 대출한 도서 조회 (서브쿼리) -->
<select id="findMostRentedBook" resultType="BookDto">
SELECT b.* FROM books b
WHERE b.id = (
SELECT book_id
FROM rentals
GROUP BY book_id
ORDER BY COUNT(*) DESC
LIMIT 1
)
</select>
Step 3: Service 작성
@Service
public class BookService {
public void modifyTable() {
bookMapper.addPriceColumn();
bookMapper.modifyTitleColumn();
}
public List<BookDto> getAboveAverageBooks() {
return bookMapper.findAboveAverageStock();
}
public BookDto getMostRentedBook() {
return bookMapper.findMostRentedBook();
}
}
🔍 자주 하는 실수와 해결법
1. XML namespace 오류
❌ <mapper namespace="BookMapper">
✅ <mapper namespace="com.example.mapper.BookMapper">2. parameterType 경로 오류
❌ parameterType="BookDto"
✅ parameterType="com.example.dto.BookDto"
또는 application.properties에 type-aliases-package 설정3. 동적쿼리 조건 작성
❌ <if test="name != null">
✅ <if test="name != null and name != ''">4. FOREACH에서 객체 필드 접근
❌ (#{title}, #{stock})
✅ (#{book.title}, #{book.stock})5. 트랜잭션 롤백 안됨
❌ try-catch로 예외 처리
✅ RuntimeException을 던져야 롤백됨💡 실습 체크리스트
실습 3 체크리스트
- RentDto 작성
- BookMapper 인터페이스 (메소드 4개)
- BookMapper.xml 작성
- BookService (@Transactional 적용)
- BookController (POST 2개)
- 재고 0일 때 예외 처리
- 대출 기록 없을 때 예외 처리
실습 4 체크리스트
- SearchDto 작성
- insertBook (useGeneratedKeys)
- searchRentals (동적쿼리 if)
- insertBooks (동적쿼리 foreach)
- Service에 @Transactional
실습 5 체크리스트
- addPriceColumn (ALTER ADD)
- modifyTitleColumn (ALTER MODIFY)
- findAboveAverageStock (서브쿼리)
- findMostRentedBook (서브쿼리 + GROUP BY)
🚀 테스트 방법
Postman으로 테스트
POST http://localhost:8080/books/rent
Content-Type: application/json
{
"bookId": 1,
"member": "홍길동"
}SQL로 직접 확인
-- 대출 전후 재고 확인
SELECT * FROM books WHERE id = 1;
-- 대출 기록 확인
SELECT * FROM rentals WHERE book_id = 1;
이 가이드를 따라 단계별로 구현하면 실습 3~5를 완성할 수 있습니다!
각 단계마다 테스트하면서 진행하세요. 화이팅! 💪
📥 파일 다운로드
위 내용을 복사하여 Spring_MyBatis_실습_가이드.md 파일로 저장하여 사용하세요.
반응형
'데이터베이스 > 마이에스큐엘' 카테고리의 다른 글
| [MySQL] Alter, 서브쿼리, 트랜잭션, 뷰 개념 및 연습문제 (0) | 2025.10.16 |
|---|---|
| [MySQL] 실습 3~5 가이드라인 (0) | 2025.10.15 |
| [MySQL] MySQL 주요 문법 및 키워드 정리 (6) | 2025.08.12 |