Skip to main content

4. ์Šคํ”„๋ง ์›น MVC

About 4 minJavaSpringSpring Bootcrashcoursejavajdkjdk8streamspringspringframeworkspringboot

4. ์Šคํ”„๋ง ์›น MVC ๊ด€๋ จ


namjunemy/TIL - [์Šคํ”„๋ง ๋ถ€ํŠธ ๊ฐœ๋…๊ณผ ํ™œ์šฉ] 4. ์Šคํ”„๋ง ์›น MVC

[์Šคํ”„๋ง ๋ถ€ํŠธ ๊ฐœ๋…๊ณผ ํ™œ์šฉ] 4. ์Šคํ”„๋ง ์›น MVC

1. ์†Œ๊ฐœ

๊ฐ„๋‹จํ•œ ์ปจํŠธ๋กค๋Ÿฌ์™€ ํ…Œ์ŠคํŠธ ์ฝ”๋“œ๋ฅผ ์ž‘์„ฑํ•œ๋‹ค. @WebMvcTest ์• ๋…ธํ…Œ์ด์…˜์„ ์‚ฌ์šฉํ•˜๋ฉด MockMvc๋ฅผ ์ฃผ์ž…๋ฐ›์•„์„œ ์‚ฌ์šฉํ•  ์ˆ˜ ์žˆ๋‹ค.

์•„๋ž˜์˜ ํ…Œ์ŠคํŠธ์—์„œ ์šฐ๋ฆฌ๋Š” ์•„๋ฌด๋Ÿฐ ์„ค์ •ํŒŒ์ผ์„ ์ž‘์„ฑํ•˜์ง€ ์•Š์•˜์ง€๋งŒ ์Šคํ”„๋ง MVC์˜ ๊ธฐ๋Šฅ์„ ์‚ฌ์šฉํ•  ์ˆ˜ ์žˆ์—ˆ๋‹ค. ์ด๊ฒƒ์ด ๊ฐ€๋Šฅํ•œ ๊ฒƒ์€ ์Šคํ”„๋ง ๋ถ€ํŠธ๊ฐ€ ์ œ๊ณตํ•ด์ฃผ๋Š” ๊ธฐ๋ณธ์„ค์ • ๋•Œ๋ฌธ์ด๋‹ค.

์ž์„ธํžˆ ๋งํ•ด์„œ spring-boot-starter์˜์กด์„ฑ์„ ์ถ”๊ฐ€ํ•˜๋ฉด์„œ ๊ฐ™์ด ๋”ฐ๋ผ์˜จ spring-boot-autoconfigure์˜์กด์„ฑ์˜ ์†์„ ๊นŒ๋ณด๋ฉด spring.factories ํŒŒ์ผ ์•ˆ์— WebMvcAutoConfiguration์ด๋ผ๋Š” ํด๋ž˜์Šค๊ฐ€ ์กด์žฌํ•˜๊ณ , ์ด ํด๋ž˜์Šค์— ์ •์˜๋œ ์„ค์ •๋“ค ๋•Œ๋ฌธ์— ์šฐ๋ฆฌ๋Š” ์Šคํ”„๋ง MVC์˜ ๊ธฐ๋Šฅ์„ ๋ฐ”๋กœ ์“ธ ์ˆ˜ ์žˆ๋‹ค.

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"));
    }
}
  • ์Šคํ”„๋ง ๋ถ€ํŠธ๊ฐ€ ์ œ๊ณตํ•ด์ฃผ๋Š” ๊ธฐ๋ณธ์„ค์ •์„ ์‚ฌ์šฉํ•˜๋ฉด์„œ, ์ถ”๊ฐ€์ ์œผ๋กœ ํ™•์žฅํ•ด์„œ ์“ฐ๊ณ  ์‹ถ์€๊ฒฝ์šฐ, @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์˜ ContentNegotiatingViewResolveropen in new window ๊ฐ€ ์–ด๋–ค 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๋กœ ์ ‘๊ทผ
    • 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/open in new window
    • ํŒŒ๋น„์ฝ˜์€ ์บ์‹œ๊ฐ€ ๋˜์–ด์žˆ์œผ๋ฏ€๋กœ, ํฌ๋กฌ์—์„œ ์บ์‹œ๋น„์šฐ๊ณ  ์ƒˆ๋กœ๊ณ ์นจ์„ ํ•˜๋ฉด ํ™•์ธํ•  ์ˆ˜ ์žˆ๋‹ค.

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์ด ๋ฆฌ์†Œ์Šค๋ฅผ ๊ณต์œ ํ•  ์ˆ˜ ์žˆ๋Š” ๊ธฐ์ˆ 
  • 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>

์ด์ฐฌํฌ (MarkiiimarK)
Never Stop Learning.