[스프링 MVC 1편] - 웹 페이지 만들기(1)

Chooooo·2023년 1월 8일
0

스프링 MVC 1편

목록 보기
8/11
post-thumbnail

이 글은 강의 : 김영한님의 - "[스프링 MVC 1편 - 백엔드 웹 개발 핵심 기술]"을 듣고 정리한 내용입니다. 😁😁


이번에는 실제 지금까지 공부했던 스프링 MVC 전체 구조를 활용해서 웹 페이지를 만들어 볼 것이다 !

프로젝트 생성

스프링 부트 프로젝트 생성

  • Project : Gradle Project (Java-Groovy)

  • Language : JAVA

  • Spring Boot : 최신 버전 사용

  • project Metadata

    • Group : hello
    • Artifact : item-service
    • Name : item-service
    • Package Name : hello.itemservice
    • packaging : Jar
    • JAVA : 11
  • Dependencies

    • Spring Web
    • Thymeleaf
    • Lombok

🎈 동작 확인

  • 기본 메인 클래스 실행(SpringmvcApplicaion.main())
  • http://localhost:8080 호출해서 Whitelabel Error Page 나오면 정상 동작.

Welcome 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는 나중에...

상품 도메인 개발

이제 진짜 개발을 시작해보자.

상품 도메인은 다음과 같은 필드가 필요하다.

  • 상품 아이디
  • 상품명
  • 가격
  • 수량

이를 코드로 구현하면..

Item - 상품 객체

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, 기본 생성자 등을 쉽게 구현한다.

ItemRepository - 상품 저장소

상품 객체 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를 활용해 할당해준다.

ItemRepositoryTest - 상품 저장소 테스트

상품을 저장하는 책임을 가진 상품 저장소(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

핵심 서비스 로직을 개발하는 동안 웹 퍼블리셔는 HTML마크업을 완료했다.
다음 파일들을 경로에 넣고 잘 동작하는지 확인해보자.

🎈 부트스트랩
해당 학습에서 CSS는 부트스트랩을 사용했다.

🎃 부트스트랩은 HTML을 편리하게 개발하기 위해 사용했다. 부트스트랩은 웹사이트를 쉽게 만들 수 있도록 도와주는 HTML, CSS JS 프레임워크이다.

  • 부트스트랩 공식 사이트 : https://getbootstrap.com
  • 부트스트랩을 다운받고 압축을 풀자.
    • 이동: 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.htm

🎃 복사가 완료되었으면 해당 HTML 리소스들은 /resources/static 정적 경로에 넣어놨기에 스프링 부트에서 정적 리소스를 제공한다. 그래서 다음 링크로 접속을 시도하면 해당 HTML을 볼 수 있다. (정적 리소스는 GET일 때만 받을 수 있는데 POST면 URL 동작 안할꺼야 Error 405)

  • http://localhost:8080/html/item.html

  • 참고로 resources/static에 넣어두었기 때문에 정적 리소스. 정적 리소스는 해당 파일을 탐색기를 통해 직접 열어도 동작한다.

🎃 정적 리소스 경로(/resources/static)에 html을 넣어두면 실제 서비스에서도 공개되는데, 실제로 서비스를 운영할때는 공개할 필요가 없는 HTML같은 리소스를 이곳에 두는건 주의해야한다. (이렇게 하면 안되는걸 의미함 !!)


상품 목록 HTML - items.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>

상품 상세 HTML - item.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>

상품 등록 폼 HTML - addForm.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>

상품 수정 폼 HTML - editForm.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>
profile
back-end, 지속 성장 가능한 개발자를 향하여

0개의 댓글