For Programmer

Part3 - 스프링 MVC프로젝트의 기본 구성(2) 본문

Spring/스프링 프로젝트

Part3 - 스프링 MVC프로젝트의 기본 구성(2)

유지광이 2020. 9. 2. 17:46
728x90

영속 영역의 CRUD 구현

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

 

1.create(insert) 처리

tbl_board 테이블은 PK칼럼으로 bno를 이용하고, 시퀀스를 이용해서 자동으로 데이터가 추가될 때 번호가 만들어지는 방식을 이용합니다. 이처럼 자동으로 PK값이 정해지는 경우에는 다음과 같은 2가지 방식으로 처리할 수 있습니다.

-insert만 처리되고 생성된 PK값을 알 필요가 없는경우

-insert만 처리되고 생성된 PK값을 알 필요가 있는경우

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

 

org.zerock.mapper.BoardMapper 인터페이스 일부

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 은 다음과 같이 내용 추가

 

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값을 미리 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);
	}

실행 결과

BoardMapperTests 클래스 일부2

@Test
	public void testInsertSelectKey() {
		BoardVO board = new BoardVO();
		board.setTitle("새로작성하는 글");
		board.setContent("새로 작성하는 내용");
		board.setWriter("newbie");

		mapper.insertSelectKey(board);

		log.info(board);
		System.out.println("-----------------------3");
	}

실행결과

* log.info(board)로 로그를 찍어볼 수 있는 이유는 boardMapper 클래스에서 @Data 어노테이션이 toString메서드를 지원하기 때문입니다. 결과값이 bno가 null 인지 값인지 차이인데 SelectKey 구문때문에 bno에 값이 대입이 되었고 bno에 대입이 되어 insetSelectKey에서는 bno값이 나오는것을 확인할 수 있습니다.

 

2.read(select) 처리

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

 

org.zerock.mapper.BoardMapper 인터페이스 일부

...생략
	public void insertSelectKey(BoardVO board);
	
	public BoardVO read(Long bno);
   }

 

BoardMapper.xml에 추가되는 <Select>

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

MyBatis는 Mapper 인터페이스의 리턴 타입에 맞게 select의 결과를 처리하기 때문에 tbl_board의 모든 칼럼은 BoardVO의 'bno,title,content,writer,updateDate'속성 값으로 처리됩니다. 좀 더 엄밀하게 말하면 MyBatis는 bno라는 칼럼이 존재하면 인스턴스의 'setBno()'를 호출하게 됩니다. MyBatis의 모든 파라미터와 리턴 타입의 처리는 get 파라미터명(),set 칼럼명()의 규칙으로 호출됩니다. 다만 위와 같이 #{속성}이 1개만 존재하는 경우에는 별도의 get 파라미터명()을 사용하지 않고 처리합니다.

 

BoardMapperTests 클래스 일부

@Test
	public void testRead() {
		// 존재하는 게시물 번호로 테스트
		BoardVO board = mapper.read(5L);

		log.info(board);
	}
  

실행결과

 

3.delete 처리

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

 

BoardMapper 인터페이스 일부

	...생략
    public BoardVO read(Long bno);

	public int delete(Long bno);
 }

 

BoardMapperTests 클래스 일부

	@Test
	public void testDelete() {

		log.info("DELETE COUNT: " + mapper.delete(3L));
   }

실행결과

4.update처리

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

 

BoardMapper인터페이스 일부

  ...생략
    	public int delete(Long bno);

	public int update(BoardVO board);
 }

 

BoardMapper.xml의 일부

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

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

 

BoardMapperTests 클래스 일부

	@Test
	public void testUpdate() {
		BoardVO board = new BoardVO();
		board.setBno(6L);
		board.setTitle("수정된 글");
		board.setContent("수정된 내용");
		board.setWriter("user00");

		int count = mapper.update(board);

		log.info("UPDATE COUNT: " + count);
		System.out.println("-----------------------6");
	}

실행결과

 

비즈니스 계층

비즈니스 계층은 고객의 요구사항을 반영하는 계층으로 프레젠테이션 계층과 영속 계층의 중간 다리 역할을 하게 됩니다. 영속 계층은 데이터베이스를 기준으로 해서 설계를 나눠 구현하지만, 비즈니스 계층은 로직을 기준으로 해서 처리하게 됩니다. 예를 들어 쇼핑몰에서 상품을 구매한다고 가정하면 해당 쇼핑몰의 로직이 물건을 구매한 회원에게는 포인트를 올려준다고 하면 영속 계층의 설계는 상품 과 회원으로 나누어서 설계하게 됩니다. 반면에 비즈니스 계층은 상품 영역과 회원 영역을 동시에 사용해서 하나의 로직을 처리하게 됩니다. 일반적으로 비즈니스 영역에 있는 객체들은 서비스 라는 용어를 많이사용합니다.

 

