앞 장을 통해 우리는 Spring 프레임워크의 가장 기본적인 구현 방식을 공부했다. Spring 프레임워크는 컴포넌트 기반 방법론을 따르고 있으며, 각각의 컴포넌트를 구현 및 등록하고 이들을 어떻게 연결해서 사용하는지 알아보았다.
당연히, 특정한 개발 방법론이 그로부터 생산될 수 있는 시스템의 종류를 제한하지는 않는다. 하지만 특정 방법론으로 개발했을때 특별히 더 효과적인 시스템들은 분명히 존재한다. 그런 측면에서 Spring 프레임워크는 웹 서비스용 서버를 만들때 특히 강력한 기능을 발휘한다. Spring에 관심을 가졌다면 웹 서비스에 관심이 있을 확률이 높다고 생각하므로, 본 장에서는 웹 서비스 개발을 관통하고있는 방법론인 MVC 방식에 대해 알아보고, 이에 특화된 Spring MVC 프레임워크에 대해 공부할 것이다.
MVC는 Model, View, Controller의 약자이다. 간단하게 설명하자면 모델은 데이터, 뷰는 입출력, 컨트롤러는 로직을 의미한다. 흔히 웹 서버가 이런 구조로 개발되며, 이 경우에는 웹 브라우저에서 구동되는 html이나 javascript 따위가 뷰이고, 서버의 비즈니스 로직이 컨트롤러이며, 컨트롤러가 사용하는 데이터가 모델이다.
결국 MVC 패턴으로 시스템을 만든다는 것은 뷰와 컨트롤러가 데이터, 즉 모델을 주고받으며 상호작용하는 시스템을 만드는 행위를 뜻한다. 뷰로부터 사용자 입력을 받아서, 이를 모델로 묶어 컨트롤러로 전달한다. 또한 컨트롤러의 처리 결과를 다시 모델로 묶어서 뷰로 전달해 사용자에게 뿌려준다. MVC 시스템은 이런 방식으로 동작한다.
사실, 이미 앞 장에서 우리는 MVC시스템의 일부를 만들어 보았다. 앞에서 @Controller 애노테이션을 사용했기 때문이다. Spring 프레임워크는 이처럼 MVC시스템을 만들기 위한 전용 애노테이션을 제공한다. Spring 프레임워크에서 이렇게 MVC시스템을 지원하는 것들을 가리켜 Spring MVC 프레임워크라고 따로 부르기도 하는데, 언제나 그렇듯 이런 이름은 크게 중요한것이 아니다. 어쨌거나 이들은 모두 Spring 프레임워크의 일부다.
우리는 이 절을 통해 웹브라우저를 통해 사용 가능한 간단한 MVC 상호작용 예제를 만들어 볼텐데, 그러기 위해서는 MVC시스템이 어떤 방식으로 구성되는지 알아야 한다. 각각이 무엇을 의미하는지는 앞에서 간단하게 언급했지만, 보다 자세히 뜯어보도록 하자.
먼저 유저는 웹 브라우저를 통해 자신이 조작할 수 있는 요소들을 볼 수 있어야 한다. 그러기 위해서는 시스템이 가진 데이터를 적절한 형태로 묶어서 웹 브라우저를 통해 그려줄 수 있어야 한다. 직관적으로 알 수 있듯이, 이는 간단한 작업이 아니다.
우선 사용자가 페이지를 요청(Http GET)하면, 해당 요청에 대한 컨트롤러가 작동해야 한다. 컨트롤러는 DB 따위에 접근해서 해당 요청에 필요한 데이터를 얻어낸 후, 이를 적절한 형태로 묶어서 뷰에 전달해야 한다. 뷰와 컨트롤러의 플랫폼이 다르기 때문에, 데이터는 json과 같은 표준 형식으로 묶여야 할 것이다.
언뜻 생각하면 뷰는 그냥 웹 브라우저를 의미하는 것으로 생각하기 쉽지만, 사실은 그렇지 않다. 물론 뷰가 그냥 웹 브라우저만을 의미할 수도 있다. 컨트롤러에서 보내는 응답이 그냥 html 파일인 경우가 그렇다. 하지만 대부분의 경우 Vue.js를 사용하거나, 아니면 다른 템플릿 엔진을 사용한다. 템플릿 엔진은 서버단에서 '응답 html을 변환하는'작업을 담당한다. 여기서 변환이란 html로 짜여진 틀 안에 실제 값들을 끼워 넣는 작업을 말한다.
이제 유저는 뷰를 통해 자신이 조작할 수 있는 페이지를 볼 수 있다. 유저는 페이지에서 자신이 원하는대로 입력을 수행하고, 최종적으로 이를 서버에 요청할 것이다. 그러면 뷰는 다시 페이지의 항목들중에서 유의미한 것들을 추려서 json과 같은 형태로 컨트롤러에게 전달해야 한다.
컨트롤러는 뷰로부터 데이터를 받아서, 자신이 처리해야 하는 비즈니스 로직을 수행해야 한다. 그리고 다시 그 결과 데이터를 뷰로 전달해야 한다. 이렇게만 설명하면 매우 간단해 보이지만, 사실 해야 할 일이 아주 많다.
앞장에서 간단하게 언급했듯이, Http Get 요청을 받아들이는것 부터가 난관이다. 소켓의 비동기 이벤트를 처리할 수 있어야 하고, 패킷을 컨트롤러 로직으로 전달할 수 있어야 한다. 그리고 컨트롤러는 십중팔구 DB같은 저장소에 접근해서 뭔가를 조작해야 할 것이다. 물론 요즘은 이런 작업을 직접 할 필요는 없게 도와주는 도구가 많이 있지만, 어쨌든 이러한 작업이 필요하다는것을 알고는 있어야 한다.
위 과정을 통해, MVC시스템의 동작 방식을 근본적으로 이해할 수 있다. MVC 시스템은 컨트롤러와 뷰가 모델(데이터)을 주고받는 방식으로 동작한다. 컨트롤러는 서버 로직이고 뷰는 java script 따위로 구성된 웹 페이지이기 때문에, 둘 사이에서 주고받는 데이터는 json과 같이 플랫폼에 종속되지 않는 표준 형식일 수 밖에 없다. 따라서 이러한 형식을 정의해서 모델을 구성해야 할 것이다.
결국 웹 페이지의 java script의 특정 항목에 id를 부여하고, 유저가 액션을 취하면 id가 부여된것들의 값을 <id - 값>의 쌍으로 묶어서(json 등), 이를 컨트롤러에 전달하는 형태가 된다. 반대의 경우도 마찬가지다. 컨트롤러가 특정 값이 id를 부여하고, 이를 Json 따위로 묶어서 뷰에 전달해야 한다.
눈치챘겠지만, Spring 프레임워크는 위에서 나열한 작업들을 모두 알아서 해준다. 물론 웹 페이지 자체는 직접 만들어야 하지만, 웹 페이지에서 '유의미한' 값들을 명시하는것도 매우 간단하게 도와준다. 모델을 쉽게 뷰에 뿌려줄 수 있고, 뷰에있는 유의미한 데이터를 모델로 묶는것을 도와준다는 뜻이다. 우리는 두가지 모두를 코드로 확인해볼 텐데, 먼저 모델을 뷰에 뿌려주는 과정을 살펴보자.
@Controller
public class MyController {
@GetMapping("/MyView")
public String showHome(Model model) {
//home 페이지에서는 이 3쌍의 데이터를 보여줘야 하고, 본 컨트롤러는 DB에서 이 값들을 가져왔다고 가정하자.
model.addAttribute("key1", "value1");
model.addAttribute("key2", "value2");
model.addAttribute("key3", "value3");
return "MyView";
}
}
@SpringBootApplication
public class Test6Application {
public static void main(String[] args) {
SpringApplication.run(Test6Application.class, args);
}
}
<!DOCTYPE html>
<html>
<head>
<meta charset="EUC-KR">
<title>My View Example</title>
</head>
<body>
<div>
<input th:value="${key1}">
</div>
<div>
<input th:value="${key2}">
</div>
<div>
<input th:value="${key3}">
</div>
</body>
</html>
위 코드가 전부다. 이렇게 하면 웹 브라우저를 통해 http://localhost:8080/MyView 경로에 접근하면 아래와 같은 화면을 볼 수 있다. (html파일은 src/main/resources/templates 하위에 MyView.html로 생성하면 된다) html에서 ${key1}로 명시한 내용이 Model에 "key1"으로 등록한 값인 "value1"으로 치환되어 노출되는것을 볼 수 있다.
어떻게 이런 일이 가능한지 차근차근 알아보자. 우선, @GetMapping 애노테이션을 통해 "/MyView" 경로에 대한 요청을 처리하게 되어있는 showHome 메소드에 Model이라는 인자가 존재하는것을 확인할 수 있다. Model은 당연히 Spring 프레임워크가 제공하는 인스턴스이며, Spring MVC 프레임워크는 우리에게 그 참조를 이런식으로 제공하는 것이다.
우리가 해당 경로로 Get 요청을 했을 때, Spring 프레임워크는 Model 인스턴스를 생성하고 그 참조를 우리의 showHome 메소드에 전달한다. 우리는 showHome 메소드 내부 이 모델 인스턴스에 값들을 세팅하고 리턴했다. 리턴값인 MyView는 해당 요청이 처리된 후 브라우저에 표시할 html의 경로로 쓰인다. 물론 이 작업은 우리 로직으로 하는게 아니고, Spring 프레임워크가 담당한다.
지금은 리턴값도 마찬가지로 MyView로 되어있으므로, 해당 요청을 했을 때 웹 브라우저에는 MyView.html 페이지가 그려지게 된다. 이 때, 우리가 방금 컨트롤러에서 세팅한 모델 인스턴스가 여전히 유효하다는것에 주의하자. 즉, 우리는 html에서 적절하게 이 모델 인스턴스에 접근해 값들을 사용할 수 있다는 뜻이다. 여기서는 th:value="${key1}"로 명시된 부분이 그 역할을 담당한다.
간단하게는, ${key1} 이렇게 명시한 부분이 {} 내부의 내용을 키 값으로 하여 모델에서 값을 찾아 치환한다고 이해하면 된다. 이는 완벽한 설명은 아니기는 하지만, 궁극적으로 본질을 관통하는 설명이다. 결과적으로 <input> 태그는 <input value="value1">과 같은 상태가 되는 셈이다. 이러한 치환 작업은 템플릿 엔진이 수행한다. 템플릿 엔진은 컨트롤러의 응답(이 경우, html 파일의 내용)을 클라이언트(웹 브라우저)로 보내기 전에 이러한 치환 가공을 해주는 역할을 수행한다.
그러면 th: 는 어디로 간 것일까? 이는 결론부터 말하면 뷰에 사용된 템플릿엔진의 이름이다. (앞서 템플릿 엔진에 대해 간단히 언급한적이 있다) 템플릿 엔진에는 JSP나 Thymeleaf 같은 것들이 있다. 이러한 템플릿들은 Spring 프레임워크 뿐 아니라 다른 어떤 플랫폼과도 사용 가능하도록 설계되어있다. 바꿔 말하면, 이 템플릿들이 Spring 프레임워크의 모델 인스턴스에 직접적으로 접근할 수는 없다는 뜻이다.
따라서 Spring 프레임워크는 이러한 뷰 템플릿이 사용하는 '요청 속성'이라는 것에 모델의 내용을 복사한다. 하지만 이는 기술적인 세부사항일 뿐이고, 생각할때는 그냥 모델이라고 생각해도 무방하다. 아무튼, th:는 우리가 사용한 뷰 템플릿인 Thymeleaf를 의미한다. 즉, th:를 명시함으로써 우리의 웹 브라우저는 이 페이지를 그릴 때 Thymeleaf 템플릿을 사용하게 되고, 그 '요청 속성'에 복사되어있는 값들 중(즉, 모델의 값들 중) 키가 key1인 것의 값으로 ${key1}을 치환하는 것이다.
모델 인스턴스는 한번의 요청과 그 수명을 같이한다. 우선 요청이 들어오면 리스터가 반응한다(이 리스너는 Spring 프레임워크에 내장된 톰캣에 구현되어있다) 그리고 리스너는 컨트롤러 메소드를 호출하고, 이 때 모델 인스턴스를 생성해서 인자로 전달한다.
그리고 컨트롤러 메소드가 리턴될 때, Spring 프레임워크는 해당 메소드의 어노테이션에 따라 각기 다른 행동을 한다. 우리는 단순히 @GetMapping 애노테이션만을 사용했는데, 이 경우 리턴값은 템플릿 엔진에서 랜더링할 html 파일의 이름이 된다(이 파일은 templates 경로에 위치해야 한다)
만약 @GetMapping 애노테이션 아래에 @RequestBody 애노테이션을 추가할 경우, 이 컨트롤러의 리턴값이 그냥 응답의 body가 된다. (템플릿 엔진을 사용하지 않는다) 이 경우 String을 직접 구성해서 리턴할수도 있지만, 보통은 객체를 리턴하는 방식으로 사용한다. 그러면 Spring 프레임워크는 이를 json으로 자동 변환해서 응답의 body에 넣는다. 웹 브라우저를 통해 이를 호출해보면 아무런 html 태그 없이 json만 나타나게 된다.
이러한 Spring 프레임워크의 내부 작동 방식에 대해서는 나중에 본격적으로 다룰것이다. 본 절에서는 그냥 뷰와 컨트롤러가 모델이라는 것을 주고받으며 작업을 한다는것을 이해하면 충분하다.
위에서는 컨트롤러가 가지고 있는 (컨트롤러가 DB로부터 가져왔다고 가정했던) 데이터를 뷰에 뿌려주는것을 보았다. Get 요청이 오면 Spring 프레임워크에 의해 모델 인스턴스가 생기고, 컨트롤러의 우리 로직에 의해 이 모델에 값이 채워져 뷰로 전달되었다.
그렇다면 이제 뷰에서 거꾸로 컨트롤러로 데이터를 보내보자. 단, 이 때 사용되는 모델은 우리가 컨트롤러에서 편집해서 보냈던 그 모델과 같은 데이터를 담고있다는 점에 주의하자. 뷰를 구성하는 html은 '로직'을 담을 수 없기 때문에, 뷰에서 입력받아서 모델에 넣어야 하는 데이터가 있다면 애초에 컨트롤러에서 뷰로 보내는 모델에 그 틀이 들어있어야 한다.
@Controller
public class MyController {
@GetMapping("/MyHome")
public String showMyHome(Model model) {
model.addAttribute("key2", new FormClass());
return "MyHome";
}
@PostMapping("/MyHome")
public String processMyHome(FormClass f) {
System.out.println(f.formValue);
return "redirect:/MyHome";
}
}
@Data
public class FormClass {
public String formValue;
}
//main 메소드는 생략
<!DOCTYPE html>
<html>
<head>
<meta charset="EUC-KR">
<title>Insert title here</title>
</head>
<body>
<form method="POST" th:object="${key2}">
<div>
<input th:field="${key2.formValue}">
</div>
<button>submit</button>
</form>
</body>
</html>
localhost:8080/MyHome 경로로 접속하면, showMyHome 메소드에 의해 모델에 <"key2", FormClass인스턴스> 쌍이 추가된다. 그리고 뷰를 구성하는 html에서 th:object="${key2}"를 통해 위에서 했던것과 동일한 방식으로 모델의 값을 가져다 사용하는것을 알 수 있다. 현재 FormClass 인스턴스만 생성하고 formValue 맴버변수에 값을 세팅하지 않았으므로, 아래와같이 페이지의 input 컴포넌트는 비어있는것을 볼 수 있다.
여기에 뭔가를 입력하고 submit 버튼을 눌렀을 때 특별한 일이 생긴다. th:object="${key2}"를 통해, 뷰는 현재 페이지의 폼이 모델의 "key2"에 해당하는 데이터를 다루고 있다는 것을 안다. 따라서 사용자가 폼을 제출하면, 현재 페이지에서 key2와 관련된것들, 예컨데 ${key2.formValue}와 같이 명시된것들을 모델의 해당 인스턴스에 추가해줘야 한다는 사실을 알 수 있다. 그리고 실제로 뷰는 그렇게 작동한다.
여기부터는 다시 Spring의 영역이다. processMyHome 메소드의 인자로 FormClass가 들어있는것을 볼 수 있는데, Spring 프레임워크는 폼으로부터 전달받은 모델(정확히는 뷰 템플릿으로부터 전달받은 '요청 속성')에서 FormClass로의 바인딩을 자동으로 수행한다. 뷰로부터 전달받은 모델을 FormClass로 가정하고 인스턴스를 생성한 뒤, key2.formValue 이런식으로 명시된 것으로부터 값들을 대응시켜 넣을 수 있기 때문에 이런 일이 가능하다.
*입력란에 아무 값이나 입력하고 submit 버튼을 누르면 입력한 값이 콘솔로 출력되는것을 확인할 수 있다.
사실 이번 절의 목적은 위에서 다룬 내용, 즉 뷰와 컨트롤러가 모델을 어떻게 주고받는가 하는 것이다. 하지만 기왕 http 호출에 대해서 알아본 김에, 이것이 실제로 사용되는 다양한 방식에 대해서도 알아보자.
앞의 예에서 우리는 모든 http 요청을 브라우저로만 확인했었다. 하지만, 사실 모든 http 요청이 웹 브라우저와 함께 쓰이는것은 아니다. 아마도 RestfulAPI 혹은 RestAPI라는 말을 들어보았을 것이다. 사실 http요청은 다음 3가지 용도로 사용될 수 있다. 이 방식들은 클라이언트가 받는 응답이 어떤 형식인지, 그리고 그 응답이 가기 전에 어떻게 가공되는지에 따라 구분된다.
서버가 웹 브라우저(클라이언트)로 보내는 응답은 단순한 html 파일일 수 있다. 이는 가장 단순한 형태이며, 가장 원시적인 형태의 웹 서비스이다. 웹 서비스가 원래 연구소에서 문서를 공유하기 위해 만들어졌다는 것을 상기하면 이는 놀랄 일이 아니다. 어떤 주소를 요청하면, 그 주소에 해당하는 페이지(html파일)을 반환해준다. 이를 '정적 컨텐츠'방식이라 부른다.
그러나 오늘날 웹 기술은 훨씬 많이 발전했고, 그 결과 우리는 템플릿 엔진이라는 것을 통해 한번 가공된 html 파일을 반환할수도 있게 되었다. 그리고 심지어 웹 브라우저 없이, 그냥 http 호출을 통해 '메소드를 호출'하는 것처럼 동작하는 API방식으로 서비스를 운용할수도 있다. 이 둘을 '템플릿 엔진'방식, 'API방식'이라 부른다(용어는 조금씩 다를 수 있다) 더 놀라운 것은, spring 프레임워크가 정적 컨텐츠, 템플릿 엔진, API방식 모두를 직접적이고 강력하게 지원한다는 것이다.
먼저 단순히 html파일을 그냥 통째로 응답하는 경우를 보자. hello-static.html파일을 응답으로 전달하고 싶다고 가정하자. 이 파일을 resources/static 하위에 위치시키면, 서버를 실행한 후 웹 브라우저에 localhost:8080/hello-static.html 을 입력했을 때 해당 html이 그대로 웹 브라우저에 랜더링된다. 이를 위해 필요한 작업은 모두 spring 프레임워크에 구현되어있기 때문에, 개발자가 할 일은 단지 html파일을 만들어 저 경로에 넣는 것 뿐이다(컨트롤러 같은것도 필요 없으며, 따라서 단 한 글자의 자바 코드도 필요하지 않다)
템플릿 엔진 방식을 사용하려는 경우, 먼저 템플릿 html파일(hello-template.html라 하자)파일을 resources/templates 하위에 위치시킨다. 그리고 GetMapping따위로 세팅되어 응답을 처리하는 메소드를 구현하고, 이것의 리턴으로 hello-template를 전달한다. 이렇게 하면 템플릿 엔진을 통해 해당 템플릿 파일을 한번 가공해서 브라우저로 응답하게 된다.
그리고 위에서 언급했듯이, 여기서 model 인스턴스를 사용할 수 있다. 컨트롤러에서는 이를 통해 템플릿 엔진에게 필요한 정보를 전달할 수 있다. 아래의 예는 'name'이라는 키로 이름 정보를 전달하고 있다. 템플릿 엔진은 model을 뜯어본 다음, html파일에서 hello 뒤의 ${name}을 인자로 받은 이름으로 치환하여 브라우저에 응답으로 전달할 것이다. 그리고 브라우저는 이를 랜더링할 것이다.
API방식으로 사용하려는 경우, @ResponseBody 애노테이션을 이용한다. 이 경우 그냥 string을 리턴하거나, 객체를 리턴할 수 있다. string을 리턴하는 경우 말 그대로 http 응답의 body에 이 string만 들어간 채로 응답된다. 따라서 만약 브라우저에서 API를 호출한다면, 브라우저는 이를 그대로 화면에 그려줄 것이다(html태그같은것이 없으므로, string내용이 그대로 브라우저에 나타난다) 객체를 리턴하는 경우, spring프레임워크는 자동으로 이를 json으로 바꿔서 body에 넣어 응답한다. 마찬가지로 브라우저를 통해 테스트한 경우라면 브라우저에서 json 텍스트를 볼 수 있을 것이다. API방식의 경우 응답을 받아 브라우저로 뭔가를 보여주려는 것이 아니라, 응답의 값들을 사용하는데 더 관심이 있는 경우에 사용된다. 따라서 브라우저를 통해 보여지는 json이나 string이 '안 예뻐' 보인다고 걱정할 필요는 없다.
본 절에서는 Spring MVC 프레임워크에서 컨트롤러와 뷰가 어떤식으로 모델을 주고받을 수 있는지를 공부했다. 애초에 페이지를 Get할때 이에 대응하는 메소드에서 모델에 필요한 요소들을 초기화해주고, html에서는 이 모델을 ${} 방식으로 접근해 get/set 할 수 있었다. 또한 폼이 제출되었을때에는 자동으로 Spring 프레임워크가 모델을 우리가 원하는 클래스로 바인딩 해주었다. 앞으로는 이를 확장해 보다 전문적인 프로그램을 만들게 될 것이다.
*마지막에 살펴본, http 호출의 3가지 사용 방식의 내용도 잘 익혀두자.
짤막 팁 (0) | 2021.05.25 |
---|---|
서블릿에서 스프링 MVC까지 - 2 (0) | 2021.05.21 |
서블릿에서 스프링 MVC까지 - 1 (0) | 2021.05.12 |
[Spring 해석] 1장. Spring이 시스템을 구성하는 방법 - 2절. 빈을 실체화 시키고 사용하기 (0) | 2020.09.16 |
[Spring 해석] 1장. Spring이 시스템을 구성하는 방법 - 1절. 빈을 등록하고 연결하기 (0) | 2020.09.14 |