인프런/스프링 MVC 1편

17)상품 도메인 개발,부트스트랩,타임리프 적용

backend dev 2023. 1. 27.

Item.java

// @Data는  @Getter @Setter @RequiredArgsConstructor @ToString @EqualsAndHashCode를 제공하기에 핵심 도메인에서 사용하기에는 위험하다(예측하지못하게 동작하는경우가 있어서)
@Getter @Setter // 자신이 판단하기에 필요한것만 가져와 사용하자.
@NoArgsConstructor //아무 파라미터도 받지않는 생성자만듬
public class Item {

    private Long id; //repository에 아이템을 저장할떄 부여된다.
    private String itemName;
    private Integer price; //Integer를 쓰는 이유는 해당 값이 안들어갈수도 있는 경우 null이 들어갈것을 대비하기 위해서다.
    private Integer quantity; //int를 쓰게되면 기본값이 0인데, price 경우는 0으로 될경우 혼란이 발생할수있으므로 Integer를 사용

    public Item(String itemName, Integer price, Integer quantity) { //아이디를 제외한 생성자 만듬
        this.itemName = itemName;
        this.price = price;
        this.quantity = quantity;
    }
}

여기서 item은 만약 데이터베이스가 존재한다면 1:1로 매핑되는 객체이므로 entity일것같다.

@Data

@Data는  @Getter @Setter @RequiredArgsConstructor @ToString @EqualsAndHashCode를 제공하기에

핵심 도메인에서 사용하기에는 위험하다 (예측하지못하게 동작하는경우가 있어서)

 

DTO(Data Transfer Object)라는 단순하게 데이터를 주고받기 위한 용도인경우는 @Data를 사용해도 무방하다.

 

DTO는 MemberRequestDTO, MemberResponseDTO 등 요청과 응답을 하기위한 객체용도 즉 단순하게 데이터를 주고받을때 사용할 객체를 의미하고

 

Entity는 데이터베이스 테이블과 1:1 매핑되는 객체를 의미한다 예를들어 Member와 같은 객체 

즉 데이터베이스에 Member라는 테이블과 매핑되는 객체를 Entity라고 한다.

 

아무튼 어디든 @Data를 사용하려면 잘 알고 사용해야한다.

@Data가 위험한 이유 

 

@Data 는 사용하기 위험하다고 말씀하셔서 질문합니다! - 인프런 | 질문 & 답변

@Data 애노테이션이 생성되는 것이 이 클래스에서 @Getter, @Setter를 빼면 @RequiredArgsConstructor와 @ToString @EqualsAndHashCode로 보이는데 이것들이 자동으로 생겨나서 위험한 게 있을까요? @Setter랑 생성자만

www.inflearn.com

ItemRepository

@Repository
public class ItemRepository {

    private static final Map<Long, Item> store = new HashMap<>();
    //멀티쓰레드환경(Spring)에서 싱글톤클래스의 멤버변수로 HashMap을 쓰면안된다. ConcurrentHashMap을 사용해야한다.
    //어차피 스프링빈으로 등록할것이고 스프링빈은 기본적으로 싱글톤이니까 크게 static을 안써도되지만
    //따로 new로 객체를 생성해서 사용할수도있는 경우에는 필드의 값이 유지되어야하므로 static을 붙여놓는다.
    private static long sequence = 0L; // 싱글톤클래스의 멤버변수에 long을 쓰면 값이 꼬일수있다.AtomicLong을 사용해야한다.


    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객체를 이용해서 수정할 값을 받아왔는데 id변수가 사용안되니 3가지값만 가지는 객체 하나를 만들어준다( 예)parameterDTO와 같은)
        Item findItem = findById(itemId);
        findItem.setItemName(updateParam.getItemName());
        findItem.setPrice(updateParam.getPrice());
        findItem.setQuantity(updateParam.getQuantity());
    }

    public void clearStroe() {
        store.clear();
    }



}

 

ItemRepositoryTest

ItemRepository에서 컨트롤 + 쉬프트 + T를 눌러 테스트를 생성한다. ( 직접 생성해도됨)

class ItemRepositoryTest {

    ItemRepository itemRepository = new ItemRepository();

    @AfterEach //테스트가 끝날때마다 store에 저장된값을 지우기 위함
    void afterEach() {
        itemRepository.clearStroe();
    }