비즈니스 계층을 위해서는 프로젝트 내 org.zerock.service라는 패키지를 작성한 후 BoardService 인터페이스와 인터페이스를 구현한 BoardServiceImpl 클래스를 선언합니다.

 

BoardService 인터페이스

package org.zerock.service;

import java.util.List;

import org.zerock.domain.BoardVO;

public interface BoardService {

	public void register(BoardVO board);

	public BoardVO get(Long bno);

	public boolean modify(BoardVO board);

	public boolean remove(Long bno);

	public List<BoardVO> getList();
}

BoardServiceImpl 클래스

package org.zerock.service;

import java.util.List;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import org.zerock.domain.BoardVO;
import org.zerock.mapper.BoardMapper;

import lombok.AllArgsConstructor;
import lombok.Setter;
import lombok.extern.log4j.Log4j;

@Log4j
@Service
@AllArgsConstructor
public class BoardServiceImpl implements BoardService {

	// spring 4.3 이상에서 자동처리
	@Setter(onMethod_ = @Autowired)
	private BoardMapper mapper;

	@Override
	public void register(BoardVO board) {
		log.info("register...." + board);

		mapper.insertSelectKey(board);
	}

	@Override
	public BoardVO get(Long bno) {
		log.info("get...." + bno);

		return mapper.read(bno);
	}

	@Override
	public List<BoardVO> getList() {

		log.info("getList............");

		return mapper.getList();
	}

	@Override
	public boolean modify(BoardVO board) {
		log.info("modify......" + board);

		return mapper.update(board) == 1;
	}

	@Override
	public boolean remove(Long bno) {
		log.info("remove..." + bno);
		return mapper.delete(bno) == 1;
	}
}

 

또한 비즈니스 계층의 인터페이스와 구현 클래스가 작성되었다면 이를 스프링의 빈으로 인식하기 위해서 root-context.xml에 @Service 어노테이션이 있는 org.zerock.service패키지를 조사하도록 해야합니다.

	<context:component-scan base-package="org.zerock.service"></context:component-scan>

 Java설정의 경우

 

비즈니스 계층의 구현과 테스트

-객체가 제대로 주입이 가능한지 테스트

-등록 작업의 구현과 테스트

-목록 작업의 구현과 테스트

-조회 작업의 구현과 테스트

-삭제/수정 구현과 테스트

 

총 4가지 테스트를 진행할 예정이며 위에서 말하는 구현은 BoardServiceImpl 클래스에 BoardService에서 작성했던 함수들의 선언에 대한 구현을 말합니다. 테스트 클래스는 src/test/java - org.zerock.service - BoardServiceTests 클래스로 생성해주시면 됩니다.

 

BoardServiceTests 클래스

package org.zerock.service;

import static org.junit.Assert.assertNotNull;

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 lombok.Setter;
import lombok.extern.log4j.Log4j;

@RunWith(SpringJUnit4ClassRunner.class)
@ContextConfiguration("file:src/main/webapp/WEB-INF/spring/root-context.xml")
// Java Config
// @ContextConfiguration(classes = {org.zerock.config.RootConfig.class} )
@Log4j
public class BoardServiceTests {

	@Setter(onMethod_ = { @Autowired })
	private BoardService service;

	// BoardService 객체가 제대로 주입이 가능한지 테스트
	@Test
	public void testExist() {

		log.info(service);
		assertNotNull(service);
	}

	// 등록 작업 테스트
	@Test
	public void testRegister() {

		BoardVO board = new BoardVO();
		board.setTitle("새로 작성하는 글");
		board.setContent("새로 작성하는 내용");
		board.setWriter("newbie");

		service.register(board);

		log.info("생성된 게시물의 번호: " + board.getBno());
	}

	// 목록(리스트)작업의 테스트
	@Test
	public void testGetList() {

		service.getList().forEach(board -> log.info(board));
	}

	// 조회 작업의 테스트
	@Test
	public void testGet() {

		log.info(service.get(4L));
	}

	// 삭제 작업의 테스트
	@Test
	public void testDelete() {

		// 게시물 번호의 존재 여부를 확인하고 테스트할 것
		log.info("REMOVE RESULT: " + service.remove(42L));

	}

	// 수정 작업의 테스트
	@Test
	public void testUpdate() {

		BoardVO board = service.get(18L);

		if (board == null) {
			return;
		}

		board.setTitle("제목 수정합니다.");
		log.info("MODIFY RESULT: " + service.modify(board));
	}
}

 

 

728x90
Comments