10월 28일 금요일 [코드로 배우는 스프링웹프로젝트] -3day(3)
Chapter 08. 영속/비즈니스 계층의 CRUD 구현
영속 계층(MyBatis쓰는 부분)의 작업의 순서
1) 테이블의 컬럼 구조를 반영하는 VO 클래스 생성
2) MyBatis의 Mapper 인터페이스 작성 / XML 처리
3) 작성한 Mapper 인터페이스 테스트
영속 계층의 구현 준비
모든 웹 어플리케이션의 최종 목적은 데이터베이스에 데이터를 기록하거나, 원하는 데이터를 가져오는 것이 목적 ==> 어느 정도의 설계가 진행되면 데이터베이스 관련 작업을 하게 됨
VO 클래스 작성
테이블 설계를 기준으로 작성하면 됨 => Lombok의 @Data를 이용해 생성자, get, set, toString()등을 만들어내는 방식을 사용함
package org.zerock.domain;
import java.util.Date;
import lombok.Data;
@Data
public class BoardVO {
private Long bno; //게시물 번호
private String title; //제목
private String content; //내용
private String writer; //작성자
private Date regdate; //생성 시간
private Date updateDate; //최종 수정 시간
}
Mapper 인터페이스와 Mapper XML
BoardMapper 인터페이스
root-context.xml에서 <mabatis-spring:scan> 태그를 이용하여 "org.zerock.mapper" 패키지를 스캔하도록 설정
이를 활용해서 패키지를 만들고, BoardMapper 인터페이스 추가하기
package org.zerock.mapper;
import java.util.List;
import org.apache.ibatis.annotations.Select;
import org.zerock.domain.BoardVO;
public interface BoardMapper {
@Select("select * from tbl_board where bno > 0") //게시물 번호가 0보다 큰 데이터 전부 가져오기
public List<BoardVO> getList();
}
이미 작성된 BoardVO 클래스를 활용해서, 필요한 SQL을 어노테이션의 속성값으로 처리할 수 있음
SQL Developer 에서 먼저 실행해서 쿼리문이 잘 작성되었는지 확인해보기!
BoardMapperTest 클래스
작성된 BoardMapper 인터페이스를 테스트할 수 있게 만드는 클래스
package org.zerock.mapper;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.test.context.ContextConfiguration;
import org.springframework.test.context.junit4.SpringJUnit4ClassRunner;
import lombok.Setter;
import lombok.extern.log4j.Log4j;
@RunWith(SpringJUnit4ClassRunner.class)
@ContextConfiguration("file:src/main/webapp/WEB-INF/spring/root-context.xml")
@Log4j
public class BoardMapperTests {
@Setter(onMethod_=@Autowired)
private BoardMapper mapper;
@Test
public void testGetList() {
mapper.getList().forEach(board -> log.info(board));
}
}
테스트 클래스는 스프링을 이용해서 BoardMapper 인터페이스의 구현체를 주입받아서 동작한다.
testGetList()의 결과는 SQL Developer에서 실행된 것과 동일해야만 정상적으로 동작함
Mapper XML 파일
src/main/resources 안에, 'org.zerock.mapper' 단계의 폴더를 만들기 (폴더 하나씩 만들기)
BoardMapper.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="org.zerock.mapper.BoardMapper">
<select id="getList" resultType="org.zerock.domain.BoardVO">
<![CDATA[
select * from tbl_board where bno > 0
]]>
</select>
</mapper>
<mapper>의 namespace 속성값을 Mapper인터페이스와 동일한 이름으로 만들어야 함
<select> 태그의 id 속성값은 메서드의 이름과 일치해야 함
resultType 속성값은 select 쿼리의 결과를 특정 클래스의 객체로 만들기 위해 설정함
XML에 사용한 CDATA 섹션 부분은 XML에서 부등호를 사용하기 위해 사용함
* CDATA 섹션 : 쿼리문을 일반적으로 분석해서 처리하는 경우가 많음. 문자를 분석해서 처리한다는 개념. 그러나 CDATA 섹션으로 묶으면 안에 있는 쿼리를 있는 그대로 처리함 (대표적으로 부등호 때문에 영역으로 묶은 것), 부등호를 XML내에서 처리하면 html의 닫는 기호로 처리되기 때문에, 헷갈리지 않기 위해서 영역으로 묶은 것임
XML에서 SQL 문을 만들어 놓았으므로, BoardMapper 인터페이스의 SQL문은 주석처리함
영속 영역의 CRUD 구현
영속 영역은 기본적으로 CRUD 작업을 하는 부분이기 때문에, 비즈니스 로직과 무관하게 CRUD 작업을 작성할 수 있다.
MyBatis는 JDBC의 PreparedStatement를 활용하여 필요한 파라미터를 처리하는 '?' 에 대해서 '${속성}'을 이용해서 처리함
create(insert) 처리
tbl_board 테이블은 프라이머리키로 bno를 이용하고, 시퀀스를 이용해서 자동으로 데이터가 추가될 때 번호가 만들어지는 방식을 사용
이렇게 자동으로 PK 값이 정해지는 경우 다음과 같은 2가지 방식으로 처리함
- insert만 처리되고 생성된 PK 값을 알 필요가 없는 경우
- insert가 실행되고 생성된 PK 값을 알아야 하는 경우
그래서 BoardMapper 인터페이스는 다음과 같이 메서드를 추가 선언함
//BoardMapper 인터페이스에 추가
public void insert(BoardVO board); //insert만 처리. bno 값 몰라도됨
public void insertSelectKey(BoardVO board); //insert가 처리되며 bno 값 알아야 함
BoardMapper.xml에 내용 추가
<insert id="insert">
insert into tbl_board(bno, title, content, writer)
values (seq_board.nextval, #{title}, #{content}, #{writer})
</insert>
<insert id="insertSelectKey">
<selectKey keyProperty="bno" order="BEFORE" resultType="long">
select seq_board.nextval from dual
</selectKey>
insert into tbl_board(bno, title, content, writer)
values (seq_board.nextval, #{title}, #{content}, #{writer})
</insert>
* insert() : 단순히 시퀀스의 다음 값을 구해서 insert 할 때 사용. insert()는 데이터가 변경되었는지만을 알려줌. 추가된 데이터의 PK값(bno)을 알 수는 없음
* insertSelectKey() : @SelectKey라는 MyBatis의 어노테이션을 사용. @Insert 할 때 SQL 문을 보면 #{bno}와 같이 이미 처리 된 결과를 이용함
@SelectKey
PK값을 미리(before) SQL 문을 통해 처리해 두고 특정한 이름으로 결과를 보관하는 방식. @Insert 할 때 SQL 문을 보면 #{bno}와 같이 이미 처리 된 결과를 이용함. @SelectKey를 이용하는 방식은 쿼리를 한 번 더 실행하는 부담이 있지만, 자동으로 추가되는 PK 값을 확인해야하는 상황에서 유용함
BoardMapperTests 클래스에 insert()에 대한 테스트 코드 작성
@Test
public void testInsert() {
BoardVO board = new BoardVO();
board.setTitle("새로 작성하는 글");
board.setContent("새로 작성하는 내용");
board.setWriter("newbie");
mapper.insert(board);
log.info(board);
}
결과물 : toString()을 이용해서 bno의 값을 알아보면, null로 비어있음을 확인
INFO : org.zerock.mapper.BoardMapperTests - BoardVO(bno=null, title=새로 작성하는 글, content=새로 작성하는 내용, writer=newbie, regdate=null, updateDate=null)
BoardMapperTests에 insertSelectKey()에 대한 테스트 코드 작성
@Test
public void testInsertSelectKey() {
BoardVO board = new BoardVO();
board.setTitle("새로 작성하는 글 select key");
board.setContent("새로 작성하는 내용 select key");
board.setWriter("newbie");
mapper.insertSelectKey(board);
log.info(board);
}
결과물 : 'select'가 먼저 돌아서 bno 값으로 처리되어 파라미터로 전달된 BoardVO의 bno 값이 지정된 것을 볼 수 있음
@SelectKey를 이용하는 방식은 쿼리를 한 번 더 실행하는 부담이 있지만, 자동으로 추가되는 PK 값을 확인해야하는 상황에서 유용함
INFO : org.zerock.mapper.BoardMapperTests - BoardVO(bno=7, title=새로 작성하는 글 select key, content=새로 작성하는 내용 select key, writer=newbie, regdate=null, updateDate=null)
read(select) 처리
insert가 된 데이터를 조회하는 작업은 PK를 이용해서 처리함
//BoardMapper 인터페이스에 추가
public BoardVO read(Long bno); //insert가 된 데이터를 조회하는 작업
//BoardMapper.xml에 추가
<select id="read" resultType="org.zerock.domain.BoardVO">
select * from tbl_board where bno = #{bno}
</select>
MyBatis는 Mapper 인터페이스의 리턴 타입에 맞게 select의 결과를 처리함 => tbl_board의 모든 칼럼은 BoardVO의 'bno,title,content,writer,regdate,updateDate' 속성값으로 처리됨
MyBatis는 bno라는 칼럼이 존재하면 인스턴스의 'setBno()'를 호출 => #{속성}이 1개만 존재하는 경우 get은 쓰지 않고 set만 호출
//BoardMapperTests 클래스 추가
@Test
public void testRead() {
//존재하는 게시물 번호로 테스트
BoardVO board = mapper.read(5L);
log.info(board);
}
mapper.read()를 호출할 경우, 현재 테이블에 있는 bno(게시글번호)의 값으로 넣어야 함. 없는 게시글 번호 넣으면 작동X
결과물
INFO : org.zerock.mapper.BoardMapperTests - BoardVO(bno=5, title=테스트 제목, content=테스트 내용, writer=user00, regdate=Mon Oct 31 08:22:10 KST 2022, updateDate=Mon Oct 31 08:22:10 KST 2022)
delete 처리
특정 데이터를 삭제하는 작업 또한 PK를 이용해서 처리하므로 조회하는 작업과 유사함
등록, 삭제, 수정과 같은 DML 작업은 몇 건의 데이터가 삭제 또는 수정 되었는지 반환할 수 있음
delete()의 메서드 리턴 타입은 int로 지정 => 정상적으로 데이터 삭제되면 '1 이상'의 값을 가짐 // 해당 게시물이 없으면 '0' 출력
//BoardMapper 인터페이스
public int delete(Long bno);
//BoardMapper.xml
<delete id="delete">
delete from tbl_board where bno = #{bno}
</delete>
//BoardMapperTests
@Test
public void testDelete() {
log.info("DELETE COUNT: " + mapper.delete(3L));
}
update 처리
게시물의 업데이트는 제목, 내용, 작성자를 수정한다고 가정
업데이트 할 때는 최종 수정 시간을 데이터베이스 내 현재 시간으로 수정
Update는 delete와 마찬가지로 '몇 개의 데이터가 수정되었는가'를 처리할 수 있게 int 타입으로 메서드를 설계함
//BoardMapper 인터페이스
public int update(BoardVO board);
//BoardMapper.xml
<update id="update">
update tbl_board set title=#{title}, content=#{content}, writer=#{writer}, updateDate=sysdate
where bno = #{bno}
</update>
updateDate는 최종 수정 시간을 의미하기 때문에 현재 시간인 sysdate로 변경
regdate는 최초 생성 시간이기 때문에 건드리지 않음
#{title},#{content},#{writer}와 같은 부분은 BoardVO 객체의 get 메서드들을 호출해서 파라미터 처리
테스트 코드는 직접 BoardVO 객체를 생성해서 처리함
//BoardMapperTests
@Test
public void testUpdate() {
BoardVO board = new BoardVO();
//실행 전 존재하는 번호인지 확인하기
board.setBno(5L);
board.setTitle("수정된 제목");
board.setContent("수정된 내용");
board.setWriter("user000");
int count = mapper.update(board);
log.info("UPDATE COUNT: " + count);
}
데이터베이스에 5번 글이 존재할 경우, 밑에와 같이 로그가 출력됨
INFO : jdbc.sqltiming - update tbl_board set title='수정된 제목', content='수정된 내용', writer='user000', updateDate=sysdate
where bno = 5
{executed in 21 msec}