Spring mvc 웹 프로젝트 작성 법

강정우·2023년 12월 10일
0

Spring-boot

목록 보기
35/73

웹 프로젝트 생성과정

1. 도메인 작성

@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;
    }

  1. int나 Long의 원시타입을 사용하지 않은 이유는 null을 넣기 위함다.
    즉, 조금 더 널널하게 개발하기 위함이다.

  2. @Data를 사용하면 굉장히 편하긴 하나(getter, setter, etc.. 다 만들어줌) 핵심 도메인 모델에 사용하기엔 굉장히 위험할 수 있다.
    왜냐하면 이게 예상치 못 하게 동작할 수 있기 때문이다.
    그래서 그냥 DTO에는 써도 되나 이것도 잘 살펴보고 써야된다.

@Repository
public class ItemRepository {
    private static final Map<Long, Item> store = new HashMap<>(); //static 사용
    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());
    }
    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();
    }
}
  • 또한 앞서 자바는 멀티 쓰레드를 이용하여 유기적으로 동작할 수 있다고 하였는데
    문제는 동시성 문제가 생기기 때문에 HashMap은 ConCurrentHashMap, long 타입도 atomic long같을 것을 써줘야한다.

  • @Respository를 들어가보면 @Component를 상속받고있기 때문에 당연 얘도 스프링이 뜰 때 컨테이너에 들어간다.

  • 또한 위 update 코드에서는 id를 제외한 나머지를 업데이트 하고 있다면 따로 ItemParameterDTO 이런거를 만들어서 해당 메서드에서 사용하는 3개의 파라미터만 넣어둬서 다른 개발자와의 혼동을 피하는 것이 좋다.
    왜냐하면 @Data로 setId() 는 만들어져 있는데 update() 메서드에서는 사용을 안 하니 "뭐지? 임의로 추가해도 되는건가?" 하고 헷갈릴 수 있기 때문이다.

중복 vs 명확성
이 둘의 대결은 언제나 명확성이 승리다.
따라서 중복을 극도로 싫어하더라고 조금이라도 애매하면 중복을 제거하여 명확성을 높히자.

2. 템플릿 작성

  • 템플릿을 작성할 때 bootstrap을 이용할 수도 있는데 이게 잘 적용이 되는지는 확인해봐야한다.
    그래서 통상 static 밑에 css 폴더를 만들어서 거기에 min 파일을 넣는데 만약 url에 입력해서 css 파일이 정상적으로 뜨지 않는다면,
    root 폴더 바로 밑에 빌드하면 나오는 out 폴더를 삭제후 다시 서버를 실행하면 될 수도 있다.

  • 또한 정적 리소스가 공개되는 resources 파일에 .html파일을 넣어두면 안된다. 왜냐하면 실제 서비스할 때도 이 경로들이 다 보이기 때문에 누군가가 접근할 수 있기 때문이다.

<!doctype html>
<html xmlns:th="http://www.thymeleaf.org">
<head>
  <meta charset="UTF-8">
  <meta name="viewport"
        content="width=device-width, user-scalable=no, initial-scale=1.0, maximum-scale=1.0, minimum-scale=1.0">
  <meta http-equiv="X-UA-Compatible" content="ie=edge">
  <title>spring-mvc-상품목록</title>
  <link th:href="@{/css/bootstrap.min.css}" rel="stylesheet">
</head>
<body>
	...
  • <html xmlns:th="http://www.thymeleaf.org"> 이 코드로 thymeleaf를 사용할 수 있고

  • <link th:href="@{/css/bootstrap.min.css}" rel="stylesheet"> 이 코드로 경로를 절대경로로 바꿔서 어디서든지 사용하도록 도와준다

<button class="btn btn-primary float-end"
	onclick="location.href='addForm.html'"
	th:onclick="|location.href='@{/basic/items/add}'|"
	type="button"
    >상품 등록
</button>
  • thymeleaf에서 링크거는거는 @{}하고 넣어주면 된다.
<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>
  • for 문은 향상된 for처럼 사용하면 된다.

3. 컨트롤러 작성

@Controller
@RequestMapping("/basic/items")
public class BasicItemController {
    private final ItemRepository itemRepository;

