-
혼자서도 열심히 인텔리제이로 김영한님 스프링 강의 듣기 깃허브도 연동한다PROGRAMMING/Spring 2023. 2. 5. 23:41
1. 프로젝트 생성 및 설정
- spring initailzer에서 생성한다.
- 롬복을 사용하기 위해서는 설정(ctrl alt s)에서 Compiler - Annotation Processors - Enable annotation processing 체크해야 한다.

롬복 사용 위한 설정 실행속도를 빠르게 하기 위해 build를 gradle이 아닌 intellij로 한다.

build and run 설정을 intelliJ로 한다.
2. intellij에서 프로젝트 생성하고 github에 repo까지 생성하기
이미 해버려서 사진은 없지만, 아마 Tools에 make new github repo 같은 게 있던 거 같다. 해당 선택지를 고르고 로그인하자. 자동으로 repo가 생성되고 (좋아요) branch등도 자유롭게 선택 가능하다.

3조가 나에게 남긴 최고의 유산 : intellij와 github 사용법 
3. 개발 전 요구사항 분석
상품 도메인 모델이 있으며, 상품을 관리하는 기능이 있어야 한다.
상품 목록 : 상품 id, 상품명, 가격, 수량
상품 상세 : 상품 목록에서 id 혹은 상품명을 누르면 이동 가능
: id, 상품명, 가격, 수량 조회 가
: [상품 수정], [목록으로] 버튼 존재
상품 등록 폼 : 상품명, 가격, 수량 입력 가능
: [상품 등록], [취소] 버튼 존재
상품 수정 폼 : 상품명, 가격, 수량 입력 가능
: id 조회 가능 (변경 불가)
: [저장], [취소] 버튼 존재

4. 상품 도메인 개발
(1) item
package spring.itemservice.domain; import lombok.Data; @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; } }Q. int를 사용하지 않은 이유는?
A. null이 들어갈 수 있기 때문이다. 상황에 맞게 int 혹은 integer를 사용하자.
내 생각 : 그냥 null도 못 들어가게 int로 처리하는 게 낫지 않을까?
Q. @Data 를 사용한 이유는?
A. 사실 @Data는 Domain에 사용하기 적합하지 않다. dto같이 왔다갔다하는 경우는 사용 가능하다. (김영한님은 dto와 domain을 따로 사용하나보다. 우리는 dto 하나만 사용하거나, vo 하나만 사용했었는데.)
그도 그럴 것이 @Data annotation의 설명을 보면 @Getter @Setter @RequiredArgsConstructor @ToString @EqualsAndHashCode 까지 모두 만들어준다. 다만, 이 예제는 간단하기에 @Getter @Setter를 사용하지 않고 @Data를 사용한다.
내 생각 : 처음엔 편리하다고 생각했는데 @Getter @Setter도 사용하자 말자 말이 나오는 상황에서 @Data를 사용하는 것은 확실히 위험부담이 늘어날 거 같다. 하지만 ... 흥미로워!
(2) ItemRepository
private static final Map<Long, Item> store = new HashMap<>(); //static private static long sequence = 0L; //static- static을 사용했다.
- 실제로는 멀티스레드 환경에서 Map을 사용해서는 안 된다. 현재 store는 싱글톤으로 생성되고 있기에, 동시에 map에 접근시 데이터가 꼬일 수 있기 때문이다.
- 동시성 문제를 고려하여 atomic, ConcurrentHashMap 등을 사용해 문제를 예방하는 것이 좋다.
public List<Item> findAll() { return new ArrayList<>(store.values()); }- 감싼 이유 : 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()); }- 사실 이 경우는 dto를 만드는 것이 맞다. (Item은 vo, update 등으로 계층을 건너는 값들은 dto로 설정하신다는 뜻인가?)
- 왜냐하면 현재 Item에는 id, itemName, price, quantity가 있다. 하지만 update되는 값은 id를 제외한 세 가지이다. 이 경우, 개발자는 왜 id를 사용하지 않는지 헷갈리게 된다.
- 비중복과 명확성 중 하나를 골라야 한다면 명확성을 고르자.
- 지금은 간단한 프로젝트라 직접 값을 넣어줬다.
(3) test
main의 ItemRepository와 같은 위치(Test 아래)에 만든다.
import static org.assertj.core.api.Assertions.*; assertThat(result.size()).isEqualTo(2); // 원래라면? Assertions.assertThat(result.size()).isEqualTo(2);- alt enter : static import 해준다. 앞에 하나한 Arrertions. 적을 필요 없다.
@AfterEach void afterEach() { itemRepository.clearStore(); }- @AfterEach 는 test가 끝날 때마다 해당 코드를 실행해주는 annotation 이다.
- 이 프로젝트는 Map<> store를 clear() 하는 메소드를 하나 생성하고, 그 메소드를 활용하는 방식을 사용했다.
5. 상품 서비스 HTML 만들기
- https://getbootstrap.com/docs/5.0/getting-started/download/ 에서 부트스트랩 다운받고 압축 해제하기
- static 밑에 css 폴더를 만들고, 그 밑에 bootstrap.min.css 파일만 추가하기
- 스프링 서버 실행 후 http://localhost:8080/css/bootstrap.min.css 로 들어가지는지 확인
- 만약 들어가지지 않는다면, out 폴더를 없애자.
- 재실행하면 자동으로 컴파일되어 만들어진다.

