RestController

MisCaminos·2021년 4월 1일
0

Server & Web

목록 보기
16/23

REST란?

REST = Representational State Transfer
REST는 하나의 URI는 하나의 고유한 resource를 대표하도록 설계된다는 개념에 전송방식을 결합해서 원하는 작업을 지정한다.

resource는 REST의 핵심개념으로 접근할 수 있고 조작할 수 있는 모든것을 뜻한다. 사람, 할일, 이미지, 비디오 등등 명사화 할 수 있는 대부분의 것을 resource라고 할 수 있다.

RESTful resource들은 추상화되어있으므로 형식이 자유롭다. XML, JSON, HTML등 다양한 형태를 가지고 client에게 전달하기 전에 데이터를 직렬화해서 그 시점에 데이터의 상태에 대한 표현을 해주면 된다.일반적으로 web의 경우에는 JSON 데이터를 처리한는 것이 더 수월해서 대부분 JSON 형태로 응답하도록 개발하는 추세이다.

REST의 특성과 규칙

1. Client와 Server 독립적 구분

Server와 client 서로간의 의존성때문에 확장에 문제가 발생하지 않도록 독립적으로 구분한다.

2. Client와 Server간의 통신시 상태 없음.

Server는 client의 상태를 기억할 필요가 없다.

3. Layered Architecture

Server와 client사이에 gateway, 방화벽, proxy가 있는것 처럼 다계층의 형태로 레이어를 추가/수정/삭제 가능해야하고 확장성이 있어야한다.

4. Cache(캐시)의 활용

Server의 응답을은 cache를 갖고 있을 수 있는데, 가지고 있는 경우 client가 이 cache를 통해서 응답을 재사용할 수 있고, 이를 통해 server의 부하를 낮추어서 server 성능 향상에 도움이 될 수 있다.

5. Code on Demand

요청이 들어오면 코드를 준다는 의미. 특정 시점에 server가 특정 기능을 수행하는 script또는 plugin을 client에 전달해서 해당 기능을 동작하도록 한다. (Code on demand의 예시로는 applet, javascript, flash가 있음)

6. REST interface

Server와 client사이 상호 작용은 일관된 통합 interface위에서 이뤄지도록 한다.

-통합 interface의 규칙:
(1) resource identification
web안에서 서로 구분할 수 있는 개념으로 URI와 같은 고유 식별자를 통해 표현할 수 있다.
(2) 표현을 통한 resource처리
같은 데이터를 표현할때에 JSON, XML, HTML 페이지와 같이 다양한 콘텐츠 유형으로 표현할 수 있다.(사람의 같은 말이 강세/어조에 따라 다른 의미를 갖는것처럼.)
(3) 자기 묘사 메시지
HTTP통신을 할때에 Http Header에 meta data 정보를 추가해서 실제 전송하는 데이터에 대한 설명을 나타내는 정보를 담을 수 있다. (일반적으로 network 통신을 할때에는 header부분에 현재 보내고 있는 데이터 패킷에 대한 메타 정보를 담아서 본문을 읽으래 해당 본문이 어떤 의도로 쓰여진 것인지 파악할 수 있게 해준다.)
(4) HATEOAS(Hypermedia As The Engine Of Application State)
RSET API를 개발할때에 단순히 데이터만 전달하지 않고 페이지를 이동할 수 있는 link 정보까지 포함한다.(web application은 여러 페이지들과 그 페이지들을 이동할 수 있는 link 정보들로 구성되어있는데, 데이터와 페이지 이동 정보를 둘다 다루면 web에 더 유용한 API가 될 수 있다.)

분산처리를 위한 server를 만들기위해 반드시 REST의 특성을 준수해야하는것이 아니다. 그러나 일반적으로 고가용성과 확장성이 중요하기때문에 REST 특성을 지켜서 server application을 개발한다. 위의 REST의 특성과 규칙을 지키는 API를 RESTful하다고 이야기하고, 그런 API를 REST API라고 한다. REST의 특성을 준수하면서 데이터를 제공하거나 특정 기능을 지원하는 데이터+기능의 집합이 REST API이다.

REST API를 client에게 제공하기위해서는 client가 접근할 수 있는 entry point를 만들어야한다. 이를 위해서 MVC의 Controller를 사용 할 수 있다.


REST API 실습

실습에서는 spring framework으로 만든 게시판 web application(project name: "spring_webtest 프로젝트")에서 댓글(reply)관련 요청을 처리하는 ReplyController를 REST API로 구현해보았다.

