Skip to main content

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

2024๋…„ 3์›” 25์ผ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"));
    }
}

2. HttpMessageConverters

HttpMessageConverters๋Š” ์Šคํ”„๋ง ํ”„๋ ˆ์ž„์›Œํฌ์—์„œ ์ œ๊ณตํ•˜๋Š” ์ธํ„ฐํŽ˜์ด์Šค์ด๋‹ค.

HTTP ์š”์ฒญ ๋ณธ๋ฌธ์„ ๊ฐ์ฒด๋กœ ๋ณ€๊ฒฝํ•˜๊ฑฐ๋‚˜, ๊ฐ์ฒด๋ฅผ HTTP ์‘๋‹ต ๋ณธ๋ฌธ์œผ๋กœ ๋ณ€๊ฒฝํ•  ๋•Œ ์‚ฌ์šฉํ•œ๋‹ค. ์‚ฌ์šฉํ•˜๋Š” HttpmessageConverter๋Š” ์—ฌ๋Ÿฌ๊ฐ€์ง€๊ฐ€ ์žˆ๊ณ , ์šฐ๋ฆฌ๊ฐ€ ์–ด๋–ค ์š”์ฒญ์„ ๋ฐ›์•˜๋Š”์ง€, ์‘๋‹ต์„ ๋ณด๋‚ด๋Š”์ง€์— ๋”ฐ๋ผ์„œ ๋ฉ”์„ธ์ง€์ปจ๋ฒ„ํ„ฐ๊ฐ€ ๋‹ฌ๋ผ์ง„๋‹ค.

{"username":"keesun", "password":"123"} <-> User
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 ์š”์ฒญ์— ์˜ํ•ด์„œ ํ•ด๋‹น ์š”์ฒญ์— ๋งž๋Š” ์‘๋‹ต์„ ๋ณด๋‚ด๋Š” ์ž‘์—…์„ ์•Œ์•„์„œ ํ•ด์ค€๋‹ค.

๊ทธ๋ž˜์„œ 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. ์ •์  ๋ฆฌ์†Œ์Šค ์ง€์›

์ •์  ๋ฆฌ์†Œ์Šค ๋งตํ•‘ "/**". ๋ฃจํŠธ๋กœ ๋งตํ•‘๋œ๋‹ค.

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 ํŽ˜์ด์ง€์™€ ํŒŒ๋น„์ฝ˜

7. ํ…œํ”Œ๋ฆฟ ์—”์ง„

8. HtmlUnit

9. ExceptionHandler

10. Spring HATEOAS

11. 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>