| Today I learned
1. @Controller와 @RestController의 차이
- 앞에서는 내가 @Controller와 @RestController에 대해 차이를 정리할 때, 잘못된 정보를 가지고 있었다.
- @Controller로도 @RestController처럼 Json 타입의 데이터를 보내는 건 가능하다.
- 그럼에도 굳이 둘을 나눠서 쓰는 건, 사용 목적에 따라 쓰임새를 나누어두기 위함으로 보인다.
* 아래 내용 원본 출처 : https://mangkyu.tistory.com/49
(1) @Controller : 주로 View를 반환한다. Model로 Data-binding 가능
@Controller | ||
View를 반환할 때 | Data를 반환할 때 | |
목적 | @Controller는 주로 View를 반환하기 위해 사용하지만, @ResponseBody로 Data를 반환할 수는 있다. | |
동작 | 1. Client는 URI 형식으로 request를 전송 2. DispatcherServlet이 HandlerMapping을 찾음 3. HandlerMapping을 통해 요청을 Controller로 위임 4. Controller는 요청 처리 후 ViewName을 반환 5. DispatcherServlet은 View Resolver를 통해 ViewName에 해당되는 View를 찾아 사용자에게 반환 |
1. Client는 URI 형식으로 request를 전송 2. DispatcherServlet이 HandlerMapping을 찾음 3. HandlerMapping을 통해 요청을 Controller로 위임 4. Controller는 요청 처리 후 객체를 반환 5. 객체는 Json으로 serialize되어 Client에 반환 |
비고 | 기본적으로 template안에 있는 html을 View로 반환 | @ResponseBody를 통해, Json 형태로 데이터를 반환 * 스프링은 HTTP accept 헤더와 Controller 반환 타입을 조합하여 적합한 HttpMessageConverter를 선택해 처리한다. |
@Controller
@RequiredArgsConstructor
public class MemberController{
private final MemberService memberService;
// View를 반환할 때
@GetMapping(value = "/member/list")
public String memberListView(Model model, @RequestParam("memberType") String memberType) {
List<MemberDto> memberList = memberService.findByMemberType(memberType);
model.addAttribute("memberList", memberList);
return "member/list";
}
// Data를 반환할 때
@GetMapping(value = "/member")
public @ResponseBody ResponseEntity<Member.Response> getMemberDetail(@RequestParam("id") String id){
return ResponseEntity.ok(memberService.findById(id));
}
}
(2) @RestController
- @Controller에 @ResponseBody가 추가된 형태와 완전히 동일하다.
- 따라서, 웹에 뷰를 전달할 때는 @Controller를 쓰고,
api 형태로 데이터를 가공해서 JSON으로 전달할 땐 @RestController를 쓴다.
@Controller | @RestController | |
목적 | 주로 View를 반환하기 위함 | JSON 형태로 객체 데이터를 반환하기 위함 |
동작 | 1. Client는 URI 형식으로 request를 전송 2. DispatcherServlet이 HandlerMapping을 찾음 3. HandlerMapping을 통해 요청을 Controller로 위임 4. Controller는 요청 처리 후 ViewName을 반환 5. DispatcherServlet은 View Resolver를 통해 ViewName에 해당되는 View를 찾아 사용자에게 반환 |
1. Client는 URI 형식으로 request를 전송 2. DispatcherServlet이 HandlerMapping을 찾음 3. HandlerMapping을 통해 요청을 Controller로 위임 4. Controller는 요청 처리 후 객체를 반환 5. 객체는 Json으로 serialize되어 Client에 반환 |
비고 | 기본적으로 template안에 있는 html을 View로 반환 | @Controller + @ResponseBody |
- 아래 두 가지 방식에 대해서, 첫번째 방식의 경우, Client가 예상하는 HTTP Status를 설정해줄 수 없다.
- 두번째 방식의 경우에는 HttpStatus를 알 수 있지만 ok() 메소드만으로만 알 수 있다.
- 세번째 방식을 사용하면 HttpStatus를 명시적으로 볼 수 있다.
@RestController
@RequiredArgsConstructor
public class MemberController{
private final MemberService memberService;
// 첫번째 방식
@GetMapping(value = "/member")
public Member.Response getMemberDetail(@RequestParam("id") String id){
return memberService.findById(id);
}
// 두번째 방식
@GetMapping(value = "/member")
public @ResponseBody ResponseEntity<Member.Response> getMemberDetail(@RequestParam("id") String id){
return ResponseEntity.ok(memberService.findById(id));
}
// 세번째 방식
@ResponseStatus(value = HttpStatus.OK)
@GetMapping(value = "/member")
public Member.Response getMemberDetail(@RequestParam("id") String id){
return memberService.findById(id);
}
}
2. ajax를 사용하는 방법
- 국비지원 과정을 들을 때, jqeury ajax에 대해 아주 간단히 보고 넘어갔었다.
- 오늘 수업에서는, ajax를 쓰기 위한 다른 방법, axios에 대해서 배웠는데, 둘 다 쓰기 나름일 것 같다.
rf. ajax는 비동기식 처리(요청의 응답을 기다리지 않고 다음을 요청 <->동기식처리)를 한다.
* 동기식 처리 : 요청의 응답을 기다린 후 응답이 오면 다음 요청을 한다.(멀티캐스팅 불가, 데이터 손실X)
* 비동기식 처리 : 요청의 응답을 기다린 후 응답이 오지 않아도 다음 요청을 한다.(멀티태스킹 가능, 데이터 손실 가능)
ajax | 제이쿼리 | axios |
공통점 | 비동기식 처리를 하는 ajax | |
차이점 | 제이쿼리 문법 | 자바스크립트 promise 패턴 |
tutorial | https://www.w3schools.com/jquery/jquery_ajax_get_post.asp | https://github.com/axios/axios |
(1) 제이쿼리 ajax
//jQuery Ajax - Request
let id = $('#id').val();
$.ajax({
url : '/course/detail'
data : {'id' : id},
cotentType : 'application/json', // request data type
dataType : 'json', // response data type
success : function(res_data){
},
error : function(err){
//alert(err.responseText);
}
});
(2) axios를 이용한 방법 *여기서는 CDN 방식으로 import했다.
1. 아래 링크에서 axios 다운로드 또는 CDN방식으로 가져오기
https://github.com/axios/axios
2. 위 링크에서 추천해준 방식에 따라 axios 작성
-- 링크의 예시
axios.post('/user', {
firstName: 'Fred',
lastName: 'Flintstone'
})
.then(function (response) {
console.log(response);
})
.catch(function (error) {
console.log(error);
});
<script>
$(document).ready(function(){
$('#submitForm').on('submit',function(){
// url, parameter 객체
let $thisForm = $(this);
let url = '/api/course/req.api';
let parameter = {
id : $thisForm.find('input[name=id]').val()
};
// axios 포맷에 맞춰 작성
axios.post(url, parameter).then(function(response){
}).catch(function(err){
});
return false;
});
});
</script>
3. Spring Security의 Principle
- Spring Securiy에는 Principle이라는 인터페이스가 있다
* 출처 : https://codevang.tistory.com/273
.
- (Spring Securiy에 대해서는 이론적인 부분을 차후 보강해나갈 예정)
- 가볍게 이해하기로는, 최종 인증 정보- Authentication - 의 상위 인터페이스이다.
- Principal을 통해서 하위 객체들의 메소드를 사용할 수 있는데, 그 중 하나가 getName() 으로 이는 User ID를 말한다.
- 한 번 해보니까, Pricipal은 로그인 세션이 끝날 때 null로 오기도 해서, 에러가 나타날 수 있었다.
- 이에 대해서 아래 블로그에서는 다른 메소드를 사용할 수 있다고 안내하는데, 이 부분은 차후 확인해보려고 한다.
https://m.blog.naver.com/PostView.naver?isHttpsRedirect=true&blogId=phrack&logNo=80202619173
/* 수강 신청 */
@PostMapping("/api/course/req.api")
public ResponseEntity<?> courseReq(Model model,
@RequestBody TakeCourseInput parameter,
Principal principal) {
parameter.setUserId(principal.getName()); // 로그인 Id 추가
boolean result = courseService.req(parameter); // 수강 신청
if (!result) {
return ResponseEntity.badRequest().body("수강신청에 실패하였습니다.");
}
return ResponseEntity.ok().body(parameter);
}
4. 비즈니스 로직 InValidation ? - Exception? or Not
- Account(RestAPI) 프로그램 계좌 등록 로직에선, Custom Exception에서 Enum타입의 ErrorCode를 전달했는데
- LMS 프로그램 수강신청 로직에선 마지막에 ServiceResult라는 클래스를 통해 invalid message를 전달했다.
- 막바지에 이렇게 코드를 수정하는 이유에 대해 강사분은,
(예상에 따라) 수강신청 시 신청이 안 되는 상황은 Error로 보기 어렵다고 판단했기 때문이라 말했다.
- 그러며 모든 상황에 대해 ResponseEntity.ok() 처리를 하고, body에 message를 전달했다.
- 이에 따라 결과적으로는 '강좌가 없는 경우', '이미 신청한 경우'에 수강신청을 하면 Response Header에 200이 반환됐다
- 사용자 입장에선 사실 200이 뜨건, 500이 뜨건, 400이 뜨건 잘 돌아가면 별 문제가 없다고 생각하겠지만,
서비스를 제공하는 관점에서 진정한 의미의 예외상황과 아닌 상황을 나누기 위해 이렇게 하지 않았을까 싶다.
package com.example.fastlms.course.model;
import lombok.AllArgsConstructor;
import lombok.Getter;
import lombok.NoArgsConstructor;
import lombok.Setter;
@Getter
@Setter
@AllArgsConstructor
@NoArgsConstructor
public class ServiceResult {
boolean result;
String message;
}
- 이 방식으로 하게 되면 아래 예시 코드처럼, 신청이 안료되지 않을 상황의 메세지를 서비스에서 받아서
/**
* 수강 신청
* - 수강 신청하려는 강좌가 없거나, 수강 신청을 이미 한 경우, 실패응답
* - 수강 신청하려는 강좌id을 포함하여 수강 신청 정보 업데이트
* @param parameter
* @return
*/
@Override
public ServiceResult req(TakeCourseInput parameter) {
ServiceResult result = new ServiceResult();
Optional<Course> optionalCourse
= courseRepository.findById(parameter.getCourseId());
if (!optionalCourse.isPresent()) {
result.setResult(false);
result.setMessage("강좌 정보가 존재하지 않습니다.");
return result;
}
Course course = optionalCourse.get();
// 이미 신청 한 경우
String[] statusList = {TakeCourseCode.STATUS_REQ, TakeCourseCode.STATUS_COMPLETE};
long count = takeCourseRepository.countByCourseIdAndUserIdAndStatusIn(
course.getId(), parameter.getUserId(), Arrays.asList(statusList)
);
if (count > 0) {
result.setResult(false);
result.setMessage("이미 신청한 강좌 정보가 존재합니다.");
return result;
}
takeCourseRepository.save(
TakeCourse.builder()
.courseId(course.getId())
.userId(parameter.getUserId())
.payPrice(course.getSalePrice())
.status(TakeCourseCode.STATUS_REQ)
.build()
);
result.setResult(true);
result.setMessage("");
return result;
}
- 컨트롤러에서 ResponseEntity의 body에 넣어주는 것이 가능하다.
/* 수강 신청 */
@PostMapping("/api/course/req.api")
public ResponseEntity<?> courseReq(Model model,
@RequestBody TakeCourseInput parameter,
Principal principal) {
parameter.setUserId(principal.getName()); // 로그인 Id 추가
ServiceResult result = courseService.req(parameter); // 수강 신청
if (!result.isResult()) {
return ResponseEntity.ok().body(parameter);
}
return ResponseEntity.ok().body(parameter);
}
5. Response 의 Header와 Body를 Json으로 전달하기
- 4번에서 잠시 보여주었던 로직에 따르면, ServiceResult의 Message는 Body에 담겨 Text 형식으로 전달되게 된다.
- 이걸 아래와 같이 Header에 Json타입으로 변환하여 Client로 전달하고 싶다.
{
"header" : {
"result": true,
"message": ""
},
"body" : {}
}
- 그럴 때는 Header 객체를 만들고 이걸 다시 객체로 싸면 된다.
>> 예를 들어 아래의 경우, ResponseResultHeader를 ResponseResult 안에서 사용했다.
package com.example.fastlms.common.model;
import lombok.*;
@Getter
@Setter
public class ResponseResultHeader {
private boolean result;
private String message;
public ResponseResultHeader(boolean result) {
this.result = result;
}
public ResponseResultHeader(boolean result, String message) {
this.result = result;
this.message = message;
}
}
package com.example.fastlms.common.model;
import lombok.*;
@Getter
@Setter
public class ResponseResult {
private ResponseResultHeader header;
private Object body;
public ResponseResult(boolean result, String message) {
header = new ResponseResultHeader(result, message);
}
public ResponseResult(boolean result) {
header = new ResponseResultHeader(result);
}
}
- 이 상태에서, 컨트롤러에서 객체타입을 ResponseEntity의 body에 담아 보내주면 위의 json 구조와 같이 데이터가 전송된다.
/* 수강 신청 */
@PostMapping("/api/course/req.api")
public ResponseEntity<?> courseReq(Model model,
@RequestBody TakeCourseInput parameter,
Principal principal) {
parameter.setUserId(principal.getName()); // 로그인 Id 추가
ServiceResult result = courseService.req(parameter); // 수강 신청
if (!result.isResult()) {
ResponseResult responseResult
= new ResponseResult(false, result.getMessage());
return ResponseEntity.ok().body(responseResult);
}
return ResponseEntity.ok().body(true);
}
- 앞서서 본 axios 를 통해 비동기식으로 보낸 요청에 대한 응답에 대해 마처 처리를 하면 아래와 같이 될 수 있다.
$(document).ready(function(){
$('#submitForm').on('submit',function(){
if (!confirm("수강신청을 하시겠습니까?")){
return false;
}
let $thisForm = $(this);
let url = '/api/course/req.api';
let parameter = {
courseId : $thisForm.find('input[name=id]').val()
};
// javascript의 promise 패턴
// async (비동기 방식)
axios.post(url, parameter).then(function(response){
response.data = response.data || {}; // Data를 객체로 초기화
response.data.header = response.data.header || {}; // Header도 객체로 초기화
if (!response.data.header.result) {
alert(response.data.header.message);
return false;
}
// 정상적일 때
alert('강좌가 정상적으로 신청되었습니다.');
location.href = '/';
}).catch(function(err){
console.log(err);
});
return false;
});
});
+ JSON 응답과 요청 처리에 대한 포스팅 글
https://owin2828.github.io/devlog/2019/12/30/spring-16.html
[Spring] JSON 응답과 요청 처리 - 낮코밤코
1. Jackson 의존 설정 Jackson은 자바 객체와 JSON 형식 문자열 간 변환을 처리하는 라이브러리로 다음과 같이 pom.xml에 의존을 추가 com.fasterxml.jackson.core jackson-databind 2.9.4 com.fasterxml.jackson.datatype jackson-d
owin2828.github.io
| 추가적으로 정리한 사항
1. MVC 모델에서 사용했던 Annotation 정리
ㄴ 자주 났던 오류 : Request를 받아올 때에, Setter를 지정해주지 않았더니 값이 넘어오지 않았다.
★ 항상 클라이언트에게 데이터를 객체에 싸서 전달 받을 때에는 Setter를 지정해주자.
ㄴ 이펙티브 자바에서 제안한 사용법 : 롬복을 쓰려거든 @Data 사용을 하기 보다는 필요한 것만 뽑아 쓰자.
(@Data에는, @EquslsAndHashCode, @toString 도 있기 때문에 불필요한 메소드가 생길 수 있다.)
domain/model | controller | service | dto | entity | repository | |
Model.Request Model.Response |
Controller | Service | Dto | Entity | Respository | |
@Getter @Setter * Setter없음 값을 못 받는다 |
@Controller @RestController @GetMapping.. @RequestParam @PathVariable @RequestBody @ResponseBody * post 맵핑 시, @RequestBody는 @RestController에서만 쓸 수 있다. |
@Service @Transactional |
@Getter @Setter @Builder @AllArgsConstructer @NoArgsConstructer ** 제약조건 @NotNull @NotBlank @Min @Max @Column |
@Entity @EnttiyListener @Id @GeneratedValue @CreatedAt @LastModifiedAt |
@Repository |
2. 헥사고날 디자인 패턴의 아주 기초적인 개념
- 레이어드 디자인 패턴은 계층 형태로 이루어져 있어, MVC 각각의 클래스간 결합도가 높은 편이지만,
- 헥사고날 디자인 패턴의 경우, 의존성을 역전시켜 결합도를 낮추어 준다.
- 패키징을 어떻게 하는 지에 대해서는 좀 더 공부를 해봐야 알 것 같다.
https://www.youtube.com/watch?v=MKfSLrwLex8
헥사고날 아키텍처(Hexagonal Architecture) 코드로 이해 해보기
EventsJdbcEntityRecordPublishedEventService 이 부분은 '만들면서 배우는 클린 아키텍쳐' 의 책을 보고 배운점과 느낌점을 설명합니다. 헥사고날 아키텍쳐란 무엇인까요? 헥사고날 아키텍쳐는 레이어드 아
happy-coding-day.tistory.com
https://www.jiniaslog.co.kr/article/view?articleId=1152
[project] 기존 프로젝트 헥사고날 아키텍처로 리아키텍쳐링하기 - Jinia's LOG'
Goal 클린 아키텍쳐, 헥사고날 아키텍쳐에 대한 간략 이해 기존 프로젝트 아키텍쳐 분석 헥사고날 아키텍쳐로 리아키텍쳐링 Architecture Clean Architecture 로버트마틴의 클린아키텍쳐에서 저자는 소프
www.jiniaslog.co.kr
| 정리
- @Controller의 경우 주로 View를 반환하기 위해 사용하며, @RestController의 경우 주로 RestAPI에서 JSON타입의 데이터를 반환할 때 사용한다. - Ajax를 통해 비동기처리 방식으로 JSON타입의 데이터를 전달할 수 있는데, 제이쿼리를 쓰거나 axios를 쓸 수 있다. - Request parameter 데이터 타입 매핑 - 일반 Form으로 전달 받은 x-www-form-urlencoded 타입은 일반 객체로 Mapping할수 있으며 - ajax를 통해 전달 받은 Json 타입의 경우 @RequestBody를 사용해야 한다. - @Controller를 통해서도 Data를 전달할 수 있는데 이때는 ResponseEntity를 통해 Response값을 싸주는 게 필요하다. ㄴ 추가적으로 @ResponseBody를 통해 body에 넣어 보내줄 수도 있다. - @RestController는 @Controller에 @ResponseBody가 묶인 형태이며, Data를 전달 하는 방식은 @Controller와 동일하다. - ResponseEntity를 통해 데이터를 보내게 되면 Status를 시각적으로 확인할 수 있다. 다만, 이 방식 보다 @ResponseStatus(value = HttpStatus.OK) 를 쓰는게 더 가독성이 높을 때도 있으니 유념하자. - Spring Security의 Principal 인터페이스는 Authentication의 상위 인터페이스로 사용자 정보를 담고 있다. getName() 메소드를 통해 사용자 아이디를 받을 수 있으나, 세션이 만료되면 null값이 올 수 있으므로 주의가 필요하다. - 비즈니스 로직에 대한 validation 처리를 할 때 항상 Exception으로 처리를 하는게 능사는 아니다. 지금과 같이 Client단에서 [수강신청]을 비동기처리를 할 때에 객체로 데이터를 묶어 사용자에게 상황을 안내하는게 더 도움이 될 수 있다. ** Response를 객체로 묶어 ResponseEntity.ok().body()에 넣어주면 JSON으로 자동 변환되어 전달 된다. |
[ 참고 및 출처 ]
부트캠프 수업 내용 정리
@Controller와 @RestController의 차이
https://mangkyu.tistory.com/49
https://yeonyeon.tistory.com/257
스프링 Securiy로그인 Principal
https://codevang.tistory.com/273
'Framework > 프로젝트로 스프링 이해하기' 카테고리의 다른 글
[LMS 만들기] 회원 탈퇴, 강좌 관리, 파일 업로드 (0) | 2022.10.10 |
---|---|
[LMS 만들기] 회원정보 수정 - 우편번호 찾기, ajax와 Rest API (0) | 2022.10.10 |
[LSM 만들기] 강좌 목록 구현하기 - 스마트에디터, 등록/수정 동시 처리 (0) | 2022.10.07 |
[LMS 만들기] 카테고리 수정, 삭제, 정렬 (0) | 2022.10.06 |
[LMS만들기] 회원 상태 변경 및 비밀번호 초기화 (0) | 2022.10.05 |