REST API에서는 HTTP Method를 사용한다.
HTTPMethod란 HTTP protocol 사용시 적용되는 method 호출 방식이다. REST API에서는 GET/POST/PUT/DELETE 4가지 method가 있고 각각 CRUD역할을 수행하는 의미를 가지고있다.

Response Body & 데이터 변환

Controller 클래스에 RestController annotation(@RestController)을 표기하면 기존 Controller에서 ResponseBody annotation을 추가한것과 동일하다. 아래 ReplyController.java와 같이 ResponseBody annotation을 따로 사용하지않고 REST API를 만들 수 있다.

ResponseBody는 실행 결과에 대한 처리를 위한 annotation인데, ResponseBody annotation이 있으면 실행 결과가 view를 거치지않고 HTTP Reponse Body에 직접 입력된다. 실행 결과가 응답에 작성된 상태에서 MappingJacksonHttpMessageConverter를 통해 JSON형태의 결과가 표현된다.

(spring 3.1부터는 JSON 표현에 대한 별도의 message converting 설정을 하지않아도 JAXB2와 Jackson library만 classpath에 추가되어있으면 JSON으로 자동 변환되었는데, spring boot에서는 이 두 library가 자동으로 이미 포함되어있다.)

Maven으로 build한 Spring Legacy Project의 경우 아래와 같이 pom.xml에서 spring version 확인 및 dependency를 추가해야한다.

<properties>
 <java-version>1.8</java-version>
 <org.springframework-version>4.3.14.RELEASE</org.springframework-version>
       :

   <plugin>
   	<groupId>org.apache.maven.plugins</groupId>
        <artifactId>maven-compiler-plugin</artifactId>
        <version>2.5.1</version>
        <configuration>
        	<source>1.8</source>
        	<target>1.8</target>
        	<compilerArgument>-Xlint:all</compilerArgument>
        	<showWarnings>true</showWarnings>
        	<showDeprecation>true</showDeprecation>
        </configuration>
    </plugin>

JSON, XML 변환을 위한 라이브러리:

<dependency>
	<groupId>com.fasterxml.jackson.core</groupId>
	<artifactId>jackson-databind</artifactId>
	<version>2.9.6</version>
</dependency>
 
<!-- https://mvnrepository.com/artifact/com.fasterxml.jackson.dataformat/jackson-dataformat-xml -->
<dependency>
	<groupId>com.fasterxml.jackson.dataformat</groupId>
	<artifactId>jackson-dataformat-xml</artifactId>
	<version>2.9.6</version>
</dependency>

Spring에선 응답 header 구현체로 ResponseEntity class를 제공한다.

REST API에서 응답 header는 client 쪽에서 meta 정보로 활용될 수 있다. client에서 header를 먼저 읽을 수 있으므로 본문 내용을 읽을 필요가 없어서 통신 효율에 더 좋다.(또한, 요청 결과에 대한 명확한 결과를 전달할 수 있다)

ResponseEntity는 HttpEntity를 상속받은 클래스로 Http응답에 대한 상태값을 표현할 수 있다.

댓글관련 CRUD 기능 요청을 처리하는 controller를 REST API로 구현한 코드는 아래와 같다:

ReplyController.java:

@RestController
public class ReplyController {
	private static final Logger log = LoggerFactory.getLogger(ReplyController.class);
	
	@Autowired
	private ReplyMapper mapper;
	
	@DeleteMapping("/bbs/reply/{rnum}")
	public ResponseEntity<String> remove(@PathVariable("rnum") int rnum){
		log.info("remove:"+rnum);
		
		return mapper.delete(rnum)==1? new ResponseEntity<String>("success", HttpStatus.OK)
				: new ResponseEntity<String>(HttpStatus.INTERNAL_SERVER_ERROR);
	}
	
	//PutMapping annotation은 주로 update할때 사용
	@PutMapping("bbs/reply/{rnum}")
	public ResponseEntity<String> modify(@RequestBody ReplyDTO dto, 
			@PathVariable("rnum") int rnum){
		log.info("rnum"+rnum);
		log.info("modify"+dto);
		
		return mapper.update(dto)==1? new ResponseEntity<String>("success", HttpStatus.OK)
				: new ResponseEntity<String>(HttpStatus.INTERNAL_SERVER_ERROR);
	}
	
	
	@GetMapping("/bbs/reply/{rnum}")
	public ResponseEntity<ReplyDTO> get(@PathVariable("rnum") int rnum){
		log.info("get:"+rnum);
		
		return new ResponseEntity<ReplyDTO>(mapper.read(rnum), HttpStatus.OK);
	}
	
