[강의요약] 스프링부트 개념과 활용 - 스프링 웹 MVC (2/2)

개인적인 학습을 위한 Inflearn - 스프링부트 개념과 활용(백기선) 강의 요약입니다.

개념과 원리 위주로 요약합니다.

이전 글 에서 이어집니다.

4부. 스프링 부트 활용


13. Thymeleaf

새 프로젝트 생성(springmvcdemo)

13-1. 템플릿 엔진

  • 주로 View를 만들 때 사용
  • 코드 제너레이션, email 템플릿 등에 사용 가능

13-2. 스프링부트가 자동 설정을 지원하는 템플릿 엔진

  • FreeMarker, Groovy, Thymeleaf, Mustache 등이 있음
  • Jsp는 지원하지 않고, 권장하지도 않음

    내장 톰캣을 포함하는 jar를 쓰면 jsp 사용이 불가함.
    톰캣이 떠있는 서버에서 java -jar 사용이 필요
    undertow의 경우 jar를 호환하지도 않음
    의존성 문제까지 발생할 수 있음

13-3. thymeleaf 사용하기

13-3-1. 의존성 추가

// pom.xml
				<dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-thymeleaf</artifactId>
            <version>2.2.0.RELEASE</version>
        </dependency>

13-3-2. 자동 설정이 파일을 찾는 위치 : /src/main/resources/templates/

13-3-3. 테스트 추가

@RunWith(SpringRunner.class)
@WebMvcTest(SampleController.class)
public class SampleControllerTest {
    @Autowired
    MockMvc mockMvc;
    
    @Test
    public void hello() throws Exception {
        // 요청 : "/hello"
        // 응답
        // - 모델 name : cherrue
        // - 뷰 이름 : hello
        mockMvc.perform(get("/hello"))
                .andExpect(status().isOk())
								.andDo(print())
                .andExpect(view().name("hello"))
                .andExpect(model().attribute("name", is("cherrue")));
    }
}

13-3-3. 컨트롤러 추가 (@restcontroller X)

응답의 string 은 응답 본문이 아닌 응답 파일의 이름

model 은 Map 이라고 생각하면 쉽다.

@Controller
public class SampleController {
    @GetMapping("/hello")
    public String hello(Model model) {
				model.addAttribute("name", "cherrue");
        return "hello";
    }
}

13-3-4. 템플릿 추가(main/resources/templates/hello.html)

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>Title</title>
</head>
<body>

</body>
</html>

13-3-5. 결과

mockMvc는 서블릿 컨테이너를 띄우지 않기 때문에 JSP라면 출력이 불가능

타임리프는 서블릿 컨테이너에 독립적인 엔진이므로 응답을 출력할 수 있다

MockHttpServletResponse:
           Status = 200
    Error message = null
          Headers = [Content-Language:"en", Content-Type:"text/html;charset=UTF-8"]
     Content type = text/html;charset=UTF-8
             Body = <!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>Title</title>
</head>
<body>

</body>
</html>
    Forwarded URL = null
   Redirected URL = null
          Cookies = []

13-4. Thymeleaf 변수 표현

docs : https://www.thymeleaf.org/doc/articles/standarddialect5minutes.html

13-4-1. 👀 (필수) Thymeleaf 네임스페이스 추가

<html lang="en" xmlns:th="http://www.thymeleaf.org">

13-4-2. variable expressions

<!DOCTYPE html>
<html lang="en" xmlns:th="http://www.thymeleaf.org">
<head>
    <meta charset="UTF-8">
    <title>Title</title>
</head>
<body>
<h1 th:text="${name}">Name</h1>
</body>
</html>

13-4-3. 결과 (localhost:8080/hello)

Untitled

14. HtmlUnit

14-1. 의존성 추가

<dependency>
            <groupId>org.seleniumhq.selenium</groupId>
            <artifactId>htmlunit-driver</artifactId>
            <version>3.58.0</version>
            <scope>test</scope>
        </dependency>
        <!-- https://mvnrepository.com/artifact/net.sourceforge.htmlunit/htmlunit -->
        <dependency>
            <groupId>net.sourceforge.htmlunit</groupId>
            <artifactId>htmlunit</artifactId>
            <version>2.58.0</version>
            <scope>test</scope>
        </dependency>

