Controller를 정의할 차례이다.
우선 HomeController를 작성하고, "/"의 URI에 해당하는 html을 정의한다. home.html에서는 Thymeleaf를 사용한다.
@Controller
@Slf4j
public class HomeController {
// Logger log = LoggerFactory.getLogger(getClass()); -> @Slf4j
@RequestMapping("/")
public String home() {
log.info("home controller");
return "home";
}
}
# th:replace
렌더링 될 때 fragments/header, bodyHeader, footer로 대체하는 코드이다.
<head th:replace="fragments/header :: header">
<div th:replace="fragments/bodyHeader :: bodyHeader" />
<div th:replace="fragments/footer :: footer" />
1. Member 등록
회원 등록을 처리할 MemberController를 정의하기에 앞서 Entity가 아닌 Controller와 View 사이의 데이터 전달에 사용할 MemberForm을 작성한다.
꼭 필수로 입력해야하는 필드가 있을 수 있다. 본 실습에서는 @NotEmpty를 설정하여 name 프로퍼티를 꼭 입력해야하는 필드로 설정한다. 이는 이제 MemberController의 @Valid와 BindingResult와 함께 강력한 UX/UX를 제공하게 된다.
@Getter @Setter
public class MemberForm {
@NotEmpty(message = "회원 이름은 필수입니다") // 값이 비어 있으면 오류 발생
private String name;
private String city;
private String street;
private String zipcode;
}
MemberController의 전체 코드이다.
@RequiredArgsConstructor를 통해 final로 선언한 MemberService의 생성자를 생략할 수 있다. 이제 Mapping 메서드를 정의해보자.
@Controller
@RequiredArgsConstructor
public class MemberController {
private final MemberService memberService;
@GetMapping("/members/new")
public String createForm(Model model) {
model.addAttribute("memberForm", new MemberForm());
return "members/createMemberForm";
}
@PostMapping("/members/new")
public String create(@Valid MemberForm form, BindingResult result) {
if (result.hasErrors()) {
return "members/createMemberForm";
}
Address address = new Address(form.getCity(), form.getStreet(), form.getZipcode());
Member member = new Member();
member.setName(form.getName());
member.setAddress(address);
memberService.join(member);
return "redirect:/";
}
}
처음 Get 방식으로 요청할 때의 메서드이다. 함수의 인자는 Model을 받지만, addAttribute를 통해 Controller에서 View로 넘어갈때 new MemberForm() 실어서 넘긴다. form의 필드들을 입력한 다음, Submit을 누르면 POST 메서드로 memberForm object를 요청한다.
@GetMapping("/members/new")
public String createForm(Model model) {
model.addAttribute("memberForm", new MemberForm());
return "members/createMemberForm";
}
// createMemberForm.html
<!DOCTYPE HTML>
...
<form role="form" action="/members/new" th:object="${memberForm}" method="post">
<div class="form-group">
...
이제 Post 방식의 요청할 때의 메서드이다. MemberForm을 전달 받기는 하지만, @Valid와 BindingResult는 무엇일까?
@PostMapping("/members/new")
public String create(@Valid MemberForm form, BindingResult result) {
if (result.hasErrors()) {
return "members/createMemberForm";
}
Address address = new Address(form.getCity(), form.getStreet(), form.getZipcode());
Member member = new Member();
member.setName(form.getName());
member.setAddress(address);
memberService.join(member);
return "redirect:/";
}
1) @NotEmpty, @Valid, BindingResult
MemberForm 객체의 name 프로퍼티를 @NotEmpty를 설정하여 반드시 추가해야한다고 정의하였다. @Valid를 통해 실행 시 이를 검증할 수 있으며, BindingResult는 @Valid에서 에러가 발생했을 때 result에 Error 관련한 데이터가 담는다.
에러가 발생했을 시 createMemberForm.html을 재호출하도록 설정하였으며, 에러 발생했을 시에 대한 처리를 담고 있다.
if (result.hasErrors()) {
return "members/createMemberForm";
}
// createMemberForm.html
<label th:for="name">이름</label>
<input type="text" th:field="*{name}" class="form-control" placeholder="이름을 입력하세요"
th:class="${#fields.hasErrors('name')}? 'form-control fieldError' : 'form-control'">
<p th:if="${#fields.hasErrors('name')}"
th:errors="*{name}">Incorrect date</p>
회원은 name과 address 프로퍼티를 필요로 하기 때문에, address를 정의하고 member 객체를 생성한 다음 각 프로퍼티를 연결짓는다.
왜 그냥 Member Entity를 바로 가져오도록 처리하지 않고 MemberForm을 정의했을까? Member Entity에 포함되어 있는 각 프로퍼티가 MemberForm을 통해 받는 값이 동일하지 않았기 때문이다. 일부가 일치하더라도 @NotEmpty, @Valid 등을 추가적으로 설정해야하기 때문에 코드의 가독성이 더욱 떨어지게 된다.
실무에서는 요구사항이 반드시 다르기 때문에 완전히 일치하기 매우 어려울 뿐더러, Entity는 핵심 비즈니스 로직을 담아야지, 화면 관련된 코드는 지양해야 한다.
Address address = new Address(form.getCity(), form.getStreet(), form.getZipcode());
Member member = new Member();
member.setName(form.getName());
member.setAddress(address);
memberService.join(member);
return "redirect:/";
2. Member 조회
MemberController에서 View로 데이터를 전달하기 위해 위와 동일하게 model에 addAttribute를 통해 값을 전달하였다. 지금은 Member의 모든 프로퍼티가 뷰를 그리는데 사용되지만, 실제 현업에서는 DTO를 정의하여 필요한 프로퍼티만 정의하여 전달할 수 있도록 처리하는 것을 권장한다.
@GetMapping("/members")
public String list(Model model) {
List<Member> members = memberService.findMembers();
model.addAttribute("members", members);
return "members/memberList";
}
1) API를 만들 때는 Entity를 절대로 외부로 반환하지 말 것
API를 만들 때는 절대 Entity를 외부로 반환하면 안된다. Entity의 로직을 바꾸면 API의 스펙이 바뀌게 된다. 뿐만 아니라 password와 같은 불필요한 정보를 노출하게 된다.
3. Item 등록, 조회
Member를 등록하고 조회하는 로직과 동일하다.
@Controller
@RequiredArgsConstructor
public class ItemController {
private final ItemService itemService;
@GetMapping("/items/new")
public String createForm(Model model) {
model.addAttribute("form", new BookForm());
return "items/createItemForm";
}
@PostMapping("/items/new")
public String create(BookForm form) {
Book book = new Book();
book.setName(form.getName());
book.setPrice(form.getPrice());
book.setStockQuantity(form.getStockQuantity());
book.setAuthor(form.getAuthor());
book.setIsbn(form.getIsbn());
itemService.saveItem(book);
return "redirect:/";
}
@GetMapping("/items")
public String list(Model model) {
List<Item> items = itemService.findItems();
model.addAttribute("items", items);
return "items/itemList";
}
@GetMapping("/items/{itemId}/edit")
public String updateItemForm(@PathVariable("itemId") Long itemId, Model model) {
Book item = (Book) itemService.findOne(itemId);
BookForm form = new BookForm();
form.setId(item.getId());
form.setName(item.getName());
form.setPrice(item.getPrice());
form.setStockQuantity(item.getStockQuantity());
form.setAuthor(item.getAuthor());
form.setIsbn(item.getIsbn());
model.addAttribute("form", form);
return "items/updateItemForm";
}
}
'🌱 Spring > JPA ①' 카테고리의 다른 글
[Spring] Item 수정(준영속 엔티티 수정 - 변경 감지와 merge) (0) | 2023.08.24 |
---|---|
[Spring] 주문 Entity, Repository, Service 개발 (0) | 2023.08.09 |
[Spring] 상품 Entity, Repository, Service 개발 (0) | 2023.08.05 |
[Spring] 회원 Service Unit Test 작성하기 (0) | 2023.08.05 |
[Spring] 회원 Service & Repository 개발 (0) | 2023.08.04 |