Part3 - 영속/비즈니스 계층의 CRUD 구현 Chapter08
PART 3의 예제는 단순한 하나의 테이블만을 이용하기 때문에 데이터베이스에 테이블, 시퀀스, 약간의 데이터들이 생성되었다면 이제는 코드를 이용해서 데이터에 대한 CRUD(Create, Read, Update, Delete) 작업을 진행 합니다.
- 테이블의 칼럼 구조를 반영하는 VO(Value Object) 클래스의 생성
- MyBatis의 Mapper 인터페이스의 작성/XML 처리
- 작성한 Mapper 인터페이스의 테스트
위의 과정 전에 먼저 JDBC 연결을 테스트 하는 과정을 거치는 것이 좋치만 SQL Developer의 연결 자체가 이미 JDBC 연결을 이용하기 때문에 예제에서는 별도의 과정을 생략하고 진행 하겠습니다.
8.1 영속 계층의 구현 준비
거의 모든 웹 애플리케이션의 최종 목적은 데이터베이스에 데이터를 기록하거나, 원하는 데이터를 가져오는 것이 목적이기 때문에 개발할 때 어느 정도의 설계가 진행되면 데이터베이스 관련 작업을 하게 됩니다.
8.1.1 VO 클래스의 작성
VO 클래스를 생성하는 작업은 테이블 설계를 기준으로 작성하면 됩니다.
현재 tbl_board 테이블의 구성은 아래와 같습니다.
프로젝트에 org.zerock.domain 패키지를 생성하고, BoardVO 클래스를 정의합니다.
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;
}
BoardVO 클래스는 Lombok을 이용해서 생성자와 getter/setter, toString() 등을 만들어내는 방식을 사용 합니다.
이를 위해서 @Data 어노테이션을 적용합니다.
8.1.2 Mapper 인터페이스와 Mapper XML
PART1에서 봤지만 MyBatis는 SQL을 처리하는데 어노테이션이나 XML을 이용할 수 있습니다.
간단한 SQL 이라면 어노테이션을 이용해서 처리하는 것이 무난하지만, SQL이 점점 복잡해지고 검색과 같이 상황에 따라 다른 SQL문이 처리되는 경우에는 어노테이션은 그다지 유용하지 못하다는 단점이 있습니다.
XML의 경우 단순 텍스트를 수정하는 과정으로 처리가 끝나지만, 어노테이션의 경우 코드를 수정하고 다시 빌드하는 등의 유지보수성이 떨어지는 이유로 기피하는 경우도 종종 있습니다.
Mapper 인터페이스
root-context.xml은 PART 1 에서 org.zerock.mapper 패키지를 스캔(조사) 하도록 이미 설정 해 본 적이 있으므로 이를 활용해서 프로젝트를 제작 합니다.
Mapper 인터페이스를 작성할 때는 리스트(select)와 등록(insert) 작업을 우선해서 작성합니다.
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")
public List<BoardVO> getList();
}
BoardMapper 인터페이스를 작성할 때는 이미 작성된 BoardVO 클래스를 적극적으로 활용해서 필요한 SQL을 어노테이션의 속성값으로 처리할 수 있습니다.
(SQL을 작성할때는 반드시 " ; " 없이 작성 합니다.)
SQL 뒤에 where bno > 0 과 같은 조건은 테이블을 검색하는데 bno라는 칼럼 조건을 주어서 Primary Key(이하 PK)를 이용하도록 유도하는 조건 입니다.
(이에 대한 자세한 설명은 페이징 처리에서 하겠습니다.)
SQL Developer에서 먼저 실행해서 결과를 확인 합니다.
SQL Developer에서 먼저 확인하는 이유는
- SQL이 문제가 없이 실행 가능한지를 확인.
- 데이터베이스의 commit을 하지 않았다면 나중에 테스트 결과가 달라지기 때문에 이를 먼저 비교
작성된 BoardMapper 인터페이스를 테스트 할 수 있게 테스트 환경인 src/test/java에 org.zerock.mappper 패키지를 작성하고 BoardMapperTests 클래스를 추가 합니다.
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));
}
}
BoardMapperTests 클래스는 스프링을 이용해서 BoardMapper 인터페이스의 구현체를 주입받아서 동작하게 됩니다.
JAVA 설정 시에는 RootConfig 클래스를 이용해서 스프링의 설정을 이용하고 있음을 명시합니다.
testGetList()의 결과는 SQL Developer에서 실행된 것과 동일해야만 정상적으로 동작한 것입니다.
실행 결과는 아래와 같이 보입니다.
Mapper XML 파일
BoardMapperTests를 이용해서 테스트가 완료되었다면 src/main/resources 내에 패키지와 동일한 org/zerock/mapper 단계의 폴더를 생성하고 XML 파일을 작성합니다.
(폴더를 한번에 생성하지 말고 하나씩 생성해야 합니다.)
파일의 폴더 구조나 이름은 무방하지만 패키지와 클래스 이름과 동일하게 해주면 나중에 혼란스러운 상황을 피할 수 있습니다.
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>
XMl을 작성할 때는 반드시 <mapper>의 namespace속성값을 Mapper 인터페이스와 동일한 이름을 주는 것에 주의하고, <select> 태그의 id속성값은 메서드의 이름과 일치하게 작성 해야 합니다.
resultType 속성의 값은 select 쿼리의 결과를 특정 클래스의 객체로 만들기 위해 설정 합니다.
XML에 사용한 CDATA 부분은 XML에서 부등호를 사용하기 위해서 사용 합니다.
XML에 SQL문이 처리 되었으니 BoardMapper 인터페이스에 SQL은 제거 합니다.
수정 후에는 반드시 테스트 코드를 이용해 기존과 동일하게 동작 하는지 확인해야 합니다.
8.2 영속 영역의 CRUD 구현
웹 프로젝트 구조에서 마지막 영역이 영속 영역이지만, 실제로 구현을 가장 먼저 할 수 있는 영역도 영속 영역 입니다.
영속영역은 기본적으로 CRUD 작업을 하기 떄문에 테이블과 VO(DTO) 등 약간의 준비만으로 비즈니스 로직과 무관하게 CRUD 작업을 작성할 수 있습니다.
MyBatis는 내부적으로 JDBC의 PreparedStatement를 활용하고 필요한 파라미터를 처리하는 "?"에 대한 치환은 '#{속성}'을 이용해서 처리 합니다.
8.2.1 create(insert) 처리
tbl_board 테이블은 PK 칼럼으로 bno를 이용하고, 시퀀스를 이용해서 자동으로 데이터가 추가될 때 번호가 만들어지는 방식을 사용합니다.
이처럼 자동으로 PK 값이 정해지는 경우에는 다음과 같은 2가지 방식으로 처리할 수 있습니다.
- insert만 처리되고 생성된 PK 값을 알 필요가 없는 경우
- insert문이 실행되고 생성된 PK 값을 알아야 하는 경우
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")
public List<BoardVO> getList();
//추가
public void insert(BoardVO board);
//추가
public void insertSelectKey(BoardVO board);
}
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>
<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 (#{bno},
#{title}, #{content}, #{writer})
</insert>
</mapper>
BoardMapper의 insert()는 단순히 시퀀스의 값을 구해서 insert 할 때 사용 합니다.
insert문은 몇 건의 데이터가 변경되었는지만을 알려주기 때문에 추가된 데이터의 PK 값을 알 수 없지만, 1번의 SQL 처리만으로도 작업이 완료되는 장점이 있습니다.
insertSelectKey()는 @SelectKey라는 MyBatis의 어노테이션을 사용합니다.
@SelectKey는 주로 PK 값을 미리(before) SQL을 통해서 처리해 두고 특정한 이름으로 결과를 보관하는 방식입니다.
@Insert 할 때 SQL문을 보면 #{bno}와 같이 이미 처리 된 결과를 이용하는 것을 볼 수 있습니다.
우선 insert()에 대한 테스트 코드를 src/test/java 내에 BoardMapperTests 클래스에 새로운 메소드로 작성해 보면 다음과 같습니다.
@Test
public void testInsert() {
BoardVO board = new BoardVO();
board.setTitle("새로 작성하는 글");
board.setContent("새로 작성하는 글 내용");
board.setWriter("newbie");
mapper.insert(board);
log.info(board);
}
테스트 코드 마지막에 log.info(board)를 작성한 이유는 Lombok이 만들어주는 toString()을 이용해서 bno 맴버변수(인스턴스 변수)의 값을 알아보기 위함입니다.
testInsert()의 실행결과 일부는 다음과 같습니다.
테스트 결과의 마지막을 살펴보면 BoardVO 클래스의 toString()의 결과가 출력되는 것을 볼 수 있는데, bno의 값이 null로 비어있는것을 확인할 수 있습니다.
@SelectKey를 이용하는 경우 테스트 코드는 아래와 같습니다.
@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);
}
testInsertSelectKey()의 테스트 결과의 일부는 아래와 같습니다.
실행되는 결과를 살펴보면 select seq_board.nextval from dual 과 같은 쿼리가 먼저 실행되고
여기에서 생성된 결과를 이용해서 bno 값으로 처리되는 것을 볼 수 있습니다.
BoardMapper의 insertSelectKey()의 @Insert 문의 SQL을 보면
insert into tbl_board(board,title,context,writer) values(#{bno},#{title},#{content},#{writer}) 와 같이 파라미터로 전달되는 BoardVO의 bno 값을 사용하게 되어 있습니다.
테스트 코드의 마지막 부분을 보면 BoardVO 객체의 bno 값이 이전과 달리 지정된 것을 볼 수 있습니다.
(시퀀스 값이라 각자 다른 값이 나올 수 있습니다)
@SelectKey를 이용하는 방식은 SQL을 한번 더 실행하는 부담이 있기는 하지만 자동으로 추가되는 PK값을 확인해야 하는 경우에서는 유용하게 사용 할 수 있습니다.
8.2.2 read(select) 처리
insert가 된 데이터를 조회하는 작업은 PK를 이용해서 처리하므로 BoardMapper의 파라매터 역시 BoardVO 클래스의 bno 타입 정보를 이용해서 처리합니다.
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")
public List<BoardVO> getList();
public void insert(BoardVO board);
public void insertSelectKey(BoardVO board);
public BoardVO read(Long bno);
}
BoardMappper.xml에 read를 추가 해줍니다.
<select id="read" resultType="org.zerock.domain.BoardVO">
select * from tbl_board where bno = #{bno}
</select>
Mybatis는 Mapper 인터페이스의 리턴 타입에 맞게 select의 결과를 처리하기 때문에
tbl_board의 모든 칼럼은 BoardVO의 bno,title,context,writer,regdate,updateDate 속성값으로 처리 됩니다.
좀 더 엄밀히 말하면 MyBatis는 bno라는 칼럼이 존재하면 인스턴스의 'setBno()'를 호출하게 됩니다.
MyBatis의 모든 파라미터와 리턴 타입의 처리는 get 파라미터명(), set 파라미터명()의 규칙으로 호출됩니다.
다만 위와 같이 #{속성}이 1개만 존재하는 경우에도 별도의 get 파라미터명()을 사용하지 않고 처리 됩니다.
현재 테이블에 존재하는 데이터의 bno 칼럼의 값을 이용해서 테스트 코드를 작성 합니다.
SQL Developer에서 select한 결과 bno 23번이 존재 하므로 23번으로 해보겠습니다.
@Test
public void testRead() {
//존재하는 게시물 번호로 테스트
BoardVO board = mapper.read(23L);
log.info(board);
}
mapper.read()를 호출한 결과는 아래와 같습니다.
8.2.3 delete 처리
특정한 데이터를 삭제하는 작업 역시 PK 값을 이용해서 처리하므로 조회하는 작업과 유사하게 처리 합니다.
등록, 삭제, 수정과 같은 DML 작업은 몇 건의 데이터가 삭제(혹은 수정) 되었는지를 반환할 수 있습니다.
public int delete(Long bno);
<delete id="delete">
delete from tbl_board where bno = #{bno}
</delete>
delete()의 메소더 리턴 타입은 int로 지정해서 만일 데이터가 정상적으로 삭제가 되면 1 이상의 값을 가지도록 작성 합니다.
테스트 코드는 현재 테이블에 존재하는 번호의 데이터를 삭제해 보고 1이라는 값이 출력 되는지를 확인 합니다.
만일 삭제할 게시물이 없다면 0을 반환 합니다.
@Test
public void testDelete() {
log.info("Delete Count "+mapper.delete(23L));
}
testDelete()의 경우 23번 데이터가 존재 했다면 아래와 같은 로그가 기록 되게 됩니다.
8.2.4 update 처리
마지막으로 update처리를 합니다.
게시물의 업데이트는 제목, 내용, 작성자를 수정한다고 가정 합니다.
업데이트 할 떄는 최종 수정 시간 칼럼(updatedate)도 현재 시간으로 수정해줍니다.
update는 delete와 마찬가지로 몇 개의 데이터가 수정 되었는지를 int형 변수로 받을수 있습니다.
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")
public List<BoardVO> getList();
public void insert(BoardVO board);
public void insertSelectKey(BoardVO board);
public BoardVO read(Long bno);
public int delete(Long bno);
public int update(BoardVO board);
}
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>
<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 (#{bno},
#{title}, #{content}, #{writer})
</insert>
<select id="read" resultType="org.zerock.domain.BoardVO">
select * from tbl_board where bno = #{bno}
</select>
<delete id="delete">
delete from tbl_board where bno = #{bno}
</delete>
<update id="update">
update tbl_board
set title = #{title},
content = #{content},
writer = #{writer},
updateDate = sysdate
where bno = #{bno}
</update>
</mapper>
SQL에서 주의 깊게 봐야 하는 부분은 updateDate 칼럼이 최종 수정 시간을 의미하는 칼럼이기 때문에 현재 시간(sysdate)로 변경해주고 있다는 점과,
regdate 칼럼은 최초 생성 시간이므로 건드리지 않는다는 점 입니다.
#{title}과 같은 부분은 파라미터로 전달된 BoardVO 객체의 getTitle()과 같은 메서드들을 호출해서 파라미터들이 처리 됩니다.
테스트 코드는 BoardVO 객체를 생성해서 23번 글을 update 해주겠습니다.
@Test
public void testUpdate() {
BoardVO board = new BoardVO();
board.setBno(23L);
board.setTitle("수정된 제목");
board.setContent("수정된 내용");
board.setWriter("jeon");
int count = mapper.update(board);
log.info("Update Count : "+count);
}
23번 데이터가 존재 한다면 아래와 같은 로그가 출력 됩니다.