본문 바로가기
Java & Kotlin/Spring

[Spring] 게시판 프로젝트 (2)

by heekng 2021. 5. 23.
반응형

[Spring] 게시판 프로젝트 (2)


Criteria 생성

빠른 페이징 처리를 위한 Criteria 클래스를 생성한다.

package com.blog.domain;

import org.springframework.web.util.UriComponentsBuilder;

import lombok.AllArgsConstructor;
import lombok.Data;

@Data
@AllArgsConstructor
public class Criteria {
	private int pageNum; //페이지 번호
	private int amount; //한 페이지에 보여줄 게시물 개수
	private String type; //검색 카테고리
	private String keyword; //검색 키워드
	
	public Criteria() {
		this(1,  10);
	}
	public Criteria(int pageNum, int amount) {
		this.pageNum = pageNum;
		this.amount = amount;
	}
	
	public String[] getTypeArr() {
		//타입을 하나의 문자열로 저장하고, 사용할 때에는 각각을 나눠 배열로 가져온다.
		return type == null ? new String[] {} : type.split("");
	}
	
	public String getListLink() {
		//UriComponentsBuilder은 쿼리스트링을 만들어주는 라이브러리이다. ?부터 시작하여 uri를 만들어준다.
		UriComponentsBuilder builder = UriComponentsBuilder.fromPath("")
				.queryParam("pageNum", this.pageNum)
				.queryParam("amount", this.amount)
				.queryParam("keyword", this.keyword)
				.queryParam("type", this.type);
		return builder.toUriString();
	}
}
  • Criteria가 만들어지지 않은 첫번째 list에 들어갈을 때 1페이지의 10개 게시물만 출력하기 위해 생성자를 재정의한다.
  • getTypeArr이라는 getter을 만들어서 이후 검색기능을 구현할 때 이용한다.
  • getListLink()메소드를 만들어서 이후 페이지 이동 중 이전에 머무르던 페이지번호와 검색정보를 입력하는 과정을 줄인다.

PageDTO 생성

페이징 처리 시 연산을 미리 해놓는 DTO를 만든다.

package com.blog.domain;

import lombok.Getter;
import lombok.ToString;

@Getter
@ToString
public class PageDTO {
	private int startPage;
	private int endPage;
	private int realEnd;
	private boolean next, prev;
	private int total;
	private Criteria cri;
	
	public PageDTO(Criteria cri, int total) {
		this.cri = cri;
		this.total = total;
		
		this.endPage = (int)(Math.ceil(cri.getPageNum() / 10.0)) * 10;
		this.startPage = endPage - 9;
		
		this.realEnd = (int)(Math.ceil((total * 1.0) / cri.getAmount()));
		if(this.realEnd < this.endPage) {
			this.endPage = realEnd;
		}
		
		this.prev = this.startPage > 1;
		this.next = this.endPage < this.realEnd;
	}
}
  • startPage : 페이징의 시작부분
  • endPage : 페이징의 끝부분
  • realEnd : 마지막 페이징 페이지
  • next, prev : 이전, 다음버튼 유무 여부 boolean
  • total : 전체 게시물 개수
  • cri: criteria 객체

