학습 내용
AJAX를 활용해 답변 추가 기능 구현
AJAX를 활용해 답변 삭제 기능 구현
질문 목록에 답변 수 보여주기 기능 추가
중복 제거 및 리팩토링
JSON API 추가 및 테스트
쉘 스크립트를 활용해 배포 자동화
Intro
기존의 MVC구조에서 서버와 클라이언트가 데이터를 주고 받는 방식은 다음과 같다.
클라이언트에서 데이터가 서버의 컨트롤러에 날라가고(프런트 컨트롤러의 역할), 컨트롤러는 모델에게 그 데이터를 전달하여 데이터베이스에 저장(페이지 컨트롤러의 역할)한다. 그리고 컨트롤러는 데이터의 변경사항을 반영하여 HTML 전체를 다시 구성(뷰의 역할)한 다음에 클라이언트에게 보내준다. 그러면 클라이언트는 전체 화면을 다시 그리게(렌더링) 된다.
계속해서 서버로부터 데이터를 받아오는 이 방식은 상당한 비용이 들어가고 사용자의 입력, 즉 클라이언트의 요청마다 전체 페이지를 다시 렌더링하게 되어 화면이 깜빡거리게 된다. 이러한 문제(?), 문제라기 보다는 사용자 경험을 높이기 위해 사용하는 방법으로, Ajax를 사용한다.
Ajax란 Asynchronous JavaScript and XML 의 약자이다. 비동기적으로 데이터를 처리하는데 JavaScript와 XML을 사용하는 기술이며, 최근에 들어서는 XML을 사용하지 않고 JSON(JavaScript Object Notation)이라는 데이터 타입을 사용하여 구현된다. 이렇게 비동기적으로 데이터를 처리하는 것이 중요해지면서, 서버측에서 구현되던 기능들이 클라이언트 부분으로 넘어가는 추세이다. 기존의 렌더링 방식이 서버 사이드 렌더링이었다면 최근 웹 개발 트렌드는 클라이언트 사이드 렌더링으로 옮겨가고 있으며, 대표적인 기술로는 Angular JS, React, Vue.js 등이 존재한다.
1. Ajax 구현
1-1. 기존의 동작 Disabled
일단, 기존에 서버 사이드 렌더링 방식으로 동작을 데이터를 처리했던 방식을 Disabled(?)해야 한다. (뭐라 표현해야할 지 모르겠다.)
즉, input [type=submit] 을 클릭했을때, form 태그에 있는 데이터가 서버로 전송되는 동작을 막아야 한다. 이 때, 이벤트 처리를 위해서 자바스크립트가 사용된다. 클릭 이벤트로부터 event 객체를 잡은 다음, event.preventDefault() 함수를 활용해주자. 그리고 이벤트를 통해 전송되려는 데이터가 무엇인지 알기 위해서, .serialize 메소드를 활용한다. serialize 함수는 각각의 form 태그에서 input, textarea의 text를 긁어 오는 메소드이다.
<form class="answer-write" method="post" action="/api/questions/{{id}}/answers">
<div class="form-group" style="padding:14px;">
<textarea class="form-control" placeholder="Update your status" name="contents"></textarea>
</div>
<input class="btn btn-success pull-right" type="submit" value="Post" />
<div class="clearfix"/>
</form>
serialize 메소드를 통해 전송된 데이터를 console.log를 통해 살펴보면 contents=text 의 모습을 갖추고 있다.
유추해보면, textarea의 text가 내용이 되고, 그 key(?)값은 name인 contents 가 TEXT 형식으로 전달되는 것을 알 수 있다.
$('.answer-write input[type=submit]').click(addAnswer);
function addAnswer(e) {
e.preventDefault();
var queryString = $('.answer-write').serialize();
var url = $('.answer-write').attr('action');
}
$('.answer-write input[type=submit]').click(addAnswer);
1-2. $.ajax 메소드의 사용
이제 jQuery의 $.ajax 함수를 사용하여 ajax를 통해 댓글 기능을 구현해보자.
type, url, data, dataType, error, success 각각에 대한 명시를 해준다. 이 외에도 정말 많은 인자들이 있지만 설정해주지 않으면 그 default로 설정되며, 구현하는 기능에 영향을 미치지 않으면 default 값으로 둔다.
type은 어떤 방식으로 데이터를 요청할 것인지, url은 데이터를 어떤 url로 요청할 것인지, data는 어떤 데이터를 보낼 것인지, dataType은 요청하고자 하는 데이터의 타입이 무엇인지, error는 데이터가 제대로 보내지지 않았을 때 어떤 처리를 해줄 것인지, success는 데이터가 제대로 보내졌을 때 어떤 처리를 해줄 것인지를 의미한다.
Ajax를 사용하여 통신하는 경우, api를 조금 다른 방식으로 설계한다. 기존의 post 방식으로 요청을 하는 API가 /comment 였다면 ajax로 데이터를 주고 받을 때는 앞에 api를 붙여 /api/comment 와 같은 형식으로 설계한다. 클릭되는 input 태그로부터 href 속성을 가져와 url이라는 변수에 저장한 다음, ajax 메소드에 사용하였다. 전달할 데이터는 위 코드에서 queryString이라는 변수로 저장하였다.
$.ajax({
type : 'post',
url : url,
data : queryString,
dataType : 'json',
error : onError,
success : onSuccess
});
데이터가 제대로 전달됬을 경우와 오류가 발생할 상황에 대한 함수를 따로 작성하였다.
cf> success의 경우에는 제대로 전달된 data와 ajax 통신 상태인 status 를 인자로 받을 수 있다.
function onError(status) {
console.log("ERROR!!" + status);
}
function onSuccess(data, status) {
console.log("SUCCESS data = " + data + "status = " + status);
}
1-3. Controller 준비하기
클라이언트의 요청을 받기 위한 컨트롤러로 기존에 있던 AnswerController를 활용하자. 이 컨트롤러에는 Post 형식의 요청을 받는 메서드 한 개가 존재한다. 그렇기 때문에 이 ajax 요청에 해당하는 부분만 담당하게 할 수 있기 때문에, ApiAnswerController로 이름을 바꾸어 json 형식으로 응답하는 컨트롤러라는 것을 ‘명시'해주자. 그리고 RequestMapping 어노테이션에 /api/를 추가해주자.
@RequestMapping("/api/questions/{questionId}/answers")
일반 컨트롤러와 json 형식의 데이터를 받는 컨트롤러는 다르다. 그렇기 때문에 어노테이션을 통해서 이 컨트롤러는 json 형식의 데이터를 받는 컨트롤러라는 것을 스프링에게 알려줘야 한다. 이 때 사용하는 어노테이션은 @RestController 이다.
@RestController
컨트롤러의 반환값은 String으로 결과값을 렌더링할 페이지를 문자열로 반환하였다. ajax를 사용하는 경우에는 새로운 페이지로 redirect 하지 않기 때문에, 메소드의 반환값을 바꿔주자.
어떤 값으로 바꿔줘야 하는가? 클라이언트에서 넘어온 데이터 정보를 데이터베이스에 저장하고, 그 값을 다시 되돌려줘서 화면에 표시해야 한다. 그렇기 때문에 메소드의 return 값은 Answer 객체가 되어야 한다. 로그인되어 있지 않다면 null 값을 return 해주고, return은 데이터베이스에 저장된 Answer 객체로 지정해주자.
@PostMapping("")
public Answer create(@PathVariable Long questionId, String contents, HttpSession session) {
if (!HttpSessionUtils.isLoginUser(session)) {
return null;
}
User sessionedUser = HttpSessionUtils.getUserFromSession(session);
Question question = questionRepository.findOne(questionId);
Answer answer = new Answer(sessionedUser, contents, question);
question.addAnswer();
return answerRepository.save(answer);
//save 메소드는 저장한 인자를 그대로 return 하고 있다. 그렇기 때문에 return 값으로 설정해줘도 무방하다.
}
1-4. JSON API 제공을 위한 Jackson 라이브러리 사용
실행을 시켜서 버튼을 클릭하여 데이터가 잘 전달되는지 확인을 해보자. Object로 나타나는 객체에 데이터 값이 formattedCreateDate 만 전달된다. Answer 객체가 갖고 있어야 할 모든 데이터가 전송이 되지 않은 것이다. 이는 RestController로 지정해줘도, getter 메서드에 대해서만 json 객체로 반환해주기 때문이다.
그렇기 때문에 Jackson이라는 라이브러리를 사용해서 필드에 있는 데이터들을 json data로 변경될 수 있게 해줘야 한다. ( 물론 getter 메서드를 추가적으로 생성해주는 방법도 있다. ) 필드에 있는 데이터들을 Json 형식으로 받기 위해서는 @JsonProperty라는 어노테이션이 필요하다.
@ManyToOne
@JoinColumn(foreignKey = @ForeignKey(name = "fk_question_writer"))
@JsonProperty
private User writer;
@JsonProperty
private String title;
@JsonProperty
private Integer countOfAnswer = 0;
// default value를 지정해줄 때는 이렇게 해줄 수 있는데 import.sql에는 따로 지정해줘야 한다
이렇게 지정해주면 된다!
# Jackson 라이브러리
json 뿐만 아니라 XML/YAML/CSV 등 다양한 형식의 데이터를 지원하는 data-processing Tool 이다. annotation 방식으로 메타 데이터를 기술할 수 있으므로 JSON 타입의 약점 중 하나인 문서화와 데이터 validation 문제를 해결할 수 있다. Spring Boot에는 기본적으로 웹 스타터에 Jackson 라이브러리가 추가되어 있어서 해당 클래스에 import만 해주면 바로 사용할 수 있다.
모바일 백엔드가 중요해지고 또 서버 사이드 렌더링에서 클라이언트 사이드 렌더링으로 웹 개발 패러다임이 조금씩 바뀌어 가면서 백엔드에서 제공하는 REST API가 중요해졌다. Spring 프레임워크를 통해서 REST API를 설계할 때, 자바 객체를 JSON 형식으로 바꿀 때 사용한다. json 처리 관련 java 라이브러리에는 가장 많이 사용한다는 다음 세 종류가 있다.
FasterXML 의 JacksonGoogle의 GsonYidong Fang의 JSON.simple
이 셋 중 성능면에서는 Jackson이 가장 좋다고 한다.
1-5. 화면에 렌더링하기
이제 전달된 데이터들을 동적으로 html을 생성하여 화면에 표시해주자. 이 작업을 위해서는 두 가지가 필요하다. 하나는 동적으로 생성할 html 코드 조각이고, 다른 하나는 코드 조각에 있는 부분들을 데이터로 replae해줄 메소드이다. 이 부분들은 우리가 직접 특정 포맷을 정의해주면 된다.
html 코드 조각들은 동적으로 생성되어야 하기 때문에 메소드에서 script 태그에 접근할 수 있도록 특정 id 값을 설정해준다.
그리고 코드 조각의 type을 명시해주는데 text/template으로 명시해준다!
<script type=“text/template” id=“template”>
<!--html code-->
</script>
format 메소드를 보면 replace 메소드와 정규 표현식을 사용하여 html 코드 조각에 있는 {0}, {1}, {2} 등의 값들을 넘어오는 데이터로 교체(replace)하도록 되어 있다. function을 통해 몇 개의 인자가가 넘어올지 모르기 때문에 arguments 객체로 받아준다.
String.prototype.format = function() {
var args = arguments;
return this.replace(/{(\d+)}/g, function(match, number) {
return typeof args[number] != 'undefined'
? args[number]
: match
;
});
};
ajax를 통해서 데이터베이스에 저장된 객체를 다시 받았으면 success시 발생할 함수에 해당 html 코드조각을 동적으로 생성해주도록 메소드를 추가해준다. script 태그에 작성되있는 html 코드 조각을 가져와서 위에서 작성한 format 메소드를 통해 우리가 원하는 데이터를 삽입해준다. 이렇게 해주면 우리가 원하는 데이터가 담긴 html 코드 조각이 완성되는 것이다.
완성된 html 코드 조각을 prepend() 함수를 사용하여 추가해주자.
function onSuccess(data, status) {
console.log(data);
var $answerTemplate = $('#answerTemplate').html();
var template = $answerTemplate.format(
data.writer.userId, data.formattedCreateDate, data.contents, data.question.id, data.id);
$('.qna-comment-slipp-articles').prepend(template);
$('.answer-write textarea').val('');//flush!
}
2. 댓글 삭제하기
2-1. 삭제기능 구현
이제 삭제하는 기능을 구현해보자.
기존에는 삭제 버튼을 클릭하면 그 요청이 서버의 컨트롤러에게 가고, 서버에서 삭제한 다음에 변경사항이 적용된 화면을 클라이언트에게 전달하게 된다. Ajax를 활용한 삭제는 어떻게 이루어지는가? $.ajax 메소드에 type을 ‘delete’로 정해주면 된다. ajax 메소드 실행 시 설정해둔 url을 통해 method가 delete 인 방식으로 서버에 요청이 가게 된다.
$( '.link-delete-article').click(deleteAnswer);
function deleteAnswer(e) {
e.preventDefault();
var url = this.attr('href');
$.ajax({
url : url,
type : 'DELETE',
dataType : 'json',
error : onError,
success : onSuccess(data, status)
});
}
아마 해보면 GET 방식으로 요청이 되는 것을 볼 수 있을 것이다.(GET not supported!!) 우린 분명 ajax 메소드에 type을 delete로 했는데 get으로 요청을 하고 get을 지원하지 않는다고 한다! 당연히 delete 요청을 보냈으니 @DeleteMapping으로 DELETE 요청받을 준비를 했것만 딴소리를 한다.
이것은 ajax 메소드가 실행되지 않고 a 태그의 역할을 수행했다는 것, 즉 e.preventDefault 메서드가 적용되지 않았다는 뜻이다. 더 나아가자면, 동적으로 생성된 html 코드 조각에 이벤트가 적용되지 않은 것이다. 위에서 말한 것처럼 새로고침(refresh)을 하고 나서야 비로소 삭제가 제대로 이루어진다.
바로 전 단계에서 댓글을 Ajax를 통해 생성해주었다. 댓글을 삭제하는 버튼도 Ajax에 의해 동적으로 생성되는 html 인 것이다. 이 경우, 동적으로 생성되는 삭제버튼에 일반 버튼과 동일한 방법으로 preventDefault 이벤트를 설정해두면 이 이벤트가 적용되지 않는다. 동적으로 생성된 html 코드 조각에 대해서는 방금 전에 언급한 것처럼 refresh를 통해 이벤트를 적용시켜주거나 추가적인 작업이 필요하기 때문이다.
이는 어떻게 해결할 것인가?
동적으로 생성되는 코드 말고, 애초에 존재했던 태그로부터 이벤트 델리게이션을 이용한다! 거의 대부분 상단에 존재하는 태그에 이벤트를 걸게 된다. 순수 자바스크립트로 코딩을 하게 되는 경우, 해당 삭제 버튼을 찾아 나서야 하지만 jQuery에서는 Event Delegation을 on 메소드를 통해 제공한다. event delegation에 대한 내용은 생략한다!
$('.qna-comment-slipp-articles').on('click', '.link-delete-article', deleteAnswer);
function deleteAnswer(e) {
e.preventDefault();
var $deleteBtn = $(this);
var url = $deleteBtn.attr('href');
$.ajax({
url : url,
type : 'DELETE',
dataType : 'json',
error : onError,
success : function (data, status) {
if (data.valid) {
$deleteBtn.closest("article").remove();
} else {
alert(data.message);
}
}
});
}
에러 발생했을 때 당황했을 수도 있기에 넌지시 건네는 꿀팁.
혹시 에러가 발생하여 뒤로 가기를 했는데 작성한 댓글이 보이지 않는다면 개발자 도구를 열어서 Disable cache를 체크해주자! 크롬이 caching 해둔 페이지로 뒤로 가는 것이기 때문에, 우리가 작성한 댓글이 분명 데이터베이스에까지 저장이 되었지만 화면에 표시되지 않는다. 새로고침을 하면 보일 것이다. 하지만 Disable cache를 체크해주면, 뒤로가기를 하면 새로고침을 한 상태에서 뒤로 가기가 이루어지기 때문에 바로 표시된다!
this란 녀석에 대한 내용을 일부러 빠뜨렸다. this란 녀석의 정체와 저렇게 this를 변수로 저장하지 않고 하는 방법에 대한 포스팅이다.
2-2. Controller 준비하기
이제 컨트롤러에 @DeleteMapping 어노테이션이 붙은 메소드를 생성해주면 된다.
무엇을 반환해야 할까? Result 객체를 반환하여 error 발생 시, 상황에 맞는 에러 메시지를 함께 전달할 수 있도록 하자. 인자로는 question의 id와 answer의 id 두 가지가 전달된다. question에서도 answer를 삭제해야 하고, answer 자체에서도 삭제하고자 하는 값을 삭제해야 하기 때문이다.
@DeleteMapping("/{id}")//@RequestMapping("/api/questions/{questionId}/answers")
public Result delete(@PathVariable Long questionId, @PathVariable Long id, HttpSession session) {
if(!HttpSessionUtils.isLoginUser(session)) {
return Result.fail("로그인 해야 합니다");
}
Answer answer = answerRepository.findOne(id);
User loginUser = HttpSessionUtils.getUserFromSession(session);
if(!answer.isSameWriter(loginUser)) {
return Result.fail("자신의 글만 삭제할 수 있습니다!");
}
answerRepository.delete(id);
Question question = questionRepository.findOne(questionId);
question.deleteAnswer();
questionRepository.save(question);
return Result.ok();
}
3. Domain Class Refactoring
Domain 클래스들 중복제거하기
각각의 Domain 클래스마다 중복되는 요소들이 존재한다. 레코드를 식별할 수 있는 id 값, 데이터가 추가된 날짜와 시간 그리고 데이터가 수정된 날짜와 시간 등이 있을 수 있겠다. 이러한 부분을 하나의 클래스로 정의한 다음에 상속을 받는 형식으로 중복 제거가 가능하다.
AbstractEntity라는 이름의 클래스를 만들어 주고,@MappedSuperClass 어노테이션을 추가해준다. 각 도메인 클래스마다 중복되는 코드를 이 클래스에 모두 옮긴다. 즉, 각 도메인 클래스들의 아이디 값과 생성되는 시간, 최종 수정된 시간 등에 대한 정보가 들어가게 된다. 공통적으로 사용하는 메소드들도 AbstractEntity 클래스로 옮긴다. 데이터가 생성되는 시간과 최종적으로 수정된 시간에 대해서는 Spring-Data에서 어노테이션을 통해 제공된다.
그리고 MyProjectApplication (SpringBoot Main Class)에 @EnableJpaAuditing 어노테이션 추가해주고, AbstractEntity에 @EntityListeners(AuditingEntityListener.class) 어노테이션을 추가해준다. (이 어노테이션에 관련된 사항은 추후에 더 공부해봐야겠다.) 이제 각 도메인 클래스에서 중복된 코드를 제거하고 이 클래스를 상속받으면 된다.
@MappedSuperclass
@EntityListeners(AuditingEntityListener.class)
public class AbstractEntity {
@Id
@GeneratedValue
@JsonProperty
private Long id;
@CreatedDate//데이터 생성 날짜를 저장하는 어노테이션
private LocalDateTime createDate;
@LastModifiedDate//자동으로 업데이트해주는 어노테이션
private LocalDateTime modifiedDate;
}
! .class 는 무엇인가?
모든 클래스 파일은 클래스로더(ClassLoader)에 의해 메모리에 올라갈 때, 클래스에 대한 정보가 담긴 객체를 생성하는데 이 객체를 클래스 객체라고 한다. 이 객체를 참조할 때 ‘클래스이름.class’의 형식을 사용한다. 즉 .class 로 호출된 객체에는 해당 클래스에 대한 모든 정보를 갖고 있다. 애너테이션 정보도 포함한다. 이 클래스 객체의 메소드를 통해서 각종 정보에 접근할 수 있다.
cf> Spirng REST API관련 좋은 글이다!
4. Swagger 라이브러리를 사용한 API Test
스웨거(Swagger) ( http://swagger.io/ )
REST API를 만들 때 문서화를 잘하는 것이 중요하다. API를 변경할 때마다 레퍼런스 문서에 똑같이 변경해주는 것도 중요하다. 이를 자동화해주는 도구가 Swagger이다. Swagger는 REST API에 대한 표준 인터페이스를 제공하는 구현체이다.
먼저 스프링 폭스와 스웨거 라이브러리 의존성을 정의해야 한다. Maven 을 사용하여 의존성 관리를 하고 있다면 pom.xml 파일에 다음 dependency를 추가해주자.
<dependency>
<groupId>io.springfox</groupId>
<artifactId>springfox-swagger2</artifactId>
<version>2.2.2</version>
</dependency>
<dependency>
<groupId>io.springfox</groupId>
<artifactId>springfox-swagger-ui</artifactId>
<version>2.2.2</version>
</dependency>
그 다음 스프링 부트 애플리케이션에 Swagger를 enable하게 한다. @EnableSwagger2 어노테이션을 추가해주면 된다.
그리고 다음 메소드 두 가지를 추가해준다.
@Bean
public Docket newsApi() {
return new Docket(DocumentationType.SWAGGER_2)
.groupName("my-project")
.apiInfo(apiInfo())
.select()
.paths(PathSelectors.ant("/api/**"))
.build();
}
private ApiInfo apiInfo() {
return new ApiInfoBuilder()
.title("My Project API")
.description("Spring REST with Swagger")
.termsOfServiceUrl("http://www-03.ibm.com/software/sla/sladb.nsf/sla/bm?Open")
.contact("Niklas Heidloff")
.license("Apache License Version 2.0")
.licenseUrl("https://github.com/IBM-Bluemix/news-aggregator/blob/master/LICENSE")
.version("2.0")
.build();
}
브라우저를 통해 swagger 가 제공하는 API explorer에 접근할 수 있다.
localhost:8080/swagger-ui.html
위 주소로 접근 가능하다.
5. 쉘 스크립트를 만들기!
배포를 위해 사용하는 명령어들을 .sh 파일인 쉘 스크립트 파일에 나열만 해주면 된다.
그리고 생성한 sh 파일을 실행시키기 위해서 권한을 755로 바꿔준다.(chmod 명령어를 사용한다!)
chmod 755 deploy.sh
vi deploy.sh
deploy.sh 파일을 작성해준다.
너무… 편해졌다!
반복주기 6. End
항상 좋은 강의 감사합니다!
짧은 수강평
6부작의 드라마를 보는 듯, 한 반복주기가 끝이 나면 다음 강의 내용이 궁금했다. 6개의 반복주기로 나눠져서 다행이다. 한 번에 다 올라왔다면 드라마를 몰아보는 것처럼 일주일 잡고 한숨에 들었을 것이다. 수강신청을 해서 억지로 듣는 수업이 아닌, 매주 강의를 기다리며 수강했다.
누군가에게는 이 강의가 맞지 않을 수도 있다. 이 강의는 Spring Boot라는 프레임워크를 사용하여 웹 애플리케이션의 전반적인 구조와 흐름에 대해 다루고 있다. 기본 원리부터 학습하는 사람이 있다면, 궁극적으로 학습하고자 하는 것의 흐름을 알아야 동기부여가 되고 이를 기반으로 다음 학습에 대한 방향을 잡아 학습하는 사람이 있다. 이 강의는 따지자면 후자에게 적합한 강의이다. 수강생은 은근슬쩍 넘어가는 프레임워크의 내부 원리에 대해 집착하지 않는 자세가 필요하다.
그래서 아직 회수되지 않은 떡밥이 많다. 수강생 스스로 회수하라는 교수의 지시라고 생각하고 강의 내용을 곱씹으면서 시즌 2를 대비해야겠다.
'Dev.BackEnd > Spring Boot' 카테고리의 다른 글
#SLiPP Spring boot, JPA 강의 - 반복주기 5 (0) | 2016.10.23 |
---|---|
#SLiPP Spring boot, JPA 강의 - 반복주기 4 (2) | 2016.10.17 |
[Spring boot] Spring-Boot에서 JDBC Driver 설정하고 사용하기 (0) | 2016.10.12 |
#SLiPP Spring boot, JPA 강의 - 반복주기 3 (0) | 2016.10.10 |
#SLiPP Spring Boot, JPA 강의 - 반복주기 2 (0) | 2016.10.03 |