Sangwon Coding

8. 영속/비즈니스 계층의 CRUD 구현 본문

Spring/코드로 배우는 스프링 웹 프로젝트

8. 영속/비즈니스 계층의 CRUD 구현

SW1 2019. 11. 17. 22:12

안녕하세요! 저번 포스팅에 이어서 바로 진행하도록 하겠습니다!

 

영속 계층의 작업은 항상 다음과 같은 순서로 진행합니다.

  • 테이블의 칼럼 구조를 반영하는 VO(Value Object) 클래스의 생성
  • MyBatis의 Mapper 인터페이스의 작성/XML 처리
  • 작성한 Mapper 인터페이스의 테스트

이제 영속 계층의 구현 준비를 해보겠습니다.

거의 모든 웹 애플리케이션의 최종 목적은 데이터베이스에 데이터를 기록하거나, 원하는 데이터를 가져오는 것이 목적이기 때문에 개발할 때 어느 정도의 설계가 진행되면 데이터베이스 관련 작업을 하게 됩니다.

 

프로젝트에 '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 어노테이션을 적용합니다.

 

이제 Mapper 인터페이스를 작성해볼건데 Mapper 인터페이스를 작성할 때는 리스트(select)와 등록(insert) 작업을 우선해서 작성합니다. org.zerock.mapper 패키지를 작성하고, BoardMapper 인터페이스를 추가합니다.

 

 

package org.zerock.mapper;

import java.util.List;

import org.zerock.domain.BoardVO;
import org.zerock.domain.Criteria;

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)를 이용하도록 유도하는 조건입니다.

 

작성된 BoardMapper 인터페이스를 테스트 할 수 있게 테스트 환경인 'src/test/java' 'org.zerock.mapper' 패키지를 작성하고 BoardMapperTests 클래스를 추가합니다.

 

 

package org.zerock.mapper;
import java.util.List;

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 org.zerock.domain.BoardVO;
import org.zerock.domain.Criteria;

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를 이용해서 테스트가 완료되었다면 src/main/resources 내에 패키지와 동일한 org/zerock/mapper 단계의 폴더를 생성하고 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에 SQL문이 처리되었으니 BoardMapper 인터페이스에 SQL은 주석처리하고 실행해보면 동일한 결과가 나오는 것을 확인하실 수 있을겁니다!

 

본격적으로 영속 영역의 CRUD 구현을 해보겠습니다.

웹 프로젝트 구조에서 마지막 영역이 영속 영역이지만, 실제로 구현을 가장 먼저 할 수 있는 영역도 영속 영역입니다. 영속 영역은 기본적으로 CRUD 작업을 하기 때문에 테이블과 VO(DTO) 등 약간의 준비만으로도 비즈니스 로직과 무관하게 CRUD 작업을 작성할 수 있습니다. MyBatis는 내부적으로 JDBC의 PreparedStatement를 활용하고 필요한 파라미터를 처리하는 '?'에 대한 치환은 '#{속성}'을 이용해서 처리합니다.

 

1. Create(insert) 처리

 

BoardMapper 인터페이스에는 위의 상황들을 고려해서 다음과 같이 메서드를 추가 선언합니다.

 

package org.zerock.mapper;

import java.util.List;

import org.zerock.domain.BoardVO;
import org.zerock.domain.Criteria;

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}와 같이 이미 처리된 결과를 이용하는 것을 볼 수 있습니다.

 

테스트를 하기 위해 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);
 }

 

 

bno에 데이터가 들어간 것을 확인할 수 있고, @SelectKey를 이용하는 바익은 SQL을 한 번 더 실행하는 부담이 있긴 하지만 자동으로 추가되는 PK를 확인해야 하는 상황에서는 유용하게 사용될 수 있습니다.

 

 

2. Read(select) 처리

 

insert가 된 데이터를 조회하는 작업은 PK를 이용해서 처리하므로 BoardMapper의 파라미터 역시 BoardVO 클래스의 bno 타입 정보를 이용해서 처리합니다.

 

BoardMapper 인터페이스에 추가

 

package org.zerock.mapper;

import java.util.List;

import org.zerock.domain.BoardVO;
import org.zerock.domain.Criteria;

public interface BoardMapper {

	//@Select("select * from tbl_board where bno > 0")
	public List<BoardVO> getList();
	
	public List<BoardVO> getListWithPaging(Criteria cri);
	
	public void insert(BoardVO board);
	
	public void insertSelectKey(BoardVO board);
	