14-2. 의존성 설명

14-2-1. htmlunit

htmlunit : html 단위 테스트를 위한 라이브러리

HtmlPage 인터페이스를 통해 각종 컨텐츠, 브라우저 타입 등을 확인할 수 있음

14-3. 테스트

HtmlUnit 이 MockMvc 보다 좋다 이런게 아니고, 취향의 문제!

WebClient 안에서 MockMvc를 이미 쓰고 있다

@RunWith(SpringRunner.class)
@WebMvcTest(SampleController.class)
public class SampleControllerTest {

    @Autowired
    MockMvc mockMvc;
    
    @Autowired
    WebClient webClient;

    @Test
    public void helloHtmlUnit() throws IOException {
        HtmlPage page = webClient.getPage("/hello");
        HtmlHeading1 h1 = page.getFirstByXPath("//h1");
        assertThat(h1.getTextContent()).isEqualToIgnoringCase("cherrue");
    }

15. ExceptionHandler

새 프로젝트(springbootexception)

15-1. 기본 에러핸들러

스프링 어플리케이션을 실행하면 기본적인 에러 핸들러가 등록이 되어있다

예를 들면 404 → Whitelabel Error Page(machine client) / json 404 (curl rest api)

BasicErrorController 소스 일부

// Whitelabel Error Page
@RequestMapping(
    produces = {"text/html"}
)
public ModelAndView errorHtml(HttpServletRequest request, HttpServletResponse response) {
		...
		return modelAndView != null ? modelAndView : new ModelAndView("error", model);
}
// json 404 not found
@RequestMapping
public ResponseEntity<Map<String, Object>> error(HttpServletRequest request) {
    HttpStatus status = this.getStatus(request);
    if (status == HttpStatus.NO_CONTENT) {
        return new ResponseEntity(status);
    } else {
        Map<String, Object> body = this.getErrorAttributes(request, this.isIncludeStackTrace(request, MediaType.ALL));
        return new ResponseEntity(body, status);
    }
}

15-2. 스프링 부트 에러 핸들링 방법

@ExceptionHandler 사용

컨트롤러 안에서 발생한 SampleException은 모두 @ExceptionHandler가 붙은 함수에서 모두 처리한다.

→ 전역적으로 설정하고 싶다면 @ControllerAdvice가 붙은 클래스를 생성하여 선언하면 된다.

→ 에러 처리 로직 자체를 고치고 싶다면 ErrorController를 구현(default BasicErrorController)

@Controller
public class SampleController {
    @GetMapping("/hello")
    public String hello() {
        throw new SampleException();
    }

    @ExceptionHandler(SampleException.class)
    public @ResponseBody AppError sampleError(SampleException e) {
        AppError appError = new AppError();
        appError.setMessage("error.app.key");
        appError.setMessage("IDK IDK IDK");
        return appError;
    }
}
// SampleException.java
public class SampleException extends RuntimeException {
}
// AppError.java
public class AppError {
    private String message;

    private String reason;

    public String getMessage() {
        return message;
    }

    public void setMessage(String message) {
        this.message = message;
    }

    public String getReason() {
        return reason;
    }

    public void setReason(String reason) {
        this.reason = reason;
    }
}

결과

$ curl localhost:8080/hello 
{"message":"IDK IDK IDK","reason":null}

15-3. 커스텀 에러 페이지

상태 코드에 따라 보일 에러페이지를 바꿔줄 수 있다

경로 : main/resources/static/ 또는 main/resources/template

파일명 : status code와 일치하거나 x로 와일드카드 사용 가능

동적인 페이지로 쓰고 싶다면 : ErrorViewResolver 구현하면 되지만 서버단에서 처리 파이프라인 짜는 게 낫다

예시 : 404.html, 5xx.html

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>Title</title>
</head>
<body>
<h1>404</h1>
</body>
</html>

15. Spring HATEOAS

신규 프로젝트 생성 : demospringhateoas

spring HATEOAS : spring에서 HATEOAS를 사용하기 위한 툴

HATEOAS : Hypermedia As The Engine Of Application State