	@PostMapping("/bbs/reply/create")
	public ResponseEntity<String> create(@RequestBody ReplyDTO dto){
		log.info("ReplyDTO:"+dto.getContent());
		log.info("ReplyDTO:"+dto.getId());
		log.info("ReplyDTO:"+dto.getBbsno());
		
		dto.setContent(dto.getContent().replaceAll("/n/r", "<br>"));
		
		int flag = mapper.create(dto);
		log.info("flag:"+flag);
		
		//삼항연산자: flag true면, 앞에 결과. false면, 뒤에 결과를 반영 
		return flag==1? new ResponseEntity<String>("success",HttpStatus.OK)
				: new ResponseEntity<>(HttpStatus.INTERNAL_SERVER_ERROR);
	}
    	@GetMapping("/bbs/reply/page")
	public ResponseEntity<String> getPage(@RequestParam("nPage") int nPage, 
	      @RequestParam("nowPage") int nowPage,
	      @RequestParam("bbsno") int bbsno, 
	      @RequestParam("col") String col, 
	      @RequestParam("word") String word) {
	 
	    int total = mapper.total(bbsno);
	    System.out.println("total num of reply:"+total);
	    String url = "read";
	 
	    int recordPerPage = 2; // 한페이지당 출력할 레코드 갯수
	 
	    String paging = Utility.rpaging(total, nowPage, recordPerPage, col, word, url, nPage, bbsno);
	 
	    return new ResponseEntity<String>(paging, HttpStatus.OK);
	 
	}
}

ResponseEntity는 응답 몸체로서 collection형태의 데이터도 담을 수 있다.

@GetMapping("/bbs/reply/list/{bbsno}/{sno}/{eno}")
public ResponseEntity<List<ReplyDTO>> getList(@PathVariable("bbsno") int bbsno,
			@PathVariable("sno") int sno,
			@PathVariable("eno") int eno){
		
	System.out.println("bbsno:"+bbsno);
	System.out.println("sno:"+sno);
	System.out.println("eno:"+eno);
	//parameter로 넘겨진 값을 받는다
	Map<String, Integer> map = new HashMap();
	map.put("sno", sno);
	map.put("eno", eno);
	map.put("bbsno", bbsno);
		
	return new ResponseEntity<List<ReplyDTO>>(mapper.list(map),HttpStatus.OK);
		
}

ajax 방식 구동

ReplyController에서 처리한 요청 데이터를 출력하거나 받아오는것을 read.jsp에서 ajax방식으로 javascript를 작성하여 처리했다.

read.jsp(게시글을 조회하는 view page):

<!-- ajax 요청 부분 -->
<!-- 비동기 통신(Ajax)은 javascript로 처리한다: breply.js -->	
<script type="text/javascript" 
src="${pageContext.request.contextPath}/js/breply.js"></script>
<!--${pageContext.request.contextPath}는 webapp 폴더 경로-->

<!-- 페이지 로딩시 댓글 목록 처리-->
 <!-- jstl내부 javascript에서 사용가능. el표현방식{bbsno}로 출력한것을 var bbsno에 넣는다. -->
 <!-- javascript로 만들어져있는 .js 파일 안에서는 el표기가 잘 인식되지않음. var에 넣어사용 -->
  <script type="text/javascript">
  var bbsno = "${dto.bbsno}";
  var sno = "${sno}";
  var eno = "${eno}";
 <!-- 댓글용 paging 및 게시판 검색 -->
  var nPage = "${nPage}";
  var nowPage = "${param.nowPage}";
  var colx = "${param.col}";
  var wordx = "${param.word}";
  //세션값 이용(sessionScope.id el값이 만약 없다면 빈값이 var session_id에 들어간다)
  var session_id = '${sessionScope.id}' 
  </script>
  
<!-- 비동기 통신 후, 받은 data를 가지고 화면에 출력되게 하는 js 코드: replyprocess.js -->
<script type="text/javascript" 
src="${pageContext.request.contextPath}/js/replyprocess.js"></script>

breply.js:

console.log('*******Reply Module........')
//breply.js는 ajax 구동을 위해 server에서 댓글을 가져오는 방법을 선언한다

//replyService에 함수 호출을 할당했기때문에
//함수 실행(getList + getPage의 선언)되고 return값을 replyService가 받음.
//즉, 실행이되면 함수에서 return되는 getList, getPage, 등의 JSON 객체가 replyService안으로 들어가는것임.

