[강의요약] 스프링부트 개념과 활용 - 스프링 웹 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>

결과 : 성공하면 아무것도 출력하지 않는다.

test_passed

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>

결과 static_resource

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);
    }
}

결과

static_resource_path

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>

결과

webjar

12. Index 페이지와 파비콘

12-1. welcome 페이지

root(/) 로 요청하면 나오는 페이지

제공 : index.html > index.템플릿 > 둘 다 없으면 에러 페이지

  • index.html : 정적 페이지
  • index.템플릿 : 동적 페이지

spring.mvc.static-path-locations 설정되어있으면 static이 root

welcome_page

12-2. 파비콘

파비콘 생성 사이트 : https://favicon.io/favicon-generator/

favicon

파비콘 파일 받기

만들거나 내려받은 favicon.ico 파일을 resources 하위에 넣고 재기동하면 파비콘이 적용 된다.

파비콘이 한 번 캐싱되면 브라우저 닫을 때 까지 유지되어서, 다른 브라우저로(safari) 열었다

favicon_page

댓글남기기