Chapter 10-1 ~ 2

ChangWoo·2023년 8월 29일
0
post-thumbnail

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

  • 비즈니스 계층의 구현까지 모든 테스트가 진행되었다면 남은 작업은 프레젠테이션 개층의 웹 구현이다.
  • 웹은 PART 2에서 실습한 내용을 기준으로 현재 프로젝트에 반영한다.

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 분석된 내용들을 반영하는 메서드를 설계한다.
< 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 {
}
  • BoardController는 @Controller 어노테이션을 추가해 스프링의 빈으로 인식할 수 있게 하고, @RequestMapping을 통해 '/board'로 시작하는 모든 처리를 BoardController가 하도록 지정한다.
  • BoardController가 속한 org.zerock.controller 패키지는 servlet-controller.xml에 기본으로 설정되어 있으므로 별도의 설정이 필요하지 않다.(Java 설정을 이용하는 경우에는 @ComponentScan을 이용)

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

  • BoardController에 전체 목록을 가져오는 처리를 먼저 작성한다.
  • BoardController는 BoardService 타입의 객체와 같이 연동해야 하므로 의존성에 대한 처리도 같이 진행한다.
< 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() 결과를 담아 전달한다(addAttribute).
  • BoardController 테스트는 스프링의 테스트 기능을 통해 확인할 수 있다.
  • src/test/java에 org.zerock.controller 패키지에 BoardControllerTests 클래스를 선언한다.
  • 테스트 코드는 기존과 다르게 진행되는데 그 이유는 웹을 개발할 때 매번 URL을 테스트 하기 위해 Tomcat과 같은 WAS를 실행하는 불편한 단계를 생략하기 위함이다.
  • 스프링의 테스트 기능을 활용하면 개발 당시에 Tomcat(WAS)을 실행하지 않고도 스프링과 웹 URL을 테스트 할 수 있다.
  • WAS를 실행하지 않기 위해서는 약간의 추가적인 코드가 필요하지만 반복적으로 서버를 실행하고 화면에 입력하고, 오류를 수정하는 단계를 줄일 수 있기 때문에 Controller를 테스트 할 때 한 번 쯤 고려해 볼 만한 방식이다.
< src/test/java 밑의 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.RoogConfig.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()
		);
	}
}
  • 테스트 클래스의 선언부에는 @WebConfiguration 어노테이션을 적용한다.
  • @WebAppConfiguration은 Servlet의 ServletContext를 이용하기 위해서인데, 스프링에서는 WebApplicationContext라는 존재를 이용하기 위해서다.
  • @Before 어노테이션이 적용된 setUp()에서는 import 할 때 JUnit을 이용해야 한다.
  • @Before가 적용된 메서드는 모든 테스트 전에 매번 실행된는 메서드가 된다.
  • MockMvc는 말 그대로 '가짜 mvc'라고 생각하면 된다.
  • 가짜로 URL과 파라미터 등을 브라우저에서 사용하는 것처럼 만들어서 Controller를 실행해 볼 수 있다.
  • testList()는 MockMvcRequestBuilders라는 존재를 이용해 GET 방식을 이용해 호출한다.
  • 이후에는 BoardController의 getList()에서 반환된 결과를 이용해 Model에 어떤 데이터들이 담겨 있는지 확인한다.
  • Tomcat을 통해 실행되는 방식이 아니므로 기존의 테스트 코드를 실행하는 것과 동일하게 실행한다.
  • testList()를 실행한 결과는 데이터베이스에 저장된 게시물들을 볼 수 있다.