var replyService = (function() {
	
	function remove(rnum, callback, error){
		$.ajax({
			type:"delete",
			url: "./reply/"+rnum,
			success: function(result, status, xhr){
				if(callback){
					callback(result)
				}
			},
			error: function(xhr,status,er){
				if(error){
					error(er);
				}
			} //error end
		}) //ajax end
	} //remove end

	function update(reply, callback, error) {
		console.log("rnum: " + reply.rnum);
		$.ajax({
			type: 'put',
			url: './reply/' + reply.rnum,
			data: JSON.stringify(reply),
			contentType: "application/json; charset=utf-8",
			success: function(result, status, xhr) {
				if (callback) {
					callback(result);
				}
			},
			error: function(xhr, status, er) {
				if (error) {
					error(er);
				}
			}
		}) // ajax end
	} //update end

	function add(reply, callback, error) {
		console.log("add reply...............");
		$.ajax({
			type: 'post',
			url: './reply/create',
			//stringify: 매개변수로 보낼때 문자열로 바꾸어서 보낸다.
			data: JSON.stringify(reply),
			contentType: "application/json;chars=utf-8",
			success: function(result, status, xhr) {
				if (callback) {
					callback(result);
				}
			},
			error: function(xhr, status, er) {
				if (error) {
					error(er);
				}
			}
		});
	}

	function get(rnum, callback, error) {
		$.get("./reply/" + rnum + ".json", function(result) {
			if (callback) {
				callback(result);
			}
		}).fail(function(xhr, status, err) {
			if (error) {
				error();
			}
		});
	}

	//결과를 받아오기
	function getList(param, callback) {
		var bbsno = param.bbsno;
		var sno = param.sno;
		var eno = param.eno;
		alert(param.bbsno);
		//jquery method getJSON(): server에게 data를 요청 (ReplyController를 호출)
		//경로상(url)에 prameter를 담아서 요청. 이 요청 url이 (ReplyController)RestController에 표기되어야함 
		$.getJSON("./reply/list/" + bbsno + "/" + sno + "/" + eno + ".json",
			//data: getJSON가 url로 요청해서 server에게서 받은 데이터를 data라고 하면 됨.
			//getJSON() 함수의 첫번째 매개변수가 url. param에서 받은 bbsno, sno, eno를 넣어서 url을 완성
			function(data) {
				alert(data);
				//if(callback): ""callback 함수가 정의가 되어있다면""을 뜻함!
				if (callback) {
					//callback함수를 실행
					callback(data);
				}
			}
		);
	}

	//위에 getJSON이기때문에 type:get
	function getPage(param, callback, error) {
		//jquery method ajax()
		$.ajax({
			type: 'get',
			//이 요청 url이 (ReplyController)RestController에 표기되어야함
			url: './reply/page',
			data: param,
			contentType: "application/text;charset=utf-8",
			//success이면 아래 function을 실행한다.
			success: function(result, status, xhr) {
				if (callback) {
					callback(result)
				}
			},
			//error라면 아래 function을 실행한다.
			error: function(xhr, status, er) {
				if (error) {
					error(er)
				}
			}
		});
	}
	;//위 함수들과, return값을 구분하기 위해 ; 넣는다

	return {
		//이름: 값 (e.g., getPage(문자열,label):getPage(위에서 define한 값(value) 즉 getPage함수))
		//즉, 각 label로 함수 이름이 반환되는것임
		getList: getList,
		getPage: getPage,
		add: add,
		get: get,
		update: update,
		remove: remove
	};

})(); //function() 호출(실행), 선언만 하는게 아니라 호출한는거임!
//선언된 함수들을 갖고있는것인 실행된것임. 자체 내용이 다 실행된것은 아니다. 아직!

replyprocess.js:

//$표기로 page loading시 바로 시작하도록 function 선언
$(function() {
	showList()
});//page loading function end

//read.jsp에서 class="chat"으로 지정된 ul 태그를 replyUL에 넣는다 
var replyUL = $(".chat");
function showList() {
	//replyService의 getList라는 이름의 함수를 접근, 불러서 getList를 호출한것임.
	//breply.js에서 정의했던 getList()의 첫번째 param에 전달되는 것:
    //replyService.getList({ bbsno: bbsno, sno: sno, eno: eno },
		//param에 bbsno, sno, eno 넘겨주는것
		//getList()의 두번째 param: callback에 전달되는 것:
		//list 매개변수가 data가 되는 것임.
		function(list) {
			var str = "";
			if (list == null || list.length == 0) {
				//list없거나 비어있으면 그냥 나가.
				return;
			}
			for (var i = 0, len = list.length || 0; i < len; i++) {
				str += "<li class='list-group-item' data-rnum='" + list[i].rnum + "'>";
				str += "<div><div class='header'><strong class='primary-font'>" + list[i].id + "</strong>";
				str += "<small class='pull-right text-muted'>" + list[i].regdate + "</small></div>";
				str += replaceAll(list[i].content, '\n', '<br>') + "</div></li>";
			}
			//.html(): jquery함수 (innerHTML와 동일함)
			//즉, read.jsp의 ul태그안에 html내용을 str로 채운다.
			replyUL.html(str);

			showReplyPage();
		} //function end
	); //getList end
}//showList end



