표현계층해서 해야하는 일은 Request Mapping Table 설계와 이에 맞게 Controller , JSP를 구현해야한다
Spring Element 는 모든 빈들에 대한 정보를 담고 있다.
안전성을 위해 항상 먼저 Test를 하고 기능검증 후 코드작성
@Log4j2
@NoArgsConstructor
// Spring Bean Container & DI 수행시키는 어노테이션
@ExtendWith(SpringExtension.class)
@ContextConfiguration(locations = {
"file:src/main/webapp/WEB-INF/**/*-context.xml"
})
// Spring MVC Framework 구동시키는 어노테이션
@WebAppConfiguration
@TestInstance(Lifecycle.PER_CLASS)
@TestMethodOrder(MethodOrderer.OrderAnnotation.class)
public class BoardControllerTests {
//WAS 구동없이 테스트 하려는 대상이 BoardController 이니,
// 이 BoardController 를 주입받아, 메소드 호출로 테스트해야지 --> 틀린생각
// @Setter(onMethod_= {@Autowired})
// private BoardController controller;
// Spring Beans Container(type:WebApplicationContext) 자체를 주입한다.
@Setter(onMethod_= {@Autowired})
private WebApplicationContext ctx;
@BeforeAll
void beforeAll() {
log.trace("beforeAll() invoked.");
assertNotNull(this.ctx);
log.info("\t+ ctx:{}", this.ctx);
}// beforeAll
@Test
@Order(1)
@DisplayName("1. BoardController.list")
@Timeout(value=3, unit =TimeUnit.SECONDS)
void testList() throws Exception {
log.trace("testList() invoked.");
//1.
//WAS 구동없이 , Controller 의 Handler method를 테스트하려면 , Spring-test 의존성에 포함된
// MockMVC 객체가 필요하다. 이 MockMVC 객체를 생성하기 위해서 ,
//바로 위에서 주입받은 Spring Beans Container가 필요하다 .
//2. 아래의 MockMvc의 메소드들은 , fluent-api , method -chaining 기법으로 사용하게된다.
//Step.1 MockMvcBuilder 객체를 어는다.
// MockMvcBuilder mockMvcBuilder = MockMvcBuilders.webAppContextSetup(ctx);
//Step.2 MockMvc 객체를 생성한다.
// MockMvc mockMvc = mockMvcBuilder.build() ;
//Step.3 RequestBuilder 객체를 생성하기
// (1) 전송 파라미터가 없는 경우에는 아래와 같이 하고
// RequestBuilder reqBuilder = MockMvcRequestBuilders.get("/board/list");
//(2) 전송 파라미터까지 보내야 하는 경우에는, 아래와 같이 한다.
// MockHttpServletRequestBuilder reqbuilder = MockMvcRequestBuilders.get("/board/list");
// reqbuilder.param(전송파라미터 이름, 전송값(가변인자));
// reqbuilder.param( "name", "koo");
// reqbuilder.param( "age", "10");
// reqbuilder.param( "hobby", "Book","Movie");
//Step4. 실제 Controller로 요청(Request) 보내기
// 요청을 보낸 결과로, Model & View 이름을 받을 수 있음
//mockMvc 의 핵심 메소드는 perform
// ResultActions resultActions = mockMvc.perform(reqBuilder);
//Step.5 step4에서 발생한 Model & View 이름을 얻어낸다.
// MvcResult mvcResult = resultActions.andReturn();
// Step.6 step5에서 얻어낸 MvcResult 의 getter 메소드를 이용해서
// Test Target 인 BoardController's handler 메소드의 수행결과인 모델과 뷰를 얻어냄
// ModelAndView modelAndView = mvcResult.getModelAndView();
// Step.7 ModelAndView 객체로부터 아래의 2가지 정보를 획득하여 출력
// (1) 반환된 뷰의 이름 (2) 반환된 비지니스 데이터 (즉, Model 객체)
// String viewName = modelAndView.getViewName();
// ModelMap model = modelAndView.getModelMap();
// log.info("\t + viewName : {} " , viewName);
// log.info("\t + model : {} " , model);
//--------------- 설명없이 쭉쭊
MockMvcBuilder mockMvcBuilder = MockMvcBuilders.webAppContextSetup(ctx);
MockMvc mockMvc = mockMvcBuilder.build() ;
RequestBuilder reqBuilder = MockMvcRequestBuilders.get("/board/list");
ModelAndView modelAndView =
mockMvc
.perform(reqBuilder)
.andReturn()
.getModelAndView();
String viewName = modelAndView.getViewName();
ModelMap model = modelAndView.getModelMap();
log.info("\t + modelAndView : {} " , modelAndView);
// view와 model의 map형식으로 되어있는 것을 확인
}// testList
}// end class
@Log4j2
@AllArgsConstructor
@RequestMapping("/board/")
@Controller
public class BoardController
/*implements InitializingBean, DisposableBean*/ {
//InitializingBean: 빈을 초기화할 기회를 주는 인터페이스
// DisposableBean: 생성된 빈 객체를 파괴하기 직전 콜백하는 메서드를 가지고 있는 인터페이스
private BoardService service;
// @Override
// public void destroy() throws Exception {
//
//
// }//destroy
// @Override
// public void afterPropertiesSet() throws Exception {
// log.trace("afterPropertiesSet() invoked.");
//
// assert this.service != null;
// log.info("\t + this.service :{}", this.service);
// }//afterPropertiesSet
@GetMapping("/list")
public void list(Model model) throws ControllerException {
log.trace("list() invoked");
try {
List<BoardVO> list = this.service.getList();
model.addAttribute("list", list); // JSP로 전달할 모델 데이터를 상자에 넣는다.
} catch (Exception e) {
throw new ControllerException(e);
}
}//list
@GetMapping("/register")
public String register(BoardDTO dto , RedirectAttributes rttrs) throws ControllerException {
log.trace("register invoked.");
try {
boolean isRegister = this.service.register(dto);
log.info("\t + isRegister:{}",isRegister);
//1. Session Scope에 아래의 이름과 값으로 바인딩해서 전달 (공유)
rttrs.addFlashAttribute("result", (isRegister)? "SUCCESS("+dto.getBno()+")": "FAILURE");
// 2. Get방식의 전송파라미터 (즉, Query String 형태로 전달, 예 ? result=FAILURE )
// rttrs.addAttribute("result", (isRegister)? "SUCCESS("+dto.getBno()+")": "FAILURE");
return "redirect: /board/list";
}catch (Exception e) {
throw new ControllerException(e);
}//try - catch
}// register
@PostMapping("/modify")
public String modify(BoardDTO dto, RedirectAttributes rttrs) throws ControllerException{
log.trace("modify({}) invoked.",dto);
try {
boolean isModify = this.service.modify(dto);
log.info("\t + isRegister:{}",isModify);
//1. Session Scope에 아래의 이름과 값으로 바인딩해서 전달 (공유)
// rttrs.addFlashAttribute("result", (isModify)? "SUCCESS("+dto.getBno()+")": "FAILURE");
// 2. Get방식의 전송파라미터 (즉, Query String 형태로 전달, 예 ? result=FAILURE )
rttrs.addAttribute("result", (isModify)? "SUCCESS("+dto.getBno()+")": "FAILURE");
return "redirect: /board/list";
}catch (Exception e) {
throw new ControllerException(e);
}//try - catch
}// modify
@PostMapping("/remove")
public String remove(BoardDTO dto, RedirectAttributes rttrs) throws ControllerException{
log.trace("modify({}) invoked.",dto);
try {
boolean isRemove = this.service.remove(dto);
log.info("\t + isRegister:{}",isRemove);
//1. Session Scope에 아래의 이름과 값으로 바인딩해서 전달 (공유)
// rttrs.addFlashAttribute("result", (isRemove)? "SUCCESS("+dto.getBno()+")": "FAILURE");
// 2. Get방식의 전송파라미터 (즉, Query String 형태로 전달, 예 ? result=FAILURE )
rttrs.addAttribute("result", (isRemove)? "SUCCESS("+dto.getBno()+")": "FAILURE");
return "redirect: /board/list";
}catch (Exception e) {
throw new ControllerException(e);
}//try - catch
}// remove
@GetMapping({"/get","/modify"})
public void get(BoardDTO dto, Model model) throws ControllerException{
log.trace("modify({}) invoked.",dto);
try {
BoardVO vo = this.service.get(dto);
log.info("\t + isRegister:{}",vo);
model.addAttribute("board",vo);
}catch (Exception e) {
throw new ControllerException(e);
}//try - catch
}// remove
}// end class
위에서 리다이렉트로 targetURL 재요청시, 필요하다면 데이터를 보낼 수 있는데 이 때 필요한것이 바로 RedirectAttributters (줄여서 rttrs)
타입의 임시상자를 Spring에서 제공한다. rttrs의 2가지 메소드가
addFlashAttribute/ addAttribute 이다.
webapp에 리다이렉트의 폴더를 따로 만든다.
위에처럼 Spring MVC 기반의 Controller handler method 안에서
소위 return "redirect:targetURL"
A > B > C 이런식으로 예제
~A~
<body>
<h1>/redirect/A.jsp</h1>
<hr>
<!-- 웹브라우저에게 302/307/308 상태코드의 리다이렉트 응답을 전송 -->
<% // scriptlet tag : 자바코드를 작성할 때 사용하는 태그
// 빨간색 용지에 최우선순위값 1을 기재해서, B.jsp 에게 보여주자!!!
// 방법2가지(B.jsp에게 데이터를 보내는 방법):
// (1) 전송파라미터로 보내자! (즉, Query String 형태로 구성해서...)
// (2) 공유영역인 Session Scope에 올려놔서 보내자!(공유시킴)
// 1st. method
response.sendRedirect("/redirect/B.jsp?priority=1&color=red");
%>
</body>
~B~
<body>
<h1>/redirect/B.jsp</h1>
<hr>
<!-- A.jsp 쪽에서 준, 쪽지의 색깔과 우선순위값을 확인하자! -->
<%
String color = request.getParameter("color");
String priority = request.getParameter("priority");
%>
<!-- <h2>1. color: <%= color %></h2>
<h2>2. priority: <%= priority %></h2> -->
<!-- --------------- -->
<% // Scriptlet tag
// 2nd. method (Session Scope 공유영역을 활용)
session.setAttribute("color", "BLUE");
session.setAttribute("priority", "1");
response.sendRedirect("/redirect/C.jsp"); // 302응답코드를 가진 응답문서가 전송
%>
</body>
~C~
<body>
<h1>/redirect/C.jsp</h1>
<hr>
<!-- B.jsp 쪽에서 준, 쪽지의 색깔과 우선순위값을 확인하자! -->
<%
// Request Scope 공유영역에 올려 놓은 공유 데이터를 활용
String color = (String) session.getAttribute("color");
String priority = (String) session.getAttribute("priority");
%>
<h2>1. color: <%= color %></h2>
<h2>2. priority: <%= priority %></h2>
</body>
A실행 순간 C로 전송 값과 함께 넘어간다.
<script src="https://cdnjs.cloudflare.com/ajax/libs/jquery/3.6.0/jquery.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/jquery-migrate/3.4.0/jquery-migrate.min.js"></script>
<script>
$(function () {
console.clear();
$('#listBtn').on('click', function () {
// [ GET /board/list ] Request
location.href = "/board/list";
}); // .onclick
$('#modifyBtn').click( () => {
// [ GET /board/modify ] Request
location.href = "/board/modify?bno=${board.bno}";
}); //click
}); // .jq
</script>
</head>
<body>
<h2>/WEB-INF/views/board/get.jsp</h2>
<hr>
<div id="container">
<form action="#">
<table>
<caption><h3>게시글 번호: ${board.bno}</h3></caption>
<tr>
<td><label for="title">제목</label></td>
<td><input type="text" name="title" size="50" value="${board.title}" readonly></td>
</tr>
<tr>
<td><label for="content">내용</label></td>
<td><textarea name="content" cols="52" rows="10" readonly>${board.content}</textarea></td>
</tr>
<tr>
<td><label for="writer">작성자</label></td>
<td><input type="text" name="writer" size="20" value="${board.writer}" readonly></td>
</tr>
<tr>
<td colspan="2"> </td>
</tr>
<tr>
<td colspan="2">
<button type="button" id="modifyBtn">MODIFY</button>
<button type="button" id="listBtn">LIST</button>
</td>
</tr>
</table>
</form>
</div>
</body>
<script src="https://cdnjs.cloudflare.com/ajax/libs/jquery/3.6.0/jquery.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/jquery-migrate/3.4.0/jquery-migrate.min.js"></script>
<script>
$(function () {
console.clear();
$('#regBtn').on('click', function () {
// [ GET /board/register ] Request
location.href = "/board/new";
}); // .onclick
}); // .jq
</script>
<script>
var result = "${param.result}";
if(result != null && result.length > 0) {
alert(result);
} // if
</script>
</head>
<body>
<h2>/WEB-INF/views/board/list.jsp</h2>
<hr>
<div id="wrapper">
<button type="button" id="regBtn">REGISTER</button>
<table border="1">
<caption>tbl_board</caption>
<thead>
<tr>
<th>bno</th>
<th>title</th>
<th>writer</th>
<th>insertTs</th>
<th>updateTs</th>
</tr>
</thead>
<tbody>
<c:forEach var="board" items="${list}">
<tr>
<td>${board.bno}</td>
<td><a href="/board/get?bno=${board.bno}">${board.title}</a></td>
<td>${board.writer}</td>
<td><fmt:formatDate pattern="yyyy-MM-dd HH:mm:ss" value="${board.insertTs}" /></td>
<td><fmt:formatDate pattern="yyyy-MM-dd HH:mm:ss" value="${board.updateTs}" /></td>
</tr>
</c:forEach>
</tbody>
<tfoot></tfoot>
</table>
</div>
</body>
<script src="https://cdnjs.cloudflare.com/ajax/libs/jquery/3.6.0/jquery.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/jquery-migrate/3.4.0/jquery-migrate.min.js"></script>
<script>
$(function () {
console.clear();
$('#listBtn').on('click', function () {
// [ GET /board/list ] Request
location.href = "/board/list";
}); // .onclick
$('#removeBtn').click( () => {
// [ POST /board/remove + bno ] Request
let formObj = $('form');
formObj.attr('action', '/board/remove');
formObj.attr('method', 'POST');
formObj.submit();
}); //click
}); // .jq
</script>
</head>
<body>
<h2>/WEB-INF/views/board/modify.jsp</h2>
<hr>
<div id="container">
<form action="/board/modify" method="POST">
<input type="hidden" name="bno" value="${board.bno}">
<table>
<caption><h3>수정할 게시글 번호: ${board.bno}</h3></caption>
<tr>
<td><label for="title">제목</label></td>
<td><input type="text" name="title" size="50" value="${board.title}"></td>
</tr>
<tr>
<td><label for="content">내용</label></td>
<td><textarea name="content" cols="52" rows="10">${board.content}</textarea></td>
</tr>
<tr>
<td><label for="writer">작성자</label></td>
<td><input type="text" name="writer" size="20" value="${board.writer}" readonly></td>
</tr>
<tr>
<td colspan="2"> </td>
</tr>
<tr>
<td colspan="2">
<button type="submit" id="submitBtn">SUBMIT</button>
<button type="button" id="removeBtn">REMOVE</button>
<button type="button" id="listBtn">LIST</button>
</td>
</tr>
</table>
</form>
</div>
</body>
<script src="https://cdnjs.cloudflare.com/ajax/libs/jquery/3.6.0/jquery.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/jquery-migrate/3.4.0/jquery-migrate.min.js"></script>
<script>
$(function () {
console.clear();
$('#listBtn').on('click', function () {
// [ GET /board/list ] Request
location.href = "/board/list";
}); // .onclick
}); // .jq
</script>
</head>
<body>
<h2>/WEB-INF/views/board/new.jsp</h2>
<hr>
<div id="wrapper">
<form action="/board/register" method="POST">
<table>
<tr>
<td><label for="title">제목</label></td>
<td><input type="text" name="title" size="50" placeholder="제목을 입력하세요" required></td>
</tr>
<tr>
<td><label for="content">내용</label></td>
<td><textarea name="content" cols="52" rows="10" placeholder="내용을 입력하세요" required></textarea></td>
</tr>
<tr>
<td><label for="writer">작성자</label></td>
<td><input type="text" name="writer" size="20" placeholder="작성자" required></td>
</tr>
<tr>
<td colspan="2"> </td>
</tr>
<tr>
<td colspan="2">
<button type="submit" id="submitBtn">SUBMIT</button>
<button type="button" id="listBtn">LIST</button>
</td>
</tr>
</table>
</form>
</div>
</body>