INFO : org.zerock.controller.BoardControllerTests - {list=[BoardVO(bno=1, title=제목 수정합니다., content=테스트 내용, writer=USER00, regdate=Wed Aug 09 11:33:55 KST 2023, updateDate=Thu Aug 10 00:18:55 KST 2023), BoardVO(bno=4, title=테스트 제목, content=테스트 내용, writer=USER00, regdate=Wed Aug 09 11:33:57 KST 2023, updateDate=Wed Aug 09 11:33:57 KST 2023), BoardVO(bno=5, title=수정된 제목, content=수정된 제목, writer=user00, regdate=Wed Aug 09 11:34:00 KST 2023, updateDate=Wed Aug 09 20:31:20 KST 2023), BoardVO(bno=21, title=새로 작성하는 글, content=새로 작성하는 내용, writer=newbie, regdate=Wed Aug 09 19:31:42 KST 2023, updateDate=Wed Aug 09 19:31:42 KST 2023), BoardVO(bno=22, title=새로 작성하는 글 select key, content=새로 작성하는 내용 select key, writer=newbie, regdate=Wed Aug 09 19:36:11 KST 2023, updateDate=Wed Aug 09 19:36:11 KST 2023), BoardVO(bno=23, title=새로 작성하는 글, content=새로 작성하는 내용, writer=newbie, regdate=Wed Aug 09 19:36:11 KST 2023, updateDate=Wed Aug 09 19:36:11 KST 2023), BoardVO(bno=24, title=새로 작성하는 글 select key, content=새로 작성하는 내용 select key, writer=newbie, regdate=Wed Aug 09 20:11:11 KST 2023, updateDate=Wed Aug 09 20:11:11 KST 2023), BoardVO(bno=25, title=새로 작성하는 글, content=새로 작성하는 내용, writer=newbie, regdate=Wed Aug 09 20:11:11 KST 2023, updateDate=Wed Aug 09 20:11:11 KST 2023), BoardVO(bno=26, title=새로 작성하는 글 select key, content=새로 작성하는 내용 select key, writer=newbie, regdate=Wed Aug 09 20:20:00 KST 2023, updateDate=Wed Aug 09 20:20:00 KST 2023), BoardVO(bno=27, title=새로 작성하는 글, content=새로 작성하는 내용, writer=newbie, regdate=Wed Aug 09 20:20:00 KST 2023, updateDate=Wed Aug 09 20:20:00 KST 2023), BoardVO(bno=28, title=새로 작성하는 글 select key, content=새로 작성하는 내용 select key, writer=newbie, regdate=Wed Aug 09 20:31:20 KST 2023, updateDate=Wed Aug 09 20:31:20 KST 2023), BoardVO(bno=29, title=새로 작성하는 글, content=새로 작성하는 내용, writer=newbie, regdate=Wed Aug 09 20:31:20 KST 2023, updateDate=Wed Aug 09 20:31:20 KST 2023), BoardVO(bno=41, title=새로 작성하는 글, content=새로 작성하는 내용, writer=newbie, regdate=Wed Aug 09 22:55:55 KST 2023, updateDate=Wed Aug 09 22:55:55 KST 2023), BoardVO(bno=42, title=새로 작성하는 글, content=새로 작성하는 내용, writer=newbie, regdate=Wed Aug 09 23:03:06 KST 2023, updateDate=Wed Aug 09 23:03:06 KST 2023), BoardVO(bno=61, title=새로 작성하는 글, content=새로 작성하는 내용, writer=newbie, regdate=Thu Aug 10 00:10:22 KST 2023, updateDate=Thu Aug 10 00:10:22 KST 2023), BoardVO(bno=62, title=새로 작성하는 글, content=새로 작성하는 내용, writer=newbie, regdate=Thu Aug 10 00:18:54 KST 2023, updateDate=Thu Aug 10 00:18:54 KST 2023)]}
INFO : org.springframework.web.context.support.GenericWebApplicationContext - Closing org.springframework.web.context.support.GenericWebApplicationContext@5f16132a: startup date [Thu Aug 10 11:47:28 KST 2023]; root of context hierarchy
INFO : com.zaxxer.hikari.HikariDataSource - HikariPool-1 - Shutdown initiated...