	public BoardVO read(Long bno);
    
}

 

BoardMapper.xml에 추가

 

	<select id="read" resultType="org.zerock.domain.BoardVO">
		select * from tbl_board where bno =
		#{bno}
	</select>

 

BoardMapperTests 클래스에 테스트 코드 추가

 

 @Test
 public void testRead() {
	 BoardVO board = mapper.read(122942L);
	 
	 log.info(board);
 }

 

 

 

3. Delete 처리

 

특정한 데이터를 삭제하는 작업 역시 PK 값을 이용해서 처리하므로 조회하는 작업과 유사하게 처리합니다. 등록, 삭제, 수정과 같은 DML 작업은 '몇 건의 데이터가 삭제(혹은 수정)되었는지'를 반환할 수 있습니다.

 

BoardMapper 인터페이스에 추가

 

package org.zerock.mapper;

import java.util.List;

import org.zerock.domain.BoardVO;
import org.zerock.domain.Criteria;

public interface BoardMapper {

	//@Select("select * from tbl_board where bno > 0")
	public List<BoardVO> getList();
	
	public List<BoardVO> getListWithPaging(Criteria cri);
	
	public void insert(BoardVO board);
	
	public void insertSelectKey(BoardVO board);
	
	public BoardVO read(Long bno);
	
	public int delete(Long bno);
	
}

 

BoardMapper.xml에 추가

 

	<delete id="delete">
		delete tbl_board where bno = #{bno}
	</delete>

 

delete()의 메서드 리턴 타입은 int로 지정해서 만일 정상적으로 데이터가 삭제되면 1 이상의 값을 가지도록 작성합니다. 테스트 코드는 현재 테이블에 존재하는 번호의 데이터를 삭제해 보고 '1'이라는 값이 출력되는지 확인합니다. 만일 해당 번호의 게시물이 없다면 '0'이 출력됩니다.

 

BoardMapperTests 클래스에 테스트 코드 추가

 

 @Test
 public void testDelete() {
	 
	 log.info("DELETE COUNT: " + mapper.delete(122942L));
 }

 

 

4. Update 처리

 

마지막으로 update를 처리합니다. 게시물의 업데이트는 제목, 내용, 작성자를 수정한다고 가정합니다. 업데이트할 때는 최종 수정시간을 데이터베이스 내 현재 시간으로 수정합니다. Update는 delete와 마찬가지로 '몇 개의 데이터가 수정되었는가'를 처리할 수 있게 int 타입으로 메서드를 설계할 수 있습니다.

 

BoardMapper 인터페이스에 추가

 

package org.zerock.mapper;

import java.util.List;

import org.zerock.domain.BoardVO;
import org.zerock.domain.Criteria;

public interface BoardMapper {

	//@Select("select * from tbl_board where bno > 0")
	public List<BoardVO> getList();
	
	public List<BoardVO> getListWithPaging(Criteria cri);
	
	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에 추가

 

	<update id="update">
		update tbl_board set title=#{title}, content=#{content}, writer=#{writer},
		updateDate = sysdate where bno = #{bno}
	</update>

 

SQL에서 주의 깊게 봐야 하는 부분은 update 칼럼이 최종 수정시간을 의미하는 칼럼이기 때문에 현재 시간으로 변경해 주고 있다는 점과, regdate 칼럼은 최초 생성 시간이므로 건드리지 않는다는 점입니다. #{title}과 같은 부분은 파라미터로 전달된 BoardVO 객체의 getTitle()과 같은 메서드들을 호출해서 파라미터들이 처리됩니다.

 

테스트 코드는 read()를 이용해서 가져온 BoardVO 객체의 일부를 수정하는 방식이나 직접 BoardVO 객체를 생성해서 처리할 수 있습니다. 예제는 객체를 생성해서 테스트를 진행합니다.

 

BoardMapperTests 클래스에 테스트 코드 추가

 

 @Test
 public void testUpdate() {
	 BoardVO board = new BoardVO();
	 board.setBno(122941L);	// 존재하는 번호인지 먼저 확인
	 board.setTitle("수정된 제목");
	 board.setContent("수정된 내용");
	 board.setWriter("user00");
	 
	 int count = mapper.update(board);
	 log.info("UPDATE COUNT: " + count);
 }

 

 

 

 

이상으로 CRUD 구현 포스팅을 마치고 다음 포스팅은 비즈니스 계층 주제로 돌아오겠습니다!

Comments