For Programmer

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

Spring/스프링 프로젝트

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

유지광이 2020. 9. 3. 17:56
728x90

프레젠테이션(웹) 계층의 CRUD 구현

비즈니스 계층의 구현까지 모든 테스트가 진행되었다면 이제 남은 작업은 프레젠테이션 계층인 웹의 구현입니다.

 

org.zerock.controller 패키지에 BoardController 클래스 생성

 

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가 속한 org.zerock.controller 패키지는 servlet-context.xml 에 기본으로 설정되어 있으므로 별도의 설정이 필요하지 않습니다.(단, Java설정을 이용하는 경우 @ComponentScan을 이용하여 따로 스캔할 수 있도록 지정필요)

 

-목록에 대한 처리와 테스트

org.zerock.controller.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()결과를 담아 전달합니다.

 

BoardController의 테스트

src/test/java 에 org.zerock.controller 패키지에 BoardControllerTests 클래스를 선언합니다.

단, 기존의 테스트 코드와는 좀 다르게 진행되는데 그 이유는 웹을 개발 할 때 매번 URL 테스트를 하기 위해서 Tomcat 과 같은 WAS를 실행하는 불편한 단계를 생략하기 위해서입니다. 스프링의 테스트 기능을 활용하면 개발 당시에 Tomcat(WAS)을 실행하지 않고도 스프링과 웹 URL을 테스트할 수 있습니다.

 

WAS를 실행하지 않기 위해서는 약간의 추가적인 코드가 필요하지만 반복적으로 서버를 실해앟고 화면에 입력하고 오류를 수정하는 단계를 줄여줄 수 있기 때문에 Controller를 테스트할 때는 좋은 방식입니다.

 

org.zerock.controller.BoardControllerTests 클래스

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)

// Test for Controller
@WebAppConfiguration

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

	@Setter(onMethod_ = { @Autowired })
	private WebApplicationContext ctx;

	private MockMvc mockMvc;

	@Before
	public void setup() {
		this.mockMvc = MockMvcBuilders.webAppContextSetup(ctx).build();
	}

	@Test
	public void testList() throws Exception {

		log.info(
	mockMvc.perform(MockMvcRequestBuilders.get("/board/list")).andReturn().getModelAndView().getModelMap());
	}
}

*테스트 클래스의 선언부에는 @WebAppConfiguration 어노테이션을 적용합니다. @WebAppConfiguration은 Servlet의 ServletContext를 이용하기 위해서인데, 스프링에서는 WebApplicationContext라는 존재를 이용하기 위해서 입니다. @Before어노테이션이 적용된 setUp( )에서는 import할 때 JUnit을 이용해야합니다. @Before가 적용된 메서드는 모든 테스트 전에 매번 실행되는 메서드가 됩니다. 

MockMvc는 말 그대로 '가짜 mvc'라고 생각하면 됩니다. 가짜로 URL과 파라미터 등을 브라우저에서 사용하는 것처럼 만들어서 Controller를 실행해 볼 수 있습니다. testList( )는 MockMvcRequestBuilders라는 존재를 이용해서 GET방식의 호출을 합니다. 이후에는 BoardController의 getList( )에서 반환된 결과를 이용해서 Model에 어떤 데이터들이 담겨 있는지 확인합니다. Tomcat을 통해서 실행되는 방식이 아니므로 기존의 테스트 코드를 실행하는 것과 동일하게 실행합니다.

 

실행결과

 

-등록 처리와 테스트

BoardController에 POST방식으로 처리되는 register( )를 작성하면 아래와 같습니다.

 

BoardController 클래스의 일부

	@PostMapping("/register")
	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을 리턴 타입으로 지정하고 RedirectAttributes 를 파라미터로 지정합니다. 이는 등록 작업이 끝난 후 다시 목록 화면으로 이동하기 위함인데, 추가적으로 새롭게 등록된 게시물의 번호를 같이 전달하기 위해서 RedirectAttributes를 이용합니다. redirect실행 이전에 수행된 모델 데이터는 소멸하기 때문에 model로 전달할 수 가없습니다. 이를 해결하기 위해 RedirectAttribute를 사용하는데 addFlashAttribute메서드가 리다이렉트 직전에 플래시에 저장하게 해줌으로 써 데이터를 전달할 수 있도록 해준다.

