Framework & Management/Spring

10월 31일 월요일 [코드로 배우는 스프링웹프로젝트] - 4day(2)

구일일구 2022. 10. 31. 16:37
반응형

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에 대한 테스트 코드를 작성하는 것에 거부감을 가지는 경우도 많다. 일정에 여유가 없다는 이유로.

프로젝트를 진행하는 멤버들의 경험치가 낮을 수록 테스트를 먼저 진행하는 습관을 가지는 것이 좋음

 

반응형