  • REST api에서 리소스에 대한 정보를 제공할 때 연관된 링크 정보들 까지 같이 제공하고, 클라이언트는 제공받은 연관된 링크를 가지고 링크에 접근하는 것
  • 서버에서 루트 페이지를 줄 때 relation이 있는 페이지 정보를 미리 클라이언트한테 제공하고, 클라이언트는 페이지를 이동하고 싶을 때 서버가 준 href를 가지고 이동
<!-- https://mvnrepository.com/artifact/org.springframework.boot/spring-boot-starter-hateoas -->
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-hateoas</artifactId>
    <version>2.2.0.RELEASE</version>
</dependency>

15-1. 자동설정 되는 기능

ObjectMapper

응답을 Json으로 만들 때 사용하는 기능. (jackson)

Jackson2ObjectMapperBuilder가 주입 → ObjectMapper 생성할 필요 없음

  • application.properties에서 objectMapper를 설정해줄 수 있음

LinkDiscovers

클라이언트에서 링크정보를 Rel 이름으로 찾을 수 있는 XPath 확장 클래스. 편의성이라고 보면 된다

15-2. 링크 달기 (deprecated)

2.2.0 RELEASE에는 Resource 객체가 없음..!

@RestController
public class SampleController {
    @GetMapping("/hello")
    public Resource<Hello> hello() {
        Hello hello = new Hello();
        hello.setPrefix("Hey,");
        hello.setName("Cherrue");

        Resource<Hello> helloResource = new Resource<>(hello);
        helloResource.add(linkTo(methodOn(SampleController.class).hello()).withSelfRel())
        return helloResource;
    }
}

16. CORS

Cross Origin Resource Sharing : Single-Origin Policy를 우회하기 위한 웹 표준 기술

Single-Origin Policy 하나의 오리진에서만 요청이 가능한 제한 정책

Origin = URI 스키마(http, https) + hostname + port

→ 기본적으로 localhost:8080과 localhost:8081 응답을 합쳐서 반환하고 싶어도 못 함

Spring 에서는 CORS를 쓰려면 빈 설정을 이것저것 해주었어야 하는데, 스프링 부트는 자동설정 지원

→ @CrossOrigin

16-1. 프로젝트 두 개 생성 (springcorsserver(8080포트), springcorsclient(18080포트))

📍 port 설정 : application.properties > server.port=18080

@SpringBootApplication
@RestController
public class SpringcorsserverApplication {
    @GetMapping("/hello")
    public String hello () {
        return "hello";
    }

    public static void main(String[] args) {
        SpringApplication.run(SpringcorsserverApplication.class, args);
    }
}
<!-- SpringcorsclientApplication/resources/static/index.html-->
<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>Title</title>
</head>
<body>
<h1>CORS client</h1>
<script src="/webjars/jquery/3.6.0/dist/jquery.min.js"></script>
<script>
    $(function() {
        $.ajax("http://localhost:8080/hello")
            .done(function(msg) {
            alert("msg");
            })
            .fail(function() {
            alert("fail");
            });
    })
</script>
</body>
</html>

결과

cors_fail

Access-Control-Allow-Origin 헤더를 확인했는데 오리진이 아니라는 오류 발생

16-2. 서버측 api 하나에 CrossOrigin 설정

@SpringBootApplication
@RestController
public class SpringcorsserverApplication {
    @CrossOrigin(origins = "http://localhost:18080")
    @GetMapping("/hello")
    public String hello () {
        return "hello";
    }

    public static void main(String[] args) {
        SpringApplication.run(SpringcorsserverApplication.class, args);
    }
}

16-3. 서버에 전역 CORS 설정

WebMvcConfiguration 구현 : 기본 설정은 그대로 두고, 내가 여기서 구현한 애들만 오버라이드 됨

@Configuration
public class WebConfig implements WebMvcConfigurer {
    @Override
    public void addCorsMappings(CorsRegistry registry) {
        registry.addMapping("/**")
                .allowedOrigins("http://localhost:18080");
    }
}

댓글남기기