이 글은 강의 : 김영한님의 - "[스프링 MVC 1편 - 백엔드 웹 개발 핵심 기술]"을 듣고 정리한 내용입니다. 😁😁
이번에는 실제 지금까지 공부했던 스프링 MVC 전체 구조를 활용해서 웹 페이지를 만들어 볼 것이다 !
스프링 부트 프로젝트 생성
Project : Gradle Project
(Java-Groovy)
Language : JAVA
Spring Boot : 최신 버전 사용
project Metadata
Jar
Dependencies
Spring Web
Thymeleaf
Lombok
🎈 동작 확인
http://localhost:8080
호출해서 Whitelabel Error Page 나오면 정상 동작.편리하게 사용할 수 있도록 Welcome Page를 추가하자
정적 리소스 이기 때문에 경로 : resources/static/index.html
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<title>Title</title>
</head>
<body>
<ul>
<li>상품 관리
<ul>
<li><a href="/basic/items">상품 관리 - 기본</a></li>
</ul>
</li>
</ul>
</body>
</html>
상품을 관리할 수 있는 서비스를 만들어보자
🎃 상품 도메인 모델
🎃 상품 관리 기능
서비스 제공 흐름을 한번 보자!
항상 컨트롤러를 통해 뷰가 호출 돼(MVC 패턴)
🎃 서비스 전체 흐름을 보면 클라이언트가 먼저 상품 목록에 들어가면(상품 목록 컨트롤러에 들어가는거지) 컨트롤러가 상품 목록 뷰를 렌더링 해, 해당 상품 목록 뷰로 이동했다면, 이 뷰에서는 상품 등록 폼으로 이동할 수 있는데(상품 등록 버튼 클릭시) 상품 등록 폼 컨트롤러에서 상품 등록 폼 뷰를 보여준다.(뷰 템플릿으로 타임리프를 쓸꺼야)
그리고 상품 등록 폼에서 값을 입력해서 상품을 저장해(저장 버튼을 클릭하면) 여기서 상품 저장 컨트롤러로 이동할꺼야 여기 상품 저장 컨트롤러에서는 상품 상세 컨트롤러로 이동할 수 있다, 해당 컨트롤러 에서는 상품 상세 뷰를 호출해서 상품 상세 폼 뷰로 이동할 수 있을 것이다. 상품 상세 폼 뷰에서 상품 수정 폼으로 (상품 수정 클릭시)역시 이동할 수 있다. 또한 상품 수정 폼에서는 저장 버튼 클릭시 상품 상세 폼으로 redirect할 것이다.
이렇게 서비스 전체 흐름을 개발할꺼야!!!
이렇게 요구사항과 도메인, 화면이 어느정도 정리되면 웹 퍼블리셔, 백엔드 개발자가 업무를 나눠 진행해야 한다.
🎃 디자이너 : 요구사항에 맞도록 디자인 후 디자인 결과물을 웹 퍼블리셔에게 전달한다.
🎃 웹 퍼블리셔 : 디자이너에게 받은 디자인을 기반으로 HTML, CSS를 만들어 개발자에게 제공한다.
🎃 백엔드 개발자 : 디자이너, 웹 퍼블리셔를 통해서 HTML 화면이 나오기 전까지 시스템을 설계하고, 핵심 비즈니스 모델을 개발한다. 이후 HTML이 나오면 이 HTML을 뷰 템플릿으로 변환해서 동적으로 화면을 그리고, 또 웹 화면의 흐름을 제어한다.
React, Vue.js와 같은 웹 클라이언트 기술을 사용하거나 웹 프론트엔드 개발자가 따로 있으면 프론트 개발자가 웹 퍼블리셔 역할까지 포함해 하는경우도 있다.
이 경우 프론트엔드 개발자가 HTML을 동적으로 생성하고 웹 화면의 흐름도 담당하기에 백엔드 개발자는 뷰 템플릿을 만지는 대신 HTTP API를 통해 웹 클라이언트가 필요로 하는 데이터와 기능을 제공하면 된다.
지금 우리가 해볼 것은 뷰 템플릿을 가지고 렌더링하는 서비스를 만드는거야 HTTP API는 나중에...
이제 진짜 개발을 시작해보자.
상품 도메인은 다음과 같은 필드가 필요하다.
이를 코드로 구현하면..
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;
}
}
🎃 Lombok의 @Data
애노테이션을 활용해 getter, setter, 기본 생성자 등을 쉽게 구현한다.
상품 객체 Item을 저장할 리포지토리를 만들어줘야 한다.
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 {
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;
}
}
🎃 @Repository
애노테이션
해당 애노테이션을 씀으로써 컴포넌트 스캔의 대상이 되도록 했다.
컴포넌트 스캔이란, 스프링이 스프링 빈(Bean)으로 등록될 준비가 된 클래스들을 스캔하여 빈으로 등록해주는 과정을 말한다. @Repository
안에는 @Component
가 있기 때문에 컴포넌트 스캔의 대상이 된다 ! (@SpringBootApplicaion
애노테이션 안에 @ComponentScan
애노테이션이 들어있기에 이건 신경 안써도 됨)
컴포넌트 스캔의 대상이 되어 Bean 객체로 등록해준다 ! → @Autowired
로 DI(의존관계 주입)을 할 수 있다 !
🎃 기본적인 상품 저장, 조회, 목록 조회, 수정 기능을 추가했다.
🎃 개발 환경에서 리포지토리 내에 store 컬렉션을 초기화 해주기 위해 clearStore를 구현한다.
🎃 아이디는 전역변수로 선언된 sequence를 활용해 할당해준다.
상품을 저장하는 책임을 가진 상품 저장소(ItemRepository)에 만든 각 메서드들을 테스트할 필요가 있다.
package hello.itemservice.domain.item;
import org.junit.jupiter.api.AfterEach;
import org.junit.jupiter.api.DisplayName;
import org.junit.jupiter.api.Test;
import java.util.List;
import static org.assertj.core.api.Assertions.assertThat;
import static org.junit.jupiter.api.Assertions.*;
@DisplayName("ItemRepository 관련 기능 테스트")
class ItemRepositoryTest {
private ItemRepository itemRepository = new ItemRepository();
@AfterEach
void afterLogic() {
itemRepository.clearStore();
}
@Test
void saveTest() {
//given
Item item = new Item("itemA", 10000, 10);
//when
Item savedItem = itemRepository.save(item);
//then
Item foundItem = itemRepository.findById(item.getId());
assertThat(foundItem).isEqualTo(savedItem);
}
@Test
void findAllTest() {
//given
Item itemA = new Item("itemA", 10000, 10);
Item itemB = new Item("itemB", 20000, 20);
itemRepository.save(itemA);
itemRepository.save(itemB);
//when
List<Item> items = itemRepository.findAll();
//then
assertThat(items).hasSize(2);
assertThat(items).contains(itemA, itemB);
}
@Test
void updateTest() {
//given
Item itemA = new Item("itemA", 10000, 10);
itemRepository.save(itemA);
//when
Item updateParam = new Item("itemB", 20000, 20);
itemRepository.update(itemA.getId(), updateParam);
//then
Item foundItem = itemRepository.findById(itemA.getId());
assertThat(foundItem.getItemName()).isEqualTo(updateParam.getItemName());
assertThat(foundItem.getQuantity()).isEqualTo(updateParam.getQuantity());
assertThat(foundItem.getPrice()).isEqualTo(updateParam.getPrice());
}
}
🎃 수행 결과 정상적으로 모두 초록불이 나오면 기능은 제대로 구현되었다고 할 수 있다.
🎃 해당 테스트할 때 기능 별로 어떤걸 테스트하고 무엇을 비교해야 하는지 생각하면서 접근하자!
(given, when, then)
핵심 서비스 로직을 개발하는 동안 웹 퍼블리셔는 HTML마크업을 완료했다.
다음 파일들을 경로에 넣고 잘 동작하는지 확인해보자.
🎈 부트스트랩
해당 학습에서 CSS는 부트스트랩을 사용했다.
🎃 부트스트랩은 HTML을 편리하게 개발하기 위해 사용했다. 부트스트랩은 웹사이트를 쉽게 만들 수 있도록 도와주는 HTML, CSS JS 프레임워크이다.
https://getbootstrap.com
https://getbootstrap.com/docs/5.0/getting-started/download/
bootstrap.min.css
를 복사해서 다음 폴더에 추가하자resources/static/css/bootstrap.min.css
🎈 HTML, css 파일
/resources/static/css/bootstrap.min.css
부트스트랩 다운로드
/resources/static/html/items.html
아래 참조
/resources/static/html/item.html
/resources/static/html/addForm.html
/resources/static/html/editForm.htm
🎃 복사가 완료되었으면 해당 HTML 리소스들은 /resources/static
정적 경로에 넣어놨기에 스프링 부트에서 정적 리소스를 제공한다. 그래서 다음 링크로 접속을 시도하면 해당 HTML을 볼 수 있다. (정적 리소스는 GET일 때만 받을 수 있는데 POST면 URL 동작 안할꺼야 Error 405)
http://localhost:8080/html/item.html
참고로 resources/static
에 넣어두었기 때문에 정적 리소스. 정적 리소스는 해당 파일을 탐색기를 통해 직접 열어도 동작한다.
🎃 정적 리소스 경로(/resources/static)에 html을 넣어두면 실제 서비스에서도 공개되는데, 실제로 서비스를 운영할때는 공개할 필요가 없는 HTML같은 리소스를 이곳에 두는건 주의해야한다. (이렇게 하면 안되는걸 의미함 !!)
경로 : resources/static/html/items.html
<!DOCTYPE HTML>
<html>
<head>
<meta charset="utf-8">
<link href="/css/bootstrap.min.css" rel="stylesheet">
</head>
<body>
<div class="container" style="max-width: 600px">
<div class="py-5 text-center">
<h2>상품 목록</h2>
</div>
<div class="row">
<div class="col">
<button class="btn btn-primary float-end"
onclick="location.href='addForm.html'" type="button">상품
등록
</button>
</div>
</div>
<hr class="my-4">
<div>
<table class="table">
<thead>
<tr>
<th>ID</th>
<th>상품명</th>
<th>가격</th>
<th>수량</th>
</tr>
</thead>
<tbody>
<tr>
<td><a href="item.html">1</a></td>
<td><a href="item.html">테스트 상품1</a></td>
<td>10000</td>
<td>10</td>
</tr>
<tr>
<td><a href="item.html">2</a></td>
<td><a href="item.html">테스트 상품2</a></td>
<td>20000</td>
<td>20</td>
</tr>
</tbody>
</table>
</div>
</div> <!-- /container -->
</body>
</html>
경로 : resources/static/html/item.html
<!DOCTYPE HTML>
<html>
<head>
<meta charset="utf-8">
<link href="/css/bootstrap.min.css" rel="stylesheet">
<style>
.container {
max-width: 560px;
}
</style>
</head>
<body>
<div class="container">
<div class="py-5 text-center">
<h2>상품 상세</h2>
</div>
<div>
<label for="itemId">상품 ID</label>
<input type="text" id="itemId" name="itemId" class="form-control"
value="1" readonly>
</div>
<div>
<label for="itemName">상품명</label>
<input type="text" id="itemName" name="itemName" class="form-control"
value="상품A" readonly>
</div>
<div>
<label for="price">가격</label>
<input type="text" id="price" name="price" class="form-control"
value="10000" readonly>
</div>
<div>
<label for="quantity">수량</label>
<input type="text" id="quantity" name="quantity" class="form-control"
value="10" readonly>
</div>
<hr class="my-4">
<div class="row">
<div class="col">
<button class="w-100 btn btn-primary btn-lg" onclick="location.href='editForm.html'" type="button">상품 수정
</button>
</div>
<div class="col">
<button class="w-100 btn btn-secondary btn-lg" onclick="location.href='items.html'" type="button">목록으로
</button>
</div>
</div>
</div> <!-- /container -->
</body>
</html>
경로 : resources/static/html/addForm.html
<!DOCTYPE HTML>
<html>
<head>
<meta charset="utf-8">
<link href="/css/bootstrap.min.css" rel="stylesheet">
<style>
.container {
max-width: 560px;
}
</style>
</head>
<body>
<div class="container">
<div class="py-5 text-center">
<h2>상품 등록 폼</h2>
</div>
<h4 class="mb-3">상품 입력</h4>
<form action="item.html" method="post">
<div>
<label for="itemName">상품명</label>
<input type="text" id="itemName" name="itemName" class="formcontrol"
placeholder="이름을 입력하세요">
</div>
<div>
<label for="price">가격</label>
<input type="text" id="price" name="price" class="form-control"
placeholder="가격을 입력하세요">
</div>
<div>
<label for="quantity">수량</label>
<input type="text" id="quantity" name="quantity" class="formcontrol"
placeholder="수량을 입력하세요">
</div>
<hr class="my-4">
<div class="row">
<div class="col">
<button class="w-100 btn btn-primary btn-lg" type="submit">상품
등록
</button>
</div>
<div class="col">
<button class="w-100 btn btn-secondary btn-lg"
onclick="location.href='items.html'" type="button">취소
</button>
</div>
</div>
</form>
</div> <!-- /container -->
</body>
</html>
경로 : resources/static/html/editForm.html
<!DOCTYPE HTML>
<html>
<head>
<meta charset="utf-8">
<link href="../css/bootstrap.min.css" rel="stylesheet">
<style>
.container {
max-width: 560px;
}
</style>
</head>
<body>
<div class="container">
<div class="py-5 text-center">
<h2>상품 수정 폼</h2>
</div>
<form action="item.html" method="post">
<div>
<label for="id">상품 ID</label>
<input type="text" id="id" name="id" class="form-control" value="1"
readonly>
</div>
<div>
<label for="itemName">상품명</label>
<input type="text" id="itemName" name="itemName" class="formcontrol" value="상품A">
</div>
<div>
<label for="price">가격</label>
<input type="text" id="price" name="price" class="form-control"
value="10000">
</div>
<div>
<label for="quantity">수량</label>
<input type="text" id="quantity" name="quantity" class="formcontrol" value="10">
</div>
<hr class="my-4">
<div class="row">
<div class="col">
<button class="w-100 btn btn-primary btn-lg" type="submit">저장
</button>
</div>
<div class="col">
<button class="w-100 btn btn-secondary btn-lg"
onclick="location.href='item.html'" type="button">취소</button>
</div>
</div>
</form>
</div> <!-- /container -->
</body>
</html>