    // @Autowired
    public BasicItemController(ItemRepository itemRepository) {
        this.itemRepository = itemRepository;
    }
}
  • 이때 생성자함수가 하나밖에 없다면 @Autowired를 생략할 수 있다고 하였다.

  • 또한 lombok에서 제공하는 @RequiredArgsConstructor도 배웠는데 이를 이용하면 final이 붙은 멤버변수만 사용해서 생성자를 자동으로 만들어준다. 그래서 생성자가 따로 필요없어진다.

@RequestParam

@PostMapping("/add")
public String addItemV1(@RequestParam("itemName") String itemName, @RequestParam("price") int price, @RequestParam("quantity") Integer quantity, Model model) {
    Item item = new Item();
    item.setItemName(itemName);
    item.setPrice(price);
    item.setQuantity(quantity);
    itemRepository.save(item);
    model.addAttribute("item", item);
    return "basic/item";
}

@ModelAttribute

@PostMapping("/add")
public String addItemV2(@ModelAttribute("item") Item item, Model model) {
    itemRepository.save(item);
    // model.addAttribute("item", item);
    return "basic/item";
}

요청 파라미터 처리

  • @ModelAttribute얘가 Item 객체를 생성하고 넘어온 파라미터를 set까지 다 해줌.

Model 추가

  • @ModelAttribute 는 중요한 한가지 기능이 더 있는데, 바로 모델(Model)에 @ModelAttribute 로 지정한 객체를 자동으로 넣어준다.
    그럼 또 이제 Model model 파라미터를 생략할 수 있어서 좋다.

  • 지금 코드를 보면 model.addAttribute("item", item) 가 주석처리 되어 있어도 잘 동작하는 것을 확인할 수 있다.

  • 모델에 데이터를 담을 때는 이름이 필요하다. 이름은 @ModelAttribute 에 지정한 name(value) 속성을 사용한다. 만약 다음과 같이 @ModelAttribute 의 이름을 다르게 지정하면 다른 이름으로 모델에 포함된다.
    @ModelAttribute("hello") Item item 이름을 hello 로 지정
    model.addAttribute("hello", item); 모델에 hello 이름으로 저장

ModelAttribute 이름 생략

/**
 * @ModelAttribute name 생략 가능
 * model.addAttribute(item); 자동 추가, 생략 가능
 * 생략시 model에 저장되는 name은 클래스명 첫글자만 소문자로 등록 Item -> item
 */
@PostMapping("/add")
public String addItemV3(@ModelAttribute Item item) {
 itemRepository.save(item);
 return "basic/item";
}
  • 즉, @ModelAttribute 다음에 지정한 클래스 이름의 첫글자를 소문자로 변환하여 자동으로 넣어준다.
    마치 스프링 빈 생성하는 규약처럼.

  • 그리고 default로 @ModelAttribute가 들어가기 때문에 그마저도 생략할 수 있다.

/**
 * @ModelAttribute 자체 생략 가능
 * model.addAttribute(item) 자동 추가
 */
@PostMapping("/add")
public String addItemV4(Item item) {
 itemRepository.save(item);
 return "basic/item";
}
  • 근데 이거까지 줄여버리면 너무 제공하는 정보가 적지 않나.. 라고 생각이 든다.

리다이렉트

  • 수정은 마지막에 뷰 템플릿을 호출하는 대신에 상품 상세 화면으로 이동하도록 리다이렉트를 호출하면 될듯하다
    스프링은 redirect:/... 으로 편리하게 리다이렉트를 지원한다.
@PostMapping("/{itemId}/edit")
public String edit(@PathVariable("itemId") Long itemId, @ModelAttribute Item item) {
    itemRepository.update(itemId, item);
    return "redirect:/basic/items/{itemId}";
}
  • 컨트롤러에 매핑된 @PathVariable 의 값은 redirect 에도 사용 할 수 있다.
// 새로 고침 버그 삭제를 위한 PRG 패턴 적용
@PostMapping("/add")
public String addItemV5(Item item) {
    itemRepository.save(item);
    return "redirect:/basic/items/" + item.getId();
}
  • 참고로 edit에서만 뿐만 아니라 상품 등록에서도 redirect를 사용하는 이유는 PRG pattern을 보면 알 수 있다.

