개발관련/코드로 배우는 스프링 웹 프로젝트(개정판)

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

개념원리 2023. 3. 19. 13:10

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

웹은 이전 PART2에서 실습한 내용을 바탕으로 현재 프로잭트를 반영 해야 합니다.

 

10.1 Controller의 작성

스프링 MVC의 Controller는하나의 클래스 내에서 여러 메서들을 작성하고, @RequestMapping 등을 이용해서 URL을 분기하는 구조로 작성 할 수 있기 때문에 하나의 클래스에서 필요한 만큼의 메서드 분기를 이용하는 구조로 작성 합니다.

 

과가에는 이 단계에서 Tomcat(WAS)을 실행하고 웹 화면을 만들어서 결과를 확인하는 방식의 코드를 작성해 왔습니다

하지만 이방식은 시간도 오래 걸리고 테스트하기엔 많은 어려움이 있습니다.

우리는 WAS를 실행하지 않고 Controller를 테스트할 수 있는 방식으로 진행 하겠습니다.

 

10.1.1 BoardController의 분석

작성하기 전에는 반드시 원하는 기능을 호출하는 방식에 대해 다음과 같이 테이블로 정리하는것이 좋습니다.

테이블에서 From 항목은 해당 URL을 호출하기 위해 별도의 입력화면이 필요하다는 것을 의미 합니다.

이에 대한 설계는 화면을 구성하는 단게에서 진행할 수 있습니다.

 

10.2 BoardController의 작성

BoardController는 org.zerock.controller 패키지에 선언하고 URL 분석된 내용들을 반영하는 메서드를 설계합니다.

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 {
	

}

BoardController는 @Controller 어노테이션을 추가해 스프링의 빈으로 인식할 수 있게 하고, 

@RequestMapping을 통해서 /board/로 시작하는 모든 처리를 BoardController가 하도록 지정 합니다.

 

BoardController가 속한 org.zerock.controller 패키지는 servlet-context.xml에 기본적으로 설정되어 있으므로 별도의 설정이 필요하지 않습니다.

(JAVA설정의 경우 @ComponentScan을 이용)

 

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

BoardController에서 전체 목록을 가져오는 처리를 먼저 작성 합니다.

BoardCOntroller는 BoardService 타입의 객체와 연동해야 하므로 의존성에 대한 처리도 같이 진행 합니다.

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

 

BoardControllersms 스프링의 테스트 기능을 이용해 테스트 할 수 있습니다.

 

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

테스트 코드는 기존과 좀 다르게 진행 되는데 웹을 개발할때 매번 URL을 테스트 하기 위해 Tomcat과 같은 WAS를 실행하는 불편함을 해소하기 위해서입니다.

 

스프링의 테스트 기능을 활용하면 개발 당시 Tomcat(WAS)를 실행하지 않고도 웹 URL을 테스트 할 수 있습니다.

 

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

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를 이용하기 위해서인데, 스프링에서는 WebApplicationConext라는 존재를 이용하기 위해서입니다.

 

@Before어노테이션이 적용된 setUp()에서는 import 할 떄 JUnit을 사용합니다.

@Before가 적용된 메서드들은 모든 테스트 전에 매번 실행되는 메서드가 됩니다.

 

Mockmvc는 말 그대로 "가짜 MVC"라고 보시면 됩니다.

가짜로 URL과 파라미터를 브라우저에서 사용하는 것 처럼 만들어서 Controller를 실행 해 볼 수 있습니다.

 

testList()는 MockMvcRequestBuilder라는 존재를 이용해서 GET방식의 호출을 합니다.

이후에는 BoardController의 getList()에서 반환된 결과를 이용해서 Model에 어떤 데이터들이 담겨있는지 확인합니다.

 

Tomcat을 이용해서 실행되는 방식이 아니므로 기존의 테스트 코드를 실행하는 것과 동일하게 실행 합니다.

 

tetList()를 실행한 결과는 데이터베이스에 저장된 게시물들을 볼 수 있습니다.

 

10.2.2 등록 처리와 테스트

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

	@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를 파라미터로 지정 합니다.

 

이는 등록 작업이 끝난 후 다시 화면 목록으로 이동하기 위함인데, 추가적으로 새롭게 등록된 게시물의 번호를 같이 전달하기 위해서 RedirectArttribute를 이용합니다.

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

 

테스트 코드는 아래와 같이 작성 합니다.

	//BoardControllerTests.class에 추가
    @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);
	}

테스트 할 때 MockMvcRequestBuilder의 post()를 이용하면 POST 방식으로 데이터를 전달 할 수 있고, param을 이용해서 전달해야 하는 파라미터를 전달 할 수 있습니다.

 