    @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);  //isEqualTo는 같은내용인지비교 ,isSameTo는 같은객체인지비교
    }
    @Test
    void findAll() {
        //given
        Item item1 = new Item("itemA", 10000, 10);
        Item item2 = new Item("itemB", 20000, 20);

        itemRepository.save(item1);
        itemRepository.save(item2);
        //when
        List<Item> result = itemRepository.findAll();
        //then
        assertThat(result.size()).isEqualTo(2);
        assertThat(result).contains(item1, item2); // 해당 객체를 가지고있는지 체크하는 Contains메소드
        /*
           // assertions will pass
          assertThat(abc).contains("b", "a");
          assertThat(abc).contains("b", "a", "b");

          // assertion will fail
          assertThat(abc).contains("d");
         */
    }
    @Test
    void updateItem() {
        //given
        Item item = new Item("itemA", 10000, 10);
        Item savedItem = itemRepository.save(item);
        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());
    }


}

상품 서비스 HTML

html 마크업 -> 디자인한것을 HTML로 돌아가도록 바꿔준것 (HTML 파일로)

 

Download

Download Bootstrap to get the compiled CSS and JavaScript, source code, or include it with your favorite package managers like npm, RubyGems, and more.

getbootstrap.com

out 에도 해당 css가 잘있는지 체크한다.

없으면 서버를 끈후 out폴더 자체를 삭제한후 다시 서버를 켜주면 생성된다.

 

필요한 페이지에 대한 HTML 추가
주의할점

가지고온 css에 대한 사용은 각 html마다 상대경로를 통해 잘 설정되어있다.

 

상품목록 - 타임리프

@Controller
@RequestMapping("/basic/items")
@RequiredArgsConstructor //생성자가 한개이고, final이 붙은 필드를 생성자 주입받고 싶을때 사용
public class BasicItemController {

    private final ItemRepository itemRepository; //스프링컨테이너있는 아이템리포지토리 스프링빈을 주입받는다.

    @GetMapping
    public String items(Model model) {
        List<Item> items = itemRepository.findAll();
        model.addAttribute("items", items); //model에 데이터를 담아준다.
        return "basic/item"; //해당 뷰로 이동 (모델도 가지고)
    }

    @PostConstruct // 해당빈이 초기화되면 실행된다.
    public void init() { //테스트용 아이템을 추가하기 위함.
        itemRepository.save(new Item("testA", 10000, 10));
        itemRepository.save(new Item("testB", 20000, 20));
    }

}

 

@PostConstruct : 빈이 초기화 되고나면 실행된다.

아래 블로그 참고

 

17.마지막)bean 생명주기 콜백 3가지방법,빈 스코프,웹 스코프,스코프와 프록시

