•ms.getMessage("hello", null, null) : locale 정보가 없으므로 messages를 사용 •ms.getMessage("hello", null, Locale.KOREA) : locale 정보가 있지만, message_ko가 없으므로 messages 를 사용
• 스프링의 SpringEL 문법 통합 • ${@myBean.doSomething()} 처럼 스프링 빈 호출 지원 • 편리한 폼 관리를 위한 추가 속성 ◦th:object (기능 강화, 폼 커맨드 객체 선택) ◦th:field , th:errors , th:errorclass
• 폼 컴포넌트 기능 ◦checkbox, radio button, List 등을 편리하게 사용할 수 있는 기능 지원 • 스프링의 메시지, 국제화 기능의 편리한 통합 • 스프링의 검증, 오류 처리 통합 • 스프링의 변환 서비스 통합(ConversionService)
• th:object : 커맨드 객체를 지정 • *{...} : 선택 변수 식이라고 한다. th:object 에서 선택한 객체에 접근 • th:field : HTML 태그의 id , name , value 속성을 자동으로 처리해줌 - 렌더링 전 <input type="text" th:field="*{itemName}" /> - 렌더링 후 <input type="text" id="itemName" name="itemName" th:value="*{itemName}" />
2. 등록 폼 예제
2.1 controller 소스
@GetMapping("/add")
public String addForm(Model model) {
model.addAttribute("item", new Item());
return "form/addForm";
}
2.2 Item 객체
@Setter @Getter
public class Item {
private Long id;
private String itemName;
private Integer price;
private Integer quantity;
public Item(String itemName, Integer price, Integer quantity) {
this.itemName = itemName;
this.price = price;
this.quantity = quantity;
}
}
※ HTML의 id가 타임리프에 의해 동적으로 만들어지기 때문에 <label for="id 값"> 으로 label의 대상이 되는 id 값을 임의로 지정할수 없으나, 타임리프는 ids.prev(...) , ids.next(...)을 제공해서 동적으로 생성되는 id 값을 사용할 수 있도록 함.
3. 멀티 체크 박스 form 전송 시 controller 처리
•서울, 부산 선택 후 form 데이터 전송 시
- 로그: item.regions=[SEOUL, BUSAN] - form에서 전송된 데이터 : regions=SEOUL&_regions=on®ions=BUSAN&_regions=on&_regions=on
•지역 선택 없이 form 데이터 전송 시
- 로그: item.regions=[] - form에서 전송된 데이터 :_regions=on&_regions=on&_regions=on
※ _regions는 앞선 checkbox 예제와 같이 웹 브라우저에서 체크를 하나도 하지 않았을 때, 클라이언트가 서버에 아무런 데이터를 보내지 않는 것을 방지함.
라디오 버튼
1. controller에 라디오 버튼 관련 데이터 로직 추가
@ModelAttribute("itemTypes")
public ItemType[] itemTypes() {
return ItemType.values();
}
•itemTypes를 등록 폼, 조회, 수정 폼에서 모두 사용하므로 @ModelAttribute사용
•ItemType.values() 를 사용하면 해당 ENUM의 모든 정보를 배열로 반환함 예) [BOOK, FOOD, ETC]
•Object 표현 - ${user.username} : user의 username을 프로퍼티로 접근 → user.getUsername() - ${user['username']} : 위와 같음 → user.getUsername() - ${user.getUsername()} : user의 getUsername() 을 직접 호출
• List 표현
- ${users[0].username} : List에서 첫 번째 회원을 찾고 username 프로퍼티 접근→ ${list.get(0).getUsername()} - ${users[0]['username']} : 위와 같음 - ${users[0].getUsername()} : List에서 첫 번째 회원을 찾고 메서드 직접 호출
• Map 표현
- ${userMap['userA'].username : Map에서 userA를 찾고, username 프로퍼티 접근 → map.get("userA").getUsername() - ${userMap['userA']['username'] : 위와 같음 - ${userMap['userA'].getUsername() : Map에서 userA를 찾고 메서드 직접 호출
• 지역변수 선언
<div th:with="first=${users[0]}">
<p>처음 사람의 이름은 <span th:text="${first.username}"></span></p>
</div>
- th:with를 선언하여 지역변수 생성, 지역변수는 선언한 태그안에서만 사용가능
기본 객체들
1. 타임리프가 제공하는 기본 객체들
•${#request} - 스프링 부트 3.0부터 제공하지 않는다.
•${#response} - 스프링 부트 3.0부터 제공하지 않는다.
•${#session} - 스프링 부트 3.0부터 제공하지 않는다.
•${#servletContext} - 스프링 부트 3.0부터 제공하지 않는다.
•${#locale}
2. 스프링부트 3.0 미만 예제
2.1 controller 소스
@GetMapping("basic-objects")
public String basicObejct(HttpSession session) {
session.setAttribute("sessionData", "Hello Session");
return "basic/basic-objects";
}
@Component("helloBean")
static class HelloBean{
public String hello(String data) {
return "hello "+data;
}
}
•반복 기능 ex) <tr th:each="user : ${users}"> - 반복시 오른쪽 컬렉션( ${users} )의 값을 하나씩 꺼내 왼쪽 변수( user )에 담아서 태그를 반복 실행 - th:each는 List 뿐만 아니라 배열, java.util.Iterable, java.util.Enumeration을 구현한 모든 객체를 반복에 사용할 수 있음.
- Map도 사용할 수 있는데 이 경우 변수에 담기는 값은 Map.Entry임
•반복 상태 유지 ex) <tr th:each="user, userStat : ${users}"> - 반복의 두번째 파라미터를 설정해서 반복의 상태를 확인 할 수 있음 -두번째 파라미터는 생략 가능한데, 생략하면 지정한 변수명 user + Stat가 됨 -여기서는 user + Stat = userStat 이므로 생략 가능
•반복 상태 유지 기능 - index : 0부터 시작하는 값 - count : 1부터 시작하는 값 - size : 전체 사이즈 - even , odd : 홀수, 짝수 여부( boolean ) - first , last :처음, 마지막 여부( boolean ) - current : 현재 객체
조건부 평가
1. 예제
1.1 controller 소스
@GetMapping("/condition")
public String condition(Model model) {
addUser(model);
return "basic/condition";
}
public void addUser(Model model) {
List<User> list = new ArrayList<>();
list.add(new User("userA", 10));
list.add(new User("userB", 30));
list.add(new User("userC", 20));
list.add(new User("userD", 33));
model.addAttribute("users",list);
}
•if, unless - 타임리프는 해당 조건이 맞지 않으면 태그 자체를 렌더링하지 않음 - 만약 다음 조건이 false 인 경우 <span>...<span> 부분 자체가 렌더링 되지 않고 사라짐 <span th:text="'미성년자'" th:if="${user.age lt 20}"></span>
•switch - * 은 만족하는 조건이 없을 때 사용하는 디폴트 조건
주석
1. 예제
1.1 controller 소스
@GetMapping("/comments")
public String comment(Model model) {
model.addAttribute("data","Spring!");
return "basic/comments";
}
•표준 HTML 주석 (<!-- ... -->) - 자바스크립트의 표준 HTML 주석은 타임리프가 렌더링 하지 않고, 그대로 남겨둠
•타임리프 파서 주석(<!--/* ... */--> , <!--/*--> ... <!--*/-->) - 타임리프 파서 주석은 타임리프의 진짜 주석이다. 렌더링에서 주석 부분을 제거함
•타임리프 프로토타입 주석(<!--/*/ ... /*/-->) - HTML 파일을 웹 브라우저에서 그대로 열어보면 HTML 주석이기 때문에 이 부분이 웹 브라우저가 렌더링하지 않음 - 타임리프 렌더링을 거치면 이 부분이 정상 렌더링 됨. - 쉽게 이야기해서 HTML 파일을 그대로 열어보면 주석처리가 되지만, 타임리프를 렌더링 한 경우에만 보이는 기능
블록
1. 예제
1.1 controller 소스
@GetMapping("/block")
public String block(Model model) {
addUser(model);
return "basic/block";
}
public void addUser(Model model) {
List<User> list = new ArrayList<>();
list.add(new User("userA", 10));
list.add(new User("userB", 30));
list.add(new User("userC", 20));
list.add(new User("userD", 33));
model.addAttribute("users",list);
}
•타임리프의 특성상 HTML 태그안에 속성으로 기능을 정의해서 사용하는데, 위와 같이 2군데의 div태그에서 model에 담긴 ${users}리스트를 사용할 경우 사용이 애매함
• 이럴경우 <th:block>을 사용하여 해결 가능
자바스크립트 인라인
1. 예제
1.1 controller 소스
@GetMapping("/javascript")
public String javascript(Model model) {
model.addAttribute("user", new User("UserA", 10));
addUser(model);
return "basic/javascript";
}
1.2 thyemleaf 소스
<!DOCTYPE html>
<html xmlns:th="http://www.thymeleaf.org">
<head>
<meta charset="UTF-8">
<title>Title</title>
</head>
<body>
<!-- 자바스크립트 인라인 사용 전 -->
<script>
var username = [[${user.username}]];
var age = [[${user.age}]];
//자바스크립트 내추럴 템플릿
var username2 = /*[[${user.username}]]*/ "test username";
//객체
var user = [[${user}]];
</script>
<!-- 자바스크립트 인라인 사용 후 -->
<script th:inline="javascript">
var username = [[${user.username}]];
var age = [[${user.age}]];
//자바스크립트 내추럴 템플릿
var username2 = /*[[${user.username}]]*/ "test username";
//객체
var user = [[${user}]];
</script>
<!-- 자바스크립트 인라인 each -->
<script th:inline="javascript">
[# th: each = "user, stat : ${users}"]
var user[[${stat.count}]] = [[${user}]];
[/]
</script>
</body>
</html>
• 자바스크립트 인라인 적용
<script th:inline="javascript">...</script>
•텍스트 렌더링 - var username = [[${user.username}]];
인라인 사용 전 결과 소스 var username = userA; 인라인 사용 후 결과 소스 var username = "userA";
- 인라인 사용 전 렌더링 결과를 보면 userA라는 변수 이름이 그대로 남아있음.
- 개발자가 기대한 것은 다음과 같은 "userA"라는 문자일 것 ※ 인라인 사용을 안할 경우 userA라는 변수명으로 사용되어서 자바스크립트 오류가 발생
※ 숫자 age의 경우에는 " 가 필요 없기 때문에 정상 렌더링 됨 ※ 인라인을 사용 할경우 렌더링 결과를 보면 문자 타입인 경우 자동으로 " 를 포함해줌
※ 추가로 자바스크립트에서 문제가 될 수 있는 문자가 포함되어 있으면 이스케이프 처리도 해줌. ex) " → \"
• 자바스크립트 내추럴 템플릿
- var username2 = /*[[${user.username}]]*/ "test username";
※ 의도 : 타임리프 없이 실행할 경우 "test username" 적용, 타임리프 렌더링 시 [[${user.username}]] 적용
인라인 사용 전 결과 소스 var username2 = /*userA*/ "test username"; 인라인 사용 후 결과 소스 var username2 = "userA";
- 인라인 사용 전 결과를 보면 정말 순수하게 그대로 해석 함.
- 따라서 내추럴 템플릿 기능이 동작하지 않고, 심지어 렌더링 내용이 주석처리 되어 버림 - 인라인 사용 후 결과를 보면 주석 부분이 제거되고, 기대한 "userA"가 정확하게 적용
• 객체
※ 타임리프의 자바스크립트 인라인 기능을 사용하면 객체를 JSON으로 자동으로 변환해줌 - var user = [[${user}]];
인라인 사용 전 결과 소스 var user = BasicController.User(username=userA, age=10); 인라인 사용 후 결과 소스 var user = {"username":"userA","age":10};
- 인라인 사용 전 소스는 객체의 toString()이 호출된 값 - 인라인 사용 후 소스는 객체를 JSON으로 자동으로 변환해줌
•자바스크립트 인라인 each 소스
<!-- 자바스크립트 인라인 each -->
<script th:inline="javascript">
[# th:each="user, stat : ${users}"]
var user[[${stat.count}]] = [[${user}]];
[/]
</script>
•자바스크립트 인라인 each 결과
<script>
var user1 = {"username":"userA","age":10};
var user2 = {"username":"userB","age":20};
var user3 = {"username":"userC","age":30};
</script>
템플릿 조각
1. 설명
•웹 페이지를 개발할 때는 상단 영역이나 하단 영역, 좌측 카테고리 등등 여러 페이지에서 함께 사용하는 영역들과 같은 공통 영역이 많이 있음.
•타임리프는 공통영역 처리를 위해 템플릿 조각과 레이아웃 기능을 지원.
2. 예제
2.1 controller 소스
@GetMapping("/fragment")
public String template() {
return "template/fragment/fragmentMain";
}
※ 이렇게 요구사항과 도메인, 화면이 어느정도 정리되면 웹 퍼블리셔, 백엔드 개발자가 업무를 나눠 진행함 •디자이너: 요구사항에 맞도록 디자인 후 디자인 결과물을 웹 퍼블리셔에게 전달. •웹 퍼블리셔: 디자이너에게 받은 디자인을 기반으로 HTML, CSS를 만들어 개발자에게 전달 •백엔드 개발자: HTML화면을 받기전까지 시스템 설계및 핵심 비즈니스 모델을 개발한다. 이후 HTML을 전달받으면 뷰 템플릿으로 변환 후 화면을 그리고 제어
상품 도메인 개발
1. 상품 도메인
1.1 필요한 상품 도메인 필드
• 상품 아이디 • 상품 명 • 가격 • 수량
1.2 상품 도메인 필드 코드
package hello.itemservice.domain.item;
import lombok.Data;
@Data
public class Item {
private Long id;
private String itemName;
private Integer price;
private Integer quantity;
public Item(String itemName, Integer price, Integer quantity) {
this.itemName = itemName;
this.price = price;
this.quantity = quantity;
}
}
2. 상품 저장소
2.1 필요한 상품 저장소 기능
• 상품 목록 • 상품 상세 • 상품 등록 • 상품 수정
2.2 상품 저장소 코드
package hello.itemservice.domain.item;
import org.springframework.stereotype.Repository;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
@Repository
public class ItemRepository {
// concurrenthashmap으로 생성해야 thread-safe하기 때문에 실제로는 그냥 HashMap을 안씀
private static final Map<Long, Item> store = new HashMap<>();
private static long sequence = 0L;
public Item save(Item item) {
item.setId(++sequence);
store.put(item.getId(), item);
return item;
}
public Item findById(Long id) {
return store.get(id);
}
public List<Item> findAll() {
return new ArrayList<>(store.values());
}
public void update(Long itemId, Item updateParam) {
Item findItem = findById(itemId);
findItem.setItemName(updateParam.getItemName());
findItem.setPrice(updateParam.getPrice());
findItem.setQuantity(updateParam.getQuantity());
}
public void clearStore() {
store.clear();
sequence = 0L;
}
}
• 기본적인 상품 저장, 조회, 목록조회, 수정 기능을 추가 • 개발환경에서 리포지토리내에 store 콜렉션을 초기화 해주기 위해 clearStore를 구현 • 아이디는 전역변수로 선언된 sequance를 활용해 할당해줌 (멀티스레드 환경에서 이슈가 생길 수 있음)
• 저장소는 아이디와 상품도메인 Item으로 HashMap 사용(멀티스레드 환경에서 이슈가 생길 수 있음)
상품 목록 구현
1. 상품 목록 controller 구현
package hello.itemservice.web.basic;
import hello.itemservice.domain.item.Item;
import hello.itemservice.domain.item.ItemRepository;
import lombok.RequiredArgsConstructor;
import org.springframework.stereotype.Controller;
import org.springframework.ui.Model;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import javax.annotation.PostConstruct;
import java.util.List;
@Controller
@RequestMapping("/basic/items")
// 롬복(Lombok)에서 제공하는 애노테이션으로 final이 붙은 멤버 변수만 사용해 생성자를 자동으로 만들어줌
// 생성자를 통해 해당 멤버변수를 자동 주입해줌
@RequiredArgsConstructor
public class BasicItemController {
private final ItemRepository itemRepository;
@GetMapping
public String items(Model model) {
List<Item> items = itemRepository.findAll();
model.addAttribute("items", items);
return "/basic/items";
}
// 컨트롤러만 구현하고 테스트를 하면 노출할 상품이 없기 때문에 프로젝트 로드시
// 해당 빈의 의존관계가 모두 주입된 후 초기화 용도로 호출
// 첨부된 메소드 init()을 수행해 2개의 아이템을 미리 추가
@PostConstruct
public void init() {
itemRepository.save(new Item("itemA", 10000, 10));
itemRepository.save(new Item("itemB", 20000, 20));
}
}
• @PostConstructor
- 컨트롤러만 구현하고 테스트를 하면 노출할 상품이 없기 때문에 프로젝트 로드시 해당 빈의 의존관계가 모두 주입된 후 초기화 용도로 호출됨
- 첨부된 메소드 init()을 수행해 2개의 아이템을 미리 추가
• @RequiredArgsConstructor - 롬복(Lombok)에서 제공하는 애노테이션으로 final이 붙은 멤버 변수만 사용해 생성자를 자동으로 만들어줌
• 타임리프를 HTML 페이지에서 사용하기 위해선 다음과같이 html 태그에 작성해줘야 함.
<html xmlns:th="http://www.thymeleaf.org">
2. 속성 변경 - th:href
th:href="@{/css/bootstrap.min.css}"
• 기존 href="value1"를 th:href="value2"로 변경해줌 • 타임리프 뷰 템플릿을 거치면 원래 값을 th:xxx 으로 변경. 만약 값이 없다면 새로 생성. • HTML을 그대로 볼 때는 href 속성이 그대로 사용되고 뷰 템플릿을 거치면 th:href의 값이 href로 대치됨•
•대부분의 HTML 태그의 속성을 th:xxx 로 변경할 수 있음
3. 타임리프 핵심
•th:xxx부분은 서버사이드에서 렌더링되고 기존 HTML 태그 속성을 대치함
•만약 th:xxx이 없으면 기존 html의 xxx속성이 그대로 사용됨 • HTML파일을 그냥 탐색기로 열어도 th:xxx 속성을 웹 브라우저에서는 읽지 못하기에 무시하고 기본 xxx속성을 읽어서 웹페이지는 깨지지않고 렌더링됨
※ 이를 네추럴 템플릿이라고 함.
4. URL링크 표현식 - @{...}
th:href="@{/css/bootstrap.min.css}"
•URL 링크를 사용하는 경우 @{...}를 사용하는데 이를 URL링크 표현식이라 함. •URL 링크 표현식을 사용하면 서블릿 컨텍스트를 자동으로 포함함.
※ 기존에는 자바의 문자열 결합처럼 +연산자와 escape 기호를 사용해 하나하나 작성해야했지만, 리터럴 대체 문법을 사용하면 자바스크립트의 백틱(``)처럼 편리하게 사용 가능.
6. th:each 반복 출력
<tr th:each="item : ${items}">
•반복은 th:each를 사용
- 모델에 포함된 items 컬렉션 데이터가 item 변수에 하나씩 포함되고, 반복문 안에서 item 변수를 사용할 수 있음
- 컬렉션의 수 만큼 <tr>...</tr> 이 하위 태그를 포함해서 생성.
7. 변수 표현식 - ${...}
<td th:text="${item.price}">10000</td>
•모델에 포함된 값이나, 타임리프 변수로 선언한 값을 조회할 수 있음.
내부적으로 item.getPrice() 프로퍼티 접근법을 사용함.
8. URL 링크 표현식2 - @{...}
th:href="@{/basic/items/{itemId}(itemId=${item.id})}"
<!-- 위와 같은 코드. 리터럴 대체 문법 활용가능. -->
th:href="@{|/basic/items/${item.id}|}"
•경로를 템플릿처럼 사용할 수 있다. •경로변수({itemId}) 뿐 아니라 쿼리 파라미터도 생성할 수 있음 ex) th:href="@{/basic/items/{itemId}(itemId=${item.id}, query='test')}" 생성 링크 - http://localhost:8080/basic/items/1?query=test
•리터럴 대체 문법을 사용해 간단하게 사용할 수도 있음
상품 상세
1. 상품 상세 controller 구현
@GetMapping("/{itemId}")
public String item(Model model, @PathVariable Long itemId) {
Item item = itemRepository.findById(itemId);
model.addAttribute("item", item);
return "/basic/item";
}
•PathVariable로 넘어온 아이템 아이디로 상품을 조회 후 모델에 추가해 뷰 템플릿을 호출
- HTML form에서 action 에 값이 없으면 현재 URL에 데이터를 전송 - 상품 등록 폼의 URL과 실제 상품 등록을 처리하는 URL을 똑같이 맞추고 HTTP 메서드로 두 기능을 구분.
상품 등록 폼: GET /basic/items/add 상품 등록 처리: POST /basic/items/add ※ 이렇게 하면 하나의 URL로 등록 폼과, 등록 처리를 깔끔하게 처리할 수 있음.
상품 등록 처리 : @ModelAttribute
※ HTML Form, HTTP 메서드는 POST 방식을 사용해 데이터를 전송 •Content-Type: application/x-www-form-urlencoded •메세지 바디에 쿼리 파라미터 형식으로 전달 - itemName=name&price=10000&quantity=10
1. 상품 등록 처리 controller - @RequestParam로 처리 v1
@PostMapping("/add")
public String saveLegacy(@RequestParam String itemName,
@RequestParam int price,
@RequestParam Integer quantity,
Model model) {
Item item = new Item();
item.setItemName(itemName);
item.setPrice(price);
item.setQuantity(quantity);
Item save = itemRepository.save(item);
model.addAttribute("item", save);
return "basic/item";
}
•@RequestParam으로 요청 파라미터 데이터를 해당 변수에 각각 받는다 •Item 객체를 생성해 전달받은 파라미터로 값을 세팅한 뒤 itemRepository저장. •저장된 item을 Model 객체에 담아 뷰에 전달
2.상품 등록 처리 controller - @ModelAttribute로 처리 v2
※ 위 방법은 간단하게 아이템을 저장하는 로직임에도 불구하고 코드가 너무 김.
전달받는 3개의 요청 파라미터도 결국 하나의 객체를 만들기 위한 파라미터들이기에 이를 한번에 매핑시켜객체로 받을수있는 @ModelAttribute애노테이션이 있음.
@PostMapping("/add")
// 요청 파라미터를 프로퍼티 접근법으로 Item 객체를 생성 하고(V1의 setXxx처럼)
// model에 넣어줌(@ModelAttribute("item")의 "item"이라는 이름으로)
public String saveV2(@ModelAttribute("item") Item item, Model model) {
itemRepository.save(item);
// model.addAttribute("item", item); // 자동 추가 생략가능
return "basic/item";
}
•@ModelAttribute 애노테이션을 활용해 요청 파라미터를 처리해줌. - Item 객체를 생성 후, 요청 파라미터의 값을 프로퍼티 접근법(setXxx)으로 입력해줌
•@ModelAttribute - Model 자동 추가 - 위 코드를 보면 Model 객체에 저장된 item을 추가해주는 로직을 주석처리함.
- Model에 @ModelAttribute로 지정한 객체를 자동으로 넣어줌
•모델에 데이터를 담을때는 이름이 필요한데 이름은 @ModelAttribute 애노테이션에 지정한 속성("item")을 사용함
- 만약 다음과 같이 @ModelAttribute의 이름을 다르게 지정하면 다른 이름으로 모델에 포함된다.
이름을 hello 로 지정 하면 → @ModelAttribute("hello") Item item
모델에 hello 이름으로 저장 → model.addAttribute("hello", item)
3.상품 등록 처리 controller - @ModelAttribute 이름 생략 처리 v3
@PostMapping("/add")
// @ModelAttribute의 이름을 넣지 않으면 클래스명(Item)의 첫글자만 소문자로 변경해서 model.addAttribute에 등록함
// Ex) @ModelAttribute HelloItem item → model.addAttribute("helloItem", item);
public String saveV3(@ModelAttribute Item item, Model model) {
itemRepository.save(item);
// model.addAttribute("item", item); // 자동 추가 생략가능
return "basic/item";
}
•@ModelAttribute 애노테이션에서 name 속성을 생략할수도 있음 - 생략하면 모델에 저장될 때 클래스명에서 첫 글자를 소문자로 변경해 등록함. ex) @ModelAttribute HelloWord helloWord이면 Model.addAttribute("helloWord", helloWord) 와 같다.
4.상품 등록 처리 controller - @ModelAttribute 전체 생략처리 v4
•상품등록페이지 및 수정페이지에서 등록이 완료된상태에서 새로고침을 하면 마지막으로 요청했던 URL 경로로 재요청을 하게 됨
•마지막에 Post 방식으로 상품등록을 했다면 해당 상품등록 요청이 재전송되어 중복등록되는 치명적인 문제가 생김.
• Post메서드로 요청이 들어온 /add Url을 처리하여 itemRepository에 저장 후 바로 view를 보여줌.
• 브라우저는 Url 정보와 post메서드로 보낸 데이터를 그대로 가지고 있어서 새로 고침 시 계속 그 url과 데이터를 가지고 다시 /add Url을 처리함.
2. PRG : Post/Redirect/Get을 이용하여 해결
•웹 브라우저의 새로 고침은 마지막에 서버에 전송한 URL로 데이터를 다시 전송함 •새로 고침 문제를 해결하려면 상품 저장 후에 뷰 템플릿으로 이동하는 것이 아니라, 상품 상세 화면으로 리다이렉트로 다른 URL(/basic/items/{id})을 호출해주면 됨 •웹 브라우저는 리다이렉트의 영향으로 상품 저장 후에 상품 상세 화면으로 이동함 •마지막에 호출한 내용이 상품 상세 화면인 GET /items/{id} 가 됨. •이후 새로고침을 해도 GET 요청은 멱등성을 보장하기에 새로 고침 문제를 해결할 수 있음.
3. PRG 적용한 상품 등록 controller 코드
@PostMapping("/add")
public String saveV5(Item item) {
itemRepository.save(item);
return "redirect:/basic/items/"+item.getId();
}
•return "redirect:/basic/items/" + item.getId(); - URL에 변수를 더해 사용하는 것은 URL 인코딩이 안되기에 위험함. ※ RedirectAttributes를 사용해서 해결 가능
4. PRG 적용한 상품 등록 controller 코드 - RedirectAttributes 적용
// RedirectAttributes 적용(URL 인코딩 기능, @PathVariable 기능, 쿼리 파라미터 추가 기능 활용가능)
@PostMapping("/add")
public String saveV6(Item item, RedirectAttributes redirectAttributes) {
Item savedItem = itemRepository.save(item);
// redirect와 관련된 속성을 넣어줌
redirectAttributes.addAttribute("itemId", savedItem.getId());
// 치환자가 없는 redirectAttributes attribute는 쿼리 파라미터 형식으로 들어감
redirectAttributes.addAttribute("status", true);
// redirectAttributes에 넣은 "itemId"값이 @PathVariable처럼 치환이 가능
return "redirect:/basic/items/{itemId}";
}
/* getClass()메서드를 통해 사용되는 클래스 타입 반환하여 삽입 */
private Logger log = LoggerFactory.getLogger(getClass());
/* 직접적으로 해당 클래스타입을 입력해줘도 된다. */
private static final Logger log = LoggerFactory.getLogger(Xxx.class);
• 롬복 사용
@Slf4j
public class TestController {
...
}
1.3 코드
package hello.springmvc.basic;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import lombok.extern.slf4j.Slf4j;
//@Controller // 반환값이 String이면 뷰 이름으로 인식하기에 뷰를 찾고 뷰가 렌더링된다.
//@Slf4j // private final Logger log = LoggerFactory.getLogger(getClass()); 안써도됨 lombok에서 제공
@RestController // return 값으로 뷰를 찾는 것이 아니라, HTTP 메시지 바디에 바로 입력한다.
public class LogTestController {
//private final Logger log = LoggerFactory.getLogger(LogTestController.class);
private final Logger log = LoggerFactory.getLogger(getClass());
@RequestMapping("/log-test")
public String logTest() {
String name = "Spring";
System.out.println("name = "+ name);
// 이렇게 사용하면 안됨
// argument에 +를 먼저 연산하고 메모리를 할당함, 쓸데없는 리소스를 낭비하게됨
log.trace("trace log="+ name);
log.trace("trace log={}",name);
log.debug("debug log={}",name);
log.info("info log={}", name);
log.warn("warn log={}", name);
log.error("error log={}", name);
return "ok";
}
}
•@RestController - @Controller는 반환값이 String이면 뷰 이름으로 인식하기에 뷰를 찾고 뷰가 렌더링됨 - @RestController는 반환 값으로 뷰를 찾는게 아니라 HTTP 메세지 바디에 바로 입력함 - 클래스레벨이아닌 메서드레벨에서 @ResponseBody를 사용하면 클래스레벨에 @Controller를 사용하더라도 바로 HTTP 메세지 바디에 입력해서 반환을 해줌
•로그 출력 포맷
시간, 로그 레벨, 프로세스 ID(PID), 쓰레드 명, 클래스 명, 로그 메세지
1.4 로그 레벨
•작성한 로그 출력코드는 5개인데 로그는 3개가 출력되는 이유
- 로그의 출력 레벨이 있음.
- 로그레벨을 설정하면 그 로그 보다 우선순위가 높은 것만 출력
- 스프링 부트에서 기본으로 설정되어 있는 로그레벨은 info
- 그렇기에 info보다 우선순위가 낮은 debug, trace는 출력되지 않음.
•로그 레벨 - TRACE > DEBUG > INFO > WARN > ERROR - 개발서버는 debug까지 출력가능 - 운영서버는 통상적으로 info만 출력
•로그를 임의로 내가 원하는대로 변경하는법
설정파일(application.properties)에서 레벨을 변경
#전체 로그 레벨 설정(기본 info)
lolgging.level.root=info
#hello.springmvc 패캐지와 그 하위 로그 레벨 설정
logging.level.hello.springmvc=[변경을 원하는 로그 레벨]
※ 로그 레벨을 info로 설정해도 해당 코드에 있는 name + "님 안녕하세요."는 자바 특성상 실행이 안되어도 연산이 되버어림.
자바 컴파일 시점에서 사용하지도 않는 debug레벨에 있는 연산을 평가해버리니 리소스 낭비.
•새로운 방식의 로그 출력 방식
log.debug("{} 님 안녕하세요.", name);
※ 로그 출력레벨이 debug 이상이면 debug내의 연산은 수행되지 않음.
1.6 로그 사용시 장점 •쓰레드 정보, 클래스 이름같은 정보를 함께 볼 수 있고, 출력 모양을 조정 가능 •로그 레벨에 따라 개발 서버에서는 모든 로그를 출력하고 운영서버에서는 출력하지 않게끔 로그를 조절 가능 • 콘솔에만 출력하는게 아니라 파일이나 네트워크 등 로그를 별도의 위치에 남길 수 있음 •특히 파일로 남길 때 일별, 특정 용량에 따라 로그를 분할하는것도 가능 •성능도 System.out보다 좋음 (내부 버퍼링, 멀티 쓰레드 등)
요청매핑(RequestMapping)
1. 기본매핑
/**
* 기본 요청
* 둘 다 허용한다 /hello-basic, /hello-basic/
* 스프링 부트 3.0 부터는 /hello-basic , /hello-basic/ 는 서로 다른 URL 요청으로 인식함
* HTTP 메서드 모두 허용 GET, POST, HEAD, PUT, PATCH, DELETE
*/
@RequestMapping("/hello-basic")
public String helloBasic() {
log.info("helloBasic");
return "ok";
}
•@RequestMapping("/hello-basic") - /hello-basic URL 호출이 오면 이 메서드가 실행되도록 매핑 - 대부분의 속성을 배열[]로 제공하기에 다중 설정도 가능 ex) @RequestMapping(value = {"/hello-basic", "/hello-go"}) - method 속성으로 HTTP 메서드를 지정하지 않으면 모든 메서드에 무관하게 호출 (GET, HEAD, POST, PATCH, DELETE)
2. HTTP 특정 method 허용 매핑
/**
* method 특정 HTTP 메서드 요청만 허용한다.
* GET, HEAD, POST, PUT, PATCH, DELETE
*/
@RequestMapping(value = "/mapping-get-v1", method = RequestMethod.GET)
public String mappingGetV1() {
log.info("mappingGetV1");
return "ok";
}
•method가 GET일 경우에만 매핑이되며 다른 방식으로 요청하면 HTTP 405(Method Not Allowd)가 반환
•매번 method 속성을 설정해서 HTTP 메서드를 지정해주는게 번거롭고 가독성도 떨어지기에 전용 애노테이션을 만들어서 해결 •GetMapping, PostMapping, PatchMapping, DeleteMapping등 이름에 의미를 부여해 더 직관적임 - 애노테이션 내부에는 @RequestMapping과 method를 미리 지정해놓음
/**
* PathVariable 사용
* 변수명이 같으면 아래와 같이 생략 가능
* @PathVariable("userId") String userid -> @PathVariable String userId
*/
@GetMapping("/mapping/{userId}")
public String mappingPath(@PathVariable String userId) {
log.info("mappingPath userId={}", userId);
return "/mapping/{userId} ok";
}
•최근 HTTP API는 위와 같이 리소스 경로에 식별자를 넣는 스타일을 선호 ex) /mapping/userA ex) /users/1 •@RequestMapping은 URL 경로를 템플릿화 할 수 있는데 @PathVariable 애노테이션을 사용하면 매칭 되는 부분을 편리하게 조회할 수 있음 •@PathVariable의 이름과 파라미터 이름이 같으면 생략 가능하다. ⇒ @PathVariable("username") String username → PathVariable String username
5. 다수의 PahVariable(경로 변수)를 사용한 매핑
/**
* PathVariable 사용 다중
*/
@GetMapping("/mapping/users/{userId}/orders/{orderId}")
public String mappingPath(@PathVariable String userId, @PathVariable Long orderId) {
log.info("mappingPath userId={}, orderId={}", userId, orderId);
return "/mapping/users/{userId}/orders/{orderId} ok";
}
•하나 이상의 PathVariable도 사용이 가능
6. 특정 파라미터 조건 매핑
/**
* 파라미터로 추가 매핑
* params="mode", // 파라미터 이름만 있어도됨
* params="!mode" // 특정 파라미터이름이 안 들어가있어야 함
* params="mode=debug" // 특정 파라미터이름과 값이여야함
* params="mode!=debug" // 특정 파라미터이름과 값이면 안됨
* params = {"mode=debug","data=good"} // 여러개의 특정 파라미터이름과 값이여야함
*/
// 특정 파라미터 정보가 있을때만 호출 됨(url경로 뿐아니라 파라미터 정보까지 추가로 더 매핑한것)
@GetMapping(value = "/mapping-param", params = "mode=debug")
public String mappingParam() {
log.info("mappingParam");
return "/mapping-param ok";
}
•특정 파라미터를 조건식으로 매핑해서 매핑여부를 결정할 수 있음 ex) http://localhost:8080/mapping-param?mode=debug •잘 사용하지 않음
7. 특정 헤더 조건 매핑
/**
* 특정 헤더로 추가 매핑
* headers="mode" // 헤더에 특정 헤더명이 들어가있어야함
* headers="!mode" // 헤더에 특정 헤더명이 안 들어가있어야함
* headers="mode=debug" // 헤더에 특정 헤더명과 값이여야함
* headers="mode!=debug" // 헤더에 특정 헤더명과 값이면 안됨
*/
@GetMapping(value = "/mapping-header", headers = "mode=debug")
public String mappingHeader() {
log.info("mappingHeader");
return "/mapping-header ok";
}
•특정 파라미터 매핑과 동일하게 헤더 역시 조건매핑이 가능
8. 미디어 타입 조건 매핑 1 - HTTP 요청 Content-Type, consume
/**
* Content-Type 헤더 기반 추가 매핑 Media Type
* consumes="application/json" // Content-Type 헤더에 Media Type이 "application/json"이여야함
* consumes="!application/json" // Content-Type 헤더에 Media Type이 "application/json"이면 안됨
* consumes="application/*" // Content-Type 헤더에 Media Type이 "application/*"이여야함
* consumes="*\/*" // Content-Type 헤더에 Media Type이 "*\/*"이여야함
* MediaType.APPLICATION_JSON_VALUE = "application/json"
*/
// 요청 컨텐트 타입이 json이 여야함
@PostMapping(value = "/mapping-consume", consumes = "application/json")
public String mappingConsumes() {
log.info("mappingConsumes");
return "/mapping-consume ok";
}
•HTTP 요청의 Content-Type 헤더를 기반으로 미디어 타입으로 매핑 •일치하지 않을 경우 HTTP 415(Unsupported Media Type)을 반환 •조건을 배열로 설정할수도 있고 상수로 제공하는 매직넘버를 사용해도 됨
- 해당 파라미터를 공백(ex: username=)으로 전송하면 빈 문자로 통과가 됨 - required가 true인 파라미터를 보내주지 않으면 400 예외(BAD_REQUEST)가 발생
- 원시타입은 null이 들어갈 수 없어서 required가 false여도 500에러가 발생
int형으로 에러가 발생하면 Integer같은 wrapper 타입을 사용해야 함 혹은 기본값을 설정해주는 defaultValue를 사용하면 됨
② defaultValue : 파라미터가 없는 경우 기본값으로 설정한 값이 적용
- 사용 방법
@RequestParam(defaultValue = "20") - 이미 기본값이 있기에 required는 의미가 없어 빼도 됨 - 빈 문자("")의 경우에도 설정한 기본 값이 적용
•@RequestParam Map
- Map을 이용해 한 번에 받을 수도 있음
- Map(key=value) 형식
•@RequestParam MultiValueMap
- 파라미터의 값이 1개가 확실하면 Map을 써도 되지만 그렇지 않다면 MultiValueMap을 사용 - MultiValueMap(key=[value1, value2, ...] 형식
ex) (key=userIds, value=[id1, id2])
2.3 @ModelAttribute어노테이션으로 데이터 사용 예제 코드 •HelloData 코드
package hello.springmvc.basic;
import lombok.Data;
// 요청 파라미터를 바인딩할 객체 HelloData
// @Getter, @Setter, @ToString, @EqualsAndHashCode, @RequiredArgsConstructor를 자동으로 적용해줌
@Data
public class HelloData {
private String username;
private int age;
}
•@ModelAttribute어노테이션으로 데이터 사용
@Controller
public class RequestParamController {
@ResponseBody
@RequestMapping("/model-attribute-v1")
// 요청 파라미터의 이름으로 HelloData 객체의 프로퍼티를 찾아서 해당 프로퍼티의 setter를 호출해서 파라미터의 값을 입력(바인딩) 한다
public String modelAttributeV1(@ModelAttribute HelloData helloData) {
log.info("helloData={}", helloData);
return "ok";
}
@ResponseBody
@RequestMapping("/model-attribute-v2")
// @ModelAttribute 생략가능
// @RequestParam도 생략 가능함 그럼 Spring은 어떤 걸 바인딩 할까
// String int Integer같은 단순 타입은 @RequestParam로 바인딩
// 나머지는 @ModelAttribute로 바인딩
public String modelAttributeV2( HelloData helloData) {
log.info("helloData={}", helloData);
return "ok";
}
}
• ?username=spring&age=20 이라는 쿼리스트링을 담아서 요청을하면 바로 HelloData 객체에 담겨서 사용할 수 있음 •스프링MVC는 @ModelAttribute가 있으면 다음을 수행 ①HelloData 객체를 생성
②요청 파라미터의 이름으로 HelloData 객체의 프로퍼티를 찾음
③ 해당 프로퍼티의 setter를 호출해서 파라미터의 값을 바인딩 ※ 파라미터 이름이 username 이면 setUsername() 메서드를 찾아 호출
•@ModelAttribute는 생략가능
- @ModelAttribute 생략 vs @RequestParam 생략
- 스프링은 해당 생략시 다음과 같은 규칙을 적용 String, int, Integer 같은 단순 타입 = @RequestParam
나머지 = @ModelAttribute (argument resolver로 지정해둔 타입은 제외)
3. HTTP 요청 파라미터 - 단순 텍스트
※ HTTP 메세지 바디를 통해 데이터가 직접 넘어오는 경우는 HTML Form 방식을 제외하고 @RequestParam, @ModelAttribute를 사용할 수 없음
3.1 예제 v1
@Controller
public class RequestBodyStringController {
@PostMapping("/request-body-string-v1")
public void requestBodyString(HttpServletRequest request, HttpServletResponse response)throws IOException {
ServletInputStream inputStream = request.getInputStream();
String messageBody = StreamUtils.copyToString(inputStream, StandardCharsets.UTF_8);
log.info("messageBody={}",messageBody);
response.getWriter().write("ok");
}
}
•HttpServletRequst에서 getInputStream()으로 읽어와서 문자열로 변환해서 읽을 수 있음
3.2 예제 v2
@Controller
public class RequestBodyStringController {
@PostMapping("/request-body-string-v2")
// InputStream(Reader): HTTP 요청 메시지 바디의 내용을 직접 조회
// OutputStream(Writer): HTTP 응답 메시지의 바디에 직접 결과 출력
public void requestBodyStringV2(InputStream inputStream, Writer responseWriter)throws IOException {
String messageBody = StreamUtils.copyToString(inputStream, StandardCharsets.UTF_8);
log.info("messageBody={}",messageBody);
responseWriter.write("ok");
}
}
•매개변수에서 바로 inputStream과 writer를 받을수도 있음
- InputStream(Reader): HTTP 요청 메세지 바디의 내용을 직접 조회 - OutputStream(Writer): HTTP 응답 메세지의 바디에 직접 결과 출력
3.3 예제 v3
@Controller
public class RequestBodyStringController {
@PostMapping("/request-body-string-v3")
// 메시지 바디 정보를 직접 조회, 요청 파라미터를 조회하는 기능과 관계 없음 @RequestParam X, @ModelAttribute X
public HttpEntity<String> requestBodyStringV3(HttpEntity<String> httpEntity)throws IOException {
// HttpEntity: HTTP header, body 정보를 편리하게 조회
String messageBody = httpEntity.getBody();
httpEntity.getHeaders();
log.info("messageBody={}",messageBody);
// HttpEntity는 응답에도 사용 가능 메시지 바디 정보 직접 반환
// 헤더 정보 포함 가능
// view 조회X
return new HttpEntity<>("ok");
}
}
• HttpEntity: HTTP header, body 정보를 편리하게 조회할 수 있게 해줌 - 메세지 바디 정보를 직접 조회 가능(getBody()) - 요청 파라미터를 조회하는 기능과 관계 없다.(@RequestParam, @ModelAttribute) •응답에서도 사용할 수 있음
- 메세지 바디 정보 직접 반환
- 헤더 정보포함도 가능
- View 조회는 안됨
3.3 예제 v3 다른방식
@Controller
public class RequestBodyStringController {
@PostMapping("/request-body-string-v3-another")
// RequestEntity : HttpMethod, url 정보가 추가, 요청에서 사용
public HttpEntity<String> requestBodyStringV3Another(RequestEntity<String> httpEntity)throws IOException {
String messageBody = httpEntity.getBody();
httpEntity.getHeaders();
log.info("messageBody={}",messageBody);
// ResponseEntity : HTTP 상태 코드 설정 가능, 응답에서 사용
return new ResponseEntity<>("ok", HttpStatus.CREATED);
}
}
•HttpEntity를 상속받은 RequestEntity, ResponseEntity 객체들도 같은 기능을 제공
- RequestEntity : HttpMethod, url 정보가 추가, 요청에서 사용
- ResponseEntity : HTTP 상태 코드 설정 가능, 응답에서 사용
ex) return new ResponseEntity("Hello World", responseHeaders, HttpStatus.CREATED)
3.4 예제 v4
@Controller
public class RequestBodyStringController {
// @ResponseBody를 사용하면 응답 결과를 HTTP 메시지 바디에 직접 담아서 전달할 수 있다.(view를 사용하지 않음)
@ResponseBody
@PostMapping("/request-body-string-v4")
// @RequestBody : HTTP 메시지 바디 정보를 편리하게 조회할 수 있다.
// 헤더 정보가 필요하다면 HttpEntity를 사용하거나 @RequestHeader를 사용하면 된다.
// 메시지 바디를 직접 조회하는 기능은 요청 파라미터를 조회하는 @RequestParam, @ModelAttribute와는 전혀 관계가 없다.
public String requestBodyStringV4(@RequestBody String messageBody)throws IOException {
log.info("messageBody={}",messageBody);
return "ok";
}
// 요청 파라미터를 조회하는 기능: @RequestParam , @ModelAttribute
// HTTP 메시지 바디를 직접 조회하는 기능: @RequestBody
}
•@RequestBody - HTTP 메세지 바디 정보를 편리하게 조회하게 해주는 애노테이션
- 만약 바디가 아니라 헤더정보가 필요하면 HttpEntity나 @RequestHeader 애노테이션을 사용하면 됨 - 요청 파라미터를 조회하는 @RequestParam, @ModelAttribute와는 관계가 없다.
4. HTTP 요청 파라미터 - Json
※ HTTP 요청 메세지 바디에는 JSON이 주로 사용됨
※JSON은 다음과 같은 구조인데, 이를 객체로 변환하여 로직을 수행
4.1 예제 v1
@Controller
public class RequestBodyJsonController {
// Jackson 라이브러리인 objectMapper
private ObjectMapper objectMapper = new ObjectMapper();
@PostMapping("/request-body-json-v1")
public void requestBodyJsonV1(HttpServletRequest request, HttpServletResponse response) throws IOException{
// HttpServletRequest를 사용해서 직접 HTTP 메시지 바디에서 데이터를 읽어와서, 문자로 변환
ServletInputStream inputStream = request.getInputStream();
String messageBody = StreamUtils.copyToString(inputStream, StandardCharsets.UTF_8);
log.info("messageBody={}", messageBody);
// 문자로 된 JSON 데이터를 Jackson 라이브러리인 objectMapper를 사용해서 자바 객체로 변환
HelloData helloData = objectMapper.readValue(messageBody, HelloData.class);
log.info("username={}, age={}", helloData.getUsername(), helloData.getAge());
response.getWriter().write("ok");
}
}
•HttpServletRequest를 사용해서 직접 HTTP 메세지 바디에서 데이터를 읽어와, 문자로 변환 •문자로 된 JSON 데이터를 Jackson 라이브러리인 ObjectMapper를 사용해 자바 객체변환
4.2 예제 v2
@Controller
public class RequestBodyJsonController {
// Jackson 라이브러리인 objectMapper
private ObjectMapper objectMapper = new ObjectMapper();
@ResponseBody
@PostMapping("/request-body-json-v2")
// @RequestBody를 사용해서 HTTP 메시지에서 데이터를 꺼내고 messageBody에 저장
public String requestBodyJsonV2(@RequestBody String messageBody) throws IOException{
log.info("messageBody={}", messageBody);
// 문자로 된 JSON 데이터인 messageBody를 objectMapper를 통해서 자바 객체로 변환
HelloData helloData = objectMapper.readValue(messageBody, HelloData.class);
log.info("username={}, age={}", helloData.getUsername(), helloData.getAge());
return "ok";
}
•@RequestBody를 사용해서 HTTP 메시지에서 데이터를 꺼내고 messageBody에 저장
•문자로 된 JSON 데이터인 messageBody 를 objectMapper 를 통해서 자바 객체로 변환
4.3 예제 v3
@Controller
public class RequestBodyJsonController {
@ResponseBody
@PostMapping("/request-body-json-v3")
// @RequestBody에 직접 만든 객체 HelloData를 지정할 수 있다.
// HttpEntity, @RequestBody를 사용하면 HTTP 메시지 컨버터가 HTTP 메시지 바디의 내용을 우리가 원하는 문자나 객체 등으로 변환해줌
// HTTP 메시지 컨버터는 문자 뿐만 아니라 JSON도 객체로 변환해주는데,
// V2의 HelloData helloData = objectMapper.readValue(messageBody, HelloData.class)를 처리해줌
// @RequestBody는 생략 불가능 (HTTP 메시지 바디가 아니라 요청 파라미터 @ModelAttribute로 처리함)
public String requestBodyJsonV3(@RequestBody HelloData helloData) throws IOException{
log.info("username={}, age={}", helloData.getUsername(), helloData.getAge());
return "ok";
}
}
•@RequestBody 객체 파라미터에 직접 만든 객체를 지정할 수 있음
•@RequestBody는 생략 불가능
- @ModelAttribute , @RequestParam 과 같은 해당 애노테이션을 생략시 다음과 같은 규칙을 적용
String , int , Integer 같은 단순 타입 = @RequestParam 나머지 = @ModelAttribute (argument resolver 로 지정해둔 타입 외)
@Controller
public class RequestBodyJsonController {
@ResponseBody
@PostMapping("/request-body-json-v4")
public String requestBodyJsonV4(HttpEntity<HelloData> httpEntity)throws IOException {
HelloData data = httpEntity.getBody();
log.info("username={}, age={}",data.getUsername(), data.getAge());
return "ok";
}
// @ResponseBody : 객체 → HTTP 메시지 컨버터 → JSON 응답
@ResponseBody
@PostMapping("/request-body-json-v5")
// @RequestBody : JSON 요청 → HTTP 메시지 컨버터 → 객체
public HelloData requestBodyJsonV5(@RequestBody HelloData data) throws IOException{
log.info("username={}, age={}", data.getUsername(), data.getAge());
return data;
}
}
•HttpEntity, @ResponseBody로 data 객체로 변환
※ 응답의 경우에도 @ResponseBody 를 사용하면 해당 객체를 HTTP 메시지 바디에 직접 넣어줄 수 있음
• @RequestBody 요청
JSON 요청 → HTTP 메시지 컨버터 →객체
•@ResponseBody
응답 객체 →HTTP 메시지 컨버터 →JSON 응답
•HTTP 메시지 컨버터
- @RequestBody, HttpEntity를 사용하면 HTTP 메시지 컨버터가HTTP 메시지 바디의 내용을 우리가 원하는 문자나 객체 등으로 변환해줌.
- HTTP 메시지 컨버터는 문자 뿐만 아니라 JSON도 객체로 변환해주는데, 우리가 방금 V2에서 했던 수동 작업을 대신 처리해줌
HTTP 응답
1. 정적 리소스, 뷰 템플릿, HTTP 메세지 사용
※ HTTP 요청에 대해서 서버에서 비즈니스 로직이 다 수행된 다음 이제 응답을 해야하는데 스프링(서버)에서 응답 데이터를 반드는 방식은 크게 세 가지가 있음
•정적 리소스 •뷰 템플릿 사용 •HTTP 메세지 사용
1.1 정적 리소스
•스프링 부트는 클래스패스에 다음 디렉토리에 있는 정적 리소스를 제공함. - /static, /public,/resources,/META-INF/resources •src/main/resources는 리소스를 보관하는 곳이고, 클래스패스의 시작 경로임 •위의 디렉토리에 리소스를 넣어두면 스프링 부트가 정적 리소스로 서비스를 제공 •예를 들어, 정적 리소스 경로 src/main/resources/static/baisc/hello-form.html에 해당 파일이 있다면 웹 브라우저에서는 컨트롤러를 통하지않고 정적리소스 경로 뒤의 경로를 입력해 바로 가져올 수 있음
1.2 뷰 템플릿
•뷰 템플릿을 거쳐서 HTML이 생성되고, 뷰가 응답을 만들어서 전달
•일반적으로 HTML을 동적으로 생성하는 용도로 사용하지만, 뷰 템플릿이 만들 수 있는 것이라면 뭐든지 가능
package hello.springmvc.basic.response;
import org.springframework.stereotype.Controller;
import org.springframework.ui.Model;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.servlet.ModelAndView;
@Controller
public class ResponseViewController {
// 정적 리소스
// 스프링 부트는 클래스패스의 다음 디렉토리에 있는 정적 리소스를 제공한다.
// src/main/resources/static , src/main/resources/public , src/main/resources , /META-INF/resources
// src/main/resources는 리소스를 보관하는 곳이고, 또 클래스패스의 시작 경로
// 다음 경로에 파일이 들어있으면 src/main/resources/static/basic/hello-form.html
// 웹 브라우저에서 다음과 같이 실행하면 된다. http://localhost:8080/basic/hello-form.html
// view 템플릿 경로 src/main/resources/templates
@RequestMapping("/response-view-v1")
public ModelAndView responseViewV1() {
ModelAndView mav = new ModelAndView("response/hello").addObject("data","hello!");
return mav;
}
@RequestMapping("/response-view-v2")
public String responseViewV2(Model model) {
model.addAttribute("data","hello!");
// @Controller와 @RequestMapping가 있으면 view의 논리 이름으로 viewResolver가 가져가서 render해줌
return "response/hello";
}
// controller경로의 이름과 view의 논리적 이름이 같으면 return값이 없어도 viewResolver가 가져가서 render해줌
@RequestMapping("/response/hello")
public void responseViewV3(Model model) {
model.addAttribute("data","hello!");
}
}
※ 뷰 템플릿 호출예제의 반환 타입이 다 다름 (ModelAndView, String, void) •ModelAndView를 반환하는 경우(responseViewV1) - 객체에서 View를 꺼내어 물리적인 뷰 이름으로 완성한 뒤 뷰를 찾아 렌더링을 한다.
•String을 반환하는 경우(responseViewV2) - @ResponseBody(혹은 클래스레벨에서 @RestController)가 없으면 response/hello라는 문자가 뷰 리졸버로 전달되어 실행되서 뷰를 찾고 렌더링함. - @ResponseBody(혹은 클래스레벨에서 @RestController)가 있으면 뷰 리졸버를 실행하지 않고 HTTP 메세지 바디에 직접 response/hello 라는 문자가 입력됨 - 위 코드에서는 /response/hello를 반환하는데 뷰 리졸버는 물리적 이름을 찾아서 렌더링을 실행 실행 결과 : templates/response/hello.html
•void를 반환하는 경우(responseViewV3) -@Controller를 사용하고 HttpServletResponse, OutputStream(Writer)같은 HTTP 메세지 바디를 처리하는 파라미터가 없으면 요청 URL을 참고해서 논리 뷰 이름으로 사용 ※ 요청 URL: /response/hello
뷰 경로 : templates/response/hello.html -이 방식은 명시성이 너무 떨어지고 이런 케이스가 나오는 경우도 거의 없어 권장하지 않음
•Thymeleaf 스프링 부트 설정
- build.gradle 파일에 하기 라이브러리를 추가하면 스프링 부트가 자동으로 ThymeleafViewResolver 와 필요한 스프링 빈들을 등록
•responseBodyJsonV2 -ResponseEntity는 HTTP 응답 코드를 설정할 수 있는데 @ResponseBody를 사용하면 설정하기가 까다로움. 그래서 이런 경우에는 @ResponseStatus 애노테이션을 이용하여 상태코드를 설정할 수 있음 -정적으로 상태코드를 작성한 것이기에 유연하지는 못함 그렇기에 동적으로 상태코드가 변경되야하는 상황이라면 ResponseEntity를 사용
•@RestController
- @Controller 대신에 @RestController 애노테이션을 사용하면, 해당 컨트롤러에 모두 @ResponseBody가 적용되는 효과가 있음
- 뷰 템플릿을 사용하는 것이 아니라, HTTP 메시지 바디에 직접 데이터를 입력
- Rest API(HTTP API)를 만들 때 사용하는 컨트롤러
- @ResponseBody는 클래스 레벨에 두면 전체 메서드에 적용되는데, @RestController 에노테이션 안에 @ResponseBody + @Controller가 적용되어 있음
HTTP 메시지 컨버터
1. HTTP 메세지 컨버터란
※ 지금까지 여러 애노테이션을 이용해서 JSON, queryString등을 @RequestBody, @ModelAttribute 등으로 편하게 객체로 변환해서 사용했다. 그런데 이쯤에서 어떻게 스프링이 객체로 변환을 해주는지 의문을 가질 필요가 있다.
1.1 @ResponseBody의 사용원리
•@ResponseBody를 사용하니 HTTP의 BODY에 문자 내용을 직접 반환하는데 그림을 보면 viewResolver 대신HttpMessageConverter가 동작한다. - 기본 문자처리: StringHttpMessageConverter - 기본 객체처리: MappingJackson2HttpMessageConverter - byte 처리등등 기타 여러 HttpMessageConverter가 기본으로 등록되어 있음
•응답 시 클라이언트의 HTTP Accept 헤더와 서버의 컨트롤러 반환 타입 정보 둘을 조합해 HttpMessageConverter가 선택됨. •스프링 MVC는 다음의 경우에 HTTP 메세지 컨버터를 적용 -HTTP 요청: @RequestBody, HttpEntity(RequestEntity) -HTTP 응답: @ResponseBody, HttpEntity(ResponseEntity)
•등록된 메세지 컨버터들이 순회하며 만족한다면 해당 컨버터를 사용하고 조건을 만족하지 않으면 다음 컨버터로 우선순위가 넘어감.
3.3 주요 컨버터 정리
•ByteArrayHttpMessageConverter : byte[] 데이터를 처리 클래스 타입: byte[], 미디어타입: */* 요청 예) @RequestBody byte[] data 응답 예) @ResponseBody return byte[] 미디어 타입 application/octet-stream
•StringHttpMessageConverter : String 문자로 데이터를 처리 클래스 타입: String, 미디어 타입: */* 요청 예) @RequestBody String data 응답 예) @ResponseBody return "ok" 미디어 타입 text/plain
•MappingJackson2HttpMessageConverte r: application/json 클래스 타입: 객체 또는 HashMap, 미디어 타입: application/json 관련 요청 예) @RequestBody HelloData data 응답 예) @ResponseBody return helloData 미디어 타입 application/json 관련
2.1동작 방식 •ArgumentResolver의 supportsParameter() 메서드를 호출해 해당 파라미터를 지원하는지 체크 - (지원할 경우) resolveArgument() 메서드를 호출해서 실제 객체를 생성 - (지원안할경우) 다음 ArgumentResolver로 우선순위가 넘어감.
•이 또한 ArgumentResolver와 비슷, 요청이 아닌 응답 값을 변환하고 처리 •컨트롤러에서 String으로 뷰 이름을 반환해도, 동작하는 이유가 이 ReturnValueHandler 덕분 •스프링은 10개가 넘는 ReturnValueHandler를 지원 ex) ModelAndView, @ResponseBody, HttpEntity, String
※ 직접 만든 프레임워크와 스프링 MVC는 명칭만 조금 다르고 거의 같은 구조와 기능으로 되어있음
- FrontController → DispatcherServlet
-handlerMappingMap →HandlerMapping
-MyHandlerAdapter →HandlerAdapter
-ModelView →ModelAndView
-viewResolver →ViewResolver
-MyView →View
2. DispatcherServlet
2.1 DispatcherServlet 개요
• FrontController와 마찬가지로 부모 클래스에서 HttpServlet을 상속받아 사용하며, 서블릿으로 동작. ◦DispatcherServelt → FrameworkServlet → HttpServletBean → HttpServlet
•스프링 부트 구동시 DispatcherServlet을 서블릿으로 자동등록하며 모든 경로(urlPatterns="/")에 대해 매핑.
• Spring MVC 역시 프론트 컨트롤러 패턴으로 구현되어 있고 DispatcherServlet이 프론트 컨트롤러의 역할
※ DispatcherServlet의 다이어그램
2.2 DispatcherServlet 요청 흐름 및 핵심 로직 분석
•DispatcherServlet요청 흐름
- 서블릿이 호출되면 HttpServlet이 제공하는 service() 메서드가 호출 - 스프링 MVC는 DispatcherServlet의 부모인 FrameworkServlet에서 service()를 오버라이드 해둠 - FrameworkServlet.service()를 시작으로 여러 메서드가 호출되며 DispatcherServlet.doDispatch()가 호출
• DispacherServlet.doDispatch() 핵심 로직 분석
protected void doDispatch(HttpServletRequest request, HttpServletResponse response) throws Exception {
HttpServletRequest processedRequest = request;
HandlerExecutionChain mappedHandler = null;
ModelAndView mv = null;
// 1. 핸들러 조회 (요청에 맞는 적절한 핸들러를 찾아 반환)
mappedHandler = getHandler(processedRequest);
if (mappedHandler == null) {
// 적절한 핸들러를 찾지 못한경우 404 에러코드를 반환
noHandlerFound(processedRequest, response);
return;
}
// 2.핸들러 어댑터 조회-핸들러를 처리할 수 있는 어댑터 (찾은 핸들러를 처리할 수 있는 핸들러 어댑터를 찾아줌, 만약 찾지 못할경우 ServletException 발생)
HandlerAdapter ha = getHandlerAdapter(mappedHandler.getHandler());
// 3. 핸들러 어댑터 실행 -> 4. 핸들러 어댑터를 통해 핸들러 실행 -> 5. ModelAndView 반환
mv = ha.handle(processedRequest, response, mappedHandler.getHandler());
processDispatchResult(processedRequest, response, mappedHandler, mv, dispatchException);
}
// 실제 코드는 복잡하게 되있는데 결국 render() 메서드를 호출
private void processDispatchResult(HttpServletRequest request, HttpServletResponse response,
HandlerExecutionChain mappedHandler, ModelAndView mv,
Exception exception) throws Exception {
// 뷰 렌더링 호출
render(mv, request, response);
}
protected void render(ModelAndView mv, HttpServletRequest request, HttpServletResponse response) throws Exception {
View view;
// 6. 뷰 리졸버를 통해서 뷰 찾기
// 7. View 반환
String viewName = mv.getViewName();
view = resolveViewName(viewName, mv.getModelInternal(), locale, request);
// 8. 뷰 렌더링
// ModelAndView에서 View를 찾아 뷰 리졸버를 이용해 뷰의 물리적 이름을 완성해서 forward 해줌
view.render(mv.getModelInternal(), request, response);
}
3. 스프링 MVC 동작 순서
① 핸들러 조회 : 핸들러 매핑을 통해 URL에 매핑된 핸들러(컨트롤러) 조회 ② 핸들러 어댑터 조회: 핸들러를 실행할 수 있는 핸들러 어댑터 조회 ③ 핸들러 어댑터 실행: 핸들러 어댑터 실행 ④ 핸들러 실행: 핸들러 어댑터가 실제 핸들러를 실행 ⑤ ModelAndView 반환: 핸들러 어댑터는 핸들러가 반환하는 정보를 ModelAndView로 변환해 반환. ⑥ viewResolver 호출: 뷰 리졸버를 찾아 실행 (JSP: InternalResourceViewResolver가 자등 등록되어 사용됨) ⑦ View 반환: 뷰 리졸버는 뷰의 논리 이름을 물이 이름으로 바꾸고 렌더링 역할을 담당하는 뷰 객체 반환. (JSP: InternalResourceView(JstlView)를 반환하는데, 내부에는 forward() 가 있음) ⑧ 뷰 렌더링: 뷰를 통해서 뷰를 렌더링
4. 스프링 MVC 인터페이스 • SpringMVC는 DispatcherServlet 코드 변경을 하지않고도 원하는 기능을 변경하거나 확장을 할 수 있음.
그리고 이런 인터페이스들을 구현해 DispatcherServlet에 등록하면 커스텀 컨트롤러를 만들 수도 있음 • 주요 인터페이스 목록 - 핸들러 매핑: org.springframework.web.servlet.HandlerMapping - 핸들러 어댑터: org.springframework.web.servlet.HandlerAdapter - 뷰 리졸버: org.springframework.web.servlet.ViewResolver - 뷰: org.springframework.web.servlet.View
핸들러 매핑과 핸들러 어댑터
※ 직접 만든 MVC 프레임워크에서는 핸들러 매핑과 핸들러 어댑터를 단순하게 Map, List 콜렉션을 이용해서 등록한 뒤 검색해서 사용함. ※springMVC에서는 어떻게 핸들러 매핑과 핸들러 어댑터를 사용하는지 확인 해보기.
1. 과거 버전의 스프링 컨트롤러(Controller인터페이스를 구현)
1.1 예제 1
package hello.servlet.web.springmvc.old;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import org.springframework.stereotype.Component;
import org.springframework.web.servlet.ModelAndView;
import org.springframework.web.servlet.mvc.Controller;
// @Controller 애노테이션과는 다른 Controller 인터페이스임
// /springmvc/old-controller 라는 이름의 스프링 빈으로 등록되었고, 스프링 빈의 이름으로 URL 매핑함.
// 스프링에서 name을 urlpatterns랑 맞춤
@Component("/springmvc/old-controller")
public class OldController implements Controller{
@Override
public ModelAndView handleRequest(HttpServletRequest request, HttpServletResponse response) throws Exception {
System.out.println("OldController.handleRequest");
return null;
}
}
1.2 스프링 MVC가 URL을 통해 컨트롤러를 찾는 방법
• 이 컨트롤러가 호출되기 위해 HandlerMapping과 HandlerAdapter가 필요 ◦HandlerMapping - 핸들러 매핑에서 이 컨트롤러를 찾을 수 있어야 함. -스프링 빈의 이름으로 핸드러를 찾을 수 있는 핸들러 매핑이 필요.
-직접 만든 MVC 프레임워크의 관련 소스
private final Map<String, Object> handlerMappingMap = new HashMap<>();
private void initHandlerMappingMap() {
handlerMappingMap.put("/front-controller/v5/v3/members/new-form", new MemberFormControllerV3());
handlerMappingMap.put("/front-controller/v5/v3/members/save", new MemberSaveControllerV3());
handlerMappingMap.put("/front-controller/v5/v3/members", new MemberListControllerV3());
handlerMappingMap.put("/front-controller/v5/v4/members/new-form", new MemberFormControllerV4());
handlerMappingMap.put("/front-controller/v5/v4/members/save", new MemberSaveControllerV4());
handlerMappingMap.put("/front-controller/v5/v4/members", new MemberListControllerV4());
}
◦HandlerAdapter -핸들러 매핑을 통해 찾은 핸들러를 실행할 수있는 핸들러 어댑터가 필요 -Controller 인터페이스를 실행할 수 있는 핸들러 어댑터를 찾아야 함.
-직접 만든 MVC 프레임워크의 관련 소스
private final List<MyHandlerAdapter> handlerAdapters = new ArrayList<>();
// 사용할 어댑터를 리스트 콜렉션에 추가하는 작업을 하는 메서드
private void initHandlerAdapters() {
handlerAdapters.add(new ControllerV3HandlerAdapter());
handlerAdapters.add(new ControllerV4HandlerAdapter());
}
※ 스프링 부트에서는 직접 만든 MVC프레임워크처럼 Map이나 List 같은 객체로 저장하는것이 아니라
인터페이스로 구현된 핸들러 매핑과 핸들러 어댑터 객체를 자동으로등록해줌
•스프링 부트가 자동 등록하는 핸들러 매핑 구현체와 핸들러 어댑터 구현체
- HandlerMapping을 찾는 우선순위
0 = RequestMappingHandlerMapping : 애노테이션 기반의 컨트롤러인 @Requestmapping에서 핸들러를 찾음. 1 = BeanNameUrlHandlerMapping : 스프링 빈의 이름으로 핸들러를 찾음. ...
- HandlerAdapter을 찾는 우선순위
0 = RequestmappingHandlerAdapter : 애노테이션 기반의 컨트롤러인 @Requestmapping를 사용한 핸들러를 처리 1 = HttpRequestHandlerAdapter : HttpRequesthandler 처리 2 = SimpleControllerHandlerAdapter : Controller 인터페이스(애노테이션 X) 처리 ...
• 예제1의 OldController의 핸들러 매핑과 핸들러 어댑터 찾고 실행하는 순서
※ 핸들러 매핑, 핸들러 어댑터 모두 순서대로 찾고 만약 없을 경우 다음 순서로 넘어간다. - 핸들러 매핑으로 핸들러 조회 : HandlerMapping에서 순서대로 수행하는데 1 = BeanNameUrlHandlerMapping에서 검색이 되기에 해당 URL을 빈 이름으로 등록한 핸들러(OldController)가 매핑되어 반환됨.
- 핸들러 어댑터 조회 : 어댑터를 순서대로 호출하며 이전에 찾은 핸들러(OldController)가 실행가능한 어댑터를 찾음. 이 중에서 SimpleControllerHandlerAdapter가 Controller 인터페이스를 실행 가능하기에 반환.
- 핸들러 어댑터 실행 : 반환된 어댑터(SimpleControllerHandlerAdapter)는 핸들러인 OldController를 내부에서 실행하면서 핸들러 정보를 넘겨주고 그 결과를 반환.
※ 스프링 부트는 InternalResourceViewResolver 라는 뷰 리졸버를 자동으로 등록하는데,
이때 application.properties 에 등록한 spring.mvc.view.prefix , spring.mvc.view.suffix 설정 정보를 사용해서 등록함.
2.1 구성도
※ 위 springMVC 구조에서 6번, 7번 과정에서 뷰 리졸버가 동작. ※ 스프링 부트는 구동시 자동 등록하는 뷰 리졸버가 많은데, 이중 몇가지를 말하면 다음과 같음.
1 = BeanNameViewResolver : 빈 이름으로 뷰를 찾아서 반환한다. (예: 엑셀 파일 생성 기능에 사용) 2 = InternalResourceViewResolver : JSP를 처리할 수 있는 뷰를 반환한다.
2.2 예제 OldController의 뷰리졸버 동작 순서 ① 핸들러 어댑터 호출 - 핸들러 어댑터를 통해 new-form 이라는 논리 뷰 이름을 획득 ② ViewResolver 호출 - new-form 이라는 뷰 이름으로 viewResolver를 순서대로 호출. - BeanNameViewResolver 는 new-form 이라는 이름의 스프링 빈으로 등록된 뷰를 찾아야 하는데 없음. - 스프링부트에 자동으로 등록된 InternalResourceViewResolver가 호출. ③ InternalResourceViewResolver - 이 뷰 리졸버는 InternalResourceView 를 반환 ④ 뷰 - InternalResourceView - InternalResourceView 는 JSP처럼 포워드 forward() 를 호출해서 처리할 수 있는 경우에 사용 ⑤ view.render() - view.render() 가 호출되고 InternalResourceView 는 forward() 를 사용해서 JSP를 실행
※ InternalResourceViewResolver는 만약 JSTL 라이브러리가 있으면 InternalResourceView를 상속받은 JstlView를 반환.
JstlView 는 JSTL 태그 사용시 약간의 부가 기능이 추가.
※ 다른 뷰는 실제 뷰를 렌더링하지만, JSP의 경우 forward() 통해서 해당 JSP로 이동(실행)해야 렌더링이 됨.
JSP를 제외한 나머지 뷰 템플릿들은 forward() 과정 없이 바로 렌더링.
※ Thymeleaf 뷰 템플릿을 사용하면 ThymeleafViewResolver 를 등록해야 함.
최근에는 라이브러리만 추가하면 스프링 부트가 이런 작업도 모두 자동화해줌
스프링 MVC 적용하기(version 1)
1. @RequestMapping
• 기존에 @WebServlet에서 urlPattern을 사용해주고, Component에 빈 이름으로 URL을 작성해서 사용해지만,
스프링 MVC가 @RequestMapping 애노테이션을 사용해서 유연하고 편리하게 컨트롤러 구현이 가능해짐.
• @RequestMapping이 사용하는 HandlerMapping과 HandlerAdapter
◦RequestMappingHandlerMapping
◦RequestMappingHandlerAdapter
※ 가장 우선순위가 높은 핸들러 매핑과 핸들러 어댑터
2. 코드
2.1 SpringMemberFormControllerV1 - 회원 등록컨트롤러
package hello.servlet.web.springmvc.v1;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.servlet.ModelAndView;
/* @Controller와 같음
// 스프링 빈으로 등록
@Component
// 핸들러로 인식
@RequestMapping
*/
// 스프링이 자동으로 스프링 빈으로 등록한다. (내부에 @Component 애노테이션이 있어서 컴포넌트 스캔의 대상이 됨)
// 스프링 MVC에서 애노테이션 기반 컨트롤러(핸들러)로 인식
// RequestMapnpingHandlerMapping에서 매핑시켜줄수 있는(사용할 수 있는) 핸들러로 인식된다는 의미.
@Controller
public class SpringMemberFormControllerV1 {
// 해당 URL이 호출되면 이 메서드가 호출. 애노테이션을 기반으로 동작하기 때문에, 메서드의 이름은 임의로 지으면 됨
@RequestMapping("/springmvc/v1/members/new-form")
// 모델과 뷰 정보를 담아 반환하면 됨
public ModelAndView process() {
return new ModelAndView("new-form");
}
}
2.2 SpringMemberSaveControllerV1 - 회원 저장컨트롤러
package hello.servlet.web.springmvc.v1;
import java.util.List;
import java.util.Map;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.servlet.ModelAndView;
import hello.servlet.domain.member.Member;
import hello.servlet.domain.member.MemberRepository;
@Controller
public class SpringMemberSaveControllerV1 {
private MemberRepository memberRepository = MemberRepository.getInstance();
@RequestMapping("/springmvc/v1/members/save")
public ModelAndView process(HttpServletRequest request, HttpServletResponse response) {
String username = request.getParameter("username");
int age = Integer.parseInt(request.getParameter("age"));
Member member = new Member(username, age);
memberRepository.save(member);
ModelAndView mv = new ModelAndView("save-result");
// 아래와 같은 소스
//mv.getModel().put("member", member);
// 스프링이 제공하는 ModelAndView를 통해 Model 데이터를 추가할 때는 addObject()를 사용하면 됨
mv.addObject("member", member);
return mv;
}
}
// urlPattern에 매핑되는 컨트롤러를 초기화해주는 메서드
private void initHandlerMappingMap() {
handlerMappingMap.put("/front-controller/v5/v3/members/new-form", new MemberFormControllerV3());
handlerMappingMap.put("/front-controller/v5/v3/members/save", new MemberSaveControllerV3());
handlerMappingMap.put("/front-controller/v5/v3/members", new MemberListControllerV3());
// V4Handler들(컨트롤러들)추가
// ControllerV4용 경로(key)와 컨트롤러를 handlerMappingMap에 추가
handlerMappingMap.put("/front-controller/v5/v4/members/new-form", new MemberFormControllerV4());
handlerMappingMap.put("/front-controller/v5/v4/members/save", new MemberSaveControllerV4());
handlerMappingMap.put("/front-controller/v5/v4/members", new MemberListControllerV4());
}
// 사용할 어댑터를 리스트 콜렉션에 추가하는 작업을 하는 메서드
private void initHandlerAdapters() {
handlerAdapters.add(new ControllerV3HandlerAdapter());
// ControllerV4도 호환이 되도록 해주는 어댑터를 handlerAdapter 콜렉션에 추가
handlerAdapters.add(new ControllerV4HandlerAdapter()); // ControllerV4HandlerAdapter 추가
}
컨트롤러version 정리
• version1: 프론트 컨트롤러를 도입
• version2: View 분리, 단순 반복 되는 뷰 로직 분리
• version3: Model 추가, 컨트롤러의 서블릿 종속성 제거, 뷰 이름 중복 제거
• version4: 단순하고 실용적인 컨트롤러 v3와 거의 비슷, 구현 입장에서 ModelView를 직접 생성해서 반환하지 않도록 View 논리 이름을 String으로 반환
•HTML 문서에 동적으로 변경해야 하는 부분만 자바 코드를 넣어 사용하기 위해 템플릿 엔진이 나옴
•템플릿 엔진을 사용하면 HTML 문서에서 필요한 곳만 코드를 적용해서 동적으로 변경할 수 있음
•템플릿 엔진의 종류 : JSP, Thymeleaf, Freemarker, Velocity 등
JSP로 웹 애플리케이션 만들기
예시 코드)
<%@ page contentType="text/html;charset=UTF-8" language="java" %>
<!-- 자바의 import 문-->
<%@ page import="hello.servlet.domain.member.Member" %>
<%@ page import="hello.servlet.domain.member.MemberRepository" %>
<%
// <% ~~ %> 이 부분에는 자바 코드를 입력할 수 있다.
// resuest, response 사용 가능
MemberRepository memberRepository = MemberRepository.getInstance();
String username = request.getParameter("username");
int age = Integer.parseInt(request.getParameter("age"));
Member member = new Member(username, age);
memberRepository.save(member);
%>
<html>
<head>
<title>Title</title>
</head>
<body>
성공
<ul>
<!-- <%= ~~ %> 이 부분에는 자바 코드를 출력할 수 있다.-->
<li>id=<%=member.getId()%></li>
<li>username=<%=member.getUsername()%></li>
<li>age=<%=member.getAge()%></li>
</ul>
<a href="/index.html">메인</a>
</body>
</html>
•JSP는 자바 코드를 그대로 다 사용할 수 있음
•코드의 상위 절반은 비즈니스 로직이고, 나머지 하위 절반만 결과를 HTML로 보여주기 위한 뷰 영역
•JAVA 코드, 데이터를 조회하는 리포지토리 등등 다양한 코드가 모두 JSP에 노출되어 있음
•JSP가 너무 많은 역할을 하게됨
MVC 패턴으로 웹 애플리케이션 만들기
1. MVC패턴의 개요
1.1 MVC 패턴이 나오게된 배경
•너무 많은 역할
- 하나의 서블릿이나 JSP만으로 비즈니스 로직과 뷰 렌더링을 모두 처리하게 되면, 너무 많은 역할을 하게되고, 유지보수가 어려워짐.
- 비즈니스 로직을 수정해도 해당 코드를 손대야 하고, UI를 수정해도 비즈니스 로직이 함께 있는 해당 파일을 수정해야 함.
•변경의 라이프 사이클
- UI 를 일부 수정하는 일과 비즈니스 로직을 수정하는 일은 각각 다르게 발생할 가능성이 매우 높고 대부분 서로에게 영향을 주지 않음.
- 변경의 라이프 사이클이 다른 부분을 하나의 코드로 관리하는 것은 유지보수하기 좋지 않음.
•기능 특화
JSP 같은 뷰 템플릿은 화면을 렌더링 하는데 최적화 되어 있기 때문에 이 부분의 업무만 담당하는 것이 가장 효과적
1.2 MVC 패턴
•Model View Controller : MVC 패턴은 하나의 서블릿이나, JSP로 처리하던 것을 컨트롤러(Controller)와 뷰(View)라는 영역으로 서로 역할을 나눈 것
- 컨트롤러: HTTP 요청을 받아서 파라미터를 검증하고, 비즈니스 로직을 실행. 뷰에 전달할 결과 데이터를 조회해서 모델에 담음.
- 모델: 뷰에 출력할 데이터를 담아서 뷰에 전달함. 따라서 뷰는 비즈니스 로직이나 데이터 접근을 몰라도 되고, 화면을 렌더링 하는 일에 집중할 수 있음
- 뷰: 모델에 담겨있는 데이터를 사용해서 화면을 그리는 일에 집중. (HTML을 생성하는 부분)
※ 컨트롤러에 비즈니스 로직을 두면 컨트롤러가 너무 많은 역할을 담당함.
그래서 일반적으로 비즈니스 로직은 서비스(Service)라는 계층을 별도로 만들어서 처리.
그리고 컨트롤러는 비즈니스 로직이 있는 서비스를 호출하는 역할을 담당.
2. MVC패턴 예시코드
2.1 컨트롤러
@WebServlet(name="mvcMemberSaveServlet", urlPatterns = "/servlet-mvc/members/save")
public class MvcMemberSaveServlet extends HttpServlet {
private MemberRepository memberRepository = MemberRepository.getInstance();
@Override
protected void service(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
String username = request.getParameter("username");
int age = Integer.parseInt(request.getParameter("age"));
Member member = new Member(username, age);
memberRepository.save(member);
//Model에 데이터를 보관한다.
request.setAttribute("member", member);
// 외부에서 직접 호출되지 않도록 WEB-INF에 jsp를 넣음
// 항성 controller에서 호출되도록 함.
String viewPath = "/WEB-INF/views/save-result.jsp";
// 컨트롤러에서 view로 이동할때 사용
RequestDispatcher dispatcher = request.getRequestDispatcher(viewPath);
// 다른 서블릿이나 JSP로 이동할 수 있는 기능 서버 내부에서 다시 호출이 발생
dispatcher.forward(request, response);
}
}
// 단순한 텍스트 메세지로 서버에 요청 했을 때 데이터 조회 ServletInputStream inputStream = request.getInputStream(); String messageBody = StreamUtils.copyToString(inputStream, StandardCharsets.UTF_8);
// Json 형식 메세지로 서버에 요청 했을 때 데이터 조회 private ObjectMapper objectMapper = new ObjectMapper();
private ObjectMapper objectMapper = new ObjectMapper();
// HTTP 응답으로 JSON을 반환할 때는 content-type을 application/json 로 지정해야 함
response.setHeader("content-type", "application/json");
response.setCharacterEncoding("utf-8");
HelloData data = new HelloData();
data.setUsername("kim");
data.setAge(20);
//{"username":"kim","age":20}
// Jackson 라이브러리가 제공하는 objectMapper.writeValueAsString()를 사용하면 객체를 JSON 문자로 변경할 수 있음
String result = objectMapper.writeValueAsString(data);
response.getWriter().write(result);