데이터융합 JAVA응용 SW개발자 기업 채용연계 연수과정 64일차 강의 정리

misung·2022년 6월 28일
0

Spring

SpringWebMvc 프로젝트 (이어서)

새 게시물에 new 마크 처리

새 게시물에 대한 기간을 정하고, (등록된지 n시간 혹은 n일 내의 게시물에 대해서) 정해진 기한 내의 게시물의 경우 new 마크를 부여해 보려고 한다.

[BoardVO] 수정

@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; 를 추가한다.

[BoardService] getArticleList() 메서드 수정

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

강사님께서 처리하신 방법은 위의 방법으로

  1. 페이징 객체에 의해 현재 얻어와야 할 게시물만 리스트에 넣어서 가져온 다음

  2. BoardVO 형의 article로 리스트를 순환하면서
    시스템의 현재 시간을 currentTimeMillis() 로 얻어오고, 게시물의 시간은 멤버변수 regDate 로 들고 있었으므로 getRegDate().getTime() 메서드로 등록 시간을 가져오도록 했다.

  3. 그리고 (now - regTime) = 즉, 현재 시스템 시간에서 게시물 등록 시간을 뺐을 때의 시간차가 (60초 60분 24시간 * 1000밀리초) 1일 내인 경우 전부 new마크를 달도록 해 놓았다.

1000밀리초 = 1초 60 = 60초
60초
60초 = 3600초 (60분)
1(=60분) * 24(시간) = 24시간 (하루)

이렇게 계산된 것.

이제 list.jsp로 이동하여 new 마크를 달아 주어 보자.

[list.jsp] new마크 추가를 위해 수정

...
<c:if test="${b.newMark}">
	<img alt="newmark" src="<c:url value='/img/icon_new.gif' />">
</c:if>
...

${articles}b 로 순환하고 있고, 해당 게시물에 대해서 newmarktrue 라면, new 표시를 해 주도록 하고 있다.

잘 뜨는 것을 확인할 수 있다.

이제는 검색 기능을 만들기로 한다.

검색 기능 만들기

[list.jsp] 검색 버튼 부분

...
<!-- 검색 버튼 -->
					<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> 태그의 idcondition 으로 식별하고, 안의 <option> 태그에서 값을 결정한 다음 value 에 넣어서 넘겨줄 것이다.

이제 검색 버튼이 눌렸을 때, URL을 적절하게 만들어서 넘겨주는 처리를 자바스크립트로 만들어보자.

[list.jsp] 검색 버튼 로직 처리 (JS)

//검색 버튼 이벤트 처리
		$('#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 를 검색어와 & 조건을 넣어서 보내줄 수 있도록 만들고 있다.

[BoardController] 검색 처리

우리는 검색을 할 때 키워드 말고도 조건을 같이 넘겨주고 있는데, 특정 조건이 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을 어떻게 써먹고 있는지 한 번 확인해 보자.

[BoardMapper.xml]

...
<!-- 중복되는 동적 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 안에 쿼리를 작성하면 쿼리 내용의 괄호나 특수문자를 
		마크업 언어로 인식하지 않고 문자열로 인식하게 됩니다.
		< (&lt;)   > (&gt;) -->	
	<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 idrefid= 의 속성값으로 주면 된다.

물론 여기만 수정된 건 아니고, 몇몇 수정된 클래스들이 있다.

[IBoardMapper], [IBoardService], [BoardService] 클래스 수정

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의 다음 부분을 고치면 된다.

[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번)

URIComponent 어떻게 동작하는지 설명

찾아보니까 제대로 설명된데다가 결과까지 확인할 수 있는 예제를 진행하셨어서.. 다음과 같다.

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

이런 식으로 출력되게 된다.
일일히 조건들에 대해서 복잡하게 따로 작성할 필요가 없어졌다!

[list.jsp] 전체적으로 훑어보기

<%@ 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>
									&nbsp;
									<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 API 모델

REST 방식으로 통신을 하는 이유는 뭘까?

애플용, 안드로이드용, PC 클라이언트용 서버를 전부 유지할 수는 없고, 각 클라이언트마다 운영체제도 다르고 통신 방식이 조금씩 다르기 때문에, 저 모든 플랫폼에 대해서 해석을 할 수 있는 파일을 서로 주고 받으면서 통신을 진행하려고 하는 게 JSON을 통한 REST API 통신이다.

기존의 XML 방식으로 통신을 해도 되기는 하는데, XML은 아무래도 마크업 언어라 내용이 길어지는 부분이 있다. 왠만한 클라이언트단에서는 XML을 해석할 수 있는 라이브러리를 가지고 있긴 하지만, 최근에는 JSON을 사용하는 경우가 많다.

이렇게 대략적인 설명만 봐서는 잘 모르겠고.. 실제로 코드를 쳐 봐야 알 것 같다.

JSON 자동변환 라이브러리 설치하기

  1. MVN Repository로 이동
  2. Jackson Databind 라이브러리 검색
  3. 가장 많이 사용하는 버전은 최신 버전이긴 한데, 안정성을 위해 구버전 중 그 다음으로 많이 쓰이는 버전을 가져오기로 함.
<!-- 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 하는거 잊지 말것.

[RestControllerTest] REST 방식 연습하기

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일 뿐이니 오해하지 말 것..

  1. 표기 방식을 이해할 것
  2. js가 아닌 것을 잘 이해하고 있을 것

클라이언트에서 보낸 JSON 데이터를 JAVA언어를 사용하는 서버에 맞게 변환하기.
//@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를 해 보자.

그러면 서버쪽 로그에 데이터를 잘 받아와서 객체에 내용을 넣고, 확인까지 잘 되는걸 볼 수 있다.


URL에 데이터 넣어서 보내기 (PathVariable 방식)

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를 만들 차례다.

[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;

}

[IUserMapper] 인터페이스 생성

... 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 스캔) 그 부분을 건들여주러 가야한다.

[mvc-config.xml] 수정

...
<mybatis-spring:scan base-package="com.spring.mvc.user.repository"/>
...

이 한 줄을 추가해주면 된다.

[UserMapperTest] 클래스 생성

src/test/java 디렉토리 하위에 ... .user 패키지를 만들고 그 안에 생성.

package com.spring.mvc.user;

public class UserMapperTest {
	
	//IUserMapper 타입의 객체를 자동 주입하세요.
	
	
	//회원 가입을 아무 아이디로 진행해 보세요.
	
	//위에서 회원 가입한 아이디로 중복 확인을 해서
	//COUNT(*)를 이용해서 1이 리턴이 되는지 확인하세요.
	
	//가입한 회원의 모든 정보를 얻어내서 출력해 보세요.
	
	//위에서 가입한 계정의 탈퇴를 진행해 보세요.
	//탈퇴가 성공했는지의 여부를 정보를 얻어오는 메서드를 통해서
	//확인해 보세요. (null 체크)

}

이 요구사항을 따라서 코드를 작성해 보면 되겠다.

일단 오늘은 이것에 대한 풀이 없이 강의가 종료되었다.

0개의 댓글