본문 바로가기
데이터베이스/마이에스큐엘

[MySQL] SQL & MyBatis 동적 쿼리 핵심 문법 & 실습 문제 풀이

by AI읽어주는남자 2025. 10. 15.
반응형

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

Step 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 파일로 저장하여 사용하세요.

반응형