요구사항
페이징 처리
페이징
- DTO
- PageRequestDTO
- PageResponseDTO
- TodoMapper -limit(), count()
- TodoMapper의 목록 처리
- TodoMapper의 count 처리
- TodoService
- TodoController
- 목록 페이지 JSP 작성
- CRUD 연동
limit을 이용한 페이징 구현 원리
많은 데이터 -> 페이징 처리를 통해 최소한의 데이터만을 출력
: DB에서 필요한 데이터만 가져와 SQL 입출력을 최소화한다.
-MySQL, MariaDB에서는 limit이라는 기능을 이용해 페이징 처리가 가능하다.
limit()
- 파라미터를 하나 혹은 두개를 받는다.
- 하나일 경우 : select * from tbl_todo order by tno desc limit 10;
-> 가져오는 데이터 수만을 이용 [첫 페이지] - 두개일 경우 : select * from tbl_todo order by tno desc limit 10, 10;
-> 건너뛰는 데이터 수와 가져오는 데이터 수 [첫 페이지를 제외한 페이지]
- 하나일 경우 : select * from tbl_todo order by tno desc limit 10;
따라서, skip할 데이터의 수는 : (i-1)페이지*10로 첫 페이지를 제외한 페이지에 표현식을 적용한다.
- select * from tbl_todo order by tno desc limit 10;
- select * from tbl_todo order by tno desc limit 10, 10;
- select * from tbl_todo order by tno desc limit 20, 10;
- select * from tbl_todo order by tno desc limit 30, 10;
- select * from tbl_todo order by tno desc limit 40, 10;
하지만, limit의 경우 식을 사용할 수 없고, 반드시 값만 받을 수 있기 때문에
select * from tbl_todo order by tno desc limit(2-1*10), 10;
과 같은 SQL문은 처리가 불가능하다.
count()
limit과 함께 페이징을 구현하기 위해 사용하는 전체 데이터 수를 반환하는 함수
전체 데이터 수를 이용해 마지막 페이징 번호를 알 수 있다.
PageRequestDTO와 PageResponseDTO
javax.validation을 이용해 데이터의 구간을 지정할 수 있다.
1. PageRequestDTO 작성
- page : 현재 페이지 번호
- 1페이지 ~ (전체 데이터 수/size)까지
- 기본값 @Builder.default, @Min 최소 1, @Positive 양수만 가능
- size : 한 페이지당 보여주는 데이터 수
- 설정가능한 한 페이지당 보여줄 데이터 개수
- 한 페이지당 10개에서 ~100개까지
- 기본값 @Builder.default, @Min 최소 10 ~ @Max 최대 100, @Positive 양수만 가능
- getSkip() : limit에서 사용하는 건너뛰는 페이지 수를 반환하는 메서드
-DTO이므로, : @Data (@Getter, @Setter, @ToString)
-DTO, VO는 파라미터를 모두 갖거나 하나도 없을 때 생성자 생성 : @NoArgsConstructor, @AllArgsConstructor
-Builder 패턴을 이용해 객체 생성 : @Builder
@Builder
@Data
@AllArgsConstructor
@NoArgsConstructor
public class PageRequestDTO {
@Builder.Default
@Min(value = 1)
@Positive
private int page = 1;
@Builder.Default
@Min(value = 10)
@Max(value = 100)
@Positive
private int size = 10;
public int getSkip(){
return (page -1) * 10;
}
}
2. PageResponseDTO 작성
-직접 DTO를 작성하는 것보다 생성자를 이용해 안전하게 처리하는 것을 목적으로 작성한다.
-@Getter : 생성자를 이용하므로, 직접 Setter를 작성할 필요가 없다.
-@Builder : PageRequestDTO와 List<TodoDTO>, 전체 데이터 개수를 이용한 생성자 작성하는 어노테이션
PageResponseDTO 구성 요소
- TodoDTO의 목록
- 전체 데이터 수
- 페이지 번호의 처리를 위한 데이터들 (시작 페이지 번호 / 끝 페이지 번호)
PageResponseDTO 구현
- page : 현재 페이지 번호
- size : 한 페이지당 보여주는 데이터 수
- total : 전체 데이터 수
- start : 시작 페이지 번호
- end : 마지막 페이지 번호
- prev : 이전 페이지 존재 여부
- next : 이후 페이지 존재 여부
- dtoList : 현재 페이지의 객체들을 담은 리스트
- @Builder 어노테이션을 이용해 작성한 생성자
- page, size는 DTO를 통해, total과 dtolist는 직접 받는다.
- start, end의 경우, 열 개의 페이지 번호를 제공한다고 가정해
먼저 마지막 페이지를 구하고 마지막 페이지를 통해 start 페이지를 찾는다.
-> end : 올림한 (현재 페이지/10.0) * 10
-올림을 하는 이유는 페이지는 데이터가 하나라도 존재한다면 페이지 수가 필요하기 때문이다.
-> start : 마지막 페이지로부터 -9번째 - 중요한건 마지막 페이지가 존재하지 않을 수도 있다는 것이다
-> 페이지 개수가 10개보다 적을 수도 있기 때문에, 직접 마지막 번호를 구해 비교한다.
last : 올림한 (전체 페이지수/한 페이지당 보여주는 데이터 수)
-올림을 하는 이유는 페이지는 데이터가 하나라도 존재한다면 페이지 수가 필요하기 때문이다.
end : last변수를 이용해 마지막 페이지 번호가 last번호보다 크면 last번호를 마지막 번호로 지정한다. - prev : 1페이지보다 크면 이전 페이지가 존재하고,
next : 전체 데이터 수가 마지막 페이지 * 한 페이지당 보여주는 데이터 수 보다 크면 다음 페이지가 존재한다.
package org.zerock.springex.dto;
import lombok.Builder;
import lombok.Getter;
import lombok.ToString;
import java.util.List;
@Getter
@ToString
public class PageResponseDTO<E> {
private int page;
private int size;
private int total;
//시작 페이지 번호
private int start;
//끝 페이지 번호
private int end;
//이전 페이지의 존재 여부
private boolean prev;
//다음 페이지의 존재 여부
private boolean next;
private List<E> dtoList;
@Builder(builderMethodName = "withAll")
public PageResponseDTO(PageRequestDTO pageRequestDTO, List<E> dtoList, int total){
this.page = pageRequestDTO.getPage();
this.size = pageRequestDTO.getSize();
this.total = total;
this.dtoList = dtoList;
this.end = (int)(Math.ceil(this.page / 10.0 )) * 10;
this.start = this.end - 9;
int last = (int)(Math.ceil((total/(double)size)));
this.end = end > last ? last: end;
this.prev = this.start > 1;
this.next = total > this.end * this.size;
}
}
TodoMapper
- PageRequestDTO를 이용해 목록 처리
- PageRequestDTO를 이용해 페이지 번호 구성
1. TodoMapper에서 PageRequestDTO를 이용해 목록 처리
(1) selectList() 메서드 추가
<select id="selectList" resultType="org.zerock.springex.domain.TodoVO">
select * from tbl_todo order by tno desc limit #{skip}, #{size};
</select>
-> limit 함수를 이용해 SQL문 작성
(2) 테스트 코드 작성
@Test
public void testSelectPaging(){
PageRequestDTO pageRequestDTO= PageRequestDTO.builder()
.page(1)
.size(10)
.build();
List<TodoVO> voList=todoMapper.selectList(pageRequestDTO);
voList.forEach(vo -> log.info(vo));
}
2. PageRequestDTO를 이용해 페이지 번호 구성
(1) TodoMapper에 getCount() 추가
-추후에 검색 기능 추가를 위해 PageRequestDTO를 파라미터로 받는다.
int getCount(PageRequestDTO pageRequestDTO);
<select id="getCount" resultType="int">
select count(tno) from tbl_todo;
</select>
(2) 테스트 코드 작성
@Test
public void testGetCount(){
PageRequestDTO pageRequestDTO= PageRequestDTO.builder()
.page(1)
.size(10)
.build();
int count = todoMapper.getCount(pageRequestDTO);
log.info(count);
}
TodoService
-PageRequestDTO를 파라미터로 받아, PageResponsDTO를 반환타입으로 하는 getList() 작성
-List<TodoDTO>, total, PageRequestDTO를 파라미터로 전달해 PageResponseDTO 생성자 호출
-List<TodoVO> : PageRequestDTO를 파라미터로 전달해, selectList() 호출
-List<TodoDTO> : 스트림으로 List<TodoVO> -> List<TodoDTO> 변환
-total : PageRequestDTO를 파라미터로 전달해 getCount() 호출
(1) TodoService에 getList() 작성
PageResponseDTO getList(PageRequestDTO pageRequestDTO);
@Override
public PageResponseDTO getList(PageRequestDTO pageRequestDTO) {
//PageRequestDTO를 통해 PageResponseDTO 생성자를 이용해 객체 생성해야하므로
//PageResponseDTO의 생성자에 필요한 파라미터 :
// TodoDTO의 목록, 전체 데이터 수, 페이지 번호의 처리를 위한 데이터들 (시작 페이지 번호 / 끝 페이지 번호)
//1. List<TodoVO> -> List<TodoDTO>
List<TodoVO> voList=todoMapper.selectList(pageRequestDTO);
List<TodoDTO> dtoList=voList.stream()
.map(vo -> modelMapper.map(vo, TodoDTO.class))
.collect(Collectors.toList());
//2. total
int total = todoMapper.getCount(pageRequestDTO);
//생성자를 이용해 객체 생성
PageResponseDTO<TodoDTO> pageResponseDTO=
PageResponseDTO.<TodoDTO>withAll()
.dtoList(dtoList)
.total(total)
//3. 페이지 번호의 처리를 위한 데이터들
.pageRequestDTO(pageRequestDTO)
.build();
return pageResponseDTO;
}
(2) 테스트 코드 작성
@Test
public void testPaging(){
//PageResponseDTO의 생성자에 필요한 파라미터 :
// TodoDTO의 목록, 전체 데이터 수, 페이지 번호의 처리를 위한 데이터들 (시작 페이지 번호 / 끝 페이지 번호)
//페이지 번호의 처리를 위한 데이터들
PageRequestDTO pageRequestDTO= PageRequestDTO.builder()
.page(1)
.size(10)
.build();
PageResponseDTO<TodoDTO> pageResponseDTO = todoService.getList(pageRequestDTO);
log.info(pageResponseDTO);
pageResponseDTO.getDtoList().stream().forEach(todoDTO -> log.info(todoDTO));
}
TodoController
1. TodoController에서 list() 메서드 작성
-PageRequestDTO를 받아 @Valid을 이용해 검증하고, 검증에 걸리면 BindingResultSet에, 검증에 통과하면 Model에 담는다.
변경 전, list() 메서드
@RequestMapping("/list")
public void list(Model model){
log.info("todo list.......");
model.addAttribute("dtoList", todoService.getAll());
}
변경 후, list() 메서드
-@Valid를 통해, 유효하지 않는 데이터가 전달되면 빌더패턴을 이용해 기본값으로 생성한 객체를 전달한다. (@Builder.default)
@GetMapping("/list")
public void list(@Valid PageRequestDTO pageRequestDTO, BindingResult bindingResult, Model model){
log.info(pageRequestDTO);
if(bindingResult.hasErrors()){
pageRequestDTO = PageRequestDTO.builder().build();
}
model.addAttribute("responseDTO", todoService.getList(pageRequestDTO));
}
-list() 메서드가 기존의 dtoList가 아닌 PageResponseDTO를 반환하므로, list.jsp도 변경해야한다.
목록 페이지 JSP 작성
1. 목록 출력 부분을 dtoList에서 responseDTO.dtoList로 변경
<c:forEach items="${responseDTO.dtoList}" var="dto">
<tr>
<th scope="row"><c:out value="${dto.tno}"/></th>
<td>
<a href="/todo/read?tno=${dto.tno}" class="text-decoration-none">
<c:out value="${dto.title}"/>
</a>
</td>
<td><c:out value="${dto.writer}"/></td>
<td><c:out value="${dto.dueDate}"/></td>
<td><c:out value="${dto.finished}"/></td>
</tr>
</c:forEach>
2. 페이지 번호와 페이지 이동 처리
-페이지 이동 확인
http://localhost:8080/todo/list?page=2&size=10 으로 페이지 이동 확인
(1) 화면에 페이지 이동을 위한 번호 출력
https://getbootstrap.kr/docs/5.1/components/pagination/
페이지네이션 기본 문법
<nav aria-label="Page navigation example">
<ul class="pagination">
<li class="page-item"><a class="page-link" href="#">Previous</a></li>
<li class="page-item"><a class="page-link" href="#">1</a></li>
<li class="page-item"><a class="page-link" href="#">2</a></li>
<li class="page-item"><a class="page-link" href="#">3</a></li>
<li class="page-item"><a class="page-link" href="#">Next</a></li>
</ul>
</nav>
-list.jsp에 <table> 태그 다음에 <div>를 구성해 화면 작성
- responseDTO를 받아 <c:forEach>를 이용해 반복문 처리
- <c:forEach>의 속성들은 EL 표현식을 이용해 처리한다.
- begin : "${responseDTO.start}"
- end : "${responseDTO.end}"
- var : "num"
<div class="float-end">
<ul class="pagination flex-wrap">
<c:forEach begin="${responseDTO.start}" end="${responseDTO.end}" var="num">
<li class="page-item">
<a class="page-link" href="#">${num}</a></li>
</c:forEach>
</ul>
</div>
(2) 화면에 prev/next/현재 페이지 처리
- responseDTO를 받아 <c:if>를 이용해 조건문 처리
- <c:if>의 속성들은 EL 표현식을 이용해 처리한다.
- <c:if>를 이용하면 참이면, 수행하고 거짓이면 수행하지 않기 때문에 노출처리를 담당한다.
- test : "${reponseDTO.prev}" / "${reponseDTO.next}"
-> 해당 값이 참이라면 다음 문장 실행
- test : "${reponseDTO.prev}" / "${reponseDTO.next}"
<div class="float-end">
<ul class="pagination flex-wrap">
<c:if test="${responseDTO.prev}">
<li class="page-item">
<a class="page-link"}">Previous</a>
</li>
</c:if>
<c:forEach begin="${responseDTO.start}" end="${responseDTO.end}" var="num">
<li class="page-item">
<a class="page-link">${num}</a></li>
</c:forEach>
<c:if test="${responseDTO.next}">
<li class="page-item">
<a class="page-link">Next</a>
</li>
</c:if>
</ul>
</div>
(3) 페이지의 이벤트 처리
-자바스크립트를 이용해 화면에서 페이지 번호 클릭시 이동하는 처리
- <a>태그에 직접 onclick 적용하는 것보다, <ul>태그에 이벤트 처리하는 것이 코드의 중복을 방지할 수 있다.
(3-1) 각 페이지 번호 처리
-'data-' 속성을 이용해 필요한 속성 추가
-'data-num' 속성으로 페이지 번호를 보관하도록 구성
<div class="float-end">
<ul class="pagination flex-wrap">
<c:if test="${responseDTO.prev}">
<li class="page-item">
<a class="page-link" data-num="${responseDTO.start -1}">Previous</a>
</li>
</c:if>
<c:forEach begin="${responseDTO.start}" end="${responseDTO.end}" var="num">
<li class="page-item ${responseDTO.page == num? "active":""} ">
<a class="page-link" data-num="${num}">${num}</a></li>
</c:forEach>
<c:if test="${responseDTO.next}">
<li class="page-item">
<a class="page-link" data-num="${responseDTO.end + 1}">Next</a>
</li>
</c:if>
</ul>
</div>
(3-2) <ul>태그가 끝난 부분에 자바스크립트를 이용해 이벤트 처리
-자바스크립트를 통해 이벤트 처리를 하는 이유:
각각의 데이터에 대해 미리 링크 처리를 하는 것이 아니라
<ul>태그에 이벤트를 등록하고, <a> 태그 클릭 시 해당 이벤트를 처리를 수행하도록
<a> 태그 클릭시 이벤트 처리 동작 과정
- 해당 태그의 data-num 속성 값을 읽어온다.
- 현재 주소인 self.location을 변경한다.
- (``) 백틱를 이용해 문자열 결합에 '+'를 이용하지 않고, 문자열 결합을 수행한다.
단, 백틱을 사용할 경우 EL 표현식이 아니기 때문에 "#{ ~ }"를 사용한다.
<script>
document.querySelector(".pagination").addEventListener("click", function (e) {
e.preventDefault()
e.stopPropagation()
const target = e.target
if(target.tagName !== 'A') {
return
}
const num = target.getAttribute("data-num")
self.location = `/todo/list?page=\${num}` //백틱(` `)을 이용해서 템플릿 처리
},false)
</script>
CRUD 연동
PageRequestDTO를 활용해 페이지 이동처리
(1) 조회 페이지로의 이동 처리
- PageRequestDTO 변경
- list.jsp 변경
PageRequestDTO 변경
-페이지 이동정보를 담는 문자열 변수인 쿼리스트링 link 추가 -> "page=1&size=10"
-페이지 이동정보를 전달하는 getLink() 메서드 작성
@Builder
@Data
@AllArgsConstructor
@NoArgsConstructor
public class PageRequestDTO {
@Builder.Default
@Min(value = 1)
@Positive
private int page = 1;
@Builder.Default
@Min(value = 10)
@Max(value = 100)
@Positive
private int size = 10;
private String link;
public int getSkip(){
return (page -1) * 10;
}
public String getLink() {
if(link == null){
StringBuilder builder = new StringBuilder();
builder.append("page=" + this.page);
builder.append("&size=" + this.size);
link = builder.toString();
}
return link;
}
}
list.jsp 변경
-PageRequestDTO의 link 변수를 이용해, 기존 링크에 page와 size를 추가한 페이지 이동 링크 처리
변경 전, list.jsp
<c:forEach items="${responseDTO.dtoList}" var="dto">
<tr>
<th scope="row"><c:out value="${dto.tno}"/></th>
<td>
<a href="/todo/read?tno=${dto.tno}" class="text-decoration-none">
<c:out value="${dto.title}"/>
</a>
</td>
<td><c:out value="${dto.writer}"/></td>
<td><c:out value="${dto.dueDate}"/></td>
<td><c:out value="${dto.finished}"/></td>
</tr>
</c:forEach>
변경 후, list.jsp
<c:forEach items="${responseDTO.dtoList}" var="dto">
<tr>
<th scope="row"><c:out value="${dto.tno}"/></th>
<td>
<a href="/todo/read?tno=${dto.tno}&${pageRequestDTO.link}" class="text-decoration-none" data-tno="${dto.tno}">
<c:out value="${dto.title}"/>
</a>
</td>
<td><c:out value="${dto.writer}"/></td>
<td><c:out value="${dto.dueDate}"/></td>
<td><c:out value="${dto.finished}"/></td>
</tr>
</c:forEach>
(2) 조회에서 목록으로 이동 처리
- TodoController 변경
- read.jsp 이벤트 처리
TodoController 변경
변경 전, read()
@GetMapping({"/read", "/modify"})
public void read(Long tno, Model model){
TodoDTO todoDTO = todoService.getOne(tno);
log.info(todoDTO);
model.addAttribute("dto", todoDTO );
}
변경 후, read()
@GetMapping({"/read", "/modify"})
public void read(Long tno, PageRequestDTO pageRequestDTO, Model model){
TodoDTO todoDTO = todoService.getOne(tno);
log.info(todoDTO);
model.addAttribute("dto", todoDTO );
}
read.jsp 이벤트 처리
-'List' 버튼 링크 변경
변경 전, 'List' 버튼 이동 링크
document.querySelector(".btn-secondary").addEventListener("click", function(e){
self.location = "/todo/list";
},false)
변경 후, 'List' 버튼 이동 링크
//목록 페이지로 이동하는 이벤트 처리
document.querySelector(".btn-secondary").addEventListener("click", function(e){
self.location = "/todo/list?${pageRequestDTO.link}"
},false)
(3) 조회에서 수정으로 이동 처리
- read.jsp 이벤트 처리
변경 전, 'Modify' 버튼 이동 링크
document.querySelector(".btn-primary").addEventListener("click", function(e){
self.location = "/todo/modify?tno="+${dto.tno}
},false)
변경 후, 'Modify' 버튼 이동 링크
document.querySelector(".btn-primary").addEventListener("click", function(e){
self.location = `/todo/modify?tno=${dto.tno}&${pageRequestDTO.link}`
},false)
(4) 수정 화면에서의 링크 처리
- modify.jsp 이벤트 처리
modify.jsp 이벤트 처리
변경 전, 'List' 버튼 이동 링크
document.querySelector(".btn-secondary").addEventListener("click",function(e) {
e.preventDefault()
e.stopPropagation()
self.location = "/todo/list";
},false);
변경 후, 'List' 버튼 이동 링크
document.querySelector(".btn-secondary").addEventListener("click",function(e) {
e.preventDefault()
e.stopPropagation()
self.location = "/todo/list?${pageRequestDTO.link}"
},false);
(5) 수정/삭제 처리 후 페이지 이동 처리 (PRG 패턴)
- modify.jsp에서 PageReqeustDTO 데이터 전달
- TodoController에서 전달받은 PageReqeustDTO를 이용해 remove() 메서드에서 페이지 이동 처리
modify.jsp에서 PageReqeustDTO 데이터 전달
<form action="/todo/modify" method="post">
<input type="hidden" name="page" value="${pageRequestDTO.page}}">
<input type="hidden" name="size" value="${pageRequestDTO.size}}">
TodoController에서 전달받은 PageReqeustDTO를 이용해 remove() 메서드에서 페이지 이동 처리
처음 remove()
@PostMapping("/remove")
public String remove(Long tno, RedirectAttributes redirectAttributes){
log.info("-------------remove------------------");
log.info("tno: " + tno);
todoService.remove(tno);
return "redirect:/todo/list";
}
변경 전, remove()
@PostMapping("/remove")
public String remove(Long tno, PageRequestDTO pageRequestDTO, RedirectAttributes redirectAttributes){
log.info("-------------remove------------------");
log.info("tno: " + tno);
todoService.remove(tno);
return "redirect:/todo/list?" + pageRequestDTO.getLink();
}
변경 후, remove()
-삭제 처리에 PageRequestDTO를 이용해 <form> 태그 정보를 수집해 목록 페이지 이동시 page는 1페이지로 이동해서 size정보 활용
@PostMapping("/remove")
public String remove(Long tno, PageRequestDTO pageRequestDTO, RedirectAttributes redirectAttributes){
log.info("-------------remove------------------");
log.info("tno: " + tno);
todoService.remove(tno);
redirectAttributes.addAttribute("page", 1);
redirectAttributes.addAttribute("size", pageRequestDTO.getSize());
return "redirect:/todo/list";
}
(6) 수정 처리 후 이동 처리 (PRG 패턴)
- TodoController에서 전달받은 PageReqeustDTO를 이용해 modify() 메서드에서 페이지 이동 처리
TodoController에서 전달받은 PageReqeustDTO를 이용해 modify() 메서드에서 페이지 이동 처리
변경 전, modify()
@PostMapping("/modify")
public String modify(@Valid TodoDTO todoDTO,
BindingResult bindingResult,
RedirectAttributes redirectAttributes){
if(bindingResult.hasErrors()) {
log.info("has errors.......");
redirectAttributes.addFlashAttribute("errors", bindingResult.getAllErrors() );
redirectAttributes.addAttribute("tno", todoDTO.getTno() );
return "redirect:/todo/modify";
}
log.info(todoDTO);
todoService.modify(todoDTO);
return "redirect:/todo/list";
}
변경 후, modify()
-수정 후 목록 이동시에 PageRequestDTO를 이용해 page와 size정보를 유지할 수 있도록 구성
@PostMapping("/modify")
public String modify(@Valid TodoDTO todoDTO,
PageRequestDTO pageRequestDTO,
BindingResult bindingResult,
RedirectAttributes redirectAttributes){
if(bindingResult.hasErrors()) {
log.info("has errors.......");
redirectAttributes.addFlashAttribute("errors", bindingResult.getAllErrors() );
redirectAttributes.addAttribute("tno", todoDTO.getTno() );
return "redirect:/todo/modify";
}
log.info(todoDTO);
todoService.modify(todoDTO);
redirectAttributes.addAttribute("page", pageRequestDTO.getPage());
redirectAttributes.addAttribute("size", pageRequestDTO.getSize());
return "redirect:/todo/list";
}
'Server Programming > Spring Boot Backend Programming' 카테고리의 다른 글
5장-1. 스프링 부트의 시작 (+Thymeleaf, RESTful, JSON, API Server) (0) | 2022.12.04 |
---|---|
4장-4. 스프링 Web MVC 구현 (3) 검색과 필터링 조건 (+ 동적 쿼리, 쿼리 스트링, URLEncoder) (0) | 2022.12.02 |
4장-2. 스프링 Web MVC 구현 (1) CRUD (+@Configuration, @Bean, 브라우저 한글 처리) (0) | 2022.11.29 |
4장-1. 스프링과 스프링 Web MVC (1) | 2022.11.29 |
3장. 세션과 필터, 쿠키와 리스너 (+ 한글 깨짐 처리 / Optional<> / 옵저버 패턴) (0) | 2022.11.27 |