Dev/SpringBoot

[Springboot] 스프링부트 화면 개발

창문닦이 2020. 1. 1. 18:32

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";
	}
}

DB 연동하여 출력된 페이지


[게시판 구현하기 - 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);//영속화
}

입력 페이지에 데이터를 반영
성공적으로 DB에 입력되어 리스트 출력

게시글 상세 조회

게시글 상세 조회

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>&nbsp;&nbsp;&nbsp;&nbsp;
<a href="deleteBoard?seq=${board.seq }">글 삭제</a>&nbsp;&nbsp;&nbsp;&nbsp;
<a href="getBoardList">리스트</a>&nbsp;&nbsp;&nbsp;&nbsp;
</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>&nbsp;&nbsp;&nbsp;&nbsp;
</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>&nbsp;&nbsp;&nbsp;&nbsp;
</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>&nbsp;&nbsp;&nbsp;&nbsp;
<a th:href="@{/deleteBoard(seq=${board.seq })}">글 삭제</a>&nbsp;&nbsp;&nbsp;&nbsp; 
<a th:href="@{/getBoardList}">리스트</a>&nbsp;&nbsp;&nbsp;&nbsp;
</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>&nbsp;&nbsp;&nbsp;&nbsp;
<a th:href="@{/deleteBoard(seq=${board.seq })}">글 삭제</a>&nbsp;&nbsp;&nbsp;&nbsp; 
<a th:href="@{/getBoardList}">리스트</a>&nbsp;&nbsp;&nbsp;&nbsp;
</body>
</html>

수정 완료된 후 리스트 페이지로 포워딩 

게시글 삭제

해당 하이퍼링크 클릭 시 게시글이 삭제 처리된다.

getBoard.html

<a th:href="@{/deleteBoard(seq=${board.seq })}">글 삭제</a>&nbsp;&nbsp;&nbsp;&nbsp; 

 

상태 변수와 날짜 포맷 지정하기

타임리프의 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>&nbsp;&nbsp;&nbsp;&nbsp;
</body>
</html>

순차적으로 번호가 붙여져서 출력되고 있다.
중간에 삭제한 게시글로 인해 seq가 연속적이지 않아도 문제 없이 출력


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의 정보를 세션을 통해 출력

<!-- 세션에 저장된 member 객체의 role 변수도 확인하여 if문 추가 -->
<a th:if="${session['member'].role == 'admin'}"
	th:href="@{/deleteBoard(seq=${board.seq })}">글 삭제</a>&nbsp;&nbsp;&nbsp;&nbsp; 

admin 계정으로 로그인시 
user 계정으로 로그인 시

인증 상태 유지하기

@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과유사하기 때문에 쉽게 사용 가능.

  • 시스템에서 발생하는 예외 유형별로 사용자에게 적절한 화면을 제공하는 기능.

참고 서적 : 누구나 끝까지 따라 할 수 있는 스프링 부트 퀵스타트