22.05.17
웹 계층 개발
홈 화면과 레이아웃
컨트롤러 설계
뷰 렌더링: URL 매핑(@RequestMapping) -> HTML(타임리프 템플릿) 변환
뷰 리소스: Bootstrap(css, js) -> 프로젝트/resources/static에 복사
*스프링 부트에 의해 static 폴더 안의 리소스들은 정적 컨텐츠로 바로 제공
*IDE가 바로 인식을 못하면 폴더 syncronize나 build project를 해준다.
스프링 부트 타임리프 기본 설정
spring:
thymeleaf:
prefix: classpath:/templates/
suffix: .html
스프링 부트 타임리프 viewName 매핑
- resources:templates/ +{ViewName}+ .html
- ViewName: 반환 문자열과 prefix, suffix 설정 정보를 사용해서 렌더링할 뷰(html)을 찾는다.
참고: 뷰 템플릿 변경사항을 서버 재시작 없이 즉시 반영하기
1. spring-boot-devtools 추가
2. html 파일 build -> Recompile
회원 등록
폼 객체를 사용해서 화면 계층(Form)과 서비스 계층(Entity)을 명확하게 분리한다.
@GetMapping(value = "/members/new")
public String createForm(Model model) {
model.addAttribute("memberForm", new MemberForm());
return "members/createMemberForm";
}
- model에 "memberForm"을 key로 하는 form 객체(필드, getter, setter)를 담아 HTML에 넘긴다.
- HTML은 <form> 태그 안에서 model의 form 객체를 꺼내 사용자 입력 값으로 필드를 채운다. (by 프로퍼티 방식)
- submit 타입의 버튼을 누르면 form 객체가 post 방식으로 컨트롤러에 전달된다.
@PostMapping(value = "/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:/";
}
표준 Validation
- @Valid: javax validation 기능을 @annotation 기반으로 사용. 여기서 에러 발생시 BindingResult 객체에 담긴다.
- BindingResult는 return 한 html 에 함께 전달되어 에러 페이지 처리에 사용할 수 있다.
- javax validation.@NotNull: null 비허용. " ", "" 허용.
- @NotEmpty: null, "" 비허용, " " 허용
- @NotBlank: null, " ", "" 비허용
스프링 2.3 이상부터는 implementation 'org.springframework.boot:spring-boot-starter-validation' 추가
참고: 타임리프에서 ?를 사용하면 null 을 무시한다.
참고: 폼 객체 vs 엔티티 직접 사용
요구사항이 정말 단순할 때는 폼 객체( MemberForm ) 없이 엔티티( Member )를 직접 등록과 수정 화면에서 사용해도 된다. 하지만 화면 요구사항이 복잡해지기 시작하면, 엔티티에 화면을 처리하기 위한 기능이 점점 증가한다. 결과적으로 엔티티는 점점 화면에 종속적으로 변하고, 이렇게 화면 기능 때문에 지저분해진 엔티티는 결국 유지보수하기 어려워진다.
실무에서 엔티티는 핵심 비즈니스 로직만 가지고 있고, 화면을 위한 로직은 없어야 한다. 화면이나 API에 맞는 폼 객체나 DTO(Data Transfer Object)를 사용하자. 그래서 화면이나 API 요구사항을 이것들로 처리하고, 엔티티는 최대한 순수하게 유지하자.
*화면의 조회 기능 구현시 예제 코드에서는 엔티티 조회 서비스를 이용했지만 화면(html)에 맞는 폼 객체로 변환하여 사용하는 것이 바람직하다. 현재는 서버 내부에서 필요한 필드만 사용하도록 하여 크게 문제는 없지만, API를 만들 때는 엔티티를 수정하면 API의 스펙이 변하는 문제가 생기기 때문에 절대 엔티티를 반환하도록 하면 안된다.
상품 수정
@GetMapping(value = "/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());
...
model.addAttribute("form", form);
return "items/updateItemForm";
}
- 수정 상품 조회: itemService.findOne(itemId) -> 기존에 저장된 Item 정보를 조회하여 폼 객체로 변환
- 조회 결과(form)를 모델 객체에 담아 뷰에 전달
@PostMapping(value = "/items/{itemId}/edit")
public String updateItem(@ModelAttribute("form") BookForm form) {
Book book = new Book();
book.setId(form.getId());
book.setName(form.getName());
...
itemService.saveItem(book);
return "redirect:/items";
}
- 사용자가 입력한 폼 정보를 엔티티로 변환하여 저장(수정)
- Book 엔티티는 현재 준영속 상태다. 따라서 영속성 컨텍스트의 지원을 받을 수 없고 데이터를 수정해도 변경 감지 기능은 동작하지 않기 때문에 직접 saveItem() 을 호출했다.
*최근 작업했던 파일 리스트: ctrl+E
*대문자로 변환: ctrl+shift+U