일 | 월 | 화 | 수 | 목 | 금 | 토 |
---|---|---|---|---|---|---|
1 | 2 | |||||
3 | 4 | 5 | 6 | 7 | 8 | 9 |
10 | 11 | 12 | 13 | 14 | 15 | 16 |
17 | 18 | 19 | 20 | 21 | 22 | 23 |
24 | 25 | 26 | 27 | 28 | 29 | 30 |
- 값 타입 컬렉션
- 트위터
- 스프링MVC
- Bean Validation
- 컬렉션 조회 최적화
- 스프링
- 임베디드 타입
- 스프링 mvc
- JPQL
- 실무활용
- 프로젝트 환경설정
- 로그인
- JPA 활용 2
- 페이징
- 예제 도메인 모델
- jpa 활용
- 기본문법
- 일론머스크
- Spring Data JPA
- JPA
- API 개발 고급
- 불변 객체
- 김영한
- JPA 활용2
- 스프링 데이터 JPA
- 타임리프
- 검증 애노테이션
- QueryDSL
- 타임리프 문법
- 벌크 연산
- Today
- Total
RE-Heat 개발자 일지
스프링 MVC 1편 - [7] 웹 페이지 만들기 [상편] 본문
출처 : https://www.inflearn.com/course/%EC%8A%A4%ED%94%84%EB%A7%81-mvc-1
인프런 김영한님의 스프링 MVC 1편 강의를 듣고 정리한 내용입니다.
[1] 프로젝트 생성
1. 스프링부트 프로젝트 생성
2. 실행 확인
기존 8080포트를 쓰고 있는 관계로 application.properties에 8081 포트로 변경
3. Welcom페이지 추가
정적페이지이므로 resources/static에 index.html 추가. 내용은 영한님이 제공해 주신 자료 복사·붙여넣기
4. UTF-8 세팅
미리 UTF-8로 세팅
5. gradle 실행 세팅(속도 향상 위해)
참고 : Jar가 아닌 War파일이며 IntelliJ가 무료버전이면 이 설정을 IntelliJ IDEA가 아닌 Gradle로 설정해야 톰캣이 정상적으로 동작한다.
[2] 요구사항 분석
■ 상품 도메인 모델
상품 ID
상품명
가격
수량
■ 상품 관리 기능
상품 목록
상품 상세
상품 등록
상품 수정
웹 개발 분업 (프론트엔드 개발자 없을 때)
디자이너 : 요구사항에 맞춰 디자인하고 웹 퍼블리셔에 넘김
웹 퍼블리셔 : 디자인 기반으로 HTML CSS 만들어 개발자에게 제공
백엔드 개발자 :
1] HTML 화면이 나오기 전 시스템을 설계하고 핵심 비즈니스 모델을 개발한다.
2] 이후 HTML을 받으면 HTML을 뷰 템플릿으로 변환해 동적으로 화면을 그리고 웹 화면 흐름을 제어한다.
단, 프론트엔드 개발자가 있으면 프론트엔드 개발자가 HTML을 동적으로 만드는 역할 + 웹 화면 흐름을 담당한다. 이럴 땐 백엔드 개발자는 API를 통해 클라이언트가 필요로 하는 데이터 및 기능을 제공하면 된다.
참고 : 회사에 따라 웹 퍼블리셔가 따로 있거나 프론트엔드 개발자가 퍼블리셔 역할까지 하는 케이스가 있다.
[3] 상품 도메인 개발
Item
@Data
public class Item {
private Long id;
private String itemName;
private Integer price;
private Integer quantity;
public Item() {
}
public Item(String itemName, Integer price, Integer quantity) {
this.itemName = itemName;
this.price = price;
this.quantity = quantity;
}
}
① 롬복 애노테이션 @Data
@Getter, @Setter, @ToString, @EqualsAndHashCode, @RequiredConstructor 기능 모두 씀
단, 예측하지 못하게 동작할 수 있기 때문에 핵심 도메인 모델에선 사용을 자제해야 한다.
② 프로퍼티 : id, itemName(item명), Price(가격), Quantity(수량)
참고]
https://joyful-class-maker.tistory.com/119
1) Id 타입 int 아닌 Long 쓰는 이유 : Long이 더 많은 값을 저장할 수 있기 때문.
2) long 대신 Long을 쓰는 이유 : Long은 값이 없으면 null로 초기화. 반면 long은 값이 없으면 0으로 초기화되는데, 실제 입력한 값이 0인지 입력되지 않아서 0인지 구분이 어렵다.
ItemRepository
@Repository
public class ItemRepository { //test 쉽게 만드는 건 ctrl+shift+T
private static final Map<Long, Item> store = new HashMap<>();
//static 실제론 HashMap쓰면 안 됨. 동시에 여러 쓰레드가 접근하므로. ConcurrentHashMap()을 사용해야 함.
private static Long sequence = 0L; //static
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()); //감싸서 반환하면 실제 store에 영향을 끼치지 않아서
}
public void update(Long itemId, Item updateParam){
//원래는 updateParam 객체(DTO를 만드는 게 낫다) id쓰는 거 헷갈릴 수 있음.
//설계상 명확한 게 낫다. 중복보단 명확성이 더 중요.
Item findItem = findById(itemId);
findItem.setItemName(updateParam.getItemName());
findItem.setPrice(updateParam.getPrice());
findItem.setQuantity(updateParam.getQuantity());
}
public void clearStore(){
store.clear();
}
}
① @Repository로 스프링빈 등록
② private static final Map<Long, Item> store = new HashMap<>();가 DB 역할을 함.
③ 메소드
1] save() : 상품 등록
2] findById() : id로 item 객체 찾아서 반환.
3] findAll() : map respository에서 모든 item 객체 반환
ArrayList<>() 활용 이유 : ArrayList로 감싸면 실제 store에 영향을 끼치지 않음.
4] update() : 상품 수정
5] clearStore() : map respository 초기화. 테스트에서 사용
ItemRepositoryTest
class ItemRepositoryTest {
ItemRepository itemRepository = new ItemRepository();
@AfterEach
void afterEach(){
itemRepository.clearStore();
}
@Test
void save() {
//given
Item item = new Item("itemA", 10000, 10);
//when
Item savedItem = itemRepository.save(item);
//then
Item findItem = itemRepository.findById(item.getId());
assertThat(findItem).isEqualTo(savedItem);
}
@Test
void findAll() {
//given
Item item1 = new Item("item1", 10000, 10);
Item item2 = new Item("item2", 10000, 10);
itemRepository.save(item1);
itemRepository.save(item2);
//when
List<Item> result = itemRepository.findAll();
//then
assertThat(result.size()).isEqualTo(2);
assertThat(result).contains(item1, item2); // item1, item2가 포함돼 있느냐?
}
@Test
void update() {
//given
Item item1 = new Item("item1", 10000, 10);
Item savedItem = itemRepository.save(item1);
Long itemId = savedItem.getId();
//when
Item updateParam = new Item("item2", 20000, 30);
itemRepository.update(itemId, updateParam);
//then
Item findItem = itemRepository.findById(itemId);
assertThat(findItem.getItemName()).isEqualTo(updateParam.getItemName());
assertThat(findItem.getPrice()).isEqualTo(updateParam.getPrice());
assertThat(findItem.getQuantity()).isEqualTo(updateParam.getQuantity());
}
}
① @AfterEach로 그때그때 itemRepository를 클리어해 줌.
=> @AfterEach : 각각의 테스트 메서드 실행 후 무조건 실행
=> @BeforeEach : 각각의 테스트 메서드 실행 전 무조건 실행
② assertThat을 자주 사용돼 static화 해주면 편하다.
import static org.assertj.core.api.Assertions.assertThat;
참고]
테스트 코드는 given[준비]/when[실행]/then[검증] 순으로 짜는 게 바람직하다.
[4] 상품 서비스 HTML
웹 퍼블리셔가 HTML 마크업을 완료하면 잘 동작하는지 확인하는 과정
부트스트랩 설치방법
- 부트스트랩을 다운로드하고 압축을 풀자.
- 이동: https://getbootstrap.com/docs/5.0/getting-started/download/
- Compiled CSS and JS 항목을 다운로드하자.
- 압축을 풀고 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.html
/resources/static에 넣어둬 스프링부트가 정적리소스로 제공한다.
접근 방법
① 절대경로로 직접 접근 가능
파일 우클릭 => Copy Path/Reference => Absolute Path를 누르면 절대경로 카피됨
② 정적리소스이므로 http://localhost:8081/html/items.html 로도 동작한다.
[5] 상품 목록 - 타임리프
본격적으로 뷰 템플릿 구현!
BasicItemController
@Controller
@RequestMapping("/basic/items")
@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";
}
/*
테스트용 데이터 추가
* */
@PostConstruct
public void init() {
itemRepository.save(new Item("itemA", 10000, 10));
itemRepository.save(new Item("itemB", 20000, 20));
}
①메소드
items() :
1] findAll()로 아이템 전체목록을 받은 후 List에 담는다.
2] 모델 객체에 items에 담음
3] resources/templates/basic/itmes.html로 render 해줌
② 애노테이션
1] @RequiredArgsConstructor :
final이 붙은 멤버 변수만 이용해 생성자를 자동으로 만들어준다.
이렇게 생성자 한 개만 있으면 스프링이 해당 생성자에 의존관계 주입(@AutoWired)
따라서 final 키워드를 빼면 안 된다. 그러면 ItemRepository 의존관계 주입 X
아래 코드는 이 애노테이션이 대체한 생성자
public BasicItemController(ItemRepository itemRepository) {
this.itemRepository = itemRepository;
}
2] @PostConstructor
의존성 주입이 이루어진 후 초기화를 수행하는 메서드. 즉, WAS가 뜨고 bean이 생성된 직후 딱 한 번만 실행됨
의존성 주입 후 실행이 보장돼 빈의 초기화를 걱정할 필요 없음.
3] @PreDestroy [참고용]
Spring이 애플리케이션 컨텍스트에서 bean을 제거하기 직전에 단 한 번 실행.
자원 반환 등 종료 처리에 사용됨.
■ 타임리프 문법
① 타임리프 사용선언
<html xmlns:th="http://www.thymeleaf.org">
② 속성 변경 th:xxxx
타임리프 뷰 템플릿을 거치면 기존 값을 th:xxx 값으로 변경한다. 값이 없으면 새로 생성함.
<html xmlns:th="http://www.thymeleaf.org">
<head>
<meta charset="utf-8">
<link th:href="@{/css/bootstrap.min.css}"
href="../css/bootstrap.min.css" rel="stylesheet">
</head>
템플릿을 거치면 href="../css/bootstrap.min.css"대신 th:href="" <이 코드로 대체됨.
onclick="location.href='addForm.html'"
th:onclick="|location.href='@{/basic/items/add}'|"
onclick도 뷰 템플릿을 거치면 th:onclick의 값으로 변경됨.
③ URL 링크 표현식
1] 표현식 1
■ 단순 URL
th:href="@{/css/bootstrap.min.css}"
타임리프에선 URL을 나타내기 위해 @{...}으로 감싼다.
과거엔 서블릿 컨텍스트란 개념이 있어서 applcationA/... 이런 식으로 썼으나 지금은 쓰지 않는다.
2] 표현식 2
■ 경로 변수가 포함된 URL
th:href="@{/basic/items/{itemId}(itemId=${item.id})}"
()안에 경로변수 itemId를 초기화 할당.
경로변수뿐만 아니라 쿼리 파라미터도 생성가능
=> 경로변수를 지정하지 않고 바로 값을 꺼내는 것도 가능하다. 단, 리터럴 대체(|...|) 문법을 써 줘야 한다.
<td><a href="item.html" th:href="@{/basic/items/{itemId}(itemId=${item.id})}" th:text="${item.id}">회원id</a></td>
<td><a href="item.html" th:href="@{|/basic/items/${item.id}|}" th:text="${item.itemName}">상품명</a></td>
■ 경로 변수 + 쿼리 파라미터가 포함된 URL
th:href="@{/basic/items/{itemId}(itemId=${item.id}, query='test')}"
이렇게 하면 생성되는 링크는 http://localhost:8080/basic/items/1?query=test
④ 리터럴 대체 |...|
타임리프는 문자와 표현식 등이 분리돼 있어 원래는 이런 식으로 코드를 작성해야 한다.
기존 ① <span th:text="'Welcome to our application, ' + ${user.name} + '!'">
기존 ② th:onclick="'location.href=' + '\'' + @{/basic/items/add} + '\''"
그러나 리터럴 대체 문법을 사용하면 위 코드를 아래 코드처럼 편리하게 사용할 수 있다.
리터럴 적용 ① <span th:text="|Welcome to our application, ${user.name}!|">
리터럴 적용 ② th:onclick="|location.href='@{/basic/items/add}'|"
⑤ 반복 출력 - th:each
<tbody>
<tr th:each="item : ${items}">
<td><a href="item.html" th:href="@{/basic/items/{itemId}(itemId=${item.id})}" th:text="${item.id}">회원id</a></td>
<td><a href="item.html" th:href="@{|/basic/items/${item.id}|}" th:text="${item.itemName}">상품명</a></td>
<td th:text="${item.price}">10000</td>
<td th:text="${item.quantity}">10</td>
</tr>
</tbody>
1] <tr th=each="item : ${items}"> 이렇게 하면 items 컬렉션 데이터[리스트]를 item 변수에 하나씩 꺼내 쓰고 반복문 안에서 item 변수를 사용할 수 있다.
2] 당연히 이 변수는 반복문 안에서만 사용 가능하다.
⑥ 변수 표현식 ${....}
모델에 포함돼 넘어온 값이나, 타임리프 변수로 선언한 값을 조회할 수 있다.
프로퍼티 접근법을 사용함. 그래서 item.id는 data.getId()와 같다.
'백엔드 > 스프링' 카테고리의 다른 글
스프링 MVC 2편 - [1] 타임리프 - 기본기능(상편) (0) | 2023.07.08 |
---|---|
스프링 MVC 1편 - [7] 웹 페이지 만들기 [하편] (0) | 2023.07.06 |
스프링 MVC 1편 - [6] 기본 기능(하편) (0) | 2023.07.02 |
스프링 MVC 1편 - [6] 기본 기능(상편) (0) | 2023.06.30 |
스프링 MVC 1편 - [5] 스프링 MVC 구조 이해 (0) | 2023.06.29 |