reidrectAttribute

@PostMapping("/add")
public String addItemV6(Item item, RedirectAttributes redirectAttributes) {
    Item saveItem = itemRepository.save(item);
    redirectAttributes.addAttribute("itemId", saveItem.getId());
    redirectAttributes.addAttribute("status", true);

    return "redirect:/basic/items/{itemId}";
}
  • 이걸로 2가지의 문제점을 해결할 수 있다.
  1. url에 직접 코드를 넣을 때 만약 변수가 한들이라면 utf-8 인코딩을 하는 코드를 추가해야한다.

  2. 저장 후 reaction이 없기 때문에 사용자 입장에서는 저장이 된지 뭔지 감이 잘 안 잡힌다.

  • 위 코드처럼 사용하면 url 인코딩은 물론이고 다 해결이 된다.
    추가적으로 addAttribute에 itemId과 status를 추가해줬는데
    itemId는 url에 명시되어 들어가는 걸 알지만 status는 뭐지? 할 수 있는데 queryParam으로 뒤에 알아서 붙어서 나간다.

  • 그래서 view 단에서 th:text%{param.status} 뭐 이런식으로 꺼내서 view를 만들어주면 된다.


thymeleaf

타임리프 핵심

  • th:xxx 가 붙은 부분은 서버사이드에서 렌더링 되고, 기존 것을 대체한다.

  • th:xxx 이 없으면 기존 html 의 xxx 속성이 그대로 사용된다.
    따라서 HTML을 파일로 직접 열었을 때, 서버로 열었을 때 처럼 똑같이 정상적으로 보이며 소스를 보면 th:xxx 가 있어도 웹 브라우저는 th: 속성을 알지 못하므로 무시한다.
    , HTML을 파일 보기를 유지하면서 템플릿 기능도 할 수 있다.

문법

URL 링크 표현식 @{...}

th:href="@{/css/bootstrap.min.css}"

  • @{...} : 타임리프는 URL 링크를 사용하는 경우 @{...} 를 사용하며 이것을 URL 링크 표현식이라 한다.
    URL 링크 표현식을 사용하면 서블릿 컨텍스트를 자동으로 포함한다.

속성 변경 th:onclick

onclick="location.href='addForm.html'"
th:onclick="|location.href='@{/basic/items/add}'|"

리터럴 대체 |...|

  • 타임리프에서 문자와 표현식 등은 분리되어 있기 때문에 더해서 사용해야 한다.
<span th:text="'Welcome to our application, ' + ${user.name} + '!'"\>

다음과 같이 리터럴 대체 문법을 사용하면, 더하기 없이 편리하게 사용할 수 있다.

<span th:text="|Welcome to our application, ${user.name}!|">

아래와 같은 위치로 쏠 때
location.href='/basic/items/add'
원래는 이렇게 표현식과 string을 조합하여 보내야하는데
th:onclick="'location.href=' + '\'' + @{/basic/items/add} + '\''"
리터럴 대체 문법을 사용하면 개편하게 사용할 수 있다.
th:onclick="|location.href='@{/basic/items/add}'|"

반복 출력 th:each

<tr th:each="item : ${items}">

  • 반복은 th:each 를 사용한다.
    이렇게 하면 모델에 포함된 items 컬렉션 데이터가 item 변수에 하나씩 포함되고, 반복문 안에서 item 변수를 사용할 수 있다.
  • 컬렉션의 수 만큼 <tr>..</tr> 이 하위 테그를 포함해서 생성된다.

변수 표현식 ${...}

<td th:text="${item.price}"\>10000</td>

  • 모델에 포함된 값이나, 타임리프 변수로 선언한 값을 조회할 수 있다.
  • 프로퍼티 접근법을 사용한다. ( item.getPrice() )

내용 변경 th:text

<td th:text="${item.price}"\>10000</td>

  • 내용의 값을 th:text 의 값으로 변경한다.
  • 여기서는 10000을 ${item.price} 의 값으로 변경한다.