페이징 처리를 위한 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="com.blog.mapper.BoardMapper">
	<sql id="criteria">
		<if test="type != null and keyword != null">
			<trim prefix="(" suffix=") AND" prefixOverrides="OR">
				<foreach item="type" collection="typeArr">
					<trim prefix="OR">
						<choose>
							<when test="type=='T'.toString">
								(TITLE LIKE '%'||#{keyword}||'%')
							</when>
							<when test="type=='C'.toString()">
								(CONTENT LIKE '%'||#{keyword}||'%')
							</when>
							<when test="type=='W'.toString()">
								(WRITER LIKE '%'||#{keyword}||'%')
							</when>
						</choose>
					</trim>
				</foreach>
			</trim>
		</if>
	</sql>

	<select id="getList" resultType="com.blog.domain.BoardVO">
		SELECT BNO, TITLE, CONTENT, WRITER, REGDATE, UPDATEDATE 
		FROM 
			(SELECT /* INDEX_DESC(BLOG_BOARD PK_BOARD) */ ROWNUM RN, BNO, TITLE, CONTENT, WRITER, REGDATE, UPDATEDATE 
			FROM BLOG_BOARD 
			WHERE 
			<include refid="criteria"/>
		<![CDATA[
			ROWNUM <= #{pageNum} * #{amount}) 
			WHERE RN > ((#{pageNum} - 1) * #{amount})
		]]>
	</select>
	
	<insert id="insert">
		<selectKey keyProperty="bno" order="BEFORE" resultType="long">
			SELECT SEQ_BLOG_BOARD.NEXTVAL FROM DUAL
		</selectKey>
		INSERT INTO BLOG_BOARD (BNO, TITLE, CONTENT, WRITER) 
		VALUES(#{bno}, #{title}, #{content}, #{writer})
	</insert>
	
	<select id="getTotal" resultType="_int">
		SELECT COUNT(BNO) FROM BLOG_BOARD 
		<where>
			<trim suffixOverrides="AND">
				<include refid="criteria"/>
			</trim>
		</where>
	</select>
	
	<select id="read" resultType="com.blog.domain.BoardVO">
		SELECT * FROM BLOG_BOARD WHERE BNO = #{bno}
	</select>
	
	<delete id="delete">
		DELETE FROM BLOG_BOARD WHERE BNO = #{bno}
	</delete>
	
	<update id="update">
		UPDATE BLOG_BOARD 
		SET TITLE = #{title}, CONTENT = #{content}, WRITER = #{writer}, UPDATEDATE = SYSDATE
		WHERE BNO = #{bno}
	</update>
</mapper>
  • <sql>태그는 쿼리문을 미리 작성해서 저장하는 용도로 이용된다.
  • <include refid="">태그는 sql태그로 작성한 쿼리문을 불러온다.
  • type과 keyword를 이용하고, 미리 정의해 둔 getTypeArr메소드를 이용해서 where절을 구성한다.

2021.05.13 - [개발/MyBatis] - [MyBatis] 동적쿼리(동적 태그)

 

[MyBatis] 동적쿼리(동적 태그)

[MyBatis] 동적쿼리(동적 태그) MyBatis를 이용해서 같은 형태의 분류만 다른 쿼리를 사용해야 할 때 id만 다르게 쿼리를 여러개 작성해야 할 때가 있다. 이러할 때에는 동적 태그를 이용해서 하나의

heekng.tistory.com


페이징 처리를 위한 BoardMapper 인터페이스, BoardService인터페이스, BoardServiceImpl 클래스 수정

BoardMapper 인터페이스

package com.blog.mapper;

import java.util.List;

import com.blog.domain.BoardVO;
import com.blog.domain.Criteria;

public interface BoardMapper {
	//페이지 게시물 리스트 가져오기
	public List<BoardVO> getList(Criteria cri);
	
	//게시물 등록
	public int insert(BoardVO board);
	
	//해당 페이지 게시물 개수 가져오기
	public int getTotal(Criteria cri);
	
	//bno로 개시물 정보 가져오기
	public BoardVO read(Long bno);
	
	//bno로 게시물 삭제하기
	public int delete(Long bno);
	
	//게시물 수정
	public int update(BoardVO board);
}

BoardService 인터페이스

package com.blog.service;

import java.util.List;

import com.blog.domain.BoardVO;
import com.blog.domain.Criteria;

public interface BoardService {
	//페이징 게시물 가져오기
	public List<BoardVO> getList(Criteria cri);
	
	//게시물 등록
	public void register(BoardVO board);
	
	//특정 게시물 가져오기
	public BoardVO get(Long bno);
	
	//게시물 삭제
	public boolean remove(Long bno);
	
	//게시물 수정
	public boolean modify(BoardVO board);
	
	//전체 게시물 가져오기
	public int getTotal(Criteria cri);
}

BoardServiceImpl 클래스

package com.blog.service;

import java.util.List;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;

import com.blog.domain.BoardVO;
import com.blog.domain.Criteria;
import com.blog.mapper.BoardMapper;

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

@Service
@Log4j
@AllArgsConstructor
public class BoardServiceImpl implements BoardService{
	@Setter(onMethod_ = @Autowired)
	private BoardMapper mapper;
	
	@Override
	public List<BoardVO> getList(Criteria cri) {
		log.info("getList......");
		return mapper.getList(cri);
	}

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

	@Override
	public BoardVO get(Long bno) {
		log.info("get..:.."+bno);
		return mapper.read(bno);
	}

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

	@Override
	public boolean modify(BoardVO board) {
		log.info("modify..:.."+board);
		return mapper.update(board) == 1;
	}

	@Override
	public int getTotal(Criteria cri) {
		log.info("get total.......");
		return mapper.getTotal(cri);
	}

}

BoardController 구성

package com.blog.controller;

import org.springframework.stereotype.Controller;
import org.springframework.ui.Model;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.ModelAttribute;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.servlet.mvc.support.RedirectAttributes;

import com.blog.domain.BoardVO;
import com.blog.domain.Criteria;
import com.blog.domain.PageDTO;
import com.blog.service.BoardService;

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

@Controller
@Log4j
@RequestMapping("/board/*")
@AllArgsConstructor
public class BoardController {
	private BoardService service;
	
	@GetMapping("/list")
	public void list(Criteria cri, Model model) {
		log.info("list");
		model.addAttribute("list", service.getList(cri));
		model.addAttribute("pageMaker", new PageDTO(cri, service.getTotal(cri)));
	}
	
	@GetMapping("/register")
	public void register(@ModelAttribute("cri") Criteria cri) {
		log.info(cri);
	}
	
	@PostMapping("/register")
	public String register(BoardVO board, /*Model model*/ RedirectAttributes rttr) {
		log.info("register : "+board);
		service.register(board);
		rttr.addFlashAttribute("result", board.getBno());
		
		return "redirect:/board/list";
	}
	
	//조회 처리와 테스트 구현
	@GetMapping({"/get", "/modify"})
	public void get(@RequestParam("bno") Long bno, @ModelAttribute("cri") Criteria cri, Model model) {
		log.info("get : "+bno);
		model.addAttribute("board", service.get(bno));
	}
	
	//수정과 삭제는 성공 시 result에 success를 담아서 view에 전달하기
	//수정 처리와 테스트 구현
	@PostMapping("/modify")
	public String modify(BoardVO board, Criteria cri, RedirectAttributes rttr) {
		log.info("modify : "+board);
		if(service.modify(board)) {
			rttr.addFlashAttribute("result", "success");
		}
		rttr.addAttribute("pageNum", cri.getPageNum());
		rttr.addAttribute("amount", cri.getAmount());
		rttr.addAttribute("keyword", cri.getKeyword());
		rttr.addAttribute("type", cri.getType());
		return "redirect:/board/list";
	}
	//삭제 처리와 테스트 구현
	@GetMapping("/remove")
	public String remove(@RequestParam("bno") Long bno, Criteria cri, RedirectAttributes rttr) {
		log.info("remove : "+bno);
		if(service.remove(bno)) {
			rttr.addFlashAttribute("result", "success");
		}
		rttr.addAttribute("pageNum", cri.getPageNum());
		rttr.addAttribute("amount", cri.getAmount());
		rttr.addAttribute("type", cri.getType());
		rttr.addAttribute("keyword", cri.getKeyword());
		return "redirect:/board/list";
	}
}
  • controller에는 @Controller어노테이션이 꼭 붙어야 한다.
  • @RequestMapping어노테이션을 작성하여 URL 주소에 따라 /board/를 거치는 요청을 해당 컨트롤러로 들어오게 설정한다.
  • @GetMapping, @PostMapping어노테이션을 통해 요청에 알맞은 메소드가 실행되게 한다.
  • @GetMapping과 @PostMapping은 방식이 다르기 때문에 같은 이름의 요청이더라도 상황에 따라 다른 메소드로 들어온다.
  • model객체를 통해 map구조의 데이터를 request객체를 이용하는 것처럼 전달한다.
  • bno와 criteria객체를 모두 파라미터로 받는 경우에 criteria객체의 필드에 bno가 있기 때문에 원하는 위치에 원하는 bno를 입력하기 어려움이 있다. 이 때 @ModelAttribute를 이용하여 어떤 이름의 파라미터가 어디로 들어갈지 지정할 수 있다.
  • redirect방식으로 페이지가 이동될 때에는 request가 초기화되기 때문에  model객체가 아닌 RedirectAttributes를 이용한다.
  • addFlashAttribute는 세션을 이용해 파라미터를 전달하며, 세션의 남용을 방지하기 위해 하나의 파라미터만 전달 가능하다.
  • 따라서 여러 개의 파라미터를 전달해야 할 때에는 컬렉션에 담아서 넘기거나 URL에 붙여서 전달하는 addattribute를 사용한다.
  • redirect방식의 페이지 이동시에는 return값에 redirect: 를 붙인다.

BoardControllerTest

mapper와 service를 테스트한 것처럼 controller 또한 테스트서버에서 테스트할 수 있다.

mapper와 service는 WAS를 이용하지 않기 때문에 servlet-context.xml이 필요하지 않았지만, controller은 WAS를 통해 구동되기 때문에 조금 더 다른 설정이 필요하다.

mockMVC 사용을 위한 servlet 버전 업데이트

<dependency>
    <groupId>javax.servlet</groupId>
    <artifactId>javax.servlet-api</artifactId>
    <version>3.1.0</version>
    <scope>provided</scope>
</dependency>

BoardControllerTests

package com.blog.controller;

import org.junit.Before;
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.springframework.test.context.web.WebAppConfiguration;
import org.springframework.test.web.servlet.MockMvc;
import org.springframework.test.web.servlet.request.MockMvcRequestBuilders;
import org.springframework.test.web.servlet.setup.MockMvcBuilders;
import org.springframework.web.context.WebApplicationContext;

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

@RunWith(SpringJUnit4ClassRunner.class)
@WebAppConfiguration
@ContextConfiguration({"file:src/main/webapp/WEB-INF/spring/root-context.xml",
	"file:src/main/webapp/WEB-INF/spring/appServlet/servlet-context.xml"})
@Log4j
public class BoardControllerTests {
	@Setter(onMethod_ = @Autowired)
	private WebApplicationContext wac;
	
	private MockMvc mockMvc;
	
	@Before
	public void setup() {
		this.mockMvc = MockMvcBuilders.webAppContextSetup(wac).build();
	}
	
//	@Test
//	public void testList() throws Exception {
//		log.info(mockMvc.perform(MockMvcRequestBuilders.get("/board/list"))
//				.andReturn()
//				.getModelAndView()
//				.getModelMap());
//	}
	
//	@Test
//	public void testRegister() throws Exception{
//		log.info(mockMvc.perform(MockMvcRequestBuilders.post("/board/register")
//				.param("title", "테스트서버 새 글 제목")
//				.param("content", "테스트 새 글 내용")
//				.param("writer", "test1234"))
//				.andReturn().getModelAndView().getViewName());
//	}
	
//	@Test
//	public void testGet() throws Exception{
//		mockMvc.perform(MockMvcRequestBuilders.get("/board/get")
//				.param("bno", "1"))
//		.andReturn().getModelAndView().getModelMap();
//	}
	
//	@Test
//	public void testModify() throws Exception{
//		log.info(mockMvc.perform(MockMvcRequestBuilders.post("/board/modify")
//				.param("bno", "1")
//				.param("title", "테스트서버에서 수정된 제목")
//				.param("content", "테스트서버에서 수정된 내용")
//				.param("writer", "testServerWriter"))
//				.andReturn().getFlashMap());
//	}
	
	@Test
	public void testRemove() throws Exception{
		log.info(mockMvc.perform(MockMvcRequestBuilders.get("/board/remove")
				.param("bno", "1"))
				.andReturn().getFlashMap());
	}
}
  • @WebAppConfiguration어노테이션 작성
  • @ContextConfiguration에 servlet-context.xml을 추가
  • MockMvc는 test를 위한 mvc이며, 마치 브라우저에서 사용하는 것처럼 만들어서 Controller을 실행해 볼 수 있다.
  • @Before어노테이션은 모든 테스트 전에 가장 먼저 실행되게 하고, MockMvcBuilders.webContextSetup(wac).build()를 이용해서 mockMvc에 ServletContext를 빌드한다.
  • mockMvc.perform(MockMvcRequestBuilders.[요청방식]([URL])로 해당 URL을 호출한다.
  • andReturn() : 응답과 결과값을 통해
  • getModelAndView() : Model에 어떤 데이터가 담겨있는지 확인
  • getModelMap() : Map형식으로 받아온다.
  • param("key", "value") : 파라미터 추가

 

반응형

'Java & Kotlin > Spring' 카테고리의 다른 글

[Spring] 트랜잭션  (0) 2021.05.26
[Spring] AOP  (0) 2021.05.26
[Spring] REST Controller 댓글 기능 Rest방식 구현  (0) 2021.05.23
[Spring] root-context.xml 구성  (0) 2021.05.22
[Spring] pom.xml 구성  (0) 2021.05.22
REST 방식  (0) 2021.05.17