👩🏻💻 Todo 데이터의 수가 많아진다면 데이터베이스에서 많은 시간이 걸릴 수 있고, 화면에서 이를 출력하는 데도 많은 시간과 자원이 소모 👩🏻💻 일반적으로는 많은 데이터를 보여주는 작업은 페이징 처리를 해서 최소한의 데이터들을 보여주는 방식을 선호 ⚡️ 페이징 처리를 하게 되면 데이터베이스에서 필요한 만큼의 최소한의 데이터를 가져오고 이를 출력하기 때문에 성능 개선에도 많은 도움이 됨
1) 페이징을 위한 SQL 연습
개발 전에 데이터베이스 상에서 원하는 동작을 미리 구현.
A. 더미 데이터 추가하기
많은 양의 데이터를 만드는 방법. 2배씩 늘어나기 때문에 몇번만 실행하면 많은 데이터를 생성함. 더미 데이터 추가를 위해 아래의 SQL을 여러번 처리.
insert into tbl_todo(title,dueDate,writer) (select title,dueDate,writer from tbl_todo);
📍 최종적으로 데이터의 수를 확인
select count(tno) from tbl_todo;
B. limit 실습
페이징 처리를 위해서는 select의 마지막 부분에는 limit 처리를 사용. 일반적으로 웹에서는 가장 최근에 등록된 데이터를 우선으로 보여주므로 가장 마지막에 등록된 데이터가 순차적으로 보이도록 쿼리를 작성
select * from tbl_todo order by tno desc;
📍 limit 뒤의 값이 한 개만 존재하는 경우 가져와야 하는 데이터의 수를 의미. 가장 최근 데이터 10개를 가져옴
select*from tbl_todo order by tno desc limit 10;
select*from tbl_todo order by tno desc limit 0, 10;
C. count()의 필요성
페이징 처리를 하기 위해서는 페이지 번호를 구성할 때 필요한 전체 데이터의 개수가 필요함. 예를 들어 전체 데이터가 30개이면 3 페이지까지만 출력해야 하는 작업에서 사용
select count(tno) from tbl_todo;
2) 페이지 처리를 위한 DTO
💡 페이지 처리는 현재 페이지 번호 page, 한 페이지당 보여주는 데이터의 수 size가 기본적으로 필요
➡️ 2개의 숫자를 매번 전달할 수도 있겠지만 나중에 확장 여부를 고려해서라도 별도의 DTO를 만들어 두는 것이 좋음
dto 패키지에 PageRequestDTO 클래스를 정의
@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) * size;
}
}
✏️ 페이지 번호 page와 한 페이지당 개수 size를 보관하는 용도 외에도 limit에서 사용하는 건너뛰기 skip 의 수를 getSkip() 만들어서 사용 ✏️ page나 size는 기본값을 가지기 위해서 Lombok의 @Builder.Default를 이용 ✏️ @Min, @Max를 이용해서 외부에서 조작하는 것에 대해서도 대비하도록 구성
3) TodoMapper의 목록 처리
TodoMapper 인터페이스는 PageRequestDTO를 파라미터로 처리하는 selectList()를 추가
<select id="selectList" resultType="com.example.spring_ex_01_2404.domain.TodoVO">
select * from tbl_todo order by tno desc limit #{skip}, #{size}
</select>
✏️ MyBatis는 기본적으로 getXXX, setXXX를 통해서 동작하기 때문에 #{skip}의 경우는 getSkip()을 호출
테스트 코드를 이용해서 TodoMapper의 selectList()가 정상적으로 동작하는지 확인
수정과 삭제는 GET 방식으로 조회한 후에 POST 방식으로 처리 사실상 GET 방식의 내용은 조회 화면과 같지만 스프링 MVC에는 여러 개의 경로를 배열과 같은 표기법을 이용해서 하나의 @GetMapping으로 처리할 수 있기 때문에 read() 기능을 수정해서 수정과 삭제에도 같은 메서드를 이용하도록 작업
TodoController의 read()를 다음과 같이 수정해서 "/todo/modify?tno=xxx"의 경로를 처리하도록 수정
@GetMapping({"/read", "/modify"})
public void read(Long tno, Model model) {
// 1) request로 전달 받은 tno를 서비스에 전달해서 2)TodoDTO를 반환받아서 3)View 에 전달
TodoDTO todoDTO = todoService.getOne(tno);
log.info(todoDTO);
model.addAttribute("dto", todoDTO);
}
/WEB-INF/view/todo 폴더에 있는 read.jsp를 그대로 복사해서 modify.jsp를 구성
👾 modify.jsp에서는 수정과 삭제 작업이 POST 방식으로 처리될 예정이므로 이를 위한 <form> 태그를 구성하고 수정이 가능한 항목들은 편집이 가능하도록 함
<delete id="delete">
DELETE FROM tbl_todo WHERE tno = #{tno}
</delete>
test 코드 작성
@Test
public void testDelete() {
// 1) tno로 데이터를 반환해서 정상 출력 확인 2) 삭제 3) 다시 tno로 데이터를 반환해서 삭제 확인.
Long tno = 2L;
TodoVO todoVO = todoMapper.selectOne(tno);
log.info(todoVO);
todoMapper.delete(tno);
todoVO = todoMapper.selectOne(tno);
log.info(todoVO);
}
👾 수정 작업에서는 화면에서 체크박스를 이용해서 완료여부 finished (boolean) 를 처리하게 됨 👾 문제는 브라우저가 체크박스가 클릭된 상태일때 전송되는 값은 "on"이라는 값을 전달 👾 TodoDTO로 데이터를 수집할 때에는 문자열 "on"을 boolean 타입으로 처리할 수 있어야 하므로 컨트롤러에서 데이터를 수집할 때 타입을 변경해 주기 위한 CheckboxFormatter 를 formatter 패키지에 추가해서 개발
public class CheckBoxFormatter implements Formatter<Boolean> {
@Override
public Boolean parse(String text, Locale locale) throws ParseException {
if (text == null) return false;
return text.equals("on");
}
@Override
public String print(Boolean object, Locale locale) {
return object.toString();
}
}
@PostMapping("/modify")
public String midify(@Valid TodoDTO todoDTO,
BindingResult bindingResult,
RedirectAttributes redirectAttributes) {
if (bindingResult.hasErrors()) {
// 유효성 검사 결과 에러가 있으면 수정 페이지로 돌아감
log.info("has error");
redirectAttributes.addFlashAttribute("errors", bindingResult.getAllErrors());
redirectAttributes.addAttribute("tno", todoDTO.getTno());
return "redirect:/todo/mofidy";
}
log.info(todoDTO);
todoService.modify(todoDTO);
return "redirect:/todo/list";
}
✓ @Valid를 이용해서 필요한 내용들을 검증하고 문제가 있는 경우에는 다시 "/todo/modify"로 이동시키는 방식을 사용 ✓ "/todo/modify"로 이동할 때는 tno 파라미터가 필요하기 때문에 RedirectAttributes의 addAttribute을 이용
/WEB-INF/view/todo/modify.jsp 에 검증된 정보를 처리하는 코드를 추가
👾 <select> 태그의 경우 resultType을 지정하는 것에 주의 ➡️ resultType은 JDBC의 ResultSet의 한 행 row을 어떤 타입의 객체로 만들것인지를 지정
<select id="selectAll" resultType="com.example.spring_ex_01_2404.domain.TodoVO">
SELECT * FROM tbl_todo ORDER BY tno DESC
</select>
마지막으로 test 폴더 내에 작성해둔 TodoMapperTests 클래스를 이용해 테스트 코드를 작성
@Test
public void testSelectAll() {
List<TodoVO> todoVOList = todoMapper.selectAll();
for (TodoVO todoVO : todoVOList) {
log.info(todoVO);
}
todoVOList.forEach(item -> log.info(item));
// for문과 foreach문 둘 중 하나 사용
}
✓ 테스트 실행 결과는 가장 나중에 추가된 데이터를 우선적으로 보여줌
2) TodoService / TodoServiceImpl의 개발
서비스 계층의 개발은 특별한 파라미터가 없는 경우 TodoMapper을 호출하는것이 전부이다. 다만, TodoMapper가 반환하는 데이터의 타입이 List<TodoVO>이기 때문에 이를 List<TodoDTO>로 변환하는 작업이 필요.
TodoService 인터페이스에 getAll() 기능을 추가
public interface TodoService {
void register(TodoDTO todoDTO);
List<TodoDTO> getAll();
}
💡 List<TodoVO>를 List<TodoDTO>로 변화하는 작업은 java8부터 지원하는 stream을 이용해서 각 TodoVO는 map()을 통해서 TodoDTO로 바꾸고,collect()를 이용해서 List로 묶어 줌
test 코드 작성후 실행
@Test
public void testGetAll() {
List<TodoDTO> todoDTOList = todoService.getAll();
for (TodoDTO todoDTO: todoDTOList) {
log.info(todoDTO);
}
todoDTOList.forEach(item -> log.info(item));
// 둘 중 하나 사용
}
3) TodoController의 처리
TodoController의 list() 기능에서 TodoService를 처리하고 Model에 데이터를 담아서 JSP로 전달
@GetMapping("/read")
public void read(Long tno, Model model) {
// 1) request로 전달 받은 tno를 서비스에 전달해서 2)TodoDTO를 반환받아서 3)View 에 전달
TodoDTO todoDTO = todoService.getOne(tno);
log.info(todoDTO);
model.addAttribute("dto", todoDTO);
}
1. register.jsp의 <form method="post"> 태그에 의해서 submit 버튼을 클릭하면 POST 방식으로 "title, dueDate, writer"를 전송하게 됨 2. TodoController에서는 TodoDTO로 바로 전달된 파라미터의 값들을 수집 3. POST 방식으로 처리된 후에는 "/register/list"로 이동해야 하므로 "redirect:/todo/list"로 이동할 수 있도록 문자열을 반환
@PostMapping("/register")
public String registerPOST(TodoDTO todoDTO) {
log.info("POST todo register");
log.info(todoDTO);
return "redirect:/todo/list";
}
✓ 한글 문제가 있기는 하지만 브라우저에서 입력한 데이터들이 수집되고 /todo/list로 이동하는 기능에 문제가 없는 것을 확인
✓ web.xml의 설정을 서버를 재시작해야 올바르게 반영되므로 톰캣을 재시작하고 한글 처리를 확인
3. @Valid를 이용한 서버사이드 유효성 검증
유효성 검증
과거의 웹 개발에는 자바 스크립트를 이용하여 브라우저에서만 유효성 검사를 진행하는 방식이 많았지만, 모바일과 같이 다양한 환경에서 서버를 이용하는 현재에는 브라우저를 사용하는 프론트쪽에서의 검증과 더불어 서버에서도 입력되는 값들을 검증하는 것이 좋음. 이러한 검증 작업은 컨트롤러에서 진행하는데 스프링MVC의 경우 @Valid와 BindingResult라는 존재를 이용해서 간단하게 처리할 수 있음
⚡️ 스프링 MVC에서 검증을 처리하기 위해서는 hibernate-validate 라이브러리가 필요 (build.gradle 에 추가)
👩🏻💻 JSP 파일을 작성하기 전, 프로젝트의 시작 단계에서 화면 디자인을 결정하는 것이 좋음 ✓ 화면 디자인 없이 개발을 진행할 때는 나중에 코드를 다시 입혀야 하는 작업을 할 수도 있기 때문 👩🏻💻 최근에는 부트스트랩 (bootstrap)이나 머터리얼(Material Design)과 같이 쉽게 웹 화면을 디자인할 수 있는 라이브러리들 덕분에 전문적인 웹 디자이너의 도움 없이도 어느정도 완성도가 있는 디자인 작업이 가능해 짐
📌 webapp의 resources 폴더에 test.html을 작성해서 부트스트랩을 적용하는 페이지를 작성
✓ 부트스트랩의 화면 구성에는 container와 row를 이용
✓ Card 컴포넌트 적용하기
부트스트랩에는 화면을 쉽게 제작할 수 있는 여러 종류의 컴포넌트를 제공 이중에서 Card라는 컴포넌트를 적용해서 현재의 화면에서 Content라는 영역을 변경
✓ Navbar 컴포넌트의 적용 화면 상단에는 간단한 메뉴를 보여줄 수 있는 Nav 혹은 Navbar 컴포넌트를 적용 공식 문서의 Navbar의 예제를 선택해서 Header 라고 출력되는 부분에 적용
✓ Footer 처리 맨 아래 <div class="row">에는 간단한 <footer>를 적용 해당 <div>를 맨 아래쪽으로 고정하기 위해서 fixed-bottom을 적용 내용이 많은 경우에는 Footer 영역으로 인해 가려질 수 있는 부분이 있으므로 z-index 값은 음수로 처리해서 가려질 수 있도록 구성
테이블레이아웃은 위젯을 표 형태로 배치할 때 주로 활용. 테이블레이아웃을 사용하여 행과 열의 수를 정하고 그 안에 위젯을 배치. <TableRow>가 행의 수, 열의 수는 <TableRow> 안에 포함된 위젯의 수로 결정
3행 4열의 테이블레이아웃
1) 테이블레이아웃의 속성
layout_span과 layout_column은 테이블레이아웃 안에 포함된 위젯에 설정하는 속성 - layout_span은 열을 합쳐서 표시하라는 의미로, 예를 들어 layout_span="2"는 현재 셀부터 2개의 셀을 합쳐서 표시 - layout_column은 지정된 열에 현재 위젯을 표시하라는 의미
stretchColumns은 <TableLayout> 자체에 설정하는 속성으로, 지정된 열의 너비를 늘이라는 의미 - stretchColumns="*"는 각 셀을 모두 같은 크기로 확장하여 전체 화면을 꽉 차는 효과를 냄. 열번호는 0부터 시작.
테이블레이아웃과 마찬가지로 위젯을 표 형태로 배치할 때 사용하지만 좀 더 직관적 테이블레이아웃에서는 다소 어려웠던 행 확장도 간단하게 할 수 있어서 유연한 화면 구성에 적합. ✓ 행과 열을 지정하는 방법 : 2행 3열을 지정하려면 layout_row 속성은 1로, layout_column 속성은 2로 설정
1) 그리드레이아웃의 속성
rowCount : 행의 수
columnCount : 열의 수
orientation : 그리드를 수평 방향으로 우선할 것인지, 수직 방향으로 우선할 것인지를 결정 - 그리드레이아웃 안에 포함될 위젯에서 자주 사용되는 속성
layout_row : 자신이 위치할 행 번호(0번부터 시작)
layout_column : 자신이 위치할 열 번호(0번부터 시작)
layout_rowSpan : 행을 지정된 수만큼 확장
layout_columnSpan : 열을 지정된 수만큼 확장
layout_gravity : 주로 fill, fill_vertical, fill_horizontal 등으로 지정
💡 layout_rowSpan이나 layout_columnSpan으로 셀 확장 시 위젯을 확장된 셀에 꽉 채우는 효과를 줌