URL 링크 표현식2(동적 url) @{...파라미터},

th:href="@{/basic/items/{itemId}(itemId=${item.id})}"

  • 동적 링크를 생성하는 코드를 봐보자.
    URL 링크 표현식을 사용하면 경로를 템플릿처럼 편리하게 사용할 수 있다.

  • 경로 변수( {itemId} ) 뿐만 아니라 쿼리 파라미터도 생성한다.
    예) th:href="@{/basic/items/{itemId}(itemId=${item.id}, query='test')}"
    생성 링크: http://localhost:8080/basic/items/1?query=test

URL 링크 간단히

th:href="@{|/basic/items/${item.id}|}"

<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>
  • 아래께 더 간단히

속성 변경 th:action

<form action="item.html" th:action method="post">
	<div>
    	...
  • HTML form에서 action 에 값이 없으면 현재 URL에 데이터를 전송한다.
  • 또한 위 코드처럼 th:action 다음을 비워두면 현재 url 주소가 자동으로 들어간다.
    따라서 상품 등록 폼의 URL과 실제 상품 등록을 처리하는 URL을 똑같이 맞추고 HTTP 메서드로 두 기능을 구분할 수 있다.
상품 등록 폼: GET /basic/items/add
상품 등록 처리: POST /basic/items/add
  • 이렇게 하면 하나의 URL로 등록 폼과, 등록 처리를 깔끔하게 처리할 수 있다.

spring boot 3.2 파라미터 에러

  • 스프링 부트 3.2부터 자바 컴파일러에 -parameters 옵션을 넣어주어야 애노테이션의 이름을 생략할 수 있다.

주로 다음 두 애노테이션에서 문제가 발생한다.

@RequestParam, @PathVariable

@RequestParam 관련

//애노테이션에 username이라는 이름이 명확하게 있다. 문제 없이 작동한다.

@RequestMapping("/request")

public String request(@RequestParam("username") String username) {

   ...

}

//애노테이션에 이름이 없다. -parameters 옵션 필요

@RequestMapping("/request")

public String request(@RequestParam String username) {

   ...

}

//애노테이션도 없고 이름도 없다. -parameters 옵션 필요

@RequestMapping("/request")

public String request(String username) {

   ...

}

@PathVariable 관련

//애노테이션에 userId라는 이름이 명확하게 있다. 문제 없이 작동한다.

public String mappingPath(@PathVariable("userId") String userId) {

    ...

}

//애노테이션에 이름이 없다. -parameters 옵션 필요

@RequestMapping("/request")

public String request(@RequestParam String username) {

   ...

}

해결법 1. 어노테이션에 명시하기

애노테이션에 이름을 생략하지 않고 다음과 같이 이름을 항상 적어준다.

@RequestParam("username") String username

@PathVariable("userId") String userId

해결법 2. 컴파일 시점에 -parameters 옵션 적용

  1. IntelliJ IDEA에서 File -> Settings를 연다. (Mac은 IntelliJ IDEA -> Settings)

  2. Build, Execution, Deployment → Compiler → Java Compiler로 이동한다.

  3. Additional command line parameters라는 항목에 다음을 추가한다.

-parameters

  1. out 폴더를 삭제하고 다시 실행한다. 꼭 out 폴더를 삭제해야 다시 컴파일이 일어난다.

해결법 3. Gradle을 사용해서 빌드하고 실행한다.

  • 앞서 더 빠르게 컴파일하기위해 default 값으로 되어있던 gradle을 IntelliJ IDEA로 바꾸어줬는데 이를 다시 gradle로 바꾼다.

자바를 컴파일할 때 매개변수 이름을 읽을 수 있도록 남겨두어야 사용할 수 있다. 컴파일 시점에 -parameters 옵션을 사용하면 매개변수 이름을 사용할 수 있게 남겨둔다.
스프링 부트 3.2 전까지는 바이트코드를 파싱해서 매개변수 이름을 추론하려고 시도했다. 하지만 스프링 부트 3.2 부터는 이런 시도를 하지 않는다.

profile
智(지)! 德(덕)! 體(체)!

0개의 댓글