반응형
[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] 동적쿼리(동적 태그)
페이징 처리를 위한 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 |