out 폴더 여기있음 
Q. 다른 html 페이지는 잘 뜨는데 왜 form에서 submit한 페이지는 error페이지가 뜨나요?
A. 우리가 만든 건 정적 html 페이지이고, get만 받는다. form은 post 방식을 사용하기에 오류가 발생하는 것이다.
(+) 배웠지만 원래 이렇게 resource 밑에 바로 만들면 안 된다. 소스가 모두 보인다. WEB-INF 밑에 넣어야 한다. 지금은 간단한 프로젝트라 정적으로 만들었고, 곧 타임리프를 사용해 동적으로 변경할 것이다.
6. 타임리프를 사용해 상품 목록을 동적으로 만들기
@Controller @RequestMapping("/basic/items") public class BasicItemController { private final ItemRepository itemRepository; @Autowired public BasicItemController(ItemRepository itemRepository) { this.itemRepository = itemRepository; } }@Controller @RequestMapping("/basic/items") @RequiredArgsConstructor public class BasicItemController { private final ItemRepository itemRepository; }- 위의 코드를 @RequiredArgsConstructor 를 사용한다면 아래와 같이 변경할 수 있다.
- 생성자 주입인가?
@PostConstruct public void init() { itemRepository.save(new Item("item1", 10000, 10)); itemRepository.save(new Item("item2", 20000, 20)); }- @PostConstruct를 사용해서 Post값을 초기화시켰다. 테스트 용도이다.
타임리프 사용하기!
- 타임리프의 특이한 점 : html 코드가 있어도 무시하고 (덮어쓰고) 진행한다.
- 뷰 템플릿으로 렌더링 될 때만 치환된다.
- natural templates! 순수 HTML을 그대로 유지하면서 뷰 템플릿도 사용할 수 있는 타임리프의 특징이다.
- 만약 html을 그대로 본다면 (정적) 'href' 속성이 사용되고, view templete을 거치면 'th:href'의 값이 'href'로 변경되며 동적으로 변경될 수 있다.
- 타임리프 핵심
- ht:xxx가 붙은 부분은 서버사이드에서 렌더링된다. 기존 것을 대체한다.
- th:xxx가 없으면 기존 html의 xxx 속성이 그대로 사용된다.
- jsp를 생각해 보자 ... 그냥 실행 절대 안 됨
- templetes 밑에 items.html을 복사했다.
// 타임리프 사용! <link th:href="@{/css/bootstrap.min.css}" rel="stylesheet"> // 원래 코드! <link href="../css/bootstrap.min.css" rel="stylesheet">th:onclick
<button class="btn btn-primary float-end" onclick="location.href='addForm.html'" th:onclick="|location.href='@{/basic/items/add}'|" type="button">상품 등록 </button>- 타임리프에서 링크를 사용할 때는 항상 @{ ... }를 붙이자!
리터럴 대체
// 내가 원하는 결과 location.href='/basic/items/add' // 타임리프 미사용시 (이거 완전 ajax jsp 아님?) th:onclick="'location.href=' + '\'' + @{/basic/items/add} + '\''" // 타임리프 사용시 th:onclick="|location.href='@{/basic/items/add}'|"th:text : 내용 변경
<td th:text="${item.price}"></td>URL 링크 표현식 1
<link th:href="@{/css/bootstrap.min.css}" rel="stylesheet">URL 링크 표현식 2
<td><a href="item.html" th:href="@{/basic/item/{itemId}(itemId=${item.id})}" th:text="${item.id}">회원id</a> </td>두 가지 방법 사용 예시 (URL)
<td><a href="item.html" th:href="@{/basic/item/{itemId}(itemId=${item.id})}" th:text="${item.id}">회원id</a> </td> <td><a href="item.html" th:href="@{|/basic/item/${item.id}|}" th:text="${item.itemName}">상품명</a> </td>
7. 상품 상세 구현
th:value
- 사실 th:text와 무슨 차이인지 잘 모르겠다
- th:text는 for문같이 text로 제공되는 걸 받는 거고, th:value는 model로 보낸 값(속성)을 ${item.id} 처럼 받는 건가?
- 그런데 for도 속성값 들고 오는 건데 ...
- 아무튼 th:text는 for문에서, th:value는 model로 받은 값을 바로 받을 때 사용했따.
// controller 코드 @GetMapping("/{itemId}") public String item(@PathVariable long itemId, Model model) { Item item = itemRepository.findByID(itemId); model.addAttribute("item", item); return "basic/item"; }// item.html 코드 <div> <label for="quantity">수량</label> <input type="text" id="quantity" name="quantity" class="form-control" value="10" th:value="${item.quantity}" readonly> </div>
8. 상품 등록 (form)
th:action
- HTML form에서 action에 값이 없으면 현재 URL에 데이터를 전송한다.
<form action="item.html" th:action method="post"> </form>- 같은 URL이지만 HTTP 메서드로 두 기능을 구분했다.
@GetMapping("/add") public String addForm() { return "basic/addForm"; } @PostMapping("/add") public String save() { return "basic/addForm"; }
9. 상품 등록 처리 : @ModelAttribute
- 위 코드에 (@ModelAttribute("item") Item item, Model model )를 사용하면 아래 코드와 같이 간편히 바꿀 수 있다.
- 가능한 이유는 @ModelAttribute를 사용하면 알아서 객체 생성과 setter를 해주기 때문이다.
@PostMapping("/add") public String addItem1(@RequestParam String itemName, @RequestParam int price, @RequestParam Integer quantity, Model model) { Item item = new Item(); item.setItemName(itemName); item.setQuantity(quantity); item.setPrice(price); itemRepository.save(item); model.addAttribute("item", item); return "basic/item"; }- 코드가 간편해졌지만, 주석친 부분까지 뺴도 된다.
- 왜냐하면 @ModelAttribute("item") 이라고 하면 주석친 부분의 역할까지 해주기 때문이다.
- setter와 객체 생성과 model에 담기를 한번에!
@PostMapping("/add") public String addItem2(@ModelAttribute("item") Item item, Model model) { itemRepository.save(item); // model.addAttribute("item", item); 자동 추가, 생략 가능 return "basic/item"; }- 만약 ("item")까지 생략한다면 클래스의 첫 글자를 소문자로 바꾸어 자동 add된다.
@PostMapping("/add") public String addItem3(@ModelAttribute Item item) { itemRepository.save(item); return "basic/item"; }- 아래의 경우에도 대상 객체는 모델에 자동 등록된다.
@PostMapping("/add") public String addItem4(Item item) { itemRepository.save(item); return "basic/item"; }
10. 상품 수정
@GetMapping("/{itemId}/edit") public String editForm(@PathVariable Long itemId, Model model) { Item item = itemRepository.findByID(itemId); model.addAttribute("item", item); return "basic/editForm"; } @PostMapping("/{itemId}/edit") public String edit(@PathVariable Long itemId, @ModelAttribute Item item) { itemRepository.update(itemId, item); return "redirect:/basic/items/{itemId}"; }- 상품 등록과 같이 HTTP 메서드로 두 기능을 구분했다.
- redirect 사용함
- html form 전송은 GET, POST만 사용한다.
- 문제가 생겼다! 상품 등록을 누르고 등록 완료 화면에서 새로고침을 계속 하면 상품이 중복 등록된다!
- 이를 해결하기 위함이 PRG 패턴이다.
11. !! PRG : Post Redirect Get !!
- 단순히 get => post로 끝낸다면 브라우저에서 내 마지막 동작은 POST이다.
- 새로고침을 하는 건 마지막 동작을 다시 하는 것이다. POST를 다시 하게 되므로 동일한 상품 정보가 다시 추가되게 된다. ID만 변경된다.

- 그래서 위 사진과 같은 로직을 사용한다.
- 웹 브라우적의 새로 고침은 마지막에 서버에 전송한 데이터를 다시 전송한다.
- 상품 저장 후 뷰 템플릿으로 이동이 아닌, 상품 상세 화면으로 redirect 해야 한다.
- 마지막에 호출한 내용이 GET /items/{id} 가 된다. 새로고침 하더라도 문제가 되지 않는다.
@PostMapping("/add") public String addItem5(Item item) { itemRepository.save(item); return "redirect:/basic/items/" + item.getId(); }- 등록 후 redirect되게 했다.
- 하지만 item.getId()와 같이 직접적인 값을 주는 것은 위험하다.
- RedirectAttributes를 사용하여 값을 인코딩 시켜 넘겨 보자.
12. RedirectAttributes
!!! 짱 신기하다!!!
- RedirectAttributes를 사영하면 URL 인코딩도 해주고, pathVariable, 쿼리 파라미터까지 처리해준다.
- pathVariable : "redirect:/basic/items/{itemId}"
- 쿼리 파라미터 : pathVariable 로 바인딩 되지 않은 것들
// controller code @PostMapping("/add") public String addItem6(Item item, RedirectAttributes redirectAttributes) { Item savedItem = itemRepository.save(item); redirectAttributes.addAttribute("itemId", savedItem.getId()); redirectAttributes.addAttribute("status", true); return "redirect:/basic/items/{itemId}"; }// item.html code <h2 th:if="${param.status}" th:text="'저장 완료'"></h2>// 아래와 같은 url이 나온다. http://localhost:8080/basic/items/3?status=true
끝!
'PROGRAMMING > Spring' 카테고리의 다른 글
[ IntelliJ ] 기본 언어 영어로 변경하기, 대소문자 무시하고 자동완성 (0) 2023.02.05