새 게시물에 대한 기간을 정하고, (등록된지 n시간 혹은 n일 내의 게시물에 대해서) 정해진 기한 내의 게시물의 경우 new 마크를 부여해 보려고 한다.
@Getter
@Setter
@NoArgsConstructor
@AllArgsConstructor
@ToString
public class BoardVO {
private int boardNo;
private String title;
private String content;
private String writer;
private Timestamp regDate;
private int viewCnt;
//new 마크 부착 여부 논리 타입 필드
private boolean newMark;
}
BoardVO 클래스에 private boolean newMark;
를 추가한다.
@Override
public List<BoardVO> getArticleList(PageVO paging) {
List<BoardVO> list = mapper.getArticleList(paging);
for(BoardVO article : list) {
long now = System.currentTimeMillis();
long regTime = article.getRegDate().getTime();
if(now-regTime < 60*60*24*1000) {
article.setNewMark(true);
}
}
return list;
}
강사님께서 처리하신 방법은 위의 방법으로
페이징 객체에 의해 현재 얻어와야 할 게시물만 리스트에 넣어서 가져온 다음
BoardVO 형의 article로 리스트를 순환하면서
시스템의 현재 시간을 currentTimeMillis()
로 얻어오고, 게시물의 시간은 멤버변수 regDate
로 들고 있었으므로 getRegDate().getTime()
메서드로 등록 시간을 가져오도록 했다.
그리고 (now - regTime) = 즉, 현재 시스템 시간에서 게시물 등록 시간을 뺐을 때의 시간차가 (60초 60분 24시간 * 1000밀리초) 1일 내인 경우 전부 new마크를 달도록 해 놓았다.
1000밀리초 = 1초 60 = 60초
60초 60초 = 3600초 (60분)
1(=60분) * 24(시간) = 24시간 (하루)
이렇게 계산된 것.
이제 list.jsp로 이동하여 new 마크를 달아 주어 보자.
...
<c:if test="${b.newMark}">
<img alt="newmark" src="<c:url value='/img/icon_new.gif' />">
</c:if>
...
${articles}
를 b
로 순환하고 있고, 해당 게시물에 대해서 newmark
가 true
라면, new 표시를 해 주도록 하고 있다.
잘 뜨는 것을 확인할 수 있다.
이제는 검색 기능을 만들기로 한다.
...
<!-- 검색 버튼 -->
<div class="row">
<div class="col-sm-2"></div>
<div class="form-group col-sm-2">
<select id="condition" class="form-control" name="condition">
<option value="title" ${param.condition == 'title'? 'selected' : ''}>제목</option>
<option value="content" ${param.condition == 'content'? 'selected' : ''}>내용</option>
<option value="writer" ${param.condition == 'writer'? 'selected' : ''}>작성자</option>
<option value="titleContent" ${param.condition == 'titleContent'? 'selected' : ''}>제목+내용</option>
</select>
</div>
<div class="form-group col-sm-4">
<div class="input-group">
<input type="text" class="form-control" name="keyword" id="keywordInput" placeholder="검색어" value="${param.keyword}">
<span class="input-group-btn">
<input type="button" value="검색" class="btn btn-cpp btn-flat" id="searchBtn">
</span>
</div>
</div>
<div class="col-sm-2">
<a href="<c:url value='/board/write' />" class="btn btn-cpp float-right">글쓰기</a>
</div>
<div class="col-sm-2"></div>
</div>
...
검색 버튼의 코드는 위와 같다.
검색 조건에 대해서는 <select>
태그의 id
인 condition
으로 식별하고, 안의 <option>
태그에서 값을 결정한 다음 value
에 넣어서 넘겨줄 것이다.
이제 검색 버튼이 눌렸을 때, URL을 적절하게 만들어서 넘겨주는 처리를 자바스크립트로 만들어보자.
//검색 버튼 이벤트 처리
$('#searchBtn').click(function() {
const keyword = $('#keywordInput').val();
const condition = $('#condition').val();
location.href="/board/list?keyword=" + keyword + "&condition=" + condition;
});
//검색창에서 엔터키 입력 시 이벤트 처리
$('#keywordInput').keydown(function(e) {
if(e.keyCode === 13) { //키가 13번이면 실행 (13 -> 엔터)
$('#searchBtn').click();
}
});
searchBtn
이라는 id를 가진 태그에 click()
이벤트가 발생하면 keywordInput
태그의 value
즉, 검색어를 가져오고 condition
의 경우 방금 살펴봤듯이 검색 조건을 가져오도록 하고 있다.
그 다음 location.href
를 검색어와 & 조건을 넣어서 보내줄 수 있도록 만들고 있다.
우리는 검색을 할 때 키워드 말고도 조건을 같이 넘겨주고 있는데, 특정 조건이 true인가 체크하면서 검색을 하려면 컨트롤러에서 if 조건만 4번을 해야 하고 (제목, 작성자, 내용, 제목+내용), 게다가 mapper에서 쿼리만 4종류를 만들어야 할 수도 있다.
//검색 처리 이후 게시글 목록 불러오기 요청
@GetMapping("/list")
public void list(SearchVO search, Model model) {
System.out.println("/board/list: GET");
System.out.println("페이지 번호: " + search.getPage());
System.out.println("검색어: " + search.getKeyword());
System.out.println("검색 조건: " + search.getCondition());
List<BoardVO> list = service.getArticleList(search);
PageCreator pc = new PageCreator();
pc.setPaging(search);
pc.setArticleTotalCount(service.countArticles(search));
System.out.println(pc);
model.addAttribute("articles", list);
model.addAttribute("pc", pc);
}
전에는 페이징 객체를 받아왔던 것과 달리 이번에는 SearchVO 객체를 받아오고 있다.
그런데 이러면 페이징 처리는 어디서 어떻게 하는 걸까?
해서 찾아보니 PageVO는 이제 PageCreator 객체가 멤버변수로써 PageVO를 들고있게 한 것 같다.
나머지 로직의 경우에는 전과 크게 특출나게 다른 건 없으니, mapper로 가서 SQL을 어떻게 써먹고 있는지 한 번 확인해 보자.
...
<!-- 중복되는 동적 SQL문을 미리 선언해 놓고 삽입하는 방식.
<include refid="sql id" /> 를 통해 삽입해서 사용합니다.
-->
<sql id="search">
<if test="condition == 'title'">WHERE title LIKE '%'||#{keyword}||'%'</if>
<if test="condition == 'writer'">WHERE writer LIKE '%'||#{keyword}||'%'</if>
<if test="condition == 'content'">WHERE content LIKE '%'||#{keyword}||'%'</if>
<if test="condition == 'titleContent'">WHERE title LIKE '%'||#{keyword}||'%'
OR content LIKE '%'||#{keyword}||'%'</if>
</sql>
...
<!-- 쿼리문을 작성할 때 '<', '>', '&'등의 기호를 사용해야 하는 경우가 생기는데,
xml 파일에서 이를 그냥 사용할 경우, 태그로 인식되는 경우가 종종 있습니다.
이럴 경우에는 해당 기호가 태그 문법이 아닌 실제 쿼리에 필요한 문자라고 인식시켜야
합니다. 이 때 사용하는 문법이 <![CDATA[... 쿼리 ...]]> 입니다.
CDATA 안에 쿼리를 작성하면 쿼리 내용의 괄호나 특수문자를
마크업 언어로 인식하지 않고 문자열로 인식하게 됩니다.
< (<) > (>) -->
<select id="getArticleList" resultMap="BoardMap">
SELECT * FROM
(
SELECT ROWNUM AS rn, tbl.* FROM
(
SELECT * FROM mvc_board
<include refid="search" />
ORDER BY board_no DESC
) tbl
)
<![CDATA[
WHERE rn > (#{page}-1) * #{cpp}
AND rn <= #{page} * #{cpp}
]]>
</select>
...
위쪽에 <sql>
태그로 미리 sql문을 정해준다. id
로 나중에 끌어다 쓸 수 있게 이름을 정해두면 되고..
내부에선 <if>
태그를 사용해서 조건을 정해 놓고 조건에 일치할 시 <if>
내의 sql문을 value로써 갖는 것 같다.
그리고 밑의 쿼리문은 지난 날의 강의와 크게 달라진 건 없지만 <include refid="search" />
이 부분이 주요하게 추가된 부분이다.
아까 위에서 만든 sql id
를 refid=
의 속성값으로 주면 된다.
물론 여기만 수정된 건 아니고, 몇몇 수정된 클래스들이 있다.
IBoardMapper.java
...
//검색 기능이 추가된 게시글 목록 조회 기능
List<BoardVO> getArticleList(SearchVO search);
...
IBoardService.java
...
//검색 기능이 추가된 게시글 목록 조회 기능
List<BoardVO> getArticleList(SearchVO search);
...
BoardService.java
...
@Override
public List<BoardVO> getArticleList(SearchVO search) {
List<BoardVO> list = mapper.getArticleList(search);
for(BoardVO article : list) {
long now = System.currentTimeMillis();
long regTime = article.getRegDate().getTime();
if(now-regTime < 60*60*24*1000) {
article.setNewMark(true);
}
}
return list;
}
...
인터페이스 클래스들은 단순히 명세를 해둔 것 뿐이므로 따로 설명할 것이 없고, 서비스 클래스의 경우 전에 페이징 객체를 getArticleList()
의 매개변수로 넘겨주었던 것을 SearchVO
객체를 넘겨주게 바뀐 것을 빼고는 나머지가 전부 동일하다.
물론 이렇게 클래스로 포장해서 보내는 방법 말고도 @Param
을 사용하는 방법이나 Map
으로 포장해서 보내는 방법도 있긴 하다.
현재까지 검색 기능 구현을 완료한 상태로 검색을 시도해 보면, 검색 후의 첫 결과 페이지 자체는 잘 표시가 된다.
하지만 다른 페이지 번호를 눌러서 이동하는 경우, 검색 결과가 사라지고 일반적인 게시물 목록이 담긴 페이지가 표시되어버린다.
이것을 고쳐야 하는데, list.jsp의 다음 부분을 고치면 된다.
<!-- 페이지 버튼 -->
...
<c:forEach var="pageNum" begin="${pc.beginPage}" end="${pc.endPage}">
<li class="page-item">
<a href="<c:url value='/board/list${pc.makeURI(pageNum)}' />" class="page-link ${pc.paging.page == pageNum ? 'page-active' : ''}" style="margin-top: 0; height: 40px; color: pink; border: 1px solid #643691;">${pageNum}</a>
</li>
</c:forEach>
...
우선 표시할 시작 페이지부터 끝 페이지까지의 번호를 PageCreator
객체로부터 얻어와 pageNum
변수로 순환하도록 한다.
중간에 makeURI가 껴 있는데..
...
//URI 파라미터를 쉽게 만들어 주는 유틸 메서드
public String makeURI(int page) {
UriComponents ucp = UriComponentsBuilder.newInstance().queryParam("page", page)
.queryParam("cpp", paging.getCpp())
.queryParam("keyword", ((SearchVO)paging).getKeyword())
.queryParam("condition", ((SearchVO)paging).getCondition())
.build();
return ucp.toUriString();
}
...
PageCreator 클래스에 정의되어 있는 부분이다.
조금 복잡하게 느껴졌는데, 차근차근 분석해보자.
.queryParam()
은 무엇일까? 다른 블로그를 참고해보니 여러 Attribute 들을 하나로 합치기 전에, 하나씩 어트리뷰트를 추가하는 메서드라고 하는 것 같다.
그리고 (SearchVO)
로 캐스팅된 부분이 있는데, SearchVO 는 PageVO를 상속받아 만들었기 때문에 paging 객체는 SearchVO 형태로 다운캐스팅이 가능한 듯.
어쨌든 차례로
사용자가 선택한 페이지 번호(page)
한 페이지당 보여줄 게시물 개수(getCpp())
검색어 (getKeyword())
검색조건 (getCondition())
을 가져오고 있다.
약간 이해가 안 되는 부분이 SearchVO
에나 keyword 와 condition이 저장되어 있는데, paging 객체를 다운캐스팅 한다고 없던 keyword와 condition이 사용 가능해지는건가?
애초에 paging 객체는 멤버변수로 들고 있지만, SearchVO 객체는 멤버변수에 없기도 하고 선언조차도 안 했어서 대체 값을 어떻게 가져오는건지.. 잘 모르겠다.
여튼 최종적으로는 만들어진 URI를 return ucp.toUriString();
이렇게 리턴하는 것 같다.
그리고 그 뒤에는 class="page-link ${pc.paging.page == pageNum ? 'page-active' : ''}
pageCreator 내의 멤버변수로 만들어 둔 PageVO 객체에 접근해서, 현재 사용자가 위치한 페이지를 얻어와서 pageNum과 비교하는데, 페이지 버튼 그릴 때 순환하다가 현재 사용자가 위치한 페이지와 일치하는 버튼에 대해서만 배경색을 바꿔주는 처리를 하는 부분이다.
이렇게 말이다. (1번)
찾아보니까 제대로 설명된데다가 결과까지 확인할 수 있는 예제를 진행하셨어서.. 다음과 같다.
package com.spring.mvc.board;
import org.junit.Test;
import org.springframework.web.util.UriComponents;
import org.springframework.web.util.UriComponentsBuilder;
public class UriComponentTest {
@Test
public void testUriComp() {
//Uri를 쉽게 작성할 수 있도록 도와주는 유틸 클래스
//UriComponentBuilder 사용하기.
UriComponents ucp = UriComponentsBuilder.newInstance().queryParam("page", 3)
.queryParam("cpp", 10)
.queryParam("keyword", "메롱")
.queryParam("condition", "title")
.build();
System.out.println("/board/list" + ucp.toUriString());
}
}
위에서 구구절절 설명했으니 설명은 생략하고,
결과를 보면 어떤 식으로 사용되는지 한 방에 알 수가 있다.
출력 결과 :
/board/list?page=3&cpp=10&keyword=메롱&condition=title
이런 식으로 출력되게 된다.
일일히 조건들에 대해서 복잡하게 따로 작성할 필요가 없어졌다!
<%@ page language="java" contentType="text/html; charset=UTF-8"
pageEncoding="UTF-8"%>
<%@ taglib prefix="c" uri="http://java.sun.com/jsp/jstl/core"%>
<%@ taglib prefix="fmt" uri="http://java.sun.com/jsp/jstl/fmt"%>
<jsp:include page="../include/header.jsp" />
<style>
header.masthead {
display: none;
}
.btn-orange {
background-color: orange;
color: white;
}
.btn-cpp {
background-color: #643691;
color: white;
}
.page-active {
background: #643691;
}
</style>
<br><br>
<!-- Begin Page Content -->
<div class="container">
<div class="row">
<div class="col-lg-2">
</div>
<div class="col-lg-8">
<div class="panel-body">
<h2 class="page-header"><span style="color: #643691;">Spring</span> 자유 게시판
<span id="count-per-page" style="float: right;">
<input class="btn btn-cpp" type="button" value="10">
<input class="btn btn-cpp" type="button" value="20">
<input class="btn btn-cpp" type="button" value="30">
</span>
</h2>
<table class="table table-bordered table-hover">
<thead>
<tr style="background-color: #643691; margin-top: 0; height: 40px; color: white; border: 0px solid #f78f24; opacity: 0.8">
<th>#번호</th>
<th>작성자</th>
<th>제목</th>
<th>작성일</th>
<th>조회수</th>
</tr>
</thead>
<!-- 게시물이 들어갈 공간 -->
<c:forEach var="b" items="${articles}">
<tr style="color: #643691;">
<td>${b.boardNo}</td>
<td>${b.writer}</td>
<td>
<a style="margin-top: 0; height: 40px; color: orange;" href="<c:url value='/board/content/${b.boardNo}${pc.makeURI(pc.paging.page)}' />">
${b.title}
</a>
<c:if test="${b.newMark}">
<img alt="newmark" src="<c:url value='/img/icon_new.gif' />">
</c:if>
</td>
<td>
<fmt:formatDate value="${b.regDate}" pattern="yyyy년 MM월 dd일 HH:mm" />
</td>
<td>${b.viewCnt}</td>
</tr>
</c:forEach>
</table>
<!-- 페이징 처리 부분 -->
<ul class="pagination justify-content-center">
<!-- 이전 버튼 -->
<c:if test="${pc.prev}">
<li class="page-item">
<a class="page-link" href="<c:url value='/board/list${pc.makeURI(pc.beginPage-1)}' />"
style="background-color: #643691; margin-top: 0; height: 40px; color: white; border: 0px solid #f78f24; opacity: 0.8">이전</a>
</li>
</c:if>
<!-- 페이지 버튼 -->
<c:forEach var="pageNum" begin="${pc.beginPage}" end="${pc.endPage}">
<li class="page-item">
<a href="<c:url value='/board/list${pc.makeURI(pageNum)}' />" class="page-link ${pc.paging.page == pageNum ? 'page-active' : ''}" style="margin-top: 0; height: 40px; color: pink; border: 1px solid #643691;">${pageNum}</a>
</li>
</c:forEach>
<!-- 다음 버튼 -->
<c:if test="${pc.next}">
<li class="page-item">
<a class="page-link" href="<c:url value='/board/list${pc.makeURI(pc.endPage+1)}' />"
style="background-color: #643691; margin-top: 0; height: 40px; color: white; border: 0px solid #f78f24; opacity: 0.8">다음</a>
</li>
</c:if>
</ul>
<!-- 페이징 처리 끝 -->
</div>
</div>
</div>
<!-- 검색 버튼 -->
<div class="row">
<div class="col-sm-2"></div>
<div class="form-group col-sm-2">
<select id="condition" class="form-control" name="condition">
<option value="title" ${param.condition == 'title'? 'selected' : ''}>제목</option>
<option value="content" ${param.condition == 'content'? 'selected' : ''}>내용</option>
<option value="writer" ${param.condition == 'writer'? 'selected' : ''}>작성자</option>
<option value="titleContent" ${param.condition == 'titleContent'? 'selected' : ''}>제목+내용</option>
</select>
</div>
<div class="form-group col-sm-4">
<div class="input-group">
<input type="text" class="form-control" name="keyword" id="keywordInput" placeholder="검색어" value="${param.keyword}">
<span class="input-group-btn">
<input type="button" value="검색" class="btn btn-cpp btn-flat" id="searchBtn">
</span>
</div>
</div>
<div class="col-sm-2">
<a href="<c:url value='/board/write' />" class="btn btn-cpp float-right">글쓰기</a>
</div>
<div class="col-sm-2"></div>
</div>
</div>
<jsp:include page="../include/footer.jsp" />
<script>
const msg = '${msg}';
if(msg === 'delSuccess') {
alert('삭제가 완료되었습니다.');
} else if(msg === 'regSuccess') {
alert('등록이 완료되었습니다.');
}
//start jQuery
$(function() {
//한 페이지당 보여줄 게시물 개수가 변동하는 이벤트 처리
$('#count-per-page .btn-cpp').click(function() {
const count = $(this).val();
location.href='/board/list?page=1&cpp=' + count;
});
//검색 버튼 이벤트 처리
$('#searchBtn').click(function() {
const keyword = $('#keywordInput').val();
const condition = $('#condition').val();
location.href="/board/list?keyword=" + keyword + "&condition=" + condition;
});
//검색창에서 엔터키 입력 시 이벤트 처리
$('#keywordInput').keydown(function(e) {
if(e.keyCode === 13) { //키가 13번이면 실행 (13 -> 엔터)
$('#searchBtn').click();
}
});
}); // end jQuery
</script>
곳곳에 위에서 만든 URI 메서드를 사용하도록 하고 있다.
강사님께 제공받은 소스에는 회원가입 페이지가 구현되어 있다.
그러나 백엔드 로직은 구현되어있지 않고 form 을 다 입력하고 submit(회원가입)을 하더라도 아무런 반응은 하지 않는다.
따라서 우리는 회원 가입을 할 수 있게 회원 가입 시스템을 구현해볼 것이다.
REST 방식으로 통신을 하는 이유는 뭘까?
애플용, 안드로이드용, PC 클라이언트용 서버를 전부 유지할 수는 없고, 각 클라이언트마다 운영체제도 다르고 통신 방식이 조금씩 다르기 때문에, 저 모든 플랫폼에 대해서 해석을 할 수 있는 파일을 서로 주고 받으면서 통신을 진행하려고 하는 게 JSON을 통한 REST API 통신이다.
기존의 XML 방식으로 통신을 해도 되기는 하는데, XML은 아무래도 마크업 언어라 내용이 길어지는 부분이 있다. 왠만한 클라이언트단에서는 XML을 해석할 수 있는 라이브러리를 가지고 있긴 하지만, 최근에는 JSON을 사용하는 경우가 많다.
이렇게 대략적인 설명만 봐서는 잘 모르겠고.. 실제로 코드를 쳐 봐야 알 것 같다.
<!-- https://mvnrepository.com/artifact/com.fasterxml.jackson.core/jackson-databind -->
<dependency>
<groupId>com.fasterxml.jackson.core</groupId>
<artifactId>jackson-databind</artifactId>
<version>2.12.3</version>
</dependency>
pom.xml 에 해당 내용 추가
...
<!-- jackson-databind: 데이터를 JSON형태로 파싱해 주는 라이브러리. -->
<dependency>
<groupId>com.fasterxml.jackson.core</groupId>
<artifactId>jackson-databind</artifactId>
<version>2.12.3</version>
</dependency>
...
이렇게 추가하고 나서 프로젝트 대상으로 Maven Update 하는거 잊지 말것.
package com.spring.mvc.test;
import java.util.Arrays;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.ResponseBody;
import org.springframework.web.bind.annotation.RestController;
@RestController
@RequestMapping("/rest")
public class RestControllerTest {
/*
@ResponseBody
- 메서드가 리턴하는 데이터를 viewResolver에게 전달하지 않고
클라이언트에게 해당 데이터를 바로 응답하게 합니다.
비동기 통신에서 주로 많이 사용합니다.
- @RestController를 사용하면 모든 메서드에
@ResponseBody를 붙인 결과와 같습니다.
*/
@GetMapping("/hello")
//@ResponseBody
public String hello() {
return "Hello World!";
}
@GetMapping("/hobby")
//@ResponseBody
public List<String> hobby() {
List<String> hobby = Arrays.asList("축구", "영화감상", "수영");
return hobby;
}
@GetMapping("/study")
public Map<String, Object> study() {
Map<String, Object> subject = new HashMap<>();
subject.put("자바", "java");
subject.put("jsp", "java server pages");
subject.put("스프링", "spring framework5");
return subject;
}
@GetMapping("/person")
public Person person() {
Person p = new Person();
p.setName("김철수");
p.setAge(30);
p.setHobby(Arrays.asList("수영", "축구", "독서"));
return p;
}
//@RequestBody
//클라이언트 쪽에서 전송하는 JSON 데이터를
//서버에서 사용하는 자바 언어에 맞게 변환하여 받을 수 있게 해 주는 아노테이션.
@PostMapping("/getObject")
public Person getObject(@RequestBody Person person) {
System.out.println("/getObject 요청이 들어옴!");
System.out.println("이름: " + person.getName());
System.out.println("나이: " + person.getAge());
System.out.println("취미: " + person.getHobby());
person.setAge(2);
return person;
}
@GetMapping("/getPath/{id}/{cpp}/{page}")
public Map<String, Object> getPath(@PathVariable String id,
@PathVariable int cpp,
@PathVariable int page) {
Map<String, Object> map = new HashMap<>();
map.put("아이디", id);
map.put("게시물개수", cpp);
map.put("페이지번호", page);
return map;
}
}
먼저 이것부터 보자.
@GetMapping("/hello")
//@ResponseBody
public String hello() {
return "Hello World!";
}
우리는 이 클래스에 대해서 /rest
라는 요청이 들어오면 전부 받고 있고, 그 중에서도 /hello
라는 요청이 들어오면 (즉, /rest/hello
) DispatcherServlet이 요청을 받을 수 있는 메서드를 검색해 보고, 이 메서드에 도달하게 될 것이다.
일단 이 메서드에서는 Hello World!
라는 String을 반환하고 있고, 이것을 ViewResolver 가 받은 후, /WEB-INF/views + 리턴한 문장 + .jsp
이런 처리를 해 줄 것이다.
지금 이렇게 살펴본 흐름의 경우는 '약속된 동기적 흐름' 이다.
간단한 데이터의 경우에는 그 과정을 무시하고 비동기적 통신을 하고 싶을 수도 있다.
이걸 당장 어디에 써먹냐? 하면, 회원아이디 중복 테스트를 하는 경우에 써먹을 수 있다.
일련의 통신 과정을 전부 거치지 않고, 중복 여부만 확인해서 화면에 값을 바로 뿌려줄 수 있는 데다가, 새 페이지에서 결과를 받고 그럴 필요 없이 현재 페이지에서 바로 결과를 표시하는 등의 행위가 가능하다.
주석처리된 @ResponseBody
는 이런 역할을 한다.
@ResponseBody - 메서드가 리턴하는 데이터를 viewResolver에게 전달하지 않고 클라이언트에게 해당 데이터를 바로 응답하게 합니다. 비동기 통신에서 주로 많이 사용합니다. - @RestController를 사용하면 모든 메서드에 @ResponseBody를 붙인 결과와 같습니다.
녹강중에는 주석 처리가 안 되어있긴 했는데, 아마 후반부에 처리가 된 것 같다. 왜냐하면 클래스 어노테이션에 @RestController
가 붙어있기 때문이다.
REST 방식 통신 테스트를 위해서 크롬 웹스토어에서 Yet Another REST Client를 설치하겠다.
설치하고 툴바에서 클릭해보면 이러한 페이지로 이동된다.
이제 저기 URL 부분에 요청을 보내면 반환되는 결과를 밑에서 확인할 수 있게 된다.
왠지 모르겠지만 hello 요청은 제대로 반환이 안 되고 있지만, hobby 요청의 경우엔 잘 된다.
이렇게 잘 받아지고 있다.
@GetMapping("/study")
public Map<String, Object> study() {
Map<String, Object> subject = new HashMap<>();
subject.put("자바", "java");
subject.put("jsp", "java server pages");
subject.put("스프링", "spring framework5");
return subject;
}
이번엔 얘를 좀 주목하자. Map 형식으로 반환을 하고 있는데, 요청을 보내면 이런 형식으로 되돌아온다.
{
"스프링": "spring framework5",
"jsp": "java server pages",
"자바": "java"
}
js로 리턴하는 것 처럼 보이긴 하지만, json 형식이라 그대로 가져다 쓸 수는 없으므로 다시 가져다 쓰려면 변환 과정을 필히 거쳐야 한다.
추가적인 테스트를 진행해보기 위해 Person 클래스를 급조했다.
Person.java
package com.spring.mvc.test;
import java.util.List;
import lombok.Getter;
import lombok.Setter;
@Getter
@Setter
public class Person {
private String name;
private int age;
private List<String> hobby;
}
RestControllerTest.java
@GetMapping("/person")
public Person person() {
Person p = new Person();
p.setName("김철수");
p.setAge(30);
p.setHobby(Arrays.asList("수영", "축구", "독서"));
return p;
}
이 경우엔 이런 결과를 받는다.
{
"name": "김철수",
"age": 30,
"hobby": [
"수영",
"축구",
"독서"
]
}
js와 표기 방식이 비슷하지만 js가 절대 아니다!!
그저 json일 뿐이니 오해하지 말 것..
//@RequestBody
//클라이언트 쪽에서 전송하는 JSON 데이터를
//서버에서 사용하는 자바 언어에 맞게 변환하여 받을 수 있게 해 주는 아노테이션.
@PostMapping("/getObject")
public Person getObject(@RequestBody Person person) {
System.out.println("/getObject 요청이 들어옴!");
System.out.println("이름: " + person.getName());
System.out.println("나이: " + person.getAge());
System.out.println("취미: " + person.getHobby());
person.setAge(2);
return person;
}
아까의 그
{
"name": "김철수",
"age": 30,
"hobby": [
"수영",
"축구",
"독서"
]
}
이걸 복사해다가 YARC의 입력창에 넣는다.
요청은 http://localhost/test/getObject
로 보내고, POST
방식으로 전송하고 내용은 저렇게 채우고 Request를 해 보자.
그러면 서버쪽 로그에 데이터를 잘 받아와서 객체에 내용을 넣고, 확인까지 잘 되는걸 볼 수 있다.
http://localhost/rest/getPath/abc1234/10/32
이런 식으로 GET 요청을 보낼 건데, /abc1234/10/32
를 각각 떼오려고 한다.
@GetMapping("/getPath/{id}/{cpp}/{page}")
public Map<String, Object> getPath(@PathVariable String id,
@PathVariable int cpp,
@PathVariable int page) {
Map<String, Object> map = new HashMap<>();
map.put("아이디", id);
map.put("게시물개수", cpp);
map.put("페이지번호", page);
return map;
}
REST API와 JSON 연습은 대략 끝났으니, 이제 진짜로 회원 관리를 하기 위해 테이블부터 생성하기로 한다.
CREATE TABLE mvc_user(
account VARCHAR2(50) PRIMARY KEY,
password VARCHAR2(100) NOT NULL,
name VARCHAR2(50) NOT NULL,
reg_date DATE DEFAULT sysdate
);
테이블을 생성했으니, UserVO를 만들 차례다.
기본 classpath 패키지 아래에 ... user.model
패키지 아래에 생성한다.
package com.spring.mvc.user.model;
import java.sql.Timestamp;
import lombok.Getter;
import lombok.Setter;
import lombok.ToString;
/*
CREATE TABLE mvc_user(
account VARCHAR2(50) PRIMARY KEY,
password VARCHAR2(100) NOT NULL,
name VARCHAR2(50) NOT NULL,
reg_date DATE DEFAULT sysdate
);
*/
@Getter
@Setter
@ToString
public class UserVO {
private String account;
private String password;
private String name;
private Timestamp regDate;
}
... user.repository
패키지 하위에 생성.
package com.spring.mvc.user.repository;
import com.spring.mvc.user.model.UserVO;
public interface IUserMapper {
//아이디 중복 체크 기능
int checkId(String account);
//회원 가입 기능
void register(UserVO user);
//회원 정보 조회 기능
UserVO selectOne(String account);
//회원 탈퇴 기능
void delete(String account);
}
근데 중요한게, 지금 이렇게 만들어놔도 mvc-config.xml 파일에 MyBatis보고 스캔하라고 정의해놓지 않았어서.. (mapper 스캔) 그 부분을 건들여주러 가야한다.
...
<mybatis-spring:scan base-package="com.spring.mvc.user.repository"/>
...
이 한 줄을 추가해주면 된다.
src/test/java
디렉토리 하위에 ... .user
패키지를 만들고 그 안에 생성.
package com.spring.mvc.user;
public class UserMapperTest {
//IUserMapper 타입의 객체를 자동 주입하세요.
//회원 가입을 아무 아이디로 진행해 보세요.
//위에서 회원 가입한 아이디로 중복 확인을 해서
//COUNT(*)를 이용해서 1이 리턴이 되는지 확인하세요.
//가입한 회원의 모든 정보를 얻어내서 출력해 보세요.
//위에서 가입한 계정의 탈퇴를 진행해 보세요.
//탈퇴가 성공했는지의 여부를 정보를 얻어오는 메서드를 통해서
//확인해 보세요. (null 체크)
}
이 요구사항을 따라서 코드를 작성해 보면 되겠다.
일단 오늘은 이것에 대한 풀이 없이 강의가 종료되었다.