...
@Controller
@RequiredArgsConstructor
public class MemberController {
private final MemberService memberService;
...
@GetMapping("/members")
public String list(Model model){
List<Member> members = memberService.findMembers();
model.addAttribute("members", members);
return "members/memberList";
}
Model
) 객체에 보관findMembers
메서드를 사용해서 데이터를 끌어옴model
에 담아서 화면에 넘김members
아래에 memberList
라는 html 파일을 추가하여 데이터 조회를 위한 뷰를 만듦templates/members/memberList.html
<!DOCTYPE HTML>
<html xmlns:th="http://www.thymeleaf.org">
<head th:replace="fragments/header :: header" />
<body>
<div class="container">
<div th:replace="fragments/bodyHeader :: bodyHeader" />
<div>
<table class="table table-striped">
<thead>
<tr>
<th>#</th>
<th>이름</th>
<th>도시</th>
<th>주소</th>
<th>우편번호</th>
</tr>
</thead>
<tbody>
<tr th:each="member : ${members}">
<td th:text="${member.id}"></td>
<td th:text="${member.name}"></td>
<td th:text="${member.address?.city}"></td>
<td th:text="${member.address?.street}"></td>
<td th:text="${member.address?.zipcode}"></td>
</tr>
</tbody>
</table>
</div>
<div th:replace="fragments/footer :: footer" />
</div> <!-- /container -->
</body>
</html>
타임리프는 위와 같이 HTML 태그를 그대로 가져다 쓰는 것이 큰 장점
모델에서 담아서 넘긴 members
리스트가 ${members}
에 바인딩됨
<tr th:each="member : ${members}">
와 같이 th:each
문법을 통해 루프를 돌리면서 데이터를 화면에 뿌리게 됨
${member.address?.city}
처럼 타임리프에서 ?
를 사용하면 null
을 무시함
address
가 없으면, then city
를 조회해오지 않는다는 의미위 코드에서 엔티티를 손을 대지 않아도 되는 상황이기 때문에 멤버 객체를 조회해올 때 member entity를
그대로 출력했지만, dto
로 변환을 해서 화면에 필요한 데이터들만 가지고 출력하는 것이 권장됨
MemberForm
) 없이 엔티티(Member
)를 직접 등록과 수정 화면에서 사용해도 됨JPA
를 사용할 때 가장 주의해야 할 점은 엔티티를 최대한 순수한 상태로 유지해야 하는 것API
를 만들 때는 절대 엔티티를 웹으로 반환하면 안됨API
는 스펙이기 때문에, 엔티티의 로직을 추가하게 되면 그로 인해 API
스펙이 변하게 됨API
스펙이 되고, 그것을 활용하는 개발팀이 괴로워짐API
로 외부에 노출하면 안됨
- 실무에서는 엔티티는 핵심 비즈니스 로직만 가지고 있고, 화면을 위한 로직은 없어야 함
- 화면이나 API에 맞는 폼 객체나 DTO를 사용해야 함
- 화면이나 API 요구사항을 이것들로 처리하고, 엔티티는 최대한 순수하게 유지해야 함