리턴 시에는 "redirect:" 접두어를 사용하는데 이를 이용하면 스프링 MVC가 내부적으로 response.sendRedirect( )를 처리해주기 때문입니다.

 

BoardControllerTests 클래스 일부

	@Test
	public void testRegister() throws Exception {

		String resultPage = mockMvc
				.perform(MockMvcRequestBuilders.post("/board/register")
						.param("title", "테스트 새글 제목")
						.param("content", "테스트 새글 내용")
						.param("writer", "user00"))
				.andReturn().getModelAndView().getViewName();

		log.info(resultPage);

	}

실행결과

 

-조회 처리와 테스트

등록처리와 유사하게 조회 처리도 BoardController를 이용해서 처리 할 수 있습니다. 특별한 경우가 아니라면 조회는 GET방식으로 처리하므로,@GetMapping을 이용합니다.

 

boardController 클래스 일부

	@GetMapping("/get")
	public void get(@RequestParam("bno") Long bno, Model model) {

		log.info("/get");
		model.addAttribute("board", service.get(bno));
	}

*BoardController의 get( ) 메서드는 bno 값을 좀 더 명시적으로 처리하는 @RequestParam을 이용해서 지정합니다.(파라미터 이름과 변수 이름을 기준으로 동작하기 때문에 생략해도 무난합니다.) 또한 화면 쪽으로 해당 번호의 게시물을 전달해야 하므로 Model을 파라미터로 지정합니다.

조회에 대한 테스트 코드

 

BoardControllerTests 클래스 일부

	@Test
	public void tetGet() throws Exception {

		log.info(mockMvc.perform(MockMvcRequestBuilders.get("/board/get").param("bno", "2"))
        	.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)) {
			rttr.addFlashAttribute("result", "success");
		}
		return "redirect:/board/list";
	}

 

BoardControllerTests 클래스 일부

	@Test
	public void testModify() throws Exception {

		String resultPage = mockMvc
				.perform(MockMvcRequestBuilders.post("/board/modify")
						.param("bno", "1")
						.param("title", "수정된 테스트 새글 제목")
						.param("content", "수정된 테스트 새글 내용")
						.param("writer", "user00"))
				.andReturn().getModelAndView().getViewName();

		log.info(resultPage);

	}

실행결과

 

-삭제 처리와 테스트

삭제 처리도 조회와 유사하게 BoardController와 테스트 코드를 작성합니다. 삭제는 반드시 POST방식으로만 처리합니다.

 

BoardController 클래스 일부

	@PostMapping("/remove")
	public String remove(@RequestParam("bno") Long bno, RedirectAttributes rttr) {
		log.info("remove.... " + bno);

		if (service.remove(bno)) {
			rttr.addFlashAttribute("result", "success");
		}
		return "redirect:/board/list";
	}

*remove( )는 삭제 후 페이지의 이동이 필요하고 또한 데이터 result 값도 전달해야하기 때문에 RedirectAttributes를 파라미터로 사용하고  'redirect'를 이용해서 삭제 처리 후에 다시 목록페이지로 이동하도록 합니다. 

 

BoardControllerTests 클래스 일부

	@Test
	public void testRemove() throws Exception {
		// 삭제전 데이터베이스에 게시물 번호 확인할 것
		String resultPage = mockMvc.perform(MockMvcRequestBuilders.post("/board/remove")
				.param("bno", "10")).andReturn().getModelAndView().getViewName();

		log.info(resultPage);
	}

실행결과

 

여기까지가 백엔드 쪽에서의 모든 처리와 테스트를 완료한 상태입니다. 다음 부터는 화면처리에 대해서 진행할 예정인데 만약 문제가 발생한다면 이제는 화면 쪽에서만 발생한다고 말 할 수있습니다.

728x90
Comments