function replaceAll(str, searchStr, replaceStr) {
	return str.split(searchStr).join(replaceStr);
}

var replyPageFooter = $(".panel-footer");

var param = "nPage=" + nPage;
param += "&nowPage=" + nowPage;
param += "&bbsno=" + bbsno;
param += "&col=" + colx;
param += "&word=" + wordx;

function showReplyPage() {
	//param에 nPage, nowPage, colx, wordx를 넣어서 getPage의 첫번째 매개변수로 전달
	replyService.getPage(param, 
		function(paging) {
			console.log(paging);
			var str = "<div><small class='text-muted'>"+paging+"</small></div>";
			console.log(str);
			replyPageFooter.html(str);
	})
}//showReplyPage end

var modal = $(".modal");
var modalInputContent = modal.find("textarea[name='content']");
var modalModBtn = $("#modalModBtn");
var modalRemoveBtn = $("#modalRemoveBtn");
var modalRegisterBtn = $("#modalRegisterBtn");

$("#modalCloseBtn").on("click", function(e) {
	modal.modal('hide');
});

$("#addReplyBtn").on("click", function(e) {
	
	if(session_id==""){ //로그인이 안된 상태라면
		if(confirm('로그인해야 댓글을 쓸 수 있습니다.')){ //confirm에는 확인,취소 선택가능. (확인=true, 취소=false)
			var url = "../member/login"; // ../의 의미: 현 bbs폴더에서 상위 경로로 간다(member 폴더로 가기위해)
			url += "?col="+colx; //?앞에까지만 url
			url +="&word="+wordx; //&로 여러 parameter를 연결
			url +="&nowPage="+nowPage;
			url +="&nPage="+nPage;
			url +="&bbsno="+bbsno;
			url +="&rurl=../bbs/read"; //rurl(reply url):가려는 url을 지정하는 parameter 
			location.href = url; //
		}//confirm end
	}else{
		modalInputContent.val("");
		modal.find("button[id != 'modalCloseBtn']").hide();
		modalRegisterBtn.show();
	
		$(".modal").modal("show");
	} //session_id end
}); //addReplyBtn on end

modalRegisterBtn.on("click", function(e) {
	if (modalInputContent.val() == '') {
		alert("댓글을 입력하세요.");
		return;
	}
	var reply = {
		content: modalInputContent.val(),
		id: session_id,
		bbsno: bbsno
	};

	//비동기 통신 요청 (add function 호출) 하면서 reply에 3개의 JSON객체를 담아서 전달
	replyService.add(reply, function(result) {
		modal.find("input").val("");
		modal.modal("hide");

		showList();

	}); //add end

}) //button click end

//내가 chat class의 li를 클릭했을대때 실행
$(".chat").on("click", "li", function(e) {
	var rnum = $(this).data("rnum");
	replyService.get(rnum, function(reply) {
		modalInputContent.val(reply.content);
		modal.data("rnum", reply.rnum);
		modal.find("button[id != 'modalCloseBtn']").hide()
		
		if(session_id==reply.id){
			modalModBtn.show();
			modalRemoveBtn.show();			
		}
		modal.modal("show");
	})//get end
}) //.chat click end


modalModBtn.on("click", function(e) {
	var reply = {
		rnum: modal.data("rnum"),
		content: modalInputContent.val()
	};
	replyService.update(reply, function(result) {
		modal.modal("hide");
		showList();
	}); //update end
}); //modalModBtn on end

modalRemoveBtn.on("click", function(e){
	var rnum = modal.data("rnum");
	replyService.remove(rnum, function(result){
		modal.modal("hide");
		showList();
	}); //remove end
}); //modalRemoveBtn on end 

References

  1. 스프링부트로 배우는 자바 웹 개발 JPub (윤석진 지음)

  2. RestController를 이용한 댓글 목록, Table 생성, Model 제작, 조회 페이지 수정(read.jsp) from lectureblue

profile
Learning to code and analyze data

0개의 댓글