본문 바로가기

Spring

[Spring] 스프링 웹 개발 방식 (Static Content, MVC, API)

Spring 웹 개발 방식은 크게 3가지 방식으로 나눌수있다. 정적 컨텐츠(Static Content), MVC(Model-View-Controller) 그리고 API(Application Programming Interface)가 있다.

 

정적 컨텐츠는 HTML(Hyper Text Markup Language)과 같은 텍스트 파일을 그대로 웹브라우저에게 전달한다. 반면, MVC(Model-View-Controller)와 Template Engine은 서버에서 동적으로 텍스트 파일에 데이터를 삽입하여 변형을 하고 렌더링하여 웹브라우저에게 보내주는 방식이다. 그리고 API(Application Programming Interface)는 주로 JSON 데이터 포맷으로 데이터만 클라이언트에게 전송하거나, 서버끼리 데이터를 주고 받거나 할때 사용한다.


1. 정적 컨텐츠(Static Content)

정적 컨텐츠는 어떠한 로직이 있는 프로그래밍한 결과물이 아니라 파일 그대로 전송하는 것이다. 그리하여 static 디렉터리에 저장된 index.html 파일과 같은 정적인 파일을 클라이언트에게 반환한다.

 

클라이언트가 url:8080(http://localhost:8080)으로 접속하여 요청받은 리소스가 루트(/) 디렉터리인 경우에는, 웹 서버인 tomcat이 스프링에게 요청을 전달하고 스프링은 /resource/static 디렉터리에서 index.html을 자동으로 반환하도록 설정되어있다.

 

아래와 같이 일반적으로 index.html은 정적 페이지로 작성하기 때문에 static디렉토리에 저장한다.

<!DOCTYPE HTML>  
<html>  
<head>  
    <title>Hello</title>  
    <meta http-equiv="Content-Type" content="text/html; charset=UTF-8" />  
</head>  
<body>  
Hello  
<a href="/hello">hello</a>  
</body>  
</html>

 

위처럼 hello-static.html 정적 파일을 요청하기 위해 http://localhost:8080/hello-static으로 url을 날리면, tomcat이 먼저 요청을 받고 스프링에 요청을 전달한다. 먼저, 스프링는 hello-static과 맵핑된 컨트롤러(Controller)의 요청을 찾는다. 

 

만일, 요청받은 리소스가 컨트롤러의 정의된 요청과 일치하는 것이 있다면, 컨트롤러가 요청을 처리한다. 그러나 컨트롤러에서 요청 받은 리소스가 정의되지 않았을 경우에는/resource/static에서 정적 파일을 찾는다.

 

이렇게 컨트롤러가 요청을 처리되지 못하면, http://localhost:8080/hello-static.html처럼 파일 형식(.html)까지 기재하여 요청해야한다. 일반적으로 컨트롤러는 리소스 요청에 대한 맵핑은 @GetMapping("hello-static")처럼 파일 형식을 생략한 채로 정의하기 때문이다.

 

이로써, 스프링의 여러 객체중에서 컨트롤러가 먼저 요청을 처리할 권한을 주는 것을 알 수 있다.


2. MVC(Model-View-Controller) & Template Engine

예전에 JSP(Java Server Page)로 개발할때는 모델(Model), 컨트롤러(Controller)와 뷰(View)가 따로 분리되어 있지 않고, 뷰에 클라이언트의 요청을 처리하는 로직, 비지니스로직, DB접근 로직을 모두 합쳐져있었다.

 

JSP는 HTML 내에 JAVA 코드를 삽입하여 웹서버에서 동적으로 웹페이지를 생성하고, 템플릿엔진으로 렌더링하여 웹브라우저에게 반환하는 서버 사이드 스크립트언어이다. 그러므로 뷰에 다가 모든 로직을 작성했던 것이다.

 

반면, 현재 주로 쓰이는 Spring MVC 패턴은 클라이언트 요청을 컨트롤러가 처리하고, 컨트롤러가 뷰에 전달할 데이터를 비지니스 로직을 수행하는 서비스(serivce)로 부터 받아서 모델에 저장하고, 뷰는 모델에서 데이터를 가져와서 HTML에 삽입하고 렌더링하여 클라이언트에게 응답하는 구조이다. 이 구조를 통해 애플리케이션의 각 부분을 독립적으로 개발하고 더 수월하게 유지보수할 수 있다.

 

View

뷰(View)는 클라이언트에게 보여질 화면에 관련된 로직을 수행한다. 
뷰는 컨트롤러 부터 받은 ViewName(HTML이나 XML 파일명 등)으로 HTML 파일을 찾은 뒤, 컨트롤러(controller)가 모델(model)에 저장한 데이터를 가져와서 HTML 파일의 변수에 삽입하고 렌더링하는 역할을 한다.

<html xmlns:th="http://www.thymeleaf.org">
<body>
<p th:text="'hello ' + ${name}">hello! empty</p>
</body>
</html>

 

스프링 부트의 Thymeleaf와 같은 템플릿 엔진(Template Engine)을 사용해서 dynamic html content를 클라이언트에게 제공할려면, /src/main/resources/templates에 dynamic html 파일을 저장해야한다.

 

뷰는 모델(Model)에 참조하여 일치하는 name 필드가 있다면, 값을 가져와서 HTML 파일의 ${name}에 삽입하고 렌더링하여 클라이언트에게 응답한다. 

 

Controller

컨트롤러(Controller)는 클라이언트의 요청을 받아, 뷰(view)에 전달할 데이터를 비지니스 로직을 수행하는 서비스(serivce)로 부터 받아서 모델에 저장하고, HTML 파일명과 같은 ViewName을 반환한다. 그러면 뷰가 ViewName으로 파일을 찾아 나머지 작업을 처리한다.이렇게 클라이언트의 요청에 대해 Service, Model, View에서 각 작업을 처리하도록 함으로써 애플리케이션의 작업 처리 흐름을 제어하기 때문에 "Controller"라고 불린다. 

@Controller
 public class HelloController {

    // 클라언트의 요청 처리
     @GetMapping("hello-mvc")
     public String helloMvc(@RequestParam("name") String name, Model model) {
        // 뷰에 전달한 데이터를 model에게 저장 (뷰에서 삽입할 데이터)
         model.addAttribute("name", name);

        // 뷰 이름 반환 (뷰에 대한 작업 처리를 위해 제어권 넘김)
         return "hello-template"; 
     }

}

 

@GetMapping("hello-mvc"): HTTP 메서드인 GET 요청이 들어오면, 위의 메서드와 맵핑되어서 호출되고 hello-mvc는 클라이언트가 요청할 리소스인 파일명이다. 즉, 이 메서드는 컨트롤러의 역할인 클라이언트의 요청을 처리하는 기능이다.

 

클라이언트가 name 파라메터에 값을 할당하여 http://localhost:8080/hello-mvc?name=spring! url로 서버에 접속할 경우에, 컨트롤러에서 name이란 파라매터로 값이 들어온다.

 

그리고 스프링 컨테이너가 Model 객체를 생성하여 helloMvc 메서드의 인자로 넣어준다. 이로써 모델(model)이 생성된다. 이후, 클라이언트로부터 받은 name 파라메터를 model 객체에 저장한다. 이로써, 컨트롤러(controller)가 모델(model)에게 데이터를 전달한 것이다.

 

이후, Thymeleaf와 같은 템플릿 엔진이 model 객체의 name의 값을 아래의 HTML 파일(hello-template.html)의 name 변수에 데이터를 삽입한다. Model 클래스는 org.springframework.ui 패키지의 클래스이기 때문에, 스프링 부트의 템플릿 엔진이 뷰의 데이터와 맵핑되는 model 객체의 데이터를 읽어올수 있다.

<html xmlns:th="http://www.thymeleaf.org">  
<body>  
<p th:text="'hello ' + ${name}">hello! empty</p>  
</body>  
</html>

 

이어서, HelloController에서 리턴값으로 문자열(hello-template)을 반환하면, ViewResolver 클래스가 hello-template.html를 찾고 템플릿 엔진이 렌더링 후에 리턴하는 역할을 한다.

 

 

스프링 부트의 템플릿 엔진은 기본적으로 컨트롤러의 반환값이 ViewName과 맵핑이 된다. 위의 예제에서 ViewName은 hello-template이다. 그래서 ViewResolver 클래스의 객체는/resource/templates/{ViewName}.html에 일치하는 리소스를 찾는다.

 

컨트롤러 반환값이 hello-template 문자열이므로 찾는 리소스는 /resource/templates/hello-mvc.html을 가리킨다. 이렇게 ViewResolver 객체는 HTML 파일을 찾은 후 thymeleaf의 template 엔진이 렌더링을한다. 이후, 렌더링 된 HTML 파일을 클라언트에게 응답 할 수 있게 된다.

 

이러한 방식은 클라이언트가 name 파라메터에 다른 값을 할당하여 URL(http://localhost:8080/hello-mvc?name=spring-boot!! 혹은 http://localhost:8080/hello-mvc?name=spring!!)을 날릴때마다 model 객체의 데이터도 변경된다. 그리고 model에서 데이터를 가져와 HTML 파일에 삽입하고 렌더링하여, 클라이언트에게 dynamic content를 전송 가능하다.


3. API(Application Programming Interface)

API란?

API(Application Programming Interface)는 애플리케이션간의 데이터를 주고 받을 수 있는 인터페이스이다. 이는 네트워크간의 인터페이스로, OSI 7 Layer에서 Application Layer인 클라이언트와 서버의 데이터 통신을 위한 인터페이스이다.

 

서버는 특정 데이터를 제공하기 위해서, API를 클라이언트가 어떻게 호출하면 좋을지 규약을 만들어 놓는다. 그리고 클라이언트는 API 규약대로 호출하여 필요한 데이터를 받을 수 있다.

 

API를 통한 데이터 응답은, 위에서 처럼 HTML 파일을 응답하는 것이 아니라, 애플리케이션에서 필요한 데이터만 전송하기 위해 사용한다. 일반적으로 API를 통해 클라이언트에서 요청한 데이터를 JSON 형식으로 변환하여 HTTP Message의 Body에 데이터 넣어서 응답을 준다.

JSON란?

JSON(JavaScript Object Notation)은 키-값 쌍(key-value pair)와 배열 자료형(array data types) 또는 기타 모든 시리얼화 가능한 값(serializable value) 으로 이루어진 객체를 전달하기 위해 사용한다.

{
    "name": "John",
    "age": 30,
    "isStudent": false,
    "courses": ["Mathematics", "Computer Science"],
    "address": {
        "city": "New York",
        "zipcode": "10001"
    }
}

 

위에서 보다시피 인간이 쉽게 읽을 수 있는 텍스트를 사용하는 표준 포맷이다. 변수명을 텍스트로 표현하고 중괄호와 대괄호를 사용하여 간결하고 직관적으로 데이터를 파악하기 수월하다.

 

API를 통해 데이터를 주고 받을때, 아주 간단한 데이터가 아니고서야 객체를 주고 받게 되는데, 이를 JSON 형식으로 주고받으면 인간이 읽기 쉬워서 데이터 파악하는데 수월하다.

 

본래는 javascript 언어로부터 파생되어 자바스크립트의 구문 형식을 따르지만 언어 독립형 데이터 포맷이지만, 대부분의 프로그래밍 언어에서 쉽게 이용할 수 있다.

 

자료의 종류에 큰 제한은 없으며, 특히 프로그램에서 변수를 표현하는 데 적합하다. { "변수명": 변수 타입의 값 }으로 표현하기 때문이다.

 

스프링에서 API 호출에 대한 요청 처리

아래의 코드에서 @ResponseBody 어노테이션이 있어서, 스프링은 HTTP Message의 Body에 데이터를 실어서 응답을 하도록 한다. 해당 요청은 HTML 파일과 같이 렌더링한 뷰를 요청한 작업이 아니기 때문에, 위에서 처럼 컨트롤러에서 ViewResolver에게 요청을 넘기지 않고 HTTPMessageConverter에게 넘긴다.

@Controller
public class HelloController {
    @GetMapping("hello-string")  
    @ResponseBody // http message의 body에 데이터 저장해서 응답  
    public String helloString(@RequestParam("name") String name) {  
        return "hello " + name;  
    }  

    @GetMapping("hello-api")  
    @ResponseBody  
    public Hello helloApi(@RequestParam("name") String name) {  
        Hello hello = new Hello();  
        hello.setName(name);  
        hello.setNo(10);  
        return hello; // JSON 형식으로 반환  
    }  

    static class Hello {  
        private String name; // { "name", "jun" }  
        private int no; // { "no", 10  }  

        public int getNo() {  
            return no;  
        }  

        public void setNo(int no) {  
            this.no = no;  
        }  

        public String getName() {  
            return name;  
        }  

        public void setName(String name) {  
            this.name = name;  
        }  
    }
}

 

위의 helloString 메서드에서 String 타입의 값인 문자열 그대로 응답을 해준다. 

 

그러나 컨트롤러에서 반환하는 값이 객체일 경우에는 디폴트로 JSON 형식으로 데이터를 변환하여 응답한다.

helloApi 메서드에서는 내부 클래스(Inner Class)인 Hello 클래스의 객체를 반환한다. 이때 객체는 기본적으로 JSON 형식으로 변환하여 응답하게 된다. 

 

이렇게 반환되는 데이터의 포맷에 맞게 HttpMessageConverter가 데이터를 변환하여 HTTP Message의 body에 데이터를 저장한다.

 

HttpMessageConverter에는 크게 두가지 분류로 나뉘는데, 문자열 포맷으로 변환을 위한 StringConverter와 JSON 포맷으로 변환을 위한 JsonConverter가 있다. 

 

컨트롤러가 반환하는 값의 타입이 String이면, StringConverter인 StringHttpMessageConverter가 동작하여 문자열로 응답을 한다. 반면, 컨트롤러가 반환하는 값의 타입이 객체(Object이거나 하위 클래스)이면, JsonConverter인 MappingJackson2HttpConverter가 동작하여 JSON 형식으로 변환하여 응답하게 된다.

 

다만, 클라이언트의 HTTP Accept Header 요청과 일치하는 컨트롤러의 메서드 반환 타입 정보를 조합해서 HttpMessageConverter가 선택된다. 만일, Accept Header가 XML이라면, 해당 형식에 맞는 HttpMessageConverter가 선택되고 XML 포맷으로 데이터를 변환해서 반환한다.


참고

  • 김영한 스프링 입문 - 코드로 배우는 스프링 부트, 웹 MVC, DB 접근 기술