[강의요약] 스프링부트 개념과 활용 - 스프링 웹 MVC (2/2)
개인적인 학습을 위한 Inflearn - 스프링부트 개념과 활용(백기선) 강의 요약입니다.
개념과 원리 위주로 요약합니다.
이전 글 에서 이어집니다.
4부. 스프링 부트 활용
13. Thymeleaf
새 프로젝트 생성(springmvcdemo)
13-1. 템플릿 엔진
- 주로 View를 만들 때 사용
- 코드 제너레이션, email 템플릿 등에 사용 가능
13-2. 스프링부트가 자동 설정을 지원하는 템플릿 엔진
- FreeMarker, Groovy, Thymeleaf, Mustache 등이 있음
-
Jsp는 지원하지 않고, 권장하지도 않음
내장 톰캣을 포함하는 jar를 쓰면 jsp 사용이 불가함.
톰캣이 떠있는 서버에서 java -jar 사용이 필요
undertow의 경우 jar를 호환하지도 않음
의존성 문제까지 발생할 수 있음
13-3. thymeleaf 사용하기
13-3-1. 의존성 추가
// pom.xml
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-thymeleaf</artifactId>
<version>2.2.0.RELEASE</version>
</dependency>
13-3-2. 자동 설정이 파일을 찾는 위치 : /src/main/resources/templates/
13-3-3. 테스트 추가
@RunWith(SpringRunner.class)
@WebMvcTest(SampleController.class)
public class SampleControllerTest {
@Autowired
MockMvc mockMvc;
@Test
public void hello() throws Exception {
// 요청 : "/hello"
// 응답
// - 모델 name : cherrue
// - 뷰 이름 : hello
mockMvc.perform(get("/hello"))
.andExpect(status().isOk())
.andDo(print())
.andExpect(view().name("hello"))
.andExpect(model().attribute("name", is("cherrue")));
}
}
13-3-3. 컨트롤러 추가 (@restcontroller X)
응답의 string 은 응답 본문이 아닌 응답 파일의 이름
model 은 Map 이라고 생각하면 쉽다.
@Controller
public class SampleController {
@GetMapping("/hello")
public String hello(Model model) {
model.addAttribute("name", "cherrue");
return "hello";
}
}
13-3-4. 템플릿 추가(main/resources/templates/hello.html)
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Title</title>
</head>
<body>
</body>
</html>
13-3-5. 결과
mockMvc는 서블릿 컨테이너를 띄우지 않기 때문에 JSP라면 출력이 불가능
타임리프는 서블릿 컨테이너에 독립적인 엔진이므로 응답을 출력할 수 있다
MockHttpServletResponse:
Status = 200
Error message = null
Headers = [Content-Language:"en", Content-Type:"text/html;charset=UTF-8"]
Content type = text/html;charset=UTF-8
Body = <!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Title</title>
</head>
<body>
</body>
</html>
Forwarded URL = null
Redirected URL = null
Cookies = []
13-4. Thymeleaf 변수 표현
docs : https://www.thymeleaf.org/doc/articles/standarddialect5minutes.html
13-4-1. 👀 (필수) Thymeleaf 네임스페이스 추가
<html lang="en" xmlns:th="http://www.thymeleaf.org">
13-4-2. variable expressions
<!DOCTYPE html>
<html lang="en" xmlns:th="http://www.thymeleaf.org">
<head>
<meta charset="UTF-8">
<title>Title</title>
</head>
<body>
<h1 th:text="${name}">Name</h1>
</body>
</html>
13-4-3. 결과 (localhost:8080/hello)
14. HtmlUnit
14-1. 의존성 추가
<dependency>
<groupId>org.seleniumhq.selenium</groupId>
<artifactId>htmlunit-driver</artifactId>
<version>3.58.0</version>
<scope>test</scope>
</dependency>
<!-- https://mvnrepository.com/artifact/net.sourceforge.htmlunit/htmlunit -->
<dependency>
<groupId>net.sourceforge.htmlunit</groupId>
<artifactId>htmlunit</artifactId>
<version>2.58.0</version>
<scope>test</scope>
</dependency>
14-2. 의존성 설명
14-2-1. htmlunit
htmlunit : html 단위 테스트를 위한 라이브러리
HtmlPage 인터페이스를 통해 각종 컨텐츠, 브라우저 타입 등을 확인할 수 있음
14-3. 테스트
HtmlUnit 이 MockMvc 보다 좋다 이런게 아니고, 취향의 문제!
WebClient 안에서 MockMvc를 이미 쓰고 있다
@RunWith(SpringRunner.class)
@WebMvcTest(SampleController.class)
public class SampleControllerTest {
@Autowired
MockMvc mockMvc;
@Autowired
WebClient webClient;
@Test
public void helloHtmlUnit() throws IOException {
HtmlPage page = webClient.getPage("/hello");
HtmlHeading1 h1 = page.getFirstByXPath("//h1");
assertThat(h1.getTextContent()).isEqualToIgnoringCase("cherrue");
}
15. ExceptionHandler
새 프로젝트(springbootexception)
15-1. 기본 에러핸들러
스프링 어플리케이션을 실행하면 기본적인 에러 핸들러가 등록이 되어있다
예를 들면 404 → Whitelabel Error Page(machine client) / json 404 (curl rest api)
BasicErrorController 소스 일부
// Whitelabel Error Page
@RequestMapping(
produces = {"text/html"}
)
public ModelAndView errorHtml(HttpServletRequest request, HttpServletResponse response) {
...
return modelAndView != null ? modelAndView : new ModelAndView("error", model);
}
// json 404 not found
@RequestMapping
public ResponseEntity<Map<String, Object>> error(HttpServletRequest request) {
HttpStatus status = this.getStatus(request);
if (status == HttpStatus.NO_CONTENT) {
return new ResponseEntity(status);
} else {
Map<String, Object> body = this.getErrorAttributes(request, this.isIncludeStackTrace(request, MediaType.ALL));
return new ResponseEntity(body, status);
}
}
15-2. 스프링 부트 에러 핸들링 방법
@ExceptionHandler 사용
컨트롤러 안에서 발생한 SampleException은 모두 @ExceptionHandler가 붙은 함수에서 모두 처리한다.
→ 전역적으로 설정하고 싶다면 @ControllerAdvice가 붙은 클래스를 생성하여 선언하면 된다.
→ 에러 처리 로직 자체를 고치고 싶다면 ErrorController를 구현(default BasicErrorController)
@Controller
public class SampleController {
@GetMapping("/hello")
public String hello() {
throw new SampleException();
}
@ExceptionHandler(SampleException.class)
public @ResponseBody AppError sampleError(SampleException e) {
AppError appError = new AppError();
appError.setMessage("error.app.key");
appError.setMessage("IDK IDK IDK");
return appError;
}
}
// SampleException.java
public class SampleException extends RuntimeException {
}
// AppError.java
public class AppError {
private String message;
private String reason;
public String getMessage() {
return message;
}
public void setMessage(String message) {
this.message = message;
}
public String getReason() {
return reason;
}
public void setReason(String reason) {
this.reason = reason;
}
}
결과
$ curl localhost:8080/hello
{"message":"IDK IDK IDK","reason":null}
15-3. 커스텀 에러 페이지
상태 코드에 따라 보일 에러페이지를 바꿔줄 수 있다
경로 : main/resources/static/ 또는 main/resources/template
파일명 : status code와 일치하거나 x로 와일드카드 사용 가능
동적인 페이지로 쓰고 싶다면 : ErrorViewResolver 구현하면 되지만 서버단에서 처리 파이프라인 짜는 게 낫다
예시 : 404.html, 5xx.html
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Title</title>
</head>
<body>
<h1>404</h1>
</body>
</html>
15. Spring HATEOAS
신규 프로젝트 생성 : demospringhateoas
spring HATEOAS : spring에서 HATEOAS를 사용하기 위한 툴
HATEOAS : Hypermedia As The Engine Of Application State
- REST api에서 리소스에 대한 정보를 제공할 때 연관된 링크 정보들 까지 같이 제공하고, 클라이언트는 제공받은 연관된 링크를 가지고 링크에 접근하는 것
- 서버에서 루트 페이지를 줄 때 relation이 있는 페이지 정보를 미리 클라이언트한테 제공하고, 클라이언트는 페이지를 이동하고 싶을 때 서버가 준 href를 가지고 이동
<!-- https://mvnrepository.com/artifact/org.springframework.boot/spring-boot-starter-hateoas -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-hateoas</artifactId>
<version>2.2.0.RELEASE</version>
</dependency>
15-1. 자동설정 되는 기능
ObjectMapper
응답을 Json으로 만들 때 사용하는 기능. (jackson)
Jackson2ObjectMapperBuilder가 주입 → ObjectMapper 생성할 필요 없음
- application.properties에서 objectMapper를 설정해줄 수 있음
LinkDiscovers
클라이언트에서 링크정보를 Rel 이름으로 찾을 수 있는 XPath 확장 클래스. 편의성이라고 보면 된다
15-2. 링크 달기 (deprecated)
2.2.0 RELEASE에는 Resource 객체가 없음..!
@RestController
public class SampleController {
@GetMapping("/hello")
public Resource<Hello> hello() {
Hello hello = new Hello();
hello.setPrefix("Hey,");
hello.setName("Cherrue");
Resource<Hello> helloResource = new Resource<>(hello);
helloResource.add(linkTo(methodOn(SampleController.class).hello()).withSelfRel())
return helloResource;
}
}
16. CORS
Cross Origin Resource Sharing : Single-Origin Policy를 우회하기 위한 웹 표준 기술
Single-Origin Policy 하나의 오리진에서만 요청이 가능한 제한 정책
Origin = URI 스키마(http, https) + hostname + port
→ 기본적으로 localhost:8080과 localhost:8081 응답을 합쳐서 반환하고 싶어도 못 함
Spring 에서는 CORS를 쓰려면 빈 설정을 이것저것 해주었어야 하는데, 스프링 부트는 자동설정 지원
→ @CrossOrigin
16-1. 프로젝트 두 개 생성 (springcorsserver(8080포트), springcorsclient(18080포트))
📍 port 설정 : application.properties > server.port=18080
@SpringBootApplication
@RestController
public class SpringcorsserverApplication {
@GetMapping("/hello")
public String hello () {
return "hello";
}
public static void main(String[] args) {
SpringApplication.run(SpringcorsserverApplication.class, args);
}
}
<!-- SpringcorsclientApplication/resources/static/index.html-->
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Title</title>
</head>
<body>
<h1>CORS client</h1>
<script src="/webjars/jquery/3.6.0/dist/jquery.min.js"></script>
<script>
$(function() {
$.ajax("http://localhost:8080/hello")
.done(function(msg) {
alert("msg");
})
.fail(function() {
alert("fail");
});
})
</script>
</body>
</html>
결과
Access-Control-Allow-Origin 헤더를 확인했는데 오리진이 아니라는 오류 발생
16-2. 서버측 api 하나에 CrossOrigin 설정
@SpringBootApplication
@RestController
public class SpringcorsserverApplication {
@CrossOrigin(origins = "http://localhost:18080")
@GetMapping("/hello")
public String hello () {
return "hello";
}
public static void main(String[] args) {
SpringApplication.run(SpringcorsserverApplication.class, args);
}
}
16-3. 서버에 전역 CORS 설정
WebMvcConfiguration 구현 : 기본 설정은 그대로 두고, 내가 여기서 구현한 애들만 오버라이드 됨
@Configuration
public class WebConfig implements WebMvcConfigurer {
@Override
public void addCorsMappings(CorsRegistry registry) {
registry.addMapping("/**")
.allowedOrigins("http://localhost:18080");
}
}
댓글남기기