스프링 웹 개발 기초
시작
웹 개발 방식
스프링에서는 정적 컨텐츠와 동적 컨텐츠를 모두 처리할 수 있습니다. 정적 컨텐츠는 서버에서 별도의 처리 없이 파일을 그대로 웹 브라우저에 전달하는 방식입니다.
반면 템플릿 엔진은 JSP, PHP와 같이 서버에서 프로그래밍을 통해 동적으로 HTML을 생성하여 전달하는 방식입니다. 이를 통해 더 풍부한 화면을 구성할 수 있습니다.
서버에서 전달하는 리소스는 다음과 같은 방법들이 있습니다.
- 정적 컨텐츠
- 파일을 그대로 웹 브라우저에 전달
- HTML, CSS, JavaScript 등의 정적 파일
- 서버에서 별도의 처리 없이 리소스를 제공
- 동적 컨텐츠 (템플릿 엔진)
- 서버에서 HTML을 동적으로 생성하여 전달
- JSP, Thymeleaf, Freemarker 등의 템플릿 엔진 사용
- 데이터를 가공하여 동적인 화면 구성 가능
- API
- JSON 형태로 데이터 전달
- 서버 간 통신이나 앱/웹 클라이언트에서 사용
- REST API 형태로 구현하는 것이 일반적
정적 컨텐츠
다음과 같이 일반적인 html 파일을 생성합니다.
<!-- src/main/resources/static/hello-static.html -->
<!DOCTYPE HTML>
<html>
<head>
<title>static content</title>
<meta http-equiv="Content-Type" content="text/html; charset=UTF-8" />
</head>
정적 컨텐츠 입니다.
</html>
hello-static.html
파일을 브라우저에서 접속하면 다음과 같은 화면이 표시됩니다.
스프링 부트는 정적 컨텐츠를 처리하는 기본 규칙이 있습니다. 웹 브라우저에서 http://localhost:8080/hello-static.html
로 요청이 들어오면 다음과 같은 순서로 처리됩니다:
- 스프링 컨테이너에서 먼저
hello-static
관련 컨트롤러를 찾습니다 - 컨트롤러가 없다면
resources/static/hello-static.html
파일을 찾습니다 - 파일이 존재하면 해당 파일을 웹 브라우저에 그대로 전달합니다
이처럼 스프링 부트는 /static
, /public
, /resources
, /META-INF/resources
디렉토리에 있는 정적 리소스들을 자동으로 제공합니다. 우리가 만든 hello-static.html
은 resources/static
경로에 있기 때문에 별도의 컨트롤러 없이도 웹 브라우저에서 직접 접근이 가능한 것입니다.
그림으로는 다음과 같습니다.
MVC와 템플릿 엔진
MVC는 Model, View, Controller의 약자로, 애플리케이션을 구성하는 요소를 세 가지 역할로 구분한 디자인 패턴입니다.
-
Model : 비즈니스 로직과 데이터를 처리하는 부분입니다. 데이터베이스와의 연동, 데이터의 유효성 검사 등을 담당합니다.
-
View : 사용자에게 보여지는 화면을 담당합니다. HTML, CSS 등을 사용하여 데이터를 시각적으로 표현하며, 사용자와의 상호작용을 위한 인터페이스를 제공합니다.
-
Controller : Model과 View를 연결하는 중간 다리 역할을 합니다. 사용자의 요청을 받아 적절한 Model에 전달하고, Model에서 처리된 데이터를 View에 전달하여 화면에 표시되도록 조정합니다.
이러한 MVC 패턴을 사용하면 각 역할이 명확히 분리되어 코드의 유지보수가 쉽고, 재사용성이 높아지며, 개발자 간의 협업이 용이해집니다.
예제를 통해 이해해보겠습니다.
Controller
// src/main/java/hello/hello_spring/controller/HelloController.java
// ...
// @GetMapping: "/hello-mvc" 경로로 들어오는 GET 요청을 처리
// @RequestParam: HTTP 요청 파라미터를 메서드 파라미터로 받음
// name: 요청 파라미터의 이름이 "name"인 값을 받아옴
// Model: 뷰에 전달할 데이터를 담는 객체
@GetMapping("hello-mvc")
public String helloMvc(@RequestParam("name") String name, Model model) {
// Model에 "name"이라는 키로 파라미터로 받은 name 값을 추가
model.addAttribute("name", name);
// "hello-template" 뷰 템플릿을 찾아 반환
return "hello-template"
}
위 컨트롤러는 다음과 같은 요청을 받습니다.
GET /hello-mvc?name=Spring
이때 @RequestParam
어노테이션의 경우 쿼리 파라미터를 받으며 required가 true 이므로 선택적으로 수집할 경우 false 로 설정해야 합니다.
View
<!-- src/main/resources/templates/hello-template.html -->
<!doctype html>
<html xmlns:th="http://www.thymeleaf.org">
<head>
<meta charset="UTF-8">
<title>Hello Template</title>
</head>
<body>
<!-- th:text - Thymeleaf의 텍스트 표현식으로 동적 텍스트 생성 -->
<!-- ${name} - 컨트롤러에서 전달된 Model의 name 속성값을 표시 -->
<!-- "hello! empty" - 서버 없이 HTML을 열었을 때 보여질 기본값 -->
<p th:text="'hello ' + ${name}">hello! empty</p>
</body>
</html>
템플릿 코드를 작성한 후 브라우저로 http://localhost:8080/hello-mvc?name=geon
으로 접속하면 다음과 같은 화면이 표시됩니다.
번외로 Thymeleaf의 장점 중 하나는 서버 없이도 HTML 파일을 직접 열어볼 수 있다는 점입니다. 이는 HTML 마크업 작업을 하는 퍼블리셔들이 별도의 서버 구동 없이도 화면을 확인하고 작업할 수 있게 해줍니다. Thymeleaf는 서버가 없을 때는 th:text와 같은 속성들을 무시하고 미리 작성해둔 기본값을 보여주기 때문입니다.
정리하자면 아래와 같습니다.
- 컨트롤러에서
GET /hello-mvc?name=geon
요청을 받음
- 컨트롤러는 MVC 패턴에서 C(Controller)에 해당하며, 사용자의 요청을 처리하고 비즈니스 로직을 조정하는 역할을 담당
@Controller
어노테이션을 통해 해당 클래스가 컨트롤러임을 명시@GetMapping("hello-mvc")
어노테이션으로 "/hello-mvc" URL에 대한 GET 요청을 이 메서드가 처리하도록 매핑
- 해당 메서드는
Model
을 파라미터로 받아 "data"라는 키에 쿼리 파라미터로 받은 name 값을 추가 (이는 MVC 패턴의 Model에 해당하며, 컨트롤러와 뷰 사이에서 데이터를 전달하는 역할을 함)
- 컨트롤러는 필요한 비즈니스 로직을 수행한 후 그 결과를 Model에 담아 View로 전달
- 뷰 리졸버(View Resolver)는 컨트롤러가 반환한 뷰 이름을 실제 뷰 파일 경로로 매핑하는 스프링의 컴포넌트입니다. 여기서는
hello-template
라는 뷰 이름을resources/templates/hello-template.html
이라는 실제 파일 경로로 변환하여 렌더링
resources/templates/hello-template.html
파일을 찾음- Thymeleaf 템플릿 엔진이 파일을 처리
${name}
를 model 에 설정한 "geon"로 치환
- 렌더링된 템플릿이 브라우저에 표시
- "hello geon"가 화면에 출력됨
API
API는 애플리케이션 간 통신을 위한 인터페이스로, 데이터를 주고받는 형식을 정의한 것입니다. 주로 JSON 형태로 데이터를 전달하며, REST API 형태로 구현하는 것이 일반적입니다.
최근에는 프론트엔드와 백엔드를 분리하는 구조가 많이 사용되면서, 백엔드에서 API를 제공하고 프론트엔드에서 이를 소비하는 형태의 개발이 증가하고 있습니다. 하지만 이는 MVC 패턴을 대체하는 것이 아니며, 사용 사례에 따라 적절한 방식을 선택하는 것이 중요합니다.
스프링 MVC 패턴은 SSR(Server Side Rendering) 방식을 사용하며, 클라이언트가 서버에 요청을 보내면 서버가 렌더링된 HTML을 반환하는 방식입니다. 이때 서버는 데이터를 처리하고 화면을 렌더링하는 역할을 담당합니다.
반면 API는 클라이언트가 서버에 요청을 보내면 서버가 데이터만 반환하는 방식입니다. 이때 서버는 데이터를 처리하는 역할만 담당하며, 화면을 렌더링하는 역할은 클라이언트에게 맡깁니다. 이러한 형태의 개발은 프론트엔드와 백엔드를 분리하는 구조로 이루어집니다.
최근 들어서는 Next.js 가 나타나면서 기존 클라이언트 측에서 렌더링 하는 부분을 다시 또 서버측에서 렌더링 하는 형태로 변화하고 있습니다.
Controller
컨트롤러를 작성합니다.
// src/main/java/hello/hello_spring/controller/HelloController.java
// ...
// @ResponseBody: HTTP 응답 본문에 직접 데이터를 작성하도록 지시
// 문자열을 그대로 반환하는 API
@GetMapping("hello-string")
@ResponseBody
public String helloString(@RequestParam("name") String name) {
return "hello " + name;
}
// 객체를 JSON 형태로 반환하는 API
@GetMapping("hello-api")
@ResponseBody
public Hello helloApi(@RequestParam("name") String name) {
Hello hello = new Hello();
hello.setName(name);
return hello;
}
// API 응답에 사용할 내부 정적 클래스
static class Hello {
// name 필드는 private으로 캡슐화
private String name;
// name 필드의 getter 메서드
public String getName() {
return name;
}
// name 필드의 setter 메서드
public void setName(String name) {
this.name = name;
}
}
위와 같이 컨트롤러를 작성한 후 브라우저로 http://localhost:8080/hello-api?name=geon
으로 접속하면 다음과 같은 화면이 표시됩니다.
흐름은 다음과 같습니다.
- 컨트롤러에서
GET /hello-api?name=geon
요청을 받음 @ResponseBody
어노테이션으로 인해 HTTP 응답 본문에 직접 데이터를 작성하도록 지시- 컨트롤러는 데이터를 처리하고 객체를 반환
HttpMessageConverter
가 동작하여 객체를 변환- 객체인 경우 기본으로
MappingJackson2HttpMessageConverter
가 동작 - 객체를 JSON 형태로 변환하여 클라이언트에게 전달
- 클라이언트는 반환된 JSON 데이터를 받아 처리
- 문자열인 경우
StringHttpMessageConverter
가 동작 - 객체인 경우
MappingJackson2HttpMessageConverter
가 동작 - 단순 GET 요청으로 JSON 데이터가 브라우저에 표시
💡 Jackson 라이브러리는 자바 객체를 JSON으로 변환하는 라이브러리