10.2.2 등록 처리와 테스트

  • 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:' 접두어를 사용하는데 이를 이용하면 스프링MVC가 내부적으로 response.sendRedirect()를 처리해 주기 때문에 편리하다.
  • 테스트 코드는 아래와 같이 작성한다.
< org.zerock.controller.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);
	}
  • 테스트할 때 MockMvcRequestBuilder의 post()를 이용하면 POST 방식으로 데이터를 전달할 수 있고, param()을 이용해 전달해야 하는 파라미터들을 지정할 수 있다. (< input >)태그와 유사한 역할
  • 이러한 방식으로 코드를 작성하면 최초 작성 시에는 일이 많다고 느껴지지만 매번 입력할 필요가 없기 때문에 오류가 발생하거나 수정하는 경우 반복적인 테스트가 수월해진다.
  • 테스트 코드의 실행 로그는 다음과 같다.
INFO : jdbc.audit - 1. PreparedStatement.new PreparedStatement returned 
INFO : jdbc.audit - 1. Connection.prepareStatement(insert into tbl_board (bno,title,content, writer) 
	values (?, ?, ?, ?)) returned net.sf.log4jdbc.sql.jdbcapi.PreparedStatementSpy@4ed15347
INFO : jdbc.audit - 1. PreparedStatement.setLong(1, 102) returned 
INFO : jdbc.audit - 1. PreparedStatement.setString(2, "테스트 새글 제목") returned 
INFO : jdbc.audit - 1. PreparedStatement.setString(3, "테스트 새글 내용") returned 
INFO : jdbc.audit - 1. PreparedStatement.setString(4, "user00") returned 
INFO : jdbc.sqlonly - insert into tbl_board (bno,title,content, writer) values (102, '테스트 새글 제목', '테스트 새글 내용', 'user00') 
INFO : jdbc.sqltiming - insert into tbl_board (bno,title,content, writer) values (102, '테스트 새글 제목', '테스트 새글 내용', 'user00') 
 {executed in 93 msec}
INFO : jdbc.audit - 1. PreparedStatement.execute() returned false
INFO : jdbc.audit - 1. PreparedStatement.getUpdateCount() returned 1
INFO : jdbc.audit - 1. PreparedStatement.isClosed() returned false
INFO : jdbc.audit - 1. PreparedStatement.close() returned 
INFO : jdbc.audit - 1. Connection.clearWarnings() returned 
INFO : org.zerock.controller.BoardControllerTests - redirect:/board/list
  • 실행되는 로그를 살펴보면 상단에 BoardVO 객체로 올바르게 데이터가 바인딩된 결과를 볼 수 있고, 중간에는 SQL의 실행 결과가 보인다.
  • 마지막에는 최종 반환 문자열을 확인할 수 있다.

10.2.3 조회 처리와 테스트

  • 등록 처리와 유사하게 조회 처리도 BoardController를 이용해 처리할 수 있다.
  • 특별한 경우가 아니라면 조회도 GET 방식으로 처리하므로, @GetMapping을 이용한다.
< org.zerorck.controller.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을 파라미터로 지정한다.
  • 조회에 대한 테스트 코드는 아래와 같다.
< org.zerock.controller.BoardControllerTests 클래스 >
	// 특정 게시글 조회
	@Test
	public void testGet() throws Exception {		
		log.info(mockMvc.perform(MockMvcRequestBuilders
				.get("/board/get")
				.param("bno", "2"))
				.andReturn()
				.getModelAndView().getModelMap());
	}
  • 특정 게시물을 조회할 때 반드시 'bno'라는 파라미터가 필요하므로 param()을 통해 추가하고 실행한다.
  • 실행되는 로그는 아래와 같다.
