10월 31일 월요일 [코드로 배우는 스프링웹프로젝트] - 4day(2)
10. 프레젠테이션(웹) 계층의 CRUD 구현
프레젠테이션 계층(화면계층)인 웹을 구현하는 부분
* 화면 계층: 화면에 보여주는 기술을 사용하는 영역(Servlet/JSP, 스프링 MVC가 담당하는 영역)
Controller의 작성
스프링 MVC의 Controller는 하나의 클래스 내에서 여러 메서드를 작성, @RequestMapping 등을 이용해 URL을 분기하는 구조로 작성할 수 있음 ➡️ 하나의 클래스에서 필요한 만큼 메서드의 분기를 이용하는 구조로 작성함
과거에는 WAS로 웹 화면을 만들어서 결과를 확인했지만, 시간도 오래걸리고 어려움이 많음 ➡️ WAS 사용하지 않고 Controller를 테스트 할 것!
BoardController의 분석
작성하기 전에 현재 원하는 기능을 호출하는 방식에 대해 테이블로 정리하는 것이 좋음
From : 해당 URL을 호출하기 위해 별도의 입력화면이 필요하다는 것을 의미함
BoardController의 작성
BoardController는 URL이 분석된 내용들을 반영하는 메서드를 설계함
//BoardController
package org.zerock.controller;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.RequestMapping;
import lombok.extern.log4j.Log4j;
@Controller
@Log4j
@RequestMapping("/board/*")
public class BoardController {
}
@Controller
스프링의 빈으로 인식할 수 있게 함
@RequestMapping
'/board'로 시작하는 모든 처리를 BoardController가 하도록 지정
목록에 대한 처리와 테스트
BoardController에서 전체 목록을 가져오는 처리를 작성 => BoardService 타입의 객체와 연동해야 하므로 의존성 처리도 같이
//BoardController
package org.zerock.controller;
import org.springframework.stereotype.Controller;
import org.springframework.ui.Model;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.zerock.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(Model model) {
log.info("list");
model.addAttribute("list", service.getList());
}
}
* BoardController는 BoardService에 대해 의존적이므로 @AllArgsConstructor를 이용해서 생성자를 만들고, 자동으로 주입하도록함 (만약 생성자를 만들지 않을 경우 @Setter(onMethod_{@Autowired} ) 를 이용해서 처리함.)
* list()는 나중에 게시물의 목록을 전달해야 하므로 Model을 파라미터로 지정, 이를 통해 BoardServiceImpl 객체의 getList() 결과를 담아서 전달함(addAttribute)
*log.info() : ""안에 들어간 내용을 콘솔에 찍기
* model.addAttribute("name", value)
Model addAttribute(String name, Object value)
- value 객체를 name 이름으로 추가한다. 뷰 코드에서는 name으로 지정한 이름을 통해서 value를 사용한다.
BoardController를 테스트하는 클래스
package org.zerock.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 //Servlet의 ServletContext를 이용하기 위함, 스프링에서는 WebApplicationContext를 이용하기 위함
@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 ctx;
private MockMvc mockMvc; //가짜mvc : 가짜로 URL과 파라미터 등을 사용하는 것처럼 만들어서 Controller 실행
@Before //모든 테스트 전에 매번 실행되는 메서드
public void setup() { //import 할 때 JUnit을 이용함
this.mockMvc = MockMvcBuilders.webAppContextSetup(ctx).build();
}
@Test
//MockMvcRequestBuilers라는 존재를 이용해 GET 방식의 호출
//BoardController의 getList()에서 반환된 결과를 이용해 Model에 어떤 데이터가 담겨있는지 확인
public void testList() throws Exception{
log.info(mockMvc.perform(MockMvcRequestBuilders.get("/board/list"))
.andReturn()
.getModelAndView()
.getModel());
}
}
@WebAppConfiguration
Servlet의 ServletContext를 이용하기 위함
스프링에서는 WebApplicationContext라는 존재를 이용하기 위함
@Before
이 어노테이션이 적용된 메서드는 모든 테스트 전에 매번 실행되는 메서드
setUp() 메서드
import 할 때 JUnit을 이용해야 함
MockMvc
가짜 mvc. 가짜로 request를 만들어서 보내는 것
가짜로 URL과 파라미터 등을 브라우저에서 사용하는 것처럼 만들어서 Controller를 실행해볼 수 있음
MockMvcRequestBuilders
가짜 MVC를 만들었으니, 그 MVC로 요청하는 빌더를 만드는 것. 이것으로 GET 방식의 호출을 할 수 있음
testList() 메서드
MockMvcRequestBuilders라는 존재를 이용해서 GET 방식의 호출을 함
➡️ BoardController의 getList()에서 반환된 결과를 이용해 Model에 어떤 데이터들이 담겨 잇는지 확인함
==> testList()를 실행한 결과로 데이터베이스에 저장된 게시물들을 볼 수 있다.
등록 처리와 테스트
//BoardController
@PostMapping("/register")
//String을 리턴타입으로 지정
//RedirectAttributes를 파라미터로 지정함 -> 등록작업이 끝난 후, 다시 목록화면으로 이동하기 위함
//추가적으로 새롭게 등록된 게시물의 번호를 같이 전달하기 위해 RedirectAttributes 이용
public String register(BoardVO board, RedirectAttributes rttr) {
log.info("register:" + board);
service.register(board);
rttr.addFlashAttribute("result", board.getBno()); //결과: 새롭게 등록된 게시물 번호 저장메소드
return "redirect:/board/list"; //다시 리스트로 리다이렉트
}
* register()메서드
String을 리턴타입으로 지정, Redirect Attributes를 파라미터로 지정
이는 등록 작업이 끝난 후 다시 목록 화면으로 이동하기 위함
추가적으로 새롭게 등록된 게시물의 번호를 같이 전달하기 위해 RedirectAttributes를 이용함
* 'redirect:'
리턴시, 사용하는 접두어
스프링 MVC가 내부적으로 response.sendRedirect()를 처리해주기 때문에 편함
@Test
public void testRegister() throws Exception{
String resultPage = mockMvc.perform(MockMvcRequestBuilders.post("/board/register")
//post 방식으로 데이터를 전달
//param()을 이용해 전달해야 하는 파라미터들을 지정할 수 있음 => 매번 입력할 필요X 수월해짐
.param("title", "테스트 새글 제목")
.param("content", "테스트 새글 내용")
.param("writer", "user00")
).andReturn().getModelAndView().getViewName();
log.info(resultPage);
}
조회 처리와 테스트
특별한 경우가 아니라면 조회는 GET 방식으로 처리. 따라서 @GetMapping으로 사용
//BoardController
@GetMapping("/get")
public void get(@RequestParam("bno") Long bno, Model model) {
//bno값을 좀 더 명시적으로 처리하기 위해 requestparam을 사용
log.info("/get");
model.addAttribute("board", service.get(bno));
//화면쪽으로 해당 번호의 게시물을 전달해야 하므로 Model을 파라미터로 지정
}
@Test
public void testGet() throws Exception{
log.info(mockMvc.perform(MockMvcRequestBuilders
.get("board/get")
.param("bno", "2")) //특정 게시물을 조회할 때 필요한 'bno' 파라미터
.andReturn()
.getModelAndView().getModelMap());
}
수정 처리와 테스트
수정 작업은 등록과 유사함
변경된 내용을 수집해서 BoardVO 파라미터로 처리하고, BoardService를 호출함
수정 작업을 시작하는 화면의 경우에는 GET 방식으로 접근하지만, 실제 작업은 POST 방식으로 동작하기 때문에 @POSTMAPPING을 이용해서 처리한다.
//BoardController
@PostMapping("/modify")
public String modify(BoardVO board, RedirectAttributes rttr) {
log.info("modify: " + board);
if(service.modify(board)) { //수정 여부를 boolean으로 처리함
rttr.addFlashAttribute("result", "success"); //성공한 경우에만 redirectattributes에 추가
}
return "redirect:/board/list";
}
@Test //수정 테스트
public void testModify() throws Exception{
String resultPage = mockMvc
.perform(MockMvcRequestBuilders.post("/board/modify")
.param("bno", "1")
.param("title", "수정된 테스트 새글 제목")
.param("content", "수정된 테스트 새글 내용")
.param("writer", "user001"))
.andReturn().getModelAndView().getViewName();
log.info(resultPage);
}
삭제 처리와 테스트
삭제는 반드시 POST 방식으로만 처리하기
@PostMapping("/remove")
public String remove(@RequestParam("bno") Long bno, RedirectAttributes rttr) {
//삭제 후, 페이지의 이동이 필요하기 때문에 RedirectAttributes 사용
log.info("remove..." + bno);
if(service.remove(bno)) {
rttr.addFlashAttribute("result", "success");
}
return "redirect:/board/list"; //삭제 처리 후 다시 목록 페이지로 이동
}
@Test //삭제 테스트
public void testRemove() throws Exception{
String resultPage = mockMvc.perform(MockMvcRequestBuilders
.post("/board/remove")
.param("bno", "5")) //파라미터를 전달할 땐 문자열로만 처리할 것. 게시물 번호 있는지 확인할 것.
.andReturn().getModelAndView().getViewName();
log.info(resultPage);
}
Controller에 대한 테스트 코드를 작성하는 것에 거부감을 가지는 경우도 많다. 일정에 여유가 없다는 이유로.
프로젝트를 진행하는 멤버들의 경험치가 낮을 수록 테스트를 먼저 진행하는 습관을 가지는 것이 좋음