인터페이스 InitializingBean, DisposableBean 방법 NetworkClient 수정 public class NetworkClient implements InitializingBean, DisposableBean { //InitializingBean 구현 -> 초기화 콜백관련 DisposableBean 소멸 콜백관련 private String url;

keeeeeepgoing.tistory.com

 items()메소드를 사용하고 반환값으로 basic/item 을 반환하는데 해당 위치에 뷰템플릿이 존재하지않는다.

static/html/item.html를  templates/basic/로 복사 한후 타임리프 템플릿으로 수정해보자.

 

타임리프 템플릿으로 수정하기

<html xmlns:th="http://www.thymeleaf.org">

여는 html 태그에 다음과 같이 코드를 추가한다. 그러면 이제 th라는 태그를 사용할 수 있게된다.

여기까지만해도 화면을 보는것은 문제가없다

<html xmlns:th="http://www.thymeleaf.org">
<head>
  <meta charset="utf-8">
  <link href="../css/bootstrap.min.css" rel="stylesheet">
</head>
<body>

css경로가 상대경로로 되어있는것을 확인할 수 있다.  타임리프문을 이용해서 절대경로로 바꿔주자.

(상대경로는 해당 파일의 위치가 변하면 수정해야하므로)

th가 있으면 타임리프는  

<link th:href="@{/css/bootstrap.min.css}"
    href="../css/bootstrap.min.css" rel="stylesheet">

이렇게 두개가 설정되어있어도 기존것을 날리고 th를 붙은것을 선택한다.

렌더링이 될때 th가 있는것으로 치환이 된다! 

정적으로 열릴경우를 대비해서 기본속성은 냅두는것같다.

(url링크를 적을때는 @{} 를 이용한다)

<div class="row">
  <div class="col">
    <button class="btn btn-primary float-end"
            onclick="location.href='addForm.html'" type="button">상품
      등록</button>
  </div>
</div>

상품등록 버튼을 보면 클릭시 addForm.html로 가게 되어있다. 그것을 타임리프로 수정해준다.

<div class="col">
  <button class="btn btn-primary float-end"
          onclick="location.href='addForm.html'"
          th:onclick="|location.href='@{/basic/items/add}'|"
          type="button">상품
    등록</button>
</div>

그리고 상품목록을 보여주는 부분도 수정한다.

  <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 -->

모든 아이템을 찾아와서 루프를 돌려서 아이템목록을 보여줘야한다.

<div>
  <table class="table">
    <thead>
    <tr>
      <th>ID</th>
      <th>상품명</th>
      <th>가격</th>
      <th>수량</th> </tr>
    </thead>
    <tbody>
    <tr th:each="item : ${items}">
      <td><a href="item.html" th:text="${item.id}">상품ID</a></td>
      <td><a href="item.html" th:text="${item.itemName}">상품명</a></td>
      <td th:text="${item.price}">상품가격</td>
      <td th:text="${item.quantity}">상품갯수</td>
    </tr>
    </tbody>
  </table>
</div>
<tr th:each="item : ${items}">

모델의 items라는 속성의 값을 가져와서 item에 하나씩 넣는다. (리스트의 iter같이)

<td><a href="item.html" th:text="${item.id}">상품ID</a></td>

href는 링크이고, 아직 타임리프 적용전이다.

th:text는 상품ID라고 적힌 저부분을 "${}" 의 내용으로 치환한다.

item.id는 곧 item.getId()와 같다.

데이터를 이용할때는 ${}를 이용한다.

이렇게 테스트로 넣은 데이터가 잘 들어간것을 확인할 수 있다.

<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}">상품가격</td>
  <td th:text="${item.quantity}">상품갯수</td>
</tr>
</tbody>

타임리프로 url링크넣는 부분을 수정해보자. url링크를 넣는부분은 @{}가 들어가야하고

{변수명}(변수명 = ${데이터}) 이런식으로 {itemId}가 없지만 () 괄호안에서 해당 변수에 대해 값을 집어넣어주는 방식으로 해준다.  마치 PathVariable처럼

이렇게 해주면 상품마다 다 다른 url이 적용되도록 (동적으로 바뀌도록) 할 수 있다.

 

혹은 PathVariable을 사용안하고 리터럴 문법과 변수표현식을 이용해서 만들어줘도 된다! 

아직 화면은 없지만, 1번 아이템의 링크를 눌렀을때 해당 url로 잘 이동하는것을 확인할 수 있다.

쿼리스트링도 추가할수있다!!

 

타임리프가 좋은점은 html태그안에서 타임리프 문법을 사용하는것이므로 서버를끄고, 해당 템플릿을 실행시켜도 기본적인 템플릿은 확인가능하다.

파일에 오른쪽클릭 -> Copy Path 클릭 -> Absolute Path 선택후 브라우저에 복붙하면 서버가 꺼져있어도 정적인 페이지를 확인가능하다.  (타임리프속성 추가할때 기존의 속성들을 지우지않았다면)

이런식으로 기본값들로 구성된 정적인 페이지를 확인할 수 있다.(타임리프 문법을 추가했어도, 타임리프는 뷰를 렌더링할때 타임리프 문법이 있는 속성으로 치환하는것이므로, 서버를 키지않고 그냥 html를 열면 타임리프 문법은 무시하고 페이지를 보여준다.)

그래서 타임리프가 natural 템플릿엔진인것이다. 서버를 사용하지않을떄는 html파일 그대로를 깨뜨리지않고 실행시켜주고 서버를 이용할때는 타임리프를 통해 렌더링되어 동적으로 페이지를 보여주게끔하는 장점이다!

나중에 추가적으로 html작업을 해도 깨질걱정이 없다는것이다.

댓글