INFO : org.zerock.controller.BoardControllerTests - {board=BoardVO(bno=4, title=테스트 제목, content=테스트 내용, writer=USER00, regdate=Wed Aug 09 11:33:57 KST 2023, updateDate=Wed Aug 09 11:33:57 KST 2023), org.springframework.validation.BindingResult.board=org.springframework.validation.BeanPropertyBindingResult: 0 errors}
INFO : org.springframework.mock.web.MockServletContext - Initializing Spring FrameworkServlet ''
INFO : org.springframework.test.web.servlet.TestDispatcherServlet - FrameworkServlet '': initialization started
INFO : org.springframework.test.web.servlet.TestDispatcherServlet - FrameworkServlet '': initialization completed in 0 ms
INFO : org.zerock.controller.BoardController - list
  • 파라미터가 제대로 수집되었는지 확인하고 SQL의 처리결과를 확인할 수 있다.
  • 마지막에는 Model에 담긴 BoardVO 인스턴스 내용을 살펴볼 수 있다.

10.2.4 수정 처리와 테스트

  • 수정 작업은 등록과 유사하다.
  • 변경된 내용을 수집해 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";
	}
  • service.modify()는 수정 여부를 boolean으로 처리하므로 이를 이용해 성공한 경우에만 RedirectAttributes에 추가한다.
  • 테스트 코드는 아래와 같다.
< org.zerock.controller.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);
	}
  • 테스트 로그는 기존과 유사하다.
INFO : jdbc.sqltiming - update tbl_board set title= '수정된 테스트 새글 제목', content= '수정된 테스트 새글 내용', writer = 'user00', updateDAte 
= sysdate where bno = 1 
 {executed in 1 msec}
INFO : jdbc.audit - 1. PreparedStatement.execute() returned false
INFO : jdbc.audit - 1. PreparedStatement.getUpdateCount() returned 1
INFO : jdbc.audit - 1. PreparedStatement.isClosed() returned false
INFO : jdbc.audit - 1. PreparedStatement.close() returned 
INFO : jdbc.audit - 1. Connection.clearWarnings() returned 
INFO : org.zerock.controller.BoardControllerTests - redirect:/board/list

10.2.5 삭제 처리와 테스트

  • 삭제 처리도 조회와 유사하게 BoardController와 테스트 코드를 작성한다.
  • 삭제는 반드시 POST 방식으로만 처리한다.
< org.zerock.controller.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";
	}
  • BoardController의 remove()는 삭제 후 페이지의 이동이 필요하므로 RedirectAttributes를 파라미터로 사용하고 'redirect'를 이용해 삭제 처리 후 다시 목록 페이지로 이동한다.
  • 테스트 코드는 기존의 등록 처리와 유사하다.
< org.zerock.controller.BoadControllerTests 클래스 >
	// 게시글 삭제
	@Test
	public void testRemove() throws Exception {
		// 삭제 전 데이터베이스에 게시물 번호 확인할 것
		String resultPage = mockMvc.perform(MockMvcRequestBuilders.post("/board/remove")
				.param("bno", "82")
				).andReturn().getModelAndView().getViewName();		
		log.info(resultPage);
	}
  • MockMvc를 이용해 파라미터를 전달할 때에는 문자열로만 처리해야 한다.
  • 테스트 전에 게시물의 번호가 존재하는지 확인하고 테스트를 실행한다.
  • 로그의 일부는 아래와 같이 SQL이 실행되는 것을 확인할 수 있다.
INFO : jdbc.sqltiming - delete from tbl_board where bno = 82 
 {executed in 2 msec}
INFO : jdbc.audit - 1. PreparedStatement.execute() returned false
INFO : jdbc.audit - 1. PreparedStatement.getUpdateCount() returned 1
INFO : jdbc.audit - 1. PreparedStatement.isClosed() returned false
INFO : jdbc.audit - 1. PreparedStatement.close() returned 
INFO : jdbc.audit - 1. Connection.clearWarnings() returned 
profile
한 걸음 한 걸음 나아가는 개발자

0개의 댓글