1 JSP 화면 개발
스프링 부트는 템플릿 엔진을 이용한 화면처리를 지원.
지원되는 템플릿 엔진 : 타임리프, Freemarker, Mustache, Grooby Templates
템플릿 엔진 이용시 데이터와 완벽하게 분리된 화면을 개발 가능. 순수하게 HTML 만을 이용한 화면 개발이 가능하고 운영 과정에서 쉽게 화면을 변경 가능.
[웹 애플리케이션 화면 개발하기]
실습 프로젝트 생성 및 환경설정
프로젝트 생성시 사용 모듈 추가: Web, Lombok JPA, H2, DevTool
DevTool은 애플리케이션을 재실행하지 않아도 수정된 소스를 자동으로 반영.
Pom.xml : jstl, org.apache.tomct.embed - jsp를 컴파일 할때 필요한 라이브러리(tomcat-embed-jasper를 사용해야 하므로)
JSP파일의 위치와 확장자를 제어하기 위해 ViewResolver를 사용한다 > 설정은 properties에 추가한다.
반영 후 웹 관련 폴더 들이 src/main/webapp밑에 자동으로 생성된다. 기본적으로 스프링부트는 웹 화면을 JSP로 구현하는 것을 권장하지 않기 떄문에 관련된 폴더를 처음부터 제공하지 않는다.
<!-- https://mvnrepository.com/artifact/javax.servlet/jstl -->
<dependency>
<groupId>javax.servlet</groupId>
<artifactId>jstl</artifactId>
<version>1.2</version>
</dependency>
<!-- https://mvnrepository.com/artifact/org.apache.tomcat.embed/tomcat-embed-jasper -->
<dependency>
<groupId>org.apache.tomcat.embed</groupId>
<artifactId>tomcat-embed-jasper</artifactId>
<version>9.0.30</version>
</dependency>
게시글 목록 조회 기능
① 게시글을 저장하는 Board 클래스
@Getter
@Setter
@ToString
public class Board {
private Long seq;
private String title;
private String writer;
private String content;
private Date createDate;
private Long cnt;
}
② 사용자의 요청에 따라 Board객체에 매핑해서 JSP화면으로 화면을 전환하는 BoardController
@Controller
public class BoardController {
@RequestMapping("/getBoardList")
public String getBoardList(Model model) {
//실제 DB연동 하지 않고 화면에 출력할 게시글 목록을 임시로 생성해서 Model 객체에 등록
List<Board> boardList = new ArrayList<Board>();
for(int i=1; i <=10; i++) {
Board b = new Board();
b.setSeq(new Long(i));
b.setTitle("제목" + i);
b.setWriter("테스터");
b.setContent(i+"번 내용입니다.");
b.setCreateDate(new Date());
b.setCnt(0L);
boardList.add(b);
}
model.addAttribute("boardList", boardList);
return "boardList";
}
}
③ 검색 결과를 출력할 글 목록 화면인 JSP 파일 생성
JSTL을 사용하기 위해 taglib 지시자 추가(core, fmt)
Board 객체에 저장된 값을 화면에 출력할 때는 EL을 사용
<%@ page language="java" contentType="text/html; charset=UTF-8" pageEncoding="UTF-8"%>
<%@ taglib prefix="c" uri="http://java.sun.com/jsp/jstl/core" %>
<%@ taglib prefix="fmt" uri="http://java.sun.com/jsp/jstl/fmt"%>
<!DOCTYPE html>
<html>
<head>
<meta content="text/html; charset=UTF-8" http-equiv="Content-type">
<title>글 목록</title>
</head>
<body>
<h1>게시글 목록</h1>
<table style="text-align: center;">
<tr>
<th bgcolor="grey" width="100">번호</th>
<th bgcolor="grey" width="100">제목</th>
<th bgcolor="grey" width="100">작성자</th>
<th bgcolor="grey" width="100">등록일</th>
<th bgcolor="grey" width="100">조회수</th>
</tr>
<c:forEach var="vo" items="${boardList }">
<tr>
<td>${vo.seq}</td>
<td align="left"><a href="getBoard?seq=${vo.seq}">${vo.title}</a></td>
<td>${vo.writer}</td>
<td><fmt:formatDate value="${vo.createDate}" pattern="yyyy-MM-dd"/></td>
<td>${vo.cnt}</td>
</tr>
</c:forEach>
</table>
<a href="insertBoard">새글 등록</a>
</body>
</html>
[데이터베이스 연동하기]
JPA 연동하기
① Board클래스를 엔티티 클래스로 변환
@Getter
@Setter
@ToString
@Entity //엔티티 클래스
public class Board {
@Id // 식별자 필드
@GeneratedValue // 자동으로 시퀀스 증가
private Long seq;
private String title;
@Column(updatable = false) // update의 경우 제외하는 속성
private String writer;
private String content;
@Column(insertable = false, // insert의 경우 제외하는 속성
updatable = false,
columnDefinition = "date default sysdate") // null이 아니라 default 설정을 추가해서 반영
private Date createDate;
@Column(insertable = false,
updatable = false,
columnDefinition = "number default 0")
private Long cnt;
}
② 테이블 생성 설정
# JPA Setting
spring.jpa.hibernate.ddl-auto=create
③ 리포지터리 인터페이스 생성
package com.studyboot.persistence;
import org.springframework.data.repository.CrudRepository;
import com.studyboot.domain.Board;
public interface BoardRepository extends CrudRepository<Board, Long>{
//단순 CRUD 만 구현할 것이므로 CrudRepository 인터페이스만 상속
}
컨트롤러에서 직접 리포지터리로 DB 연동을 처리해도 되지만, 실제 프로젝트에서 Repository를 이용하지 않고 비즈니스 컴포넌트에서 제공하는 인터페이스를 사용한다. (그래야 다형성을 기반으로 좀 더 유연한 프레젠테이션 레이어를 개발할 수 있다.)
비즈니스 컴포넌트 만들기
① ServiceImpl 클래스 생성
@Service
public class BoardServiceImpl implements BoardService {
@Autowired
private BoardRepository boardRepo;
/**
* <pre>
* getBoardLists
* 게시글 리스트 조회
* </pre>
* */
public List<Board> getBoardLists(Board board){
return (List<Board>) boardRepo.findAll(); //Board 테이블의 모든 객체를 가져와 리턴하는데 형변환시켜줌
}
/**
* <pre>
* insertBoard
* 게시글 입력
* </pre>
* */
public void insertBoard() {
}
/**
* <pre>
* getBoard
* 게시글 상세 조회
* </pre>
* */
public Board getBoard() {
return null;
}
/**
* <pre>
* updateBoard
* 게시글 수정
* </pre>
* */
public void updateBoard() {
}
/**
* <pre>
* deleteBoard
* 게시글 삭제
* </pre>
* */
public void deleteBoard() {
}
}
② Service 인터페이스 생성 (이클립스 단축키 : alt + shift + T)
public interface BoardService {
/**
* <pre>
* getBoardLists
* 게시글 리스트 조회
* </pre>
* */
List<Board> getBoardLists(Board board);
/**
* <pre>
* insertBoard
* 게시글 입력
* </pre>
* */
void insertBoard();
/**
* <pre>
* getBoard
* 게시글 상세 조회
* </pre>
* */
Board getBoard();
/**
* <pre>
* updateBoard
* 게시글 수정
* </pre>
* */
void updateBoard();
/**
* <pre>
* deleteBoard
* 게시글 삭제
* </pre>
* */
void deleteBoard();
}
비즈니스 컴포넌트 사용하기
- Controller에서 비즈니스 컴토넌트를 호출하도록 수정
@Controller
public class BoardController {
@Autowired
private BoardService boardService;
@RequestMapping("/getBoardList")
public String getBoardList(Model model, Board board) {
List<Board> boardList = boardService.getBoardLists(board);//게시글 목록 가져오기
model.addAttribute("boardList", boardList);
return "boardList";
}
}
[게시판 구현하기 - CRUD]
게시글 등록
① 등록화면
<%@ page language="java" contentType="text/html; charset=UTF-8" pageEncoding="UTF-8"%>
<!DOCTYPE html>
<html>
<head>
<meta content="text/html; charset=UTF-8" http-equiv="Content-type">
<title>글 작성</title>
</head>
<body>
<h3>게시글 등록</h3>
<hr>
<form action="insertBoard" method="post">
<table border="1" cellpadding="0" cellspacing="0" style="text-align: center;">
<tr>
<td bgcolor="#e3e3e3" width="70">제목</td>
<td align="left"><input type="text" name="title"/></td>
</tr>
<tr>
<td bgcolor="#e3e3e3" width="70">작성자</td>
<td align="left"><input type="text" name="writer" size="10"/></td>
</tr>
<tr>
<td bgcolor="#e3e3e3" width="70">내용</td>
<td align="left"><textarea name="content" cols="40" rows="10"></textarea></td>
</tr>
<tr>
<td colspan="2" align="center">
<input type="submit" value="등록하기"/>
</td>
</tr>
</table>
</form>
</body>
</html>
② 등록처리 후 리다이렉트
BoardController.class
@GetMapping("/insertBoard")
public String insertBoardView(Board board) {
return "insertBoard"; //게시글 작성 페이지
}
@PostMapping("/insertBoard")
public String insertBoard(Board board) {
boardService.insertBoard(board);
return "redirect:getBoardList";//게시글 작성 후 리스트 페이지로 리다이렉트
}
BoardServiceImpl.class
public void insertBoard(Board board) {
boardRepo.save(board);//영속화
}
게시글 상세 조회
① 게시글 상세 조회
BoardController.class
@GetMapping("/getBoard")
public String getBoard(Board board, Model model) {
model.addAttribute("board", boardService.getBoard(board));
return "getBoard"; //게시글 상세 페이지
}
public Board getBoard(Board board) {
return boardRepo.findById(board.getSeq()).get();
}
② 게시글 상세 화면
<%@ page language="java" contentType="text/html; charset=UTF-8" pageEncoding="UTF-8"%>
<%@ taglib prefix="c" uri="http://java.sun.com/jsp/jstl/core" %>
<%@ taglib prefix="fmt" uri="http://java.sun.com/jsp/jstl/fmt"%>
<!DOCTYPE html>
<html>
<head>
<meta content="text/html; charset=UTF-8" http-equiv="Content-type">
<title>글 상세</title>
</head>
<body>
<h3>게시글 상세</h3>
<hr>
<form action="updateBoard" method="post">
<input name="seq" type="hidden" value="${board.seq}">
<table border="1" cellpadding="0" cellspacing="0" style="text-align: center;">
<tr>
<td bgcolor="#e3e3e3" width="70">제목</td>
<td align="left"><input type="text" name="title" value="${board.title}"/></td>
</tr>
<tr>
<td bgcolor="#e3e3e3" width="70">작성자</td>
<td align="left">${board.writer}</td>
</tr>
<tr>
<td bgcolor="#e3e3e3" width="70">내용</td>
<td align="left"><textarea name="content" cols="40" rows="10">${board.content }</textarea></td>
</tr>
<tr>
<td bgcolor="#e3e3e3" width="70">등록일</td>
<td align="left"><fmt:formatDate value="${board.createDate }" pattern="yyyy-MM-dd"></fmt:formatDate></td>
</tr>
<tr>
<td bgcolor="#e3e3e3" width="70">조회수</td>
<td align="left">${board.cnt }</td>
</tr>
<tr>
<td colspan="2" align="center">
<input type="submit" value="수정하기"/>
</td>
</tr>
</table>
</form>
<hr>
<a href="insertBoardView">글 등록</a>
<a href="deleteBoard?seq=${board.seq }">글 삭제</a>
<a href="getBoardList">리스트</a>
</body>
</html>
게시글 수정
① 수정할 게시글 조회
② 수정된 내용 설정후 save 메소드 호출
③ JPA가 영속성 컨텍스트에 저장된 스냅샷과 비교하여 변경값으로 수정하는 UPDATE문을 SQL 저장소에 등록
④ 게시글 리스트 페이지로 포워딩
BoardController.class
@PostMapping("/updateBoard")
public String updateBoard(Board board) {
boardService.updateBoard(board);
return "forward:getBoardList";//게시글 수정 후 포워딩
}
BoardServiceImpl.class
public void updateBoard(Board board) {
Board beforeBoard = boardRepo.findById(board.getSeq()).get();
beforeBoard.setTitle(board.getTitle());
beforeBoard.setContent(board.getContent());
boardRepo.save(beforeBoard);//UPDATE구문을 SQL저장소로 등록
}
게시글 삭제
① 사용자가 선택한 게시글 삭제
② 글 목록 화면으로 포워딩
BoardController.class
@GetMapping("/deleteBoard")
public String deleteBoard(Board board) {
boardService.deleteBoard(board);
return "forward:getBoardList";//게시글 삭제 후 포워딩
}
BoardServiceImpl.class
public void deleteBoard(Board board) {
boardRepo.deleteById(board.getSeq());
}
2 타임리프 적용
스프링 부트가JSP를 권장하지 않는 가장 큰이유는 스프링 부트가 독립적으로 실행 가능한 애플리케이션을 빠르고 쉽게 만드는 것을 목표로 하기 때문이다. 따라서 웹 애플리케이션도 WAR가 아닌 JAR로 패키징할 수 있도록 지원한다. 화면개발을 JSP로 할 경우 WAR로 패키징해야 하고, WAR 패키징은 복잡한 폴더 구조를 구성해야 한다. 따라서 스프링 부트는 템플릿 엔진을 이용해서 화면 개발을 권장한다.
[타임리프 퀵스타트]
템플릿 엔진이란?
일반적인 웹 애플리케이션은 DB에 있는 데이터를 HTML, CSS같은 정적인 마크업과 결합하여 사용자가 요청한 결과화면을 제공한다. 템플릿 엔진은 사용자에게 제공되는 화면과 데이터를 분리하여 관리할 수 있다. 템플릿 엔진은 데이터와 이 데이터를 표현해줄 템플릿을 결합해주는 도구이다. 템플릿은 HTML과 같은 Markup이고, 데이터는 데이터베이스에 저장된 데이터를 의미한다. 템플릿 엔진을 이용하면 고정된 데이터에 다양한 템플릿을 적용할 수있다수 있다. > 그래서 데이터와 분리된 화면 개발 및 관리가 가능하다.
타임리프 환경 설정
① pom.xml 설정 추가 - 타임리프 스타터
타임리프는 별도 설정이 없으면 기본 파일 확장자를 .html로 사용한다. 인코딩은 UTF-8, MIME 타입은 text/html 이다.
② 캐시 관련 설정 추가
서버 내부의 캐시는 true로 설정되어있는데 이 설정을 유지할 경우 타임리프로 개발된 화면을 수정했을 때 매번 프로젝트를 다시 시작해야 한다. 개발시에는 작성환 화면을 서버에 캐싱하지 않도록 설정을 추가한다.
application.properties
# Thymeleaf Cache Setting
spring.thymeleaf.cache=false
타임리프 출력해보기
스프링 부트 웹 리소스 위치
웹 파일 |
경로 |
정적 html 파일 |
src/main/resources/static src/main/public |
웹 페이지 대표 아이폰 favicon |
src/main/resources/favicon.ico |
템플릿 |
src/main/resources/templates html-Thymeleaf tpl-Groovy ftl-Freemaker vm-velocity |
타임리프가 인지하는 템플릿 파일의 기본위치는 src/main/resources/templates 이다. .html 파일을 작성한다.
① 컨트롤러 작성 - BoardController
@GetMapping("/hello")
public void hello(Model model) {
model.addAttribute("greeting","Hello 타임리프");
}
② 템플릿 파일의 경로에 html 파일 작성
루트 엘리먼트인 <html>에 타임리프 네임스페이스를 선언(http://www.thymeleaf.org">). 이 선언을 통해 HTML 파일 내에서 타임리프가 제공하는 태그와 속성들을 사용할 수 있다. 타임리프의 th:text 속성은 화면에 단순한 문자열을 출력할 때 사용
<html xmlns:th="http://www.thymeleaf.org">
<head>
<title>타임리프</title>
<meta http-equiv="Content-Type" content="text/html; charset=UTF-8">
</head>
<body th:align="center">
<h1 th:text="${greeting}"></h1>
</body>
</html>
타임리프 플러그인 설치
타임리프가 제공하는 수많은 속성을 외워서 사용하기는 매우 어렵다. 타임리프 플러그인은 타임리프가 제공하는 다양한 속성을 자동으로 완성해준다. Thymeleaf Plugin for Eclipse 설치 후 재기동. 이제 HTML 파일에서 타임리프 네임스페이스를 선언하면 단축키를 통해 타임리프의 다양한 속성들을 자동완성할 수 있다.
설치 완료 후 프로젝트 마우스 우클릭 > Thymeleaf > Add Thymeleaf Nature 선택
[타임리프로 게시판 프로그램 개발하기]
게시글 등록
insertBoard.html
<html xmlns:th="http://www.thymeleaf.org">
<head>
<meta content="text/html; charset=UTF-8" http-equiv="Content-type">
<title>글 목록</title>
</head>
<body>
<h1>게시글 목록</h1>
<table th:align="center" border="1" th:cellpadding="0" th:cellspacing="0" th:width="700">
<tr>
<th bgcolor="#e3e3e3" width="100">번호</th>
<th bgcolor="#e3e3e3" width="200">제목</th>
<th bgcolor="#e3e3e3" width="150">작성자</th>
<th bgcolor="#e3e3e3" width="150">등록일</th>
<th bgcolor="#e3e3e3" width="100">조회수</th>
</tr>
<tr th:each="board : ${boardList}">
<td th:text="${board.seq}"></td>
<td th:text="${board.title}"></td>
<td th:text="${board.writer}"></td>
<td th:text="${board.createDate}"></td>
<td th:text="${board.cnt}"></td>
</tr>
</table>
<a th:href="@{/insertBoard}">글 등록</a>
</body>
</html>
게시글 목록 조회
타임리프는 반복적인 출력을 위해 th:each 속성을 제공. 자바의 배열, List, Map 같은 컬렉션에 저장된 데이터를 반복적으로 처리 가능
getBoardList.html
<html xmlns:th="http://www.thymeleaf.org">
<head>
<meta content="text/html; charset=UTF-8" http-equiv="Content-type">
<title>글 목록</title>
</head>
<body>
<h1>게시글 목록</h1>
<table th:align="center" border="1" th:cellpadding="0" th:cellspacing="0" th:width="700">
<tr>
<th bgcolor="#e3e3e3" width="100">번호</th>
<th bgcolor="#e3e3e3" width="200">제목</th>
<th bgcolor="#e3e3e3" width="150">작성자</th>
<th bgcolor="#e3e3e3" width="150">등록일</th>
<th bgcolor="#e3e3e3" width="100">조회수</th>
</tr>
<tr th:each="board : ${boardList}">
<td th:text="${board.seq}"></td>
<td ><a th:href="@{/getBoard(seq=${board.seq})}" th:text="${board.title}"></a></td>
<td th:text="${board.writer}"></td>
<td th:text="${board.createDate}"></td>
<td th:text="${board.cnt}"></td>
</tr>
</table>
<a th:href="@{/insertBoard}">글 등록</a>
</body>
</html>
상세화면 출력
타임리프의 링크는 기존링크와 비교하면 훨씬 편리하며 기능도 다양하다.
th.href=“@{이동할 경로(파라미터 key=value)}”
getBoard.html
<html xmlns:th="http://www.thymeleaf.org">
<head>
<meta content="text/html; charset=UTF-8" http-equiv="Content-type">
<title>글 상세</title>
</head>
<body th:align="center">
<h3>게시글 상세</h3>
<hr>
<table border="1" th:cellpadding="0" th:cellspacing="0" th:align="center">
<tr>
<td bgcolor="#e3e3e3" width="70" th:text="제목"></td>
<td><input type="text" name="title" th:value="${board.title}"/></td>
</tr>
<tr>
<td bgcolor="#e3e3e3" width="70" th:text="작성자"></td>
<td th:text="${board.writer}"></td>
</tr>
<tr>
<td bgcolor="#e3e3e3" width="70" th:text="내용"></td>
<td><textarea name="content" cols="40" rows="10" th:text="${board.content }"></textarea></td>
</tr>
<tr>
<td bgcolor="#e3e3e3" width="70">등록일</td>
<td th:text="${board.createDate }"></td>
</tr>
<tr>
<td bgcolor="#e3e3e3" width="70">조회수</td>
<td th:text="${board.cnt }"></td>
</tr>
</table>
<hr>
<a th:href="@{/insertBoard}">글 등록</a>
<a th:href="@{/deleteBoard(seq=${board.seq })}">글 삭제</a>
<a th:href="@{/getBoardList}">리스트</a>
</body>
</html>
게시글 수정
getBoard.html
<html xmlns:th="http://www.thymeleaf.org">
<head>
<meta content="text/html; charset=UTF-8" http-equiv="Content-type">
<title>글 상세</title>
</head>
<body th:align="center">
<h3>게시글 상세</h3>
<hr>
<form th:action="@{updateBoard}" method="post">
<input name="seq" type="hidden" th:value="${board.seq}">
<table border="1" th:cellpadding="0" th:cellspacing="0" th:align="center">
<tr>
<td bgcolor="#e3e3e3" width="70" th:text="제목"></td>
<td><input type="text" name="title" th:value="${board.title}"/></td>
</tr>
<tr>
<td bgcolor="#e3e3e3" width="70" th:text="작성자"></td>
<td th:text="${board.writer}"></td>
</tr>
<tr>
<td bgcolor="#e3e3e3" width="70" th:text="내용"></td>
<td><textarea name="content" cols="40" rows="10" th:text="${board.content }"></textarea></td>
</tr>
<tr>
<td bgcolor="#e3e3e3" width="70">등록일</td>
<td th:text="${board.createDate }"></td>
</tr>
<tr>
<td bgcolor="#e3e3e3" width="70">조회수</td>
<td th:text="${board.cnt }"></td>
</tr>
<tr>
<td colspan="2" align="center">
<input type="submit" value="수정하기"/>
</td>
</tr>
</table>
</form>
<hr>
<a th:href="@{/insertBoard}">글 등록</a>
<a th:href="@{/deleteBoard(seq=${board.seq })}">글 삭제</a>
<a th:href="@{/getBoardList}">리스트</a>
</body>
</html>
게시글 삭제
해당 하이퍼링크 클릭 시 게시글이 삭제 처리된다.
getBoard.html
<a th:href="@{/deleteBoard(seq=${board.seq })}">글 삭제</a>
상태 변수와 날짜 포맷 지정하기
타임리프의 th:each 속성에서는 현재 컬렉션의 상태정보를 저장하는 상태 변수를 선언할 수있다수 있다. 1부터 자동으로 증가되는 값을 일련번호로 사용하고 싶다면 이 상태변수를 선언해서 쓰면된다.
getBoardList.html
<html xmlns:th="http://www.thymeleaf.org">
<head>
<meta content="text/html; charset=UTF-8" http-equiv="Content-type">
<title>글 목록</title>
</head>
<body>
<h1>게시글 목록</h1>
<table th:align="center" border="1" th:cellpadding="0" th:cellspacing="0" th:width="700">
<tr>
<th bgcolor="#e3e3e3" width="100">번호</th>
<th bgcolor="#e3e3e3" width="200">제목</th>
<th bgcolor="#e3e3e3" width="150">작성자</th>
<th bgcolor="#e3e3e3" width="150">등록일</th>
<th bgcolor="#e3e3e3" width="100">조회수</th>
</tr>
<tr th:each="board, state : ${boardList}"><!-- 현재 컬렉션의 상태정보를 저장하는 상태변수 state 선언 후 사용 -->
<td th:text="${state.count}"></td><!-- 1부터 자동으로 1씩 증가하는 값을 출력하는 count -->
<td ><a th:href="@{/getBoard(seq=${board.seq})}" th:text="${board.title}"></a></td>
<td th:text="${board.writer}"></td>
<td th:text="${#dates.format(board.createDate, 'yyyy-MM-dd')}"></td><!-- 날짜 형식 지정 -->
<td th:text="${board.cnt}"></td>
</tr>
</table>
<a th:href="@{/insertBoard}">글 등록</a>
</body>
</html>
3 사용자 인증과 예외처리
로그인과 권한 제어 기능을 구현하는 방법은 다양하다. 이번 실습에서는 세션(HttpSession)을 이용해서 간단하게 구현.
[로그인 인증 처리하기]
로그인 화면 개발
인덱스 페이지 추가 : 스프링 부트는 static 폴더에 작성된 index.html 파일을 무조건 웰컴 파일로 인식한다. 추가한 인덱스 페이지를 보기 위해서는 애플리케이션을 재실행 한다.
<html>
<head>
<title>메인 페이지</title>
<meta http-equiv="Content-Type" content="text/html; charset=UTF-8">
</head>
<body>
<h3 align="center">게시판 프로그램</h3>
<hr>
<p align="center"><a href="getBoardList">글 목록</a></p>
<p align="center"><a href="login">로그인</a></p>
</body>
</html>
로그인 컴포넌트 개발
① 로그인 컨트롤러
package com.studyboot.controller;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.GetMapping;
@Controller
public class LoginController {
@GetMapping("/login")
public void loginView() {
}
}
② 로그인 화면
login.html
<html xmlns:th="http://www.thymeleaf.org">
<head>
<title>타임리프</title>
<meta http-equiv="Content-Type" content="text/html; charset=UTF-8">
</head>
<body th:align="center">
<h1>로그인</h1>
<form th:action="login" method="post">
<table th:align="center" border="1" th:cellpadding="0" th:cellspacing="0">
<tr>
<td bgcolor="#e3e3e3" th:text="아이디"></td>
<td><input name="id" type="text" size="10"></td>
</tr>
<tr>
<td bgcolor="#e3e3e3" th:text="비밀번호"></td>
<td><input name="password" type="password" size="10"></td>
</tr>
<tr>
<td colspan="2" align="center">
<input type="submit" value="로그인">
</td>
</tr>
</table>
</form>
</body>
</html>
③ 로그인 컴포넌트 - 엔티티
회원 정보를 저장하는 테이블과 매핑되는 엔티티
package com.studyboot.domain;
import javax.persistence.Entity;
import javax.persistence.Id;
import lombok.Getter;
import lombok.Setter;
import lombok.ToString;
@Getter
@Setter
@ToString
@Entity
public class Member {
@Id
private String id;
private String password;
private String name;
private String role;
}
④ 로그인 컴포넌트 - repository
package com.studyboot.persistence;
import org.springframework.data.repository.CrudRepository;
import com.studyboot.domain.Member;
public interface MemberRepository extends CrudRepository<Member, String>{
}
⑤ 테스트 데이터 입력
package com.studyboot;
import java.util.Date;
import org.junit.jupiter.api.Test;
import org.junit.runner.RunWith;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.test.context.junit4.SpringRunner;
import com.studyboot.domain.Board;
import com.studyboot.domain.Member;
import com.studyboot.persistence.BoardRepository;
import com.studyboot.persistence.MemberRepository;
@RunWith(SpringRunner.class)
@SpringBootTest
public class DataInitializeTest {
@Autowired
private MemberRepository memberRepo;
@Autowired
private BoardRepository boardRepo;
@Test
public void TestDataInsert() {
Member mem1 = new Member();
mem1.setId("mem1");
mem1.setName("hello");
mem1.setPassword("123");
mem1.setRole("user");
memberRepo.save(mem1); //회원정보를 먼저 영속성 컨텍스트에 저장
Member mem2 = new Member();
mem2.setId("mem2");
mem2.setName("bye");
mem2.setPassword("456");
mem2.setRole("admin");
memberRepo.save(mem2);
for (int i=1; i<=3; i++) {
Board b = new Board();
b.setWriter("hello");
b.setTitle("hello가 작성한 글" + i);
b.setContent("내용"+i);
boardRepo.save(b); //게시글 엔티티 저장
}
for (int i=1; i<=3; i++) {
Board b = new Board();
b.setWriter("bye");
b.setTitle("bye가 작성한 글" + i);
b.setContent("내용"+i);
boardRepo.save(b);
}
}
}
⑥ 서비스 구현 클래스, 서비스 인터페이스
package com.studyboot.service;
import java.util.Optional;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import com.studyboot.domain.Member;
import com.studyboot.persistence.MemberRepository;
@Service
public class MemberServieImpl implements MemberService {
@Autowired
private MemberRepository memberRepo;
@Override
public Member getMember(Member member) {
Optional<Member> findMember = memberRepo.findById(member.getId()); // 특정 회원 검색
if(findMember.isPresent()) {
return findMember.get();
} else {
return null;
}
}
}
컨트롤러에서 비즈니스 컴포넌트 사용하기
① 로그인 인증 구현
@SessionAttributes : 세션에 상태 정보를 저장할 때 사용
@SessionAttributes("member") // 세션에 상태정보를 저장할 때 사용
@Controller
public class LoginController {
@Autowired
private MemberService memberService;
@GetMapping("/login")
public void loginView() {
}
@PostMapping("/login")
public String login(Member member, Model model) {
Member findMember = memberService.getMember(member);
// 로그인 시도 회원정보 일치에 따라 분기처리
if(findMember != null && findMember.getPassword().equals(member.getPassword())) {
model.addAttribute("member", findMember);
return "forward:getBoardList"; // 리스트 페이지 포워딩
} else {
return "redirect:login"; // 로그인 페이지 리다이렉트
}
}
}
② 로그인 상태 정보 이용
SpringEL : 스프링에서 제공하는 표현 언어.
getBoardList.html
<!-- 세션에 member라는 이름으로 저장된 Member의 name변수에 접근 -->
<h3><font color="orange" th:text="${session['member'].name}"></font>님 안녕하세요?!</h3>
<!-- 세션에 저장된 member 객체의 role 변수도 확인하여 if문 추가 -->
<a th:if="${session['member'].role == 'admin'}"
th:href="@{/deleteBoard(seq=${board.seq })}">글 삭제</a>
인증 상태 유지하기
@SessionAttributes("member") //로그인 사용자만 게시판 기능을 사용하도록 인증상태 유지를 위해 세션 활용
@Controller
public class BoardController {
@Autowired
private BoardService boardService;
@GetMapping("/hello")
public void hello(Model model) {
model.addAttribute("greeting","Hello 타임리프");
}
@ModelAttribute("member") // 이 어노테이션을 통해 세션에 이 메소드가 반환하는 Member객체가 제일 먼저 세션에 등록된다.
public Member setMember() {
return new Member();
}
@RequestMapping("/getBoardList")
public String getBoardList(@ModelAttribute("member") Member member, Model model, Board board) {
if(member.getId() == null) {
return "redirect:login";//로그인 아이디가 없을 경우 로그인 페이지로 리다이렉트
}
List<Board> boardList = boardService.getBoardLists(board);//게시글 목록 가져오기
model.addAttribute("boardList", boardList);
return "getBoardList";
}
@GetMapping("/insertBoard")
public String insertBoardView(Board board) {
return "insertBoard"; //게시글 작성 페이지
}
@PostMapping("/insertBoard")
public String insertBoard(@ModelAttribute("member") Member member, Board board) {
if(member.getId() == null) {
return "redirect:login";//로그인 아이디가 없을 경우 로그인 페이지로 리다이렉트
}
boardService.insertBoard(board);
return "redirect:getBoardList";//게시글 작성 후 리스트 페이지로 리다이렉트
}
@GetMapping("/getBoard")
public String getBoard(@ModelAttribute("member") Member member, Board board, Model model) {
if(member.getId() == null) {
return "redirect:login";//로그인 아이디가 없을 경우 로그인 페이지로 리다이렉트
}
model.addAttribute("board", boardService.getBoard(board));
return "getBoard"; //게시글 상세 페이지
}
@PostMapping("/updateBoard")
public String updateBoard(@ModelAttribute("member") Member member, Board board) {
if(member.getId() == null) {
return "redirect:login";//로그인 아이디가 없을 경우 로그인 페이지로 리다이렉트
}
boardService.updateBoard(board);
return "forward:getBoardList";//게시글 수정 후 포워딩
}
@GetMapping("/deleteBoard")
public String deleteBoard(@ModelAttribute("member") Member member, Board board) {
if(member.getId() == null) {
return "redirect:login";//로그인 아이디가 없을 경우 로그인 페이지로 리다이렉트
}
boardService.deleteBoard(board);
return "forward:getBoardList";//게시글 삭제 후 포워딩
}
}
로그아웃 처리
사용자가 로그아웃을 링크 클릭시, 현재 브라우저와 매핑된 세션을 강제로 종료하고 인덱스 페이지로 이동하는 것으로 구현하면 된다.
getBoardList.html
<a th:href="@{/logout}">LOGOUT</a>
LoginController.class
@GetMapping("/logout")
public String logout(SessionStatus status) {
status.setComplete();
return "redirect:index.html";
}
[예외 처리]
사용자가 시스템을 사용하다가 문제가 발생해도 이에 대한 처리를 하지 않았다. 대부분의 웹 애플리케이션은 사용자의 부주의나 시스템 운영 과정에서 발생된 문제에 대해 적절한 처리와 함께 관련 화면을 사용자에게 제공.
사용자 정의 예외 발생시키기
① 예외의 개념
자바는 시스템에서 발생되는 문제를 시스템에러와 예외로 구분한다. 시스템에러는 개발자가 제어할 수 없는 문제이므로 제외하고 관리할 수 있는 예외에 집중한다. 일반적인 자바 애플리케이션이라면 try~catch-finally 구문을 통해 예외를 처리할 수 있다. 스프링 기반의 웹애플리케이션은 스프링에서 지원하는 예외 처리 기법을 이용한다.
@ControllerAdvice : 모든 컨트롤러에서 발생하는 예외를 일괄적으로 처리하는 것. 전역 예외처리.
@ExceptionHandler : 각 컨트롤러마다 발생하는 예외를 개별적으로 처리하는 것. 로컬 예외처리.
② 사용자 정의 예외
자바의 예외는 Checked Exception, Unchecked Exception으로으로 구분된다.
Checked Exception : 컴파일 시점에서 발생하는 예외
Unchecked Exception : 컴파일은 통과하지만 실행시점에 발생하는 예외.
게시판 프로그램을 실행시에 발생할 수 있는 모든 예외의 최상위 부모로 사용할 클래스를 RuntimeExceiption을 상속해서 구현
package com.studyboot.exception;
//실행시에 발생할 수 있는 모든 예외의 최상위 부모로 사용하기 위해 상속하여 반영
public class BoardException extends RuntimeException{
private static final long serialVersionUID = 1L;
public BoardException(String message) {
super(message);
}
}
package com.studyboot.exception;
//Board 엔티티가 없을 경우 발생하는 예외 클래스
public class BoardNotFoundException extends BoardException{
private static final long serialVersionUID = 1L;
public BoardNotFoundException(String message) {
super(message);
}
}
③ 예외 발생
테스트를 위해서 예외를 발생시킬 링크 추가.
index.html
<html>
<head>
<title>메인 페이지</title>
<meta http-equiv="Content-Type" content="text/html; charset=UTF-8">
</head>
<body>
<h3 align="center">게시판 프로그램</h3>
<hr>
<p align="center"><a href="getBoardList">글 목록</a></p>
<p align="center"><a href="login">로그인</a></p>
<br>
<p align="center">
<a href="boardError">BoardException 발생</a>
<a href="illefalArgumentError">illefalArgumentError</a>
<a href="sqlError">SQLException 발생</a>
</p>
</body>
</html>
④ ExceptionController 작성 - 예외 요청이 들어왓을때 해당 예외를 강제로 발생
package com.studyboot.controller;
import java.sql.SQLException;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.RequestMapping;
import com.studyboot.exception.BoardException;
@Controller
public class ExceptionController {
@RequestMapping("/boardError")
public String boardError() {
throw new BoardException("검색된 게시글이 없습니다.");
}
@RequestMapping("/illegalArgumentError")
public String illegalArgumentError() {
throw new IllegalArgumentException("부적절한 인자가 전달되었습니다.");
}
@RequestMapping("/sqlError")
public String sqlError() throws SQLException {
throw new SQLException("SQL 구문에 오류가 있습니다.");
}
}
예외 처리하기
① 예외 처리기 작성
발생되는 모든 예외에 대해서 적잘한 예외화면으로 연결해주는 전역 예외처리기 생성 - GlobalExceptionHandler.class
package com.studyboot.exception;
import org.springframework.ui.Model;
import org.springframework.web.bind.annotation.ControllerAdvice;
import org.springframework.web.bind.annotation.ExceptionHandler;
@ControllerAdvice //컨트롤러에서 발생하는 모든 예외를 GlobalExceptionHandler 객체가 처리.
public class GlobalExceptionHandler {
@ExceptionHandler(BoardException.class) // 발생한 예외에 따라 다른 화면을 출력하기 위해서 사용
public String handleCustomException(BoardException exception, Model model) {
model.addAttribute("exception", exception);
return "/errors/boardError";
}
@ExceptionHandler(Exception.class) // 모든 예외의 최상위 부모인 Exception 타입의 객체를 처리
public String handleException(Exception exception, Model model) {
model.addAttribute("exception", exception);
return "/errors/globalError";
}
}
② 예외 전용 페이지 작성
src/main/resources/templates/error 폴더에 작성
발생된 예외의 타입에 따라 다른 화면을 서비스 할 수 있다.
boardError.html
<!DOCTYPE html>
<html xmlns:th="http://www.thymeleaf.org">
<head>
<meta http-equiv="Content-Type" content="text/html; charset=UTF-8">
</head>
<body>
<h1><font color="green">BoardException 발생</font></h1>
<a th:href="@{/}">메인 화면으로</a><hr>
<table th:align="center" th:cellpadding="0" th:cellspacing="0">
<tr>
<td bgcolor="#e3e3e3" th:align="left">
예외 메시지 : [[${exception.message}]]
</td>
</tr>
<tr th:each="trace : ${exception.stackTrace}">
<td th:text="${trace}"></td>
</tr>
</table>
</body>
</html>
globalError.html
<!DOCTYPE html>
<html xmlns:th="http://www.thymeleaf.org">
<head>
<meta http-equiv="Content-Type" content="text/html; charset=UTF-8">
</head>
<body>
<h1><font color="orange">Exception 발생</font></h1>
<a th:href="@{/}">메인 화면으로</a><hr>
<table th:align="center" th:cellpadding="0" th:cellspacing="0">
<tr>
<td bgcolor="#e3e3e3" th:align="left">
예외 메시지 : [[${exception.message}]]
</td>
</tr>
<tr th:each="trace : ${exception.stackTrace}">
<td th:text="${trace}"></td>
</tr>
</table>
</body>
</html>
예외 처리 재정의하기
① 컨트롤러마다 별도의 예외 처리 로직을 구현하고 싶으면 컨트롤러에 @ExceptionHandler 를 붙인 메소트 추가 생성
@Controller
public class ExceptionController {
//생략
//컨트롤러마다 별도의 예외처리 로직을 구현하고 싶을 때 @ExceptionHandler 사용
@ExceptionHandler(SQLException.class)
public String numberFormatError(SQLException exception, Model model) {
model.addAttribute("exception", exception);
return "/errors/sqlError";
}
}
② 해당 Exception클래스에 해당하는 별도의 예외 화면을 파일로 생성
<!DOCTYPE html>
<html xmlns:th="http://www.thymeleaf.org">
<head>
<meta http-equiv="Content-Type" content="text/html; charset=UTF-8">
</head>
<body>
<h1><font color="red">SQLException 발생</font></h1>
<a th:href="@{/}">메인 화면으로</a><hr>
<table th:align="center" th:cellpadding="0" th:cellspacing="0">
<tr>
<td bgcolor="#e3e3e3" th:align="left">
예외 메시지 : [[${exception.message}]]
</td>
</tr>
<tr th:each="trace : ${exception.stackTrace}">
<td th:text="${trace}"></td>
</tr>
</table>
</body>
</html>
정리
-
템플릿 엔진을 이용한 화면개발 기본문법은 JSP에서의 EL , JSTL과유사하기 때문에 쉽게 사용 가능.
-
시스템에서 발생하는 예외 유형별로 사용자에게 적절한 화면을 제공하는 기능.
참고 서적 : 누구나 끝까지 따라 할 수 있는 스프링 부트 퀵스타트
'Dev > SpringBoot' 카테고리의 다른 글
[SpringBoot] 웹 애플리케이션 통합 (0) | 2020.01.04 |
---|---|
[SpringBoot] 스프링 부트 시큐리티 (0) | 2020.01.02 |
[SpringBoot] 스프링 데이터 JPA (0) | 2019.12.28 |
[SpringBoot] JPA 퀵 스타트 (0) | 2019.12.23 |
[SpringBoot] 테스트와 로깅 (0) | 2019.12.18 |