이러한 방식으로 코드를 작성하면 최초에는 일이 많다고 느껴질 수 있지만 매번 입력할 필요가 없기 때문에 오류가 발생하거나 수정하는 경우 반복적인 테스트가 수월해집니다.

 

테스트 코드를 실행하면 아래와 같은 결과를 볼 수 있습니다.

실행되는 로그를 살펴보면 BoardVO 객체로 올바르게 데이터가 바인딩 된 결과를볼 수 있고,

중간에는 SQL 실행결과가 보입니다.

 

마지막에는 최종 반환 문자열을 확인할 수 있습니다.

 

10.2.3 조회 처리와 테스트

등록처리와 유사하게 조회 처리도 BoardController를 이용해서 처리 할 수 있습니다.

 

특별한 경우가 아니라면 조회는 GET 방식으로 처리하므로, @GetMapping을 이용합니다.

	//org.zerock.controller.boardController.java에 추가
    
        @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을 파라미터로 지정 합니다.

 

get()에 대한 테스트 코드를 아래와 같이 작성 합니다.

	//org.zerock.controller.BoardControllerTests.java 에 추가
    @Test
	public void testGet() throws Exception{
		log.info(mockMvc.perform(MockMvcRequestBuilders
				.get("/board/get").param("bno", "3"))
				.andReturn()
				.getModelAndView()
				.getModelMap()
			);
	}

특정 게시물을 조회할때 반드시 bno라는 파라미터가 있어야 하므로 param()을 통해서 추가하고 실행 합니다.

실행되는 로그는 아래와 같습니다.

파라미터가 제대로 수집되었는지 확인하고 SQL 처리결과를 확인 할 수  있습니다.

마지막에는 Model에 담겨있는 BoardVO 인스턴스의 내용을 살펴볼 수 있습니다.

 

10.2.4 수정 처리와 테스트

수정 작업은 등록 작업과 유사 합니다.

변경된 내용을 수집해서 BoardVO 파라미터로 처리하고, BoardService를 호출합니다.

수정 작업을 시작하는 화면의 경우에는 GET 방식으로 접근하지만 실제 작업은 POST 방식으로 동작 하므로 @PostMapping을 이용해 처리 하겠습니다.

//org.zerock.controller.BoardController.java에 추가

	@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";
	}

service.modiry()는 수정 여부를 boolean으로 처리하므로 이를 이용해서 성공한 경우에만 RedirectAttributes에 추가 합니다.

 

테스트 코드는 아래와 같이 작성 합니다. 

	//org.zerock.controller.BoardControllerTests.java 에 추가
	@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);
	}

 

테스트 로그는 기존과 유사합니다.

 

10.2.5 삭제 처리와 테스트

삭제 처리도 조회와 유사하게 BoardController와 테스트 코드를 작성 합니다.

삭제는 반드시 POST방식으로만 처리합니다.

//org.zerock.controller.BoardController.java에 추가
	@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";
				
	}

BoardController의 remove()는 삭제 후 페이지의 이동이 필요하므로 RedirectAttributes를 파라미터로 사용 하였고, "redirect를 이용해서 삭제 처리 후에 다시 목록 페이지로 이동 합니다. 

 

테스트 코드는 아래와 같습니다.

//org.zerock.controller.BoardControllerTests.java 에 추가
	@Test
	public void testRemove() throws Exception{
		String resultPage = mockMvc.perform(MockMvcRequestBuilders.post("/board/remove")
							.param("bno", "3"))
							.andReturn()
							.getModelAndView()
							.getViewName();
		
		log.info(resultPage);
		
	}

 

MockMvc를 이용해서 파라미터를 전달할 때에는 문자열로만 처리해야 합니다.

테스트 전에 게시물의 번호가 존재하는지 확인하고 테스트를 실행합니다.

로그의 일부는 아래와 같이 SQL이 실행되는 것을 확인할 수 있습니다.

 

경우에 따라서는 Controller에 대한 테스트 코드를 작성하는 것에 대해 거부감을 가지는 경우도 많습니다.

 

대부분은 일정에 여유가 없다는 이유로 테스트를 작성하지 않는 경우가 많은데 프로젝트를 진행하는 맴버들의 경험치가 낮을 수록 테스트를 먼저 진행하는 습관을 갖는것이 좋습니다.

 

반복적인 입력과 수정, WAS의 재시작 시간을 고려해보면 Controller에 대한 테스트를 진행하는 선택이 더 빠른 결과를 낳을 수 도 있습니다