4. ์คํ๋ง ์น MVC
4. ์คํ๋ง ์น MVC ๊ด๋ จ
1. ์๊ฐ
๊ฐ๋จํ ์ปจํธ๋กค๋ฌ์ ํ
์คํธ ์ฝ๋๋ฅผ ์์ฑํ๋ค. @WebMvcTest
์ ๋
ธํ
์ด์
์ ์ฌ์ฉํ๋ฉด MockMvc
๋ฅผ ์ฃผ์
๋ฐ์์ ์ฌ์ฉํ ์ ์๋ค.
์๋์ ํ ์คํธ์์ ์ฐ๋ฆฌ๋ ์๋ฌด๋ฐ ์ค์ ํ์ผ์ ์์ฑํ์ง ์์์ง๋ง ์คํ๋ง MVC์ ๊ธฐ๋ฅ์ ์ฌ์ฉํ ์ ์์๋ค. ์ด๊ฒ์ด ๊ฐ๋ฅํ ๊ฒ์ ์คํ๋ง ๋ถํธ๊ฐ ์ ๊ณตํด์ฃผ๋ ๊ธฐ๋ณธ์ค์ ๋๋ฌธ์ด๋ค.
์์ธํ ๋งํด์ spring-boot-starter
์์กด์ฑ์ ์ถ๊ฐํ๋ฉด์ ๊ฐ์ด ๋ฐ๋ผ์จ spring-boot-autoconfigure
์์กด์ฑ์ ์์ ๊น๋ณด๋ฉด spring.factories
ํ์ผ ์์ WebMvcAutoConfiguration
์ด๋ผ๋ ํด๋์ค๊ฐ ์กด์ฌํ๊ณ , ์ด ํด๋์ค์ ์ ์๋ ์ค์ ๋ค ๋๋ฌธ์ ์ฐ๋ฆฌ๋ ์คํ๋ง MVC์ ๊ธฐ๋ฅ์ ๋ฐ๋ก ์ธ ์ ์๋ค.
@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"));
}
}
@RestController
public class UserController {
@GetMapping("/hello")
public String hello() {
return "hello";
}
}
- ์คํ๋ง ๋ถํธ๊ฐ ์ ๊ณตํด์ฃผ๋ ๊ธฐ๋ณธ์ค์ ์ ์ฌ์ฉํ๋ฉด์, ์ถ๊ฐ์ ์ผ๋ก ํ์ฅํด์ ์ฐ๊ณ ์ถ์๊ฒฝ์ฐ, @Configuration + WebMvcConfigurer ๋ก ์ถ๊ฐ ์ค์ ํ์ผ์ ๋ง๋ค ์ ์๋ค.
- ์ฃผ์ํ ์ ์ @Configuration + @EnableWebMvc ์ ๋ ธํ ์ด์ ์ ํด๋์ค์ ๋ถ์ด๊ฒ ๋๋ฉด, ๊ธฐ๋ณธ์ค์ ์ ์ฌ์ฉํ ์ ์๊ณ Web MVC์ ๋ํ ๋ชจ๋ ์ค์ ์ ํด๋น ํด๋์ค์์ ์ ์ํด์ผ ํ๋ค.
2. HttpMessageConverters
HttpMessageConverters๋ ์คํ๋ง ํ๋ ์์ํฌ์์ ์ ๊ณตํ๋ ์ธํฐํ์ด์ค์ด๋ค.
HTTP ์์ฒญ ๋ณธ๋ฌธ์ ๊ฐ์ฒด๋ก ๋ณ๊ฒฝํ๊ฑฐ๋, ๊ฐ์ฒด๋ฅผ HTTP ์๋ต ๋ณธ๋ฌธ์ผ๋ก ๋ณ๊ฒฝํ ๋ ์ฌ์ฉํ๋ค. ์ฌ์ฉํ๋ HttpmessageConverter๋ ์ฌ๋ฌ๊ฐ์ง๊ฐ ์๊ณ , ์ฐ๋ฆฌ๊ฐ ์ด๋ค ์์ฒญ์ ๋ฐ์๋์ง, ์๋ต์ ๋ณด๋ด๋์ง์ ๋ฐ๋ผ์ ๋ฉ์ธ์ง์ปจ๋ฒํฐ๊ฐ ๋ฌ๋ผ์ง๋ค.
{"username":"keesun", "password":"123"} <-> User
@RequestBody
@ResponseBody
- ์๋์์ User(๊ฐ์ฒด)๋ฅผ ๋ฆฌํดํ ๋๋ ๊ธฐ๋ณธ์ ์ผ๋ก JsonMessageConverter๊ฐ ์ฌ์ฉ์ด๋๊ณ , Stringํ์ ์ ์ดํดํ ๋๋ StringMessageConverter๊ฐ ์ฌ์ฉ์ด ๋๋ค. int๋ ๋ง์ฐฌ๊ฐ์ง๋ก StringMessageConverter์ด๋ค.
- @RestController๋ฉด @ResponseBody๋ ์๋ตํด๋ ๋๋ค.
- MessageConverter๋ฅผ ํ๊ณ ๊ฐ์ฒด๋ฅผ ์๋ต ๋ณธ๋ฌธ์ผ๋ก ๋ฐ๊พผ๋ค.
- ๊ทธ๋ฅ @Controller๋ฅผ ์ฌ์ฉํ ๊ฒฝ์ฐ์๋ @ResponseBody๋ฅผ ๋ฃ์ด์ผ MessageConverter๊ฐ ์ ์ฉ์ด๋๋ค.
- @Controller์์ @ResponseBody๋ฅผ ์ ์ธํ์ง ์์ผ๋ฉด BeanNameViewResolver์ ์ํด์ ViewName์ ํด๋นํ๋ ๋ทฐ๋ฅผ ์ฐพ์ผ๋ ค๊ณ ์๋ํ๋ค.
@PostMapping("/user") public @ResponseBody User create(@RequestBody User user) { ... return new User( ... ); }
ํ ์คํธ
- MockMvc๋ฅผ ์ฌ์ฉํด์ post์์ฒญ์ ๋ํ ์๋ต์ ํ์ธํ๊ธฐ ์ํ ํ ์คํธ์ฝ๋์ด๋ค.
- ์์ฒญ์ @RestController์์ @RequestBody๊ฐ json์ ํ์ฑํด์ User ๊ฐ์ฒด๋ก ๋ง๋ค๊ณ , JsonMessageConverter์ ์ํด User๊ฐ JSON ํํ๋ก ๋ง๋ค์ด์ ธ์ return ๋๋ค.
- ํ ์คํธ์ฝ๋์์๋ jsonPath๋ฅผ ์ฌ์ฉํด์ ๊ฒฐ๊ณผ๋ก ๋ฐ์ status์ JSON์ ํ๋กํผํฐ๋ฅผ ๊ฒ์ฆํ๋ค.
- POST์์ฒญ์ ์์ฒญ ํ์ ์ JSON์ด๊ณ , ์๋ต์ ์ํ๋ ํ์ ์ ์๋ฏธํ๋ accept ํค๋๋ JSON์ผ๋ก ์ธํ ํ๋ค.
package io.namjune.springbootconceptandutilization.user;
import static org.hamcrest.Matchers.equalTo;
import static org.hamcrest.Matchers.is;
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get;
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.post;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.content;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.jsonPath;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.autoconfigure.web.servlet.WebMvcTest;
import org.springframework.http.MediaType;
import org.springframework.test.context.junit4.SpringRunner;
import org.springframework.test.web.servlet.MockMvc;
@RunWith(SpringRunner.class)
@WebMvcTest(UserController.class)
public class UserControllerTest {
@Autowired
MockMvc mockMvc;
@Test
public void createUser_JSON() throws Exception {
String userJson = "{\"username\":\"namjune\", \"password\":\"123\"}";
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("namjune"))))
.andExpect(jsonPath("$.password", is(equalTo("123"))));
}
}
@RestController
public class UserController {
@GetMapping("/hello")
public String hello() {
return "hello";
}
@PostMapping("/users/create")
public User create(@RequestBody User user) {
return user;
}
}
@Getter
@Setter
public class User {
private Long id;
private String username;
private String password;
}
3. ViewResolve
์คํ๋ง๋ถํธ์ ๋ฑ๋ก ๋์ด์๋ ์คํ๋ง ์น MVC์ ContentNegotiatingViewResolver ๊ฐ ์ด๋ค contentType์ผ ๋ ์ด๋ค ์๋ต์ ๋ณด๋ด๊ณ , accept header ์์ฒญ์ ์ํด์ ํด๋น ์์ฒญ์ ๋ง๋ ์๋ต์ ๋ณด๋ด๋ ์์ ์ ์์์ ํด์ค๋ค.
- https://docs.spring.io/spring/docs/5.0.7.RELEASE/spring-framework-reference/web.html#mvc-multiple-representations
๊ทธ๋์ Accept header๋ฅผ XML ํ์ ์ผ๋ก ์ค์ ํ๊ณ xpath๋ฅผ ์ด์ฉํด์ XML๋ก ๋ฐ๋ ์๋ต์ ๊ฒ์ฆํ๋ ํ ์คํธ์ฝ๋๋ฅผ ์์ฑํ๊ณ ์คํ์์ผ๋ณด๋ฉด 406 HttpMediaTypeNotAcceptableException์ด ๋จ์ด์ง๋ค.
package io.namjune.springbootconceptandutilization.user;
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get;
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.post;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.content;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.xpath;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.autoconfigure.web.servlet.WebMvcTest;
import org.springframework.http.MediaType;
import org.springframework.test.context.junit4.SpringRunner;
import org.springframework.test.web.servlet.MockMvc;
@RunWith(SpringRunner.class)
@WebMvcTest(UserController.class)
public class UserControllerTest {
@Autowired
MockMvc mockMvc;
@Test
public void createUser_JSON() throws Exception {
String userJson = "{\"username\":\"namjune\", \"password\":\"123\"}";
mockMvc.perform(post("/users/create")
.contentType(MediaType.APPLICATION_JSON_UTF8)
.accept(MediaType.APPLICATION_XML)
.content(userJson)
)
.andExpect(status().isOk())
.andExpect(xpath("/User/username").string("namjune"))
.andExpect(xpath("/User/password").string("123"));
}
}
์คํ๋ง ๋ถํธ์ HttpMessageConverters๋ HttpMessageConvertersAutoConfiguration ํด๋์ค๋ก ์ธํด์ ์ ์ฉ์ด ๋๋ค. ํด๋น ํด๋์ค๋ฅผ ์ฐพ์๋ณด๋ฉด spring-boot-autoconfigure์์กด์ฑ ์๋์ http/ ์์ ์กด์ฌํ๋ค. ์ด ํด๋์ค์ @Import ๋ก ์ ์ธ๋์ด์๋ JacksonHttpMessageConvertersConfiguration ํด๋์ค๋ฅผ ๋ณด๋ฉด MappingJackson2XmlHttpMessageConverterConfiguration ํด๋์ค๊ฐ ์ ์ ๋์ด์๊ณ , MappingJackson2XmlHttpMessageConverterConfiguration ํด๋์ค์๋ @ConditionalOnClass(XmlMapper.class)์ผ๋ก XmlMapperํด๋์ค๊ฐ classpath์ ์กด์ฌํด์ผ๋ง ๋ฑ๋ก๋๋๋ก ์ ์๋์ด ์๋ค. 406 ์๋ฌ๊ฐ ๋จ์ด์ง ์ด์ ๋ xml๋ก ๋ณํํ ์ ์๋ ์ปจ๋ฒํฐ๊ฐ ๋ฑ๋ก๋์ด์์ง ์๊ธฐ ๋๋ฌธ์ด๋ค.
@Configuration
class JacksonHttpMessageConvertersConfiguration {
...
@Configuration
@ConditionalOnClass(XmlMapper.class)
@ConditionalOnBean(Jackson2ObjectMapperBuilder.class)
protected static class MappingJackson2XmlHttpMessageConverterConfiguration {
@Bean
@ConditionalOnMissingBean
public MappingJackson2XmlHttpMessageConverter mappingJackson2XmlHttpMessageConverter(
Jackson2ObjectMapperBuilder builder) {
return new MappingJackson2XmlHttpMessageConverter(
builder.createXmlMapper(true).build());
}
}
...
}
XML ๋ฉ์์ง ์ปจ๋ฒํฐ๋ฅผ classpath์ ์ถ๊ฐํด์ฃผ๋ฉด ๋ฌธ์ ๋ฅผ ํด๊ฒฐํ ์ ์๋ค. ์์กด์ฑ์ ์ถ๊ฐํด์ฃผ๋ฉด ๋๋ค. ์ด์ ์์์ ์์ฑํ XML ๊ฒ์ฆ ํ ์คํธ๋ฅผ ํต๊ณผ์ํฌ ์ ์๋ค.
๋ณดํต JSON์ ๋ง์ด ์ฌ์ฉํ๊ธฐ ๋๋ฌธ์ ์ถ๊ฐ์ค์ ์ ์๋ฌด๊ฒ๋ ํ์ง ์์๋ ๋์ง๋ง, XML๋ก ๋ด๋ณด๋ด๊ณ ์ถ์ ๊ฒฝ์ฐ ์์กด์ฑ์ ์ถ๊ฐํด์ ํ์ํ ๋ฉ์ธ์ง ์ปจ๋ฒํฐ๋ฅผ ํ ์คํธ ํ ์ ์๋ค.
dependencies {
...
implementation('com.fasterxml.jackson.dataformat:jackson-dataformat-xml:2.9.6')
...
}
4. ์ ์ ๋ฆฌ์์ค ์ง์
์ ์ ๋ฆฌ์์ค ๋งตํ "/**". ๋ฃจํธ๋ก ๋งตํ๋๋ค.
๊ธฐ๋ณธ ๋ฆฌ์์ค ์์น
classpath:/static
classpath:/public
classpath:/resources/
classpath:/META-INF/resources
์) "/hello.html" ์ ๊ทผ์ /static/hello.html ์๋ต
spring.mvc.static-path-pattern: ๋งตํ ์ค์ ๋ณ๊ฒฝ ๊ฐ๋ฅ
- application.yml์์
spring.mvc.static-path-pattern: /static/**
์ผ๋ก ์ค์ ๋ณ๊ฒฝ์ - localhost:8080/hello.html => localhost:8080/static/hello.html๋ก ์ ๊ทผ
- application.yml์์
spring.mvc.static-locations: ๋ฆฌ์์ค ์ฐพ์ ์์น ๋ณ๊ฒฝ ๊ฐ๋ฅ
- ๊ธฐ์กด์ ๊ธฐ๋ณธ ๋ฆฌ์์ค ์์น๋ฅผ ์ฌ์ฉํ์ง ์๊ณ ๋ณ๊ฒฝํ ์์น๋ง ์ฌ์ฉํ๋ฏ๋ก ๊ถ์ฅํ์ง ์๋ ๋ฐฉ๋ฒ์ด๋ค.
- ์ด๋ฐฉ๋ฒ ๋ณด๋ค๋ WebMvcConfigurer๋ฅผ ๊ตฌํ์์๋ฐ์์ addResourceHandlers๋ก ์ปค์คํฐ๋ง์ด์ง ํ๋ ๋ฐฉ๋ฒ์ด ๋ ์ข๋ค. ๊ธฐ๋ณธ ๋ฆฌ์์ค ์์น๋ฅผ ์ฌ์ฉํ๋ฉด์ ์ถ๊ฐ๋ก ํ์ํ ๋ฆฌ์์ค์์น๋ง ์ ์ํด์ ์ฌ์ฉํ ์ ์๋ค.
- localhost:8080/m/hello.html ์ ๊ทผ์ resources/m/hello.html ๋ฆฌํด
- ์ฌ๊ธฐ์ ์ฃผ์ํ ์ ์ ์บ์ ์ค์ ์ ๋ฐ๋ก ํด์ผ ํ๋ค๋ ๊ฒ์ด๋ค. ๊ธฐ๋ณธ ๋ฆฌ์์ค๋ค์ ๊ธฐ์กด์ ์บ์ฑ ์ ๋ต์ด ์ ์ฉ๋์ด ์๋ค.
- ์ปค๋ฐ๋ก๊ทธ
- https://github.com/namjunemy/spring-boot-concept-and-utilization/commit/f7b9d05f0ae1b9cc61c1e21c6b1c6b3e2473a9f1
@Configuration public class WebConfig implements WebMvcConfigurer { @Override public void addResourceHandlers(ResourceHandlerRegistry registry) { registry.addResourceHandler("/m/**") .addResourceLocations("classpath:/m/") .setCachePeriod(20); } }
๊ธฐ๋ณธ ๋ฆฌ์์ค ์์น์ ์๋ ๋ฆฌ์์ค๋ค์ ResourceHttpRequestHandler๊ฐ ์ฒ๋ฆฌํ๋๋ฐ, ๋ธ๋ผ์ฐ์ ์์ 304 status์ ๋ด๋ ค์ฃผ๋ ๊ฒฝ์ฐ๊ฐ ์๋ค.
- html ํ์ผ์ด ๋ณ๊ฒฝ๋๋ ์๊ฐ ํ์ผ์ Last-Modified ๋ผ๋ ์ต์ข ๋ณ๊ฒฝ์๊ฐ์ด ๊ธฐ๋ก๋๋๋ฐ,
- ๋ธ๋ผ์ฐ์ ์์๋ ์ฒ์์ htmlํ์ผ์ ์์ฒญํ๊ณ 200 status๋ฅผ ๋ฐ์ผ๋ฉด ํด๋น ์๊ฐ์ If-Modified-Since์ ๊ธฐ๋กํด ๋๋๋ค.
- ๊ทธ๋ฆฌ๊ณ ๋ธ๋ผ์ฐ์ ์์ ๋ค์ html์์ฒญ์ ํ๊ณ ์๋ต์ ๋ฐ์๋, resopnse ํค๋์ ๋์ด์ค๋ Last-Modified ์๊ฐ์ด request ํค๋์ ๋ณด๋ธ If-Modified-Since์ ๊ฐ๋ค๋ฉด, html ํ์ผ์ ๋ณ๊ฒฝ์ด ์์๋ค๋ ์๋ฏธ์ด๋ฏ๋ก ๋ค์ ๋ฆฌ์์ค๋ฅผ ๋ฐ์์ค์ง ์๊ณ 304 status์ ์บ์๋ ํ์ผ์ ๋ด๋ ค์ค๋ค.
- ํ์ง๋ง html์ด ๋ณ๊ฒฝ๋์ ๊ฒฝ์ฐ response ํค๋์ ๋์ด์ค๋ Last-Modified ์๊ฐ์ด request ํค๋์ ๋๊ธด If-Modified-Since ์๊ฐ ์ดํ์ด๋ฏ๋ก ์๋ก ๋ฆฌ์์ค๋ฅผ ๋ฐ์์ 200 status๋ฅผ ๋ฐํํ๋ค.
5. ์น JAR
์๋ฐ์คํฌ๋ฆฝํธ ๋ผ์ด๋ธ๋ฌ๋ฆฌ๋ฅผ webjarํํ๋ก dependency๋ฅผ ์ถ๊ฐํด์ ์ฌ์ฉํ ์ ์๋ค.
์คํ๋ง ๋ถํธ์์ ์ถ๊ฐ๋ก ์ ๊ณตํ๋ ๊ธฐ๋ฅ์ด์๋๋ฐ, jquery์ ๋ฒ์ ์ด ์ฌ๋ผ๊ฐ ๋๋ง๋ค ๋ฒ์ ์ ์ผ์ผํ ๋ฐ๊ฟ์ฃผ์ง ์์๋ ๋๋ค. ์ด ๊ธฐ๋ฅ์ ์ฌ์ฉํ๋ ค๋ฉด webjars-locator-core ์์กด์ฑ์ ์ถ๊ฐํด์ผ ํ๋ค.
์ด๊ฒ์ ๋ด๋ถ์ ์ธ ๋์์ springframework์ resource chaining์ ์ํด์ ์ด๋ฃจ์ด์ง๋ค. ํ์ํ๋ค๋ฉด ๋ ์์ธํ ๊ณต๋ถํ์.
dependencies {
...
compile group: 'org.webjars.bower', name: 'jquery', version: '3.3.1'
compile group: 'org.webjars', name: 'webjars-locator-core', version: '0.36'
...
}
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Title</title>
</head>
<body>
Hello static resource!
<script src="/webjars/jquery/dist/jquery.min.js"></script>
<script>
$(function() {
alert('ready!');
});
</script>
</body>
</html>
6. Index ํ์ด์ง์ ํ๋น์ฝ
์คํ๋ง ๋ถํธ์ ์ ์ ๋ฆฌ์์ค 4๊ฐ์ง ๊ธฐ๋ณธ ์์น์ค ์๋ฌด ๊ณณ์ด๋ index.html์ ๋๋ฉด๋๋ค.
- classpath:/static
- classpath:/public
- classpath:/resources/
- classpath:/META-INF/resources
๊ทธ๋ฌ๋ฉด ์คํ๋ง๋ถํธ๋
- index.html์ ์ฐพ์๋ณด๊ณ ์์ผ๋ฉด ์ ๊ณต
- index.ํ ํ๋ฆฟ ์ฐพ์๋ณด๊ณ ์์ผ๋ฉด ์ ๊ณต
- ๋ ๋ค ์์ผ๋ฉด ์๋ฌํ์ด์ง๋ฅผ ๋ด๋ณด๋ธ๋ค.
favicon.io
- ๊ธฐ๋ณธ๋ฆฌ์์ค ์์น์ ์์ ํ์ผ๋ช ์ผ๋ก ์์น์ํจ๋ค.
- ํ์ด์ฝ ๋ง๋ค๊ธฐ: https://favicon.io/
- ํ๋น์ฝ์ ์บ์๊ฐ ๋์ด์์ผ๋ฏ๋ก, ํฌ๋กฌ์์ ์บ์๋น์ฐ๊ณ ์๋ก๊ณ ์นจ์ ํ๋ฉด ํ์ธํ ์ ์๋ค.
7. ํ ํ๋ฆฟ ์์ง
์คํ๋ง ๋ถํธ๊ฐ ์๋ ์ค์ ์ ์ง์ํ๋ ํ ํ๋ฆฟ ์์ง
- FreeMarker
- Groovy
- Thymeleaf
- Mustache
JSP๋ฅผ ๊ถ์ฅํ์ง ์๋ ์ด์
JAR ํจํค์ง ํ ๋๋ ๋์ํ์ง ์๊ณ , WAR ํจํค์ง ํด์ผํจ.
Undertow๋ JSP๋ฅผ ์ง์ํ์ง ์์
JSP Limitations
- https://docs.spring.io/spring-boot/docs/current/reference/htmlsingle/#boot-features-jsp-limitations
ํ ํ๋ฆฟ ์์ง ํ ์คํธ ์ฝ๋ ์์ฑ
- ์ปจํธ๋กค๋ฌ ๋์, ๋ทฐ ๋ค์, ๋ชจ๋ธ ๊ฐ ๊ฒ์ฆ ํ
์คํธ ์ฝ๋
- https://github.com/namjunemy/spring-boot-concept-and-utilization/commit/3c3084115acacec3908df83c96f6b7d7fbd15a62
- ๋ทฐ์ ํ
ํ๋ฆฟ ๋ด์ฉ ๊ฒ์ฆํ๋ ํ
์คํธ ์ฝ๋
- https://github.com/namjunemy/spring-boot-concept-and-utilization/commit/1b41c0b500c282b16ed272f51be5e769e42d5cd5
- ์ปจํธ๋กค๋ฌ ๋์, ๋ทฐ ๋ค์, ๋ชจ๋ธ ๊ฐ ๊ฒ์ฆ ํ
์คํธ ์ฝ๋
8. HtmlUnit
HTML ํ ํ๋ฆฟ ๋ทฐ ํ ์คํธ๋ฅผ ๋ณด๋ค ์ ๋ฌธ์ ์ผ๋ก ํ์
http://htmlunit.sourceforge.net/
http://htmlunit.sourceforge.net/gettingStarted.html
Html์ ๋จ์ ํ ์คํธ ํ๊ธฐ ์ํด
htmlunit ์์กด์ฑ์ ์ถ๊ฐํ๋ค
webClient๋ก ์์ฒญ ํ์ด์ง, ํ๊ทธ, ์๋ฆฌ๋จผํธ ๋ฑ์ ๊ฐ์ ธ์์ ๋จ์ ํ ์คํธ ํ๋ค.
์ปค๋ฐ๋ก๊ทธ
- https://github.com/namjunemy/spring-boot-concept-and-utilization/commit/246c37868c5301b5c8d48f4c3d0ab09b5b54de3a
9. ExceptionHandler
- ์คํ๋ง @MVC ์์ธ ์ฒ๋ฆฌ ๋ฐฉ๋ฒ
- @ControllerAdvice
- @ExchangeHandler
- ์คํ๋ง ๋ถํธ๊ฐ ์ ๊ณตํ๋ ๊ธฐ๋ณธ ์์ธ ์ฒ๋ฆฌ๊ธฐ
- ์ปค๋ฐ๋ก๊ทธ
- https://github.com/namjunemy/spring-boot-concept-and-utilization/commit/1bb61ee2dabdca6b7b050e3948eba9ed5d6f5780
- BasicErrorController
- HTML๊ณผ JSON ์๋ต ์ง์
- ์ปค์คํฐ๋ง์ด์ง ๋ฐฉ๋ฒ
- ErrorController ๊ตฌํ
- ์ปค๋ฐ๋ก๊ทธ
- ์ปค์คํ
์๋ฌ ํ์ด์ง
- ์ํ ์ฝ๋ ๊ฐ์ ๋ฐ๋ผ ์๋ฌ ํ์ด์ง ๋ณด์ฌ์ฃผ๊ธฐ
- ์ปค๋ฐ๋ก๊ทธ
- https://github.com/namjunemy/spring-boot-concept-and-utilization/commit/d44eec590fb2b42e4c615ccb122182d7db893d15
- ์ปค๋ฐ๋ก๊ทธ
- src/main/resources/static|template/error/
- 404.html
- 5xx.html
- ErrorViewResolver ๊ตฌํ
- ์ํ ์ฝ๋ ๊ฐ์ ๋ฐ๋ผ ์๋ฌ ํ์ด์ง ๋ณด์ฌ์ฃผ๊ธฐ
10. Spring HATEOAS
Hypermedia As The Engine Of Application State
- ์๋ฒ
- ํ์ฌ ๋ฆฌ์์ค์ ์ฐ๊ด๋ ๋งํฌ ์ ๋ณด๋ฅผ ํด๋ผ์ด์ธํธ์๊ฒ ์ ๊ณตํ๋ค.
- ํด๋ผ์ด์ธํธ
- ์ฐ๊ด๋ ๋งํฌ ์ ๋ณด๋ฅผ ๋ฐํ์ผ๋ก ๋ฆฌ์์ค์ ์ ๊ทผํ๋ค.
- ์ฐ๊ด๋ ๋งํฌ ์ ๋ณด
- Relation
- Hypertext Reference
- spring-boot-stater-hateoas ์์กด์ฑ ์ถ๊ฐ
- https://spring.io/understanding/HATEOAS
- https://spring.io/guides/gs/rest-hateoas/
- https://docs.spring.io/spring-hateoas/docs/current/reference/html/
- ์๋ฒ
ObjectMapper ์ ๊ณต(stater-web์ด ์ ๊ณตํด์ ์ฐ๋ฆฌ๋ stater-hateoas๋ฅผ ์ถ๊ฐํ์ง ์์๋ ์ฌ์ฉ๊ฐ๋ฅํ๋ค.)
- spring.jackson.*
- Jackson2ObjectMapperBuilder
LinkDiscovers ์ ๊ณต
- ํด๋ผ์ด์ธํธ์์ ๋งํฌ ์ ๋ณด๋ฅผ Rel ์ด๋ฆ์ผ๋ก ์ฐพ์๋ ์ฌ์ฉํ ์ ์๋ XPath ํ์ฅ ํด๋์ค
์ปจํธ๋กค๋ฌ์์ Resource ์ถ๊ฐ๋ก Http Response Body์ ๋งํฌ ์ถ๊ฐํ๊ธฐ, ํ ์คํธ ์ฝ๋๋ก ์ถ๊ฐ๋ ๋งํฌ ๊ฒ์ฆ ์์
https://github.com/namjunemy/spring-boot-concept-and-utilization/commit/0593873da6833a9330f238107f685b7f41e6b52c
Controller
- org.springframework.hateoas.Resource ํด๋์ค๋ฅผ ์ด์ฉํด์ ์ปจํธ๋กค๋ฌ์ hello() ๋ฉ์๋์ link๋ฅผ selfRel๋ก ๋ฑ๋กํ๋ค.
package io.namjune.springbootconceptandutilization.controller; import static org.springframework.hateoas.mvc.ControllerLinkBuilder.linkTo; import static org.springframework.hateoas.mvc.ControllerLinkBuilder.methodOn; import io.namjune.springbootconceptandutilization.Hello; import org.springframework.hateoas.Resource; import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.RestController; @RestController public class SampleController { @GetMapping("/hello") public Resource<Hello> hello() { Hello hello = new Hello(); hello.setPrefix("Hey,"); hello.setName("NJ"); Resource<Hello> helloResource = new Resource<>(hello); helloResource.add(linkTo(methodOn(SampleController.class).hello()).withSelfRel()); return helloResource; } }
- ControllerTest
package io.namjune.springbootconceptandutilization.controller; import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get; import static org.springframework.test.web.servlet.result.MockMvcResultHandlers.print; import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.jsonPath; import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; import org.junit.Test; import org.junit.runner.RunWith; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.boot.test.autoconfigure.web.servlet.WebMvcTest; import org.springframework.test.context.junit4.SpringRunner; import org.springframework.test.web.servlet.MockMvc; @RunWith(SpringRunner.class) @WebMvcTest(SampleController.class) public class SampleControllerTest { @Autowired MockMvc mockMvc; @Test public void hello() throws Exception { mockMvc.perform(get("/hello")) .andDo(print()) .andExpect(status().isOk()) .andExpect(jsonPath("$._links.self").exists()); } }
- ๊ฒฐ๊ณผ
MockHttpServletResponse: Status = 200 Error message = null Headers = {Content-Type=[application/hal+json;charset=UTF-8]} Content type = application/hal+json;charset=UTF-8 Body = {"prefix":"Hey,","name":"NJ","_links":{"self":{"href":"http://localhost/hello"}}} Forwarded URL = null Redirected URL = null Cookies = []
11. CORS
SOP์ CORS
- Single-Origin Policy
- ๋จ์ผ Origin์๋ง ์์ฒญ์ ๋ณด๋ผ ์ ์๋ค๋ ๊ฒ์ ์๋ฏธํ๋ ์ ์ฑ
- ๊ธฐ๋ณธ์ ์ผ๋ก SOP๊ฐ ์ ์ฉ๋์ด ์์ด์, Origin์ด ๋ค๋ฅด๋ฉด ํธ์ถํ ์ ์๋ค.
- REST API๊ฐ http://localhost:8080 ์ ํตํด์ ์๋น์ค ๋๊ณ ์๊ณ , 18080 ํฌํธ๋ฅผ ์ฌ์ฉํ๋ ์ ํ๋ฆฌ์ผ์ด์ ์์ ๊ทธ REST API๋ฅผ ํธ์ถํ๋ ค๊ณ ํ๋ค. ๊ธฐ๋ณธ์ ์ผ๋ก SOP์ ์๋ฐ ๋๊ธฐ ๋๋ฌธ์ ํธ์ถํ์ง ๋ชปํ๋ค.
- Cross-Origin Resource Sharing
- SOP๋ฅผ ์ฐํํ๊ธฐ ์ํ ํ์ค
- ์๋ก ๋ค๋ฅธ Origin์ด ๋ฆฌ์์ค๋ฅผ ๊ณต์ ํ ์ ์๋ ๊ธฐ์
- Single-Origin Policy
Origin?
- URI ์คํค๋ง (http, https)
- hostname (io.namjune, localhost)
- ํฌํธ(8080, 18080)
Spring MVC @CrossOrigin
์คํ๋ง ๋ถํธ์์ @CrossOrigin์ ๊ดํ ๋น ์ค์ ๋ค์ ์๋์ผ๋ก ํด์ฃผ๊ธฐ ๋๋ฌธ์ ๊ทธ๋ฅ ์ฌ์ฉํ๋ฉด ๋๋ค. ๋๋ WebMvcConfigurer ์ฌ์ฉํด์ ๊ธ๋ก๋ฒ๋ก ์ค์ ํ ์ ์๋ค.
@Controller๋ @RequestMapping์ ์ถ๊ฐํ๊ฑฐ๋
- https://docs.spring.io/spring/docs/5.0.7.RELEASE/spring-framework-reference/web.html#mvc-cors
@RestController @RequestMapping("/account") public class AccountController { @CrossOrigin @GetMapping("/{id}") public Account retrieve(@PathVariable Long id) { // ... } @DeleteMapping("/{id}") public void remove(@PathVariable Long id) { // ... } }
@CrossOrigin(origins = "http://domain2.com", maxAge = 3600) @RestController @RequestMapping("/account") public class AccountController { @GetMapping("/{id}") public Account retrieve(@PathVariable Long id) { // ... } @DeleteMapping("/{id}") public void remove(@PathVariable Long id) { // ... } }
WebMvcConfigurer ์ฌ์ฉํด์ ๊ธ๋ก๋ฒ ์ค์
- ๋ชจ๋ api๋ฅผ localhost:9090์ CORS ํ์ฉํ๋๋ก ๋ฑ๋ก
@Configuration public class WebConfig implements WebMvcConfigurer { @Override public void addCorsMappings(CorsRegistry registry) { registry.addMapping("/**") .allowedOrigins("http://localhost:9090"); } }
ajax๋ก CORS ๋์ ํ์ธํ๊ธฐ
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Title</title>
</head>
<body>
<h1>CORS Test</h1>
<script src="/webjars/jquery/dist/jquery.min.js"></script>
<script>
$(function() {
$.ajax("http://localhost:8080/hello")
.done(function(msg) {
alert(msg);
})
.fail(function() {
alert("fail");
});
});
</script>
</body>
</html>