[강의요약] 스프링부트 개념과 활용 - 스프링 웹 MVC (1/2)
개인적인 학습을 위한 Inflearn - 스프링부트 개념과 활용(백기선) 강의 요약입니다.
개념과 원리 위주로 요약합니다.
이전 글 에서 이어집니다.
4부. 스프링 부트 활용
7. 스프링 웹 MVC 1부 : 소개
7-1. 스프링 부트 MVC 를 이용해 간단한 테스트 만들기
프로젝트 만들기 : spring-boot-starter-web, test 주입
// UserControllerTest.java
@RunWith(SpringRunner.class)
@WebMvcTest(UserController.class)
public class UserControllerTest {
@Autowired
MockMvc mockMvc;
@Test
public void hello() throws Exception {
mockMvc.perform(get("/hello"))
.andExpect(status().isOk())
.andExpect(content().string("hello"));
}
}
// UserController
@RestController
public class UserController {
@GetMapping("/hello")
public String hello() {
return "hello";
}
}
MockMvc : WebMvcTest 어노테이션을 사용하면 주입받을 수 있는 객체
위와 같이 코딩하면 간단한 MVC를 바로 만들어서 쓴 것
가능한 이유 : spring-boot-autoconfigurer > spring.factories > WebMvcAutoConfiguration 이 자동 적용
7-2. 스프링 MVC 확장과 재정의
자동 설정의 기본 기능에서 추가로 설정하고 싶다면? 확장 = @Configuration + WebMvcConfigurer
자동 설정의 기본 기능들도 모두 내가 설정하고 싶다면? 재정의 = @Configuration + @EnableWebMvc
@Configuration
//@EnableWebMvc : 이걸 붙이면 기본 설정이 다 없어짐
public class WebConfig implements WebMvcConfigurer {
// void configureAsyncSupport(AsyncSupportConfigurer configurer) {}
// WebMvcConfigurer 에 선언된 함수를 구현하면 된다.
}
8. HttpMessageConverters
HttpMessageConverters : Http 요청 본문을 역직렬화하거나, 객체를 응답으로 직렬화할 때 사용
- @RequestBody, @ResponseBody 가 붙으면 알아서 사용됨
- @RestController인 경우에는 @ResponseBody 생략 가능
- @Controller를 사용하는 경우 View Name Resolver가 view 파일을 찾음
@RestController
public class UserController {
@GetMapping("/hello")
public String hello() {
return "hello";
}
@PostMapping("/user")
//public @ResponseBody User Create(@RequestBody User user) {
public /*@ResponseBody*/ User Create(@RequestBody User user) {
return null;
}
}
content-type 헤더에 따라 사용되는 컨버터가 바뀜
여러 종류의 값이 포함된 객체라면(컴포지션 하다면) Json이 기본적으로 사용됨
↔ 문자열 하나면 StringConverter가 사용됨
8-1. 유저 생성 테스트 코드 작성
🚧 User 객체는 자바빈 규약에 따라 getter/setter 를 가져야 한다.
// test.user.UserControllerTest.java
@Test
public void createUser_JSON() throws Exception {
String userJson = "{\n" +
" \"username\": \"cherrue\",\n" +
" \"password\": \"123\"\n" +
"}";
mockMvc.perform(post("/users/create")
.contentType(MediaType.APPLICATION_JSON_UTF8)
.accept(MediaType.APPLICATION_JSON_UTF8)
.content(userJson))
.andExpect(status().isOk())
.andExpect(jsonPath("$.username", is(equalTo("cherrue"))))
.andExpect(jsonPath("$.password", is(equalTo("123"))));
}
// UserController.Java
@RestController
public class UserController {
@GetMapping("/hello")
public String hello() {
return "hello";
}
//public @ResponseBody User Create(@RequestBody User user) {
@PostMapping("/users/create")
public /*@ResponseBody*/ User Create(@RequestBody User user) {
return user;
}
}
// User.java
public class User {
private Long id;
private String username;
private String password;
public Long getId() {
return id;
}
public void setId(Long id) {
this.id = id;
}
public String getUsername() {
return username;
}
public void setUsername(String username) {
this.username = username;
}
public String getPassword() {
return password;
}
public void setPassword(String password) {
this.password = password;
}
}
9. ViewResolve
요청의 포맷 : header > contentType
응답의 포맷 : accept header
accept header 가 없다면?
어떤 요청이 들어오면 그 요청으로 만들 수 있는 모든 답을 찾아낸다
최종적으로 accept header를 보고 제일 적절한 걸 반환
accept header가 안 들어오는 요청은 format(/path?format=pdf) 이라는 파라미터를 받을 수 있다.
// UserControllerTest.java
@Test
public void createUser_XML() throws Exception {
String userJson = "{\n" +
" \"username\": \"cherrue\",\n" +
" \"password\": \"123\"\n" +
"}";
mockMvc.perform(post("/users/create")
.contentType(MediaType.APPLICATION_JSON_UTF8)
.accept(MediaType.APPLICATION_XML)
.content(userJson))
.andExpect(status().isOk())
.andExpect(xpath("/User/username").string("cherrue"))
.andExpect(xpath("/User/password").string("123"));
}
위 테스트 소스를 작성하면 알아서 XML로 뱉어지는데, ContentNegotiatingViewResolver가 작업해 준 것
결과
Resolved Exception:
Type = org.springframework.web.HttpMediaTypeNotAcceptableException
java.lang.AssertionError: Status
Expected :200
Actual :406
📌 문제발생 XML 메시지 컨버터가 없어서 406 에러 발생
HttpMessageConvertersConfiguration > JacksonHttpMessageConvertersConfiguration
@ConditionalOnClass({XmlMapper.class}) // XmlMapper가 있을 때 등록하라는 설정
@ConditionalOnBean({Jackson2ObjectMapperBuilder.class})
protected static class MappingJackson2XmlHttpMessageConverterConfiguration {
protected MappingJackson2XmlHttpMessageConverterConfiguration() {
}
@Bean
@ConditionalOnMissingBean
public MappingJackson2XmlHttpMessageConverter mappingJackson2XmlHttpMessageConverter(Jackson2ObjectMapperBuilder builder) {
return new MappingJackson2XmlHttpMessageConverter(builder.createXmlMapper(true).build());
}
}
🎀 해결 : XmlMapper을 클래스 패스에 추가하기 = 의존성 추가하기
// Pom.xml
<dependency>
<groupId>com.fasterxml.jackson.dataformat</groupId>
<artifactId>jackson-dataformat-xml</artifactId>
<version>2.9.6</version>
</dependency>
결과 : 성공하면 아무것도 출력하지 않는다.
10. 정적 리소스 지원
동적으로 만들어야 하는 페이지가 아닌, 바로 서비스하면 되는 파일을 특정 경로에 넣으면 스프링이 서비스
10-1. 정적 리소스 매핑 “/”**
예시) /hello.html 요청 → /static/hello.html 파일 서비스
기본값
- classpath:/static
- classpath:/public
- classpath:/resources
- classpath:/META-INF/resources
<!-- src.main.resources.static / hello.html-->
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Title</title>
</head>
<body>
Hello static method!
</body>
</html>
결과
10-1-1. 정적 리소스 캐싱
If-Modified-Since 에 최근 변경 시간을 기록하여 304 응답만 보내 속도를 높인다.
- Http status 304 : Not Modified. 변경 사항이 없어 다시 보내지 않겠다는 응답 코드
10-1-2. 정적 리소스 기본값 변경
기본 url 매핑 경로 변경 : spring.mvc.static-path-pattern
정적 리소스 탐색 위치 변경
- spring.mvc.static-path-locations (모든 기본값이 날아가서 비추천)
- WebMvcConfigurer.addResourceHandlers 구현 (설정한 값만 추가 됨)
// {APP_HOME}/config/WebConfig.java
@Configuration
//@EnableWebMvc : 이걸 붙이면 기본 설정이 다 없어짐
public class WebConfig implements WebMvcConfigurer {
// void configureAsyncSupport(AsyncSupportConfigurer configurer) {}
@Override
public void addResourceHandlers(ResourceHandlerRegistry registry) {
WebMvcConfigurer.super.addResourceHandlers(registry);
registry.addResourceHandler("m/**")
.addResourceLocations("classpath:/m/") // 반드시 /로 끝내야 매핑이 잘 된다.
.setCachePeriod(20);
}
}
결과
11. 웹 JAR
웹 JAR : 동적으로 컨텐츠를 생산할 때 사용할 Front 라이브러리를 jar로 묶은 것
대부분 maven 중앙 저장소에도 올라와있어서 pom.xml에 추가하여 사용 가능
// pom.xml
<!-- https://mvnrepository.com/artifact/org.webjars.bower/jquery -->
<dependency>
<groupId>org.webjars.bower</groupId>
<artifactId>jquery</artifactId>
<version>3.3.1</version>
</dependency>
<!-- hello.html -->
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Title</title>
</head>
<body>
Hello static method!
<script src="/webjars/jquery/3.3.1/dist/jquery.min.js"></script>
<script>
$(function() {
alert("ready"); // 화면이 로딩 되면 alert 발생
});
</script>
</body>
</html>
11-1. webjars-locator-core
webjars의 버전이 바뀌면 여기저기 하드 코딩된 버전을 바꾸는 것이 어려움
webjars-locator-core는 이걸 알아서 해준다
스프링의 resource chaining(resource handler + resource transformer)의 기능을 활용한 것
// pom.xml
<!-- https://mvnrepository.com/artifact/org.webjars/webjars-locator-core -->
<dependency>
<groupId>org.webjars</groupId>
<artifactId>webjars-locator-core</artifactId>
<version>0.35</version>
</dependency>
<!-- hello.html -->
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Title</title>
</head>
<body>
Hello static method!
<!-- 이제 버전이 없어도 된다. -->
<!-- <script src="/webjars/jquery/3.3.1/dist/jquery.min.js"></script> -->
<script src="/webjars/jquery/dist/jquery.min.js"></script>
<script>
$(function() {
alert("ready");
});
</script>
</body>
</html>
결과
12. Index 페이지와 파비콘
12-1. welcome 페이지
root(/) 로 요청하면 나오는 페이지
제공 : index.html > index.템플릿 > 둘 다 없으면 에러 페이지
- index.html : 정적 페이지
- index.템플릿 : 동적 페이지
spring.mvc.static-path-locations 설정되어있으면 static이 root
12-2. 파비콘
파비콘 생성 사이트 : https://favicon.io/favicon-generator/
만들거나 내려받은 favicon.ico 파일을 resources 하위에 넣고 재기동하면 파비콘이 적용 된다.
파비콘이 한 번 캐싱되면 브라우저 닫을 때 까지 유지되어서, 다른 브라우저로(safari) 열었다
댓글남기기