※ 이 포스트는 스프링 실습 과정에서 작성하기 때문에 정보가 부정확할 수 있는 부분이 있습니다.
따라서 참고만 해주시고 틀린 부분이 있을 경우 알려주시면 감사하겠습니다.
이번 포스트는 김영한님의 '스프링 mvc 2편, 백엔드 웹 개발' 강의의 검증 부분을 일부 인용하였습니다.
Validation과 예외처리에 대해 더 공부하고 싶으신 분은 인프런에서 해당 강의를 수강하시길 추천합니다.
이번 포스트도 프로젝트 진행 중 validation 코드를 추가하다가 정리해보면 어떨까싶어 정리하는 글입니다. 기초를 다룰 것이기에 스프링에 validation을 적용하는 개괄에 대해 다루고 자세한 내용은 다음 포스트에 이어 다루겠습니다. 이번 포스트의 큰 줄기는 다음과 같습니다.
1. 검증(validation)의 소개
2. 스프링 통합 없는 검증 직접 처리
3. Bean validation
4. 스프링 통합시 검증 로직
1. 검증(validation)의 소개
# Verfication과 Validation
- 우선 개발에서 검증을 언급하기 전에 Verfication과 Validation 두 개를 헷갈려하는 분이 계실 것 같아 간단하게 정리하고 가겠습니다. 두 단어는 검증, 검토로 번역되듯 비슷하지만 뉘앙스가 조금 다릅니다. 위키 백과에는 다음과 같이 나와 있습니다.
Verfication : 제품을 올바르게 빌드하고 있는가를 검증, 개발자 중심, 코드의 예외 자체를 검사
Validation : 우리가 올바른 제품을 빌드하고 있는가를 검증, 사용자 중심, 실제 작동 유무를 검사
아무래도 개발 현장에선 Validation 을 주로 다루지만 굳이 둘을 나누자면 verfication은 요구 사항에 맞춰 process를 잘 지키고 있는지 확인하는 좀 더 정적인 검증이고 validation은 빌드된 결과물이 사용자 입장에서 올바르게 작동하는지 확인하는 좀 더 동적인 검증이라고 이해하면 됩니다. 그렇지만 앞서 얘기했듯 개발 현장에서 주로 사용되는 개념은 validation이며 validation 은 여러 검증 과정 중 사용자가 올바른 값을 입력했는지 확인하는 단계를 주로 의미합니다.
# 유효성 검증
- 굳이 개발이 아니더라도 값을 다루는 일이라면 유효성 검증(검사)는 반드시 수반되어야할 과정입니다. 엑셀만 해도 유효성 검사가 있고 프로덕트 출시 전에 V&V 테스트를 하는 등 많이 보셨을텐데요. 이전 포스트에서 페이징을 다뤘었는데 이전 버튼을 눌렀을때 page가 0이 되면 안되니 검증 메소드로 이를 처리하는 과정이 있었고 이 역시 유효성 검증의 일부입니다. 일단 웹 어플리케이션을 빌드할 때 클라이언트 입력 값을 검증하는 과정은 두 번 이뤄지는 것이 일반적입니다. 한 번은 프론트엔드단에서, 한 번은 서버단에서 말이죠.
# 서버에서의 유효성 검증
- 클라이언트 검증, 쉽게 말해서 react.js나 vue.js같은 웹 프론트엔드 프레임워크로 만들어진 View에서 입력한 값을 검증은 필수적이고 처음 봤을땐 이 단계만 거치면 될 것 같지만 그렇지 않습니다. View에서 입력된 값이 json에 담겨 post요청으로 날아올 때 그 json 파일은 누구나 쉽게 조작할 수 있기때문입니다. 해커나 다른 사용자가 악의적으로 조작하는 것뿐만아니라 일반적인 사용자가 값을 제대로 입력했다고 생각하고 요청을 보내는 과정에서도 값이 변형될 여지가 존재합니다. 그렇기때문에 서버 검증으로 올바른 값이 넘어왔는지 2차 검증을 해야하는 것은 당연하고 개발시 검증 로직을 잘 개발하는 것이 프로덕트 코드를 짜는 것보다 어려워지는 경우도 많기에 이를 소홀히 할 수도 있는데 그래선 안됩니다. 컨트롤러의 중요한 역할 중 하나는 HTTP 요청이 정상인지 검증하는 것입니다! 그렇기 때문에 서버 개발자가 검증 기능을 공부하는 것은 매우 기본적인 것이며 당연히 검증은 global한 표준 기술이기에 스프링에서 개발자를 위한 통합 기능을 제공하며 그에 대한 것은 뒤에서 살펴보겠습니다.
2. 스프링 통합 없는 검증 직접 처리
#일반적인 검증
- 검증을 처리하는 가장 쉽고 고전적인 방법은 아마 if나 try catch를 사용한 메소드를 작성하는 것입니다. 하지만 매번 컨트롤러에 그런 검증 메소드를 추가하면
@PostMapping("/")
public String Home(@ModelAttribute PostForm postform, Model model){
try {
if(postform.getTitle == null) {
throw new BaseException(PRODUCT_NAME_CAN_NOT_BE_EMPTY};
if(postform.getContent == null) {
...
따라서 검증 로직을 컨트롤러에 일일이 쓰지 말라고 스프링은 착하게도 검증에 대한 표준을 미리 만들어서 개발자들에게 제공해줍니다. 스프링의 검증 기능을 소개하기 전에 한가지 짚고 넘어갈 건 조금 머리아프더라도 최소한 BindingResult, Validator(검증기) 정도를 사용해서 직접 검증을 해보고 넘어가는 것을 추천합니다. 조금 복잡해도 안하고 넘어가면 이후 만나는 장애 대응을 하는데 큰 차이가 있을테니까요.
3. Bean validation
#개요
- 우리는 스프링을 학습하면서 순수 자바코드로 코딩을 했을때 불편했던 점들을 어노테이션을 사용해서 획기적으로 개선하곤 합니다. Bean validation도 똑같습니다. Bean validation은 쉽게 우리가 컨트롤러에서 입력값을 받을때 객체에 대한 유효성 검사를 일일이 하지 않고 입력 Form, Dto에 특정 제약을 걸어놓는 것만으로 스프링이 알아서 검증을 해주는 매우 편리한 기능입니다. 물론 이는 대부분의 입력값 검증 과정은 일반적으로 통용되기에 미리 어노테이션으로 만들어두기에 가능한 것이고 그를 스프링이 제공하는 하는 것뿐이기에 개발자가 판단하에 사용하지 않을 수도 있겠죠?
#Bean validation
- 먼저 Bean Validation은 스프링에서만 사용되는 특정한 구현체가 아니라 자바의 Bean Validation 2.0(JSR-380)이라는 기술 표준입니다. 쉽게 이야기해서 검증 애노테이션과 여러 인터페이스의 모음이고 JPA가 기술 표준, Hibernate가 그 구현체인 것처럼 Bean Validation을 구현한 기술중에 JPA와 함께 일반적으로 사용하는 구현체는 하이버네이트 Validator입니다. JavaScript와 JAVA처럼 이 구현체도 ORM과는 관련 없습니다.
우선 하이버네이트 Validator를 사용하려면 build.gradle에 다음 의존성을 추가해야합니다.
implementation 'org.springframework.boot:spring-boot-starter-validation'
이것을 추가하고 나면
@Getter
@NoArgsConstructor
public class PostsSaveRequestDto {
@NotEmpty
@Size(max = 10)
private String title;
@NotEmpty
private String content;
@NotEmpty
@Size(max = 10)
private String author;
...
처럼 입력이 들어오는 객체의 필드 위에 해당하는 어노테이션만 붙여주면 검증이 가능합니다.
참고
javax.validation.constraints.NotNull 과 org.hibernate.validator.constraints.Range처럼 어노테이션 import가 다를 수 있는 것에 의아할 수 있습니다.
이때 javax.validation 으로 시작하면 특정 구현에 관계없이 제공되는 표준 인터페이스이고, org.hibernate.validator 로 시작하면 하이버네이트 validator 구현체를 사용할 때만 제공되는 검증 기능입니다.
실무에서는 대부분 하이버네이트 validator를 사용하니 어떤 import든 자유롭게 사용해도 됩니다.
# @Valid와 @Validated
- Bean validation을 사용하는 코드들을 보면 @Valid를 사용할 때가 있고 @Validated를 사용할 때도 있습니다. 따라서 @Valid와 @Validated의 차이점을 먼저 언급하고 가겠습니다. 사실 두 어노테이션의 기능은 비슷하고
public String postSave(@Validated @ModelAttribute post)
public String postSave(@Valid @ModelAttribute post)
public String postSave(@ModelAttribute @Valid post)
위 코드처럼 위치에 대한 자유도(@ModelAttribute 양쪽에 사용 가능)도 같고 세 코드 모두 검증이 잘 작동합니다. 그렇다면 도대체 뭐가 어떻게 다를까요? 여러 가지 차이점이 있습니다만 크게 세가지를 소개할텐데 1, 2번은 차이가 있으나 크게 중요하지 않은 항목이고 3번 하나만 주의 깊게 살펴보면 됩니다.
1. 제공하는 주체가 다릅니다.
- @Valid는 JAVA에서
import javax.validation.Valid;
@Validation은 앞서말한 Spring에서의 검증 구현체에서
import org.springframework.validation.annotation.Validated;
제공하는 기능입니다.
2. @Validated는 @Valid의 기능을 포함, 유효성 검증 그룹 지정 기능도 존재합니다.
- @Validated의 Docs를 보면
@Target({ElementType.TYPE, ElementType.METHOD, ElementType.PARAMETER})
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface Validated {
/**
* Specify one or more validation groups to apply to the validation step
* kicked off by this annotation.
* <p>JSR-303 defines validation groups as custom annotations which an application declares
* for the sole purpose of using them as type-safe group arguments, as implemented in
* {@link org.springframework.validation.beanvalidation.SpringValidatorAdapter}.
* <p>Other {@link org.springframework.validation.SmartValidator} implementations may
* support class arguments in other ways as well.
*/
Class<?>[] value() default {};
}
이렇게 적혀 있습니다. 내용이 많지만 중요한 것은 JSR-303의 검증 기능을 사용한다는 것과 유효성 검증 그룹 기능이 포함되어 있다는 것입니다. 유효성 검증 그룹 기능은 이후 다시 언급하겠지만 최근에는 거의 사용하지 않고 간단하게 같은 모델 객체를 등록할때와 수정할때 다른 설정을 사용하게끔하는 기능이라고 이해하고 넘어가면됩니다. 중요한 것은 @Valid가 JSR-303의 자바 표준 스펙이고 docs를 보면 @Validation은 그 기능을 포함하고 있으며, 하이버네이트 Validator 의존성만 등록하면 @Valid를 @Validated로 바꿔도 된다는 것입니다.
3. @Validated는 AOP를 기반으로 합니다. 그래서 @Valid는 컨트롤러에서만 동작하며 @Valid는 Service,Repository 등에서도 동작합니다. 또한 발생하는 Exception도 서로 다릅니다.
AOP(Aspect Oriented Programming)에 대한 포스트는 이후 쓸 것이지만 여기서 간단하게 설명하면 크게 어플리케이션의 설게를 '주요 비즈니스 로직'과 '부가 비즈니스 로직'으로 분리하고 로그, 검증 등 사용자의 입장에서 보면 중요하지 않은 기능인 '부가 비즈니스 로직'을 '주요 비즈니스 로직' 사이사이에 끼워서 프로그래밍하는 방식을 말합니다. 예를 들어 로그를 뿌리는 것은 개발자 입장에선 정말 중요하지만 프로덕트 상으론 주 비즈니스 로직은 아니고 컨트롤러, 서비스 등 중요한 component를 구성할때 매번 로그에 대한 코드를 작성해주는 것은 적절하지 않습니다. 그럴때 컨트롤러, 서비스들이 돌아가는 '주요 비즈니스 로직'을 미리 만들어두고 컨트롤러 > 서비스로 가는 과정 사이에 '부가 비즈니스 로직'으로 따로 작성해두었던 로그를 집어 넣는 것이라고 보면됩니다. AOP는 프로그래밍 방식으로 매우 긴 호흡을 가지기에 간단한 설명만 남기고 후에 포스트로 제대로 작성하겠습니다.
- @Valid는 특정 ArgumnetResolver에 의해 유효성 검증이 실행됩니다. 스프링을 사용하신다면 모든 요청은 디스패쳐 서블릿에서 컨트롤러로 전달되는 것을 아실텐데 이때 RequestResponseBodyMethodProcessor라는 ArgumentResolver의 구현체가 json을 객체로 변환해주는데 검증에서 오류가 생기면 MethodArgumentNotValidException가 발생합니다. 이 예외를 디스패처서블릿이 받으면 404에러를 발생시킵니다.
과정이 복잡한 것처럼 보이지만 위 사진을 보면 쉽게 파악할 수 있듯 예외는 디스패처서블릿과 컨트롤러 사이에서 처리되며 그렇기때문에 주요 특징은 1 @Valid는 컨트롤러에서만 동작하고 2 MethodArgumentNotValidException을 검증 오류 발생시 반환합니다.
- @Validated를 설명하기에 앞서 @Valid가 컨트롤러에서만 동작하는 것을 짚은 바에 의문을 품으실 수도 있습니다. '서버 검증에 있어 컨트롤러에 들어오는 값만 검증하면 되지않나?'라는 생각을 하실 수 있는데 어느정도 맞는 말일 수 있지만 개발하다보면 컨트롤러 뿐만아니라 서비스, 리포지토리에서도 검증이 필요할 때가 존재합니다. 이럴 경우 @Valid는 사용하지 못하겠죠? 이 때 사용하는 것이 @Validated입니다.
디스패처서블릿과 컨트롤러 사이에서 오류를 처리하는 @Valid와 달리 특정 ArgumnetResolver가 동작하는 것이 아닌 AOP 기반으로 메소드 요청을 가로채서 검증을 처리합니다. 앞서 AOP 설명의 예시에서 컨트롤러 > 서비스 사이로 가는 요청 사이에 로그를 집어넣는다고 했었는데 이를 좀 더 살펴보면 중간 과정에서 인터셉터가 가로채고 AOP가 적용되는어 로깅 과정이 수행되는데 검증 역시 비슷한 과정이 이뤄집니다.
@Validated가 동작하는 과정 중 바이트코드 조작, 프록시 등 복잡한 요소가 많기때문에 자세한 설명 대신 거대한 흐름만 보자면 계층 사이 요청이 이동할 때 요청을 인터셉터가 가로채서 검증을 수행하고 이 과정에서 오류가 발생하면 애초에 요청이 다음 계층으로 전달되지 않게끔합니다. 중요한 것은 1 @Validated는 컨트롤러 뿐만 아니라 다른 Component에서도 동작하고 2 검증 오류 발생시 ConstraintViolationException를 반환합니다.
- @Valid와 @Validated의 차이점을 알아봤습니다만 예제를 실습하는 수준에서 큰 차이는 존재하지 않습니다. 하지만 이후 언급할 그룹 기능과 AOP를 통한 모듈화, 성능에 미치는 영향 등을 고려하여 실무에서 어떤 것을 사용해야할지 결정해야 할 것 같네요.
4. 스프링 통합시 검증 로직
# @ModelAttribute와 @RequestBody
- 스프링 mvc를 사용할 때 어플리케이션이 작동하는 방식은 크게 3가지로 나뉩니다.(물론 실무에서는 다릅니다만 예제 실습시에는 보통 그렇습니다)
1. @ModelAttribute를 사용하는 경우(주로 SSR[Server-Side Rendering])
@RequiredArgsConstructor
@Controller
Public class PostsController {
private final PostsService postsService
@GetMapping("/")
public String PostsList(Model model) {
model.addAttribute("post", new Post());
return "posts";
}
@PostMapping("/new")
public String postSave(@Valid @ModelAttribute post, BindingResult bindingResult, RedirectAttributes redirectAttributes){
if (bindingResult.hasErrors()) {
return posts;
}
Post SavedPost = new Post();
savedPost.setTitle(post.getTitle());
...
postsService.save(post)
redirectAttributes.addAttribute("postId", savedPost.getId());
return "posts";
}
//GetMapping 생략
@PostMapping("/{postId}/edit")
public String edit(@PathVariable Long postId, @Valid @ModelAttribute post) {
itemRepository.update(postId, post);
return "/{postId}/edit";
}
예제를 위해 매우 간단한 컨트롤러 코드를 짜봤습니다. 해당 코드는 매우 안좋은 코드지만 에제로 쓸거니까 이해바랍니다 ㅎㅎ. 해당 코드는 큰 문제를 내포하고 있습니다.
@Getter
@NoArgsConstructor
@Entity
public class Posts extends BaseTimeEntity {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long Id;
@NotEmpty
@Column(length = 500, nullable = false)
private String title;
@NotEmpty
@Column(columnDefinition = "TEXT", nullable = false)
private String content;
...
일단 model로 전달해주는 post와 Service에서 받는 post가 중복됩니다. 이는 post endtity의 프로퍼티인 title 필드 등에 @NotEmpty를 붙임으로 mvc에서 매우 중요한 class인 entity의 코드가 복잡해지게하며 post가 entity가 아니더라도 이런 중요한 도메인에 코드가 길어진다면 작동이야 할 수 있지만 매우 좋지 않은 코드임이 분명합니다. 하지만 더 큰 문제는 따로 있습니다.
@PostMapping("/new")
public String postSave(@Valid @ModelAttribute post, BindingResult bindingResult, RedirectAttributes redirectAttributes){
...
@PostMapping("/{postId}/edit")
public String edit(@PathVariable Long postId, @Valid @ModelAttribute post) {
...
postSave와 edit 메소드가 받는 post가 동일합니다. 보통 글을 등록할때와 수정할때를 떠올려보면 글을 등록할때는 작성자를 써야하지만 수정할때는 필요없습니다. 하지만 동일한 객체를 받는만큼 두 경우에 다른 검증 로직을 적용할 수 없습니다. 등록과 수정의 검증 로직을 다르게 적용할 수 있는 방법이 없을까요? 이 때 사용되는 것이 앞에 소개한 @Validated의 group 검증 기능입니다, 하지만 앞서 언급했듯 최근에는 이 코드가 복잡해서 잘 사용하지 않습니다.
- 최근에는 그룹 검증을 사용하는 것보다는 다음과 같이 객체를 나누는 방법을 사용합니다. 사실 예제니까 앞서 말한 이런 오류들을 방치하는거지 실제론 이를 방지하기 위해 보통 설계시 클라이언트에게 입력받는 값을 글 등록시 postAddForm, postAddRequestDto 등, 글 수정시 postUpdateForm, postUpdateRequestDto 등으로 postList를 보여주기 위해 서비스에서 컨트롤러가 받아오는 값을 List<postResponseDto> 등으로 구성해주면됩니다.
Dto(Data Transfer Object)에 대해서 잘 모르시는 분들을 위해 설명하면 Dto란 계층 간에 데이터를 전달하기 위한 용도로만 사용하는 객체 덩어리입니다. 예를 들어 post란 중요 도메인이 제목, 내용, 작성자, 생성날짜 네 필드를 필요로 한다고 생각하면 사용자에게 글 생성시 받아야할 필드 값은 제목, 내용 둘 뿐입니다. 이 때 post를 직접 사용하지 않고 postRequestDto란 객체를 따로 만들어서 필드에 제목, 내용만 추가해 놓고 글 생성시 해당 객체로 입력 받으면 충분합니다. 글이 작성되는 과정에서 컨트롤러는 postRequestDto를 받아 서비스로 전달하지만 서비스가 리포지토리로 이를 전달하는 과정에서 나머지 필드를 서버에서 받아와 post로 만들어줍니다.
위 과정에서 postRequstDto는 단순히 컨트롤러에서 서비스로 전달되는 객체 덩어리일 뿐이며 해당 클래스는 특정 비즈니스 메소드가 필요하지 않고 post의 일부 필드만 가지면 충분합니다. 이처럼 mvc패턴에선 요청을 받을때 사용하는 RequestDto와 응답을 보낼때 사용하는 ResponseDto를 사용합니다.(이뿐만 아니라 Entity를 직접 이동시키면 안되는 이유 등 다른 사용 이유들이 많지만 생략하겠습니다.
* 잘 모르신다면 검색을 통해 DAO, VO 등 다른 object들과 비교해서 공부하시길 추천합니다.
2. @RequestBody 사용 (주로 CSR[Client-Side Rendering])
@ModelAttribute의 경우 1 View에서 RequestParam으로 객체를 받아옴 2 컨트롤러에서 받아온 객체를 가공 3 model에 해당 객체를 가공한 내용을 View로 전달 이라는 과정을 거치고 주로 JSP, thymleaf를 사용해서 서버에서 렌더링을 모두 처리하여 Client에게 html 결과물을 보여주는 방식을 사용하고 이를 SSR(Server-Side Rendering)이라 합니다. 따라서 @ModelAttribute는 url에 정보를 담는 쿼리 스트링이나 PostForm 등 HTTP 요청 파라미터를 다룰때 사용합니다. 하지만 최근 react.js나 vue.js를 사용하여 프론트엔드를 서버와 분리하여 서버는 단지 JSON API 요청을 받아 객체를 만들고 가공한 결과를 다시 API 응답을 보내주기만 하여 Client측에서 렌더링하도록 하는 방식인 CSR(Client-Side Rendering)을 많이 사용하며 이 경우 컨트롤러는 @ModelAttribute가 아닌 @RequestBody를 사용해 HTTP Body의 데이터를 객체로 변환합니다.
이 둘의 큰 차이는 검증 실패시 컨트롤러에서 객체가 생성되냐의 여부입니다. @ModelAttribute의 경우 쿼리 스트링에 적힌 파라미터들이 객체로 변환될때 필드가 여러 개라면 하나씩 처리하기 때문에 특정 필드에 오류가 발생해도 다른 필드는 정상적으로 처리되어 일부 검증에 실패하더라도 객체가 생성됩니다. 반면 HttpMessageConverter가 JSON을 객체로 만들어주는 @RequestBody의 경우 전달된 JSON의 필드 중 일부가 검증에 실패하면 객체는 생성되나 예외로 인해 컨트롤러를 호출하지 않습니다. 당연히 컨트롤러가 호출되지 않으니 valiator도 적용되지 않습니다.
위의 사진을 보면 제목과 내용 밑에 검증 실패가 되어 출력된 오류메세지를 볼 수 있는데 위에서 설명한 것처럼 필드별로 확인하여 이렇게 필드별로 BindingResult에 담긴 검증 실패 메세지를 직접 Model에 담아 View에 전달하여 View에서 직접 내가 원하는 메세지로 출력할 수 있는 @ModelAttribute와는 달리 @RequestBody는 API요청을 다루기에 조금 복잡한 과정을 거쳐야합니다. 원래라면 BasicErrorController 등 따로 에러 처리에 대한 코드를 정의해두어야 하는 과정 등이 있지만 결과적으로 실무에서 제일 많이 쓰이는 방법은 @ExceptionHandler와 @ControllerAdvice를 이용하는 것입니다.
public class ApiExceptionController {
@ResponseStatus(HttpStatus.BAD_REQUEST)
@ExceptionHandler(IllegalArgumentException.class)
public ErrorResult illegalExHandle(IllegalArgumentException e) {
return new ErrorResult("BAD", e.getMessage());
}
@RestControllerAdvice
public class PostsApiControllerAdvice {
@ExceptionHandler(MethodArgumentNotValidException.class)
public ResponseEntity<Map<String, String>> handleValidationExceptions(MethodArgumentNotValidException ex) {
Map<String, String> errors = new HashMap<>();
ex.getBindingResult().getAllErrors()
.forEach(c -> errors.put(((FieldError) c).getField(), c.getDefaultMessage()));
return ResponseEntity.badRequest().body(errors);
}
위 코드 중 첫번째는 @ExceptionHandler를 이용해 특정 Exception을 잡아내서 그 오류를 ErrorResult나 HttpEntity 등으로 client에게 보여주는 것이고 두번째는 @ExceptionHandler를 컨트롤러에서 사용하면 기존 컨트롤러 로직과 같이 코드에 기술되어 코드가 길어지기때문에 @ControllerAdvice를 생성하여 따로 @ExceptionHandler 로직을 작성해두는 코드입니다. 이 과정 역시 조금 자세하게 다뤄야하지만 ExceptionResolver 등 검증 기초편에서 다루기엔 먼저 알아야할 내용이 많은 것 같아 이 내용은 다음에 포스트하도록 하겠습니다.
이렇게 validation의 기초 부분을 정리해봤는데 아직 부족한 부분이 많은 것 같습니다. 특히 마지막을 @RequestBody를 사용할 때 어려운 점이 더 많은 뉘앙스로 끝냈고 실제로도 API를 통한 설계에 검증을 적용하는 것이 더 알아야할 것이 많습니다. 하지만 CSR 방식의 홈페이지가 많은 요즘 이 방법도 알아둬야겠죠. 앞서 언급했듯 다음 포스트에서 검증에 대해 더 자세히 다루도록 하겠습니다.
refer
https://mangkyu.tistory.com/174
https://www.inflearn.com/course/%EC%8A%A4%ED%94%84%EB%A7%81-mvc-2/dashboard
스프링 MVC 2편 - 백엔드 웹 개발 활용 기술 - 인프런 | 강의
웹 애플리케이션 개발에 필요한 모든 웹 기술을 기초부터 이해하고, 완성할 수 있습니다. MVC 2편에서는 MVC 1편의 핵심 원리와 구조 위에 실무 웹 개발에 필요한 모든 활용 기술들을 학습할 수 있
www.inflearn.com
'Spring' 카테고리의 다른 글
Spring_정리5_Spring의 구조 훑어보기_FrontController (0) | 2022.05.09 |
---|---|
Spring_정리4_Spring의 구조 훑어보기_MVC (0) | 2022.05.08 |
Spring_정리3_Spring의 구조 훑어보기_서블릿 (0) | 2022.05.05 |
Spring_정리2_Spring 이전 JAVA 웹 개발의 역사 훑어보기 (0) | 2022.04.29 |
Spring_1_게시판 페이징 (0) | 2022.04.22 |