Dev/SpringBoot

[SpringBoot] 웹 애플리케이션 통합

창문닦이 2020. 1. 4. 19:09

1 비즈니스 레이어 개발

프레젠테이션 레이어(MVC) : 애플리케이션 사용자와의 커뮤니케이션 담당.

비즈니스 레이어(AOP, IoC) : 비즈니스 로직 처리.

 

[비즈니스 컴포넌트 구조 이해하기]

프로젝트 생성 설정

pom.xml
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
	xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
	<modelVersion>4.0.0</modelVersion>
	<parent>
		<groupId>org.springframework.boot</groupId>
		<artifactId>spring-boot-starter-parent</artifactId>
		<version>2.2.2.RELEASE</version>
		<relativePath/> <!-- lookup parent from repository -->
	</parent>
	<groupId>com.studyboot</groupId>
	<artifactId>Chapter08</artifactId>
	<version>0.0.1-SNAPSHOT</version>
	<name>Chapter08</name>
	<description>Demo project for Spring Boot</description>

	<properties>
		<java.version>1.8</java.version>
		<maven-jar-plugin.version>3.1.1</maven-jar-plugin.version>
	</properties>

	<dependencies>
		<dependency>
			<groupId>junit</groupId>
			<artifactId>junit</artifactId>
			<scope>test</scope>
	    </dependency>
		<dependency>
			<groupId>org.springframework.boot</groupId>
			<artifactId>spring-boot-starter-data-jpa</artifactId>
		</dependency>
		<dependency>
			<groupId>org.springframework.boot</groupId>
			<artifactId>spring-boot-starter-thymeleaf</artifactId>
		</dependency>
		<dependency>
			<groupId>org.springframework.boot</groupId>
			<artifactId>spring-boot-starter-web</artifactId>
		</dependency>

		<dependency>
			<groupId>org.springframework.boot</groupId>
			<artifactId>spring-boot-devtools</artifactId>
			<scope>runtime</scope>
			<optional>true</optional>
		</dependency>
		<dependency>
			<groupId>com.h2database</groupId>
			<artifactId>h2</artifactId>
			<scope>runtime</scope>
		</dependency>
		<dependency>
			<groupId>org.projectlombok</groupId>
			<artifactId>lombok</artifactId>
			<optional>true</optional>
		</dependency>
		<dependency>
			<groupId>org.springframework.boot</groupId>
			<artifactId>spring-boot-starter-test</artifactId>
			<scope>test</scope>
			<exclusions>
				<exclusion>
					<groupId>org.junit.vintage</groupId>
					<artifactId>junit-vintage-engine</artifactId>
				</exclusion>
			</exclusions>
		</dependency>
	</dependencies>

	<build>
		<plugins>
			<plugin>
				<groupId>org.springframework.boot</groupId>
				<artifactId>spring-boot-maven-plugin</artifactId>
			</plugin>
		</plugins>
	</build>

</project>
application.properties
############################################
###            Business Layer            ###
############################################
# Webapplication Type Setting
spring.main.web-application-type=none

# Banner Setting
spring.main.banner-mode=off

# DatsSource Setting
spring.datasource.driver-class-name=org.h2.Driver
spring.datasource.url=jdbc:h2:tcp://localhost/~/test
spring.datasource.username=sa
spring.datasource.password=

# JPA Setting
spring.jpa.hibernate.ddl-auto=update
spring.jpa.generate-ddl=false
spring.jpa.show-sql=true
spring.jpa.database-platform=org.hibernate.dialect.H2Dialect

# JPA 구현체로 사용할 하이버네이트가 생성한 sql을 콘솔에 출력할지 여부
spring.jpa.properties.hibernate.format_sql=true

# Logging Setting
logging.level.org.hibernate=info

 

비즈니스 컴포넌트 구조

비즈니스 로직을 처리할 비즈니스 컴포넌트는 엔티티, 리포지터리, 서비스 인터페이스, 서비스 구현 클래스로 구성.

① 엔티티 : 테이블과 매핑되는 엔티티 클래스

② 리포지터리 : 엔티티를 이용하여 실질적인 CRUD 처리하는 인터페이스

③ 서비스 인터페이스, ④ 서비스 구현 클래스  : 리포지터리를 통해 데이터베이스 연동을 포함한 비즈니스 로직을 처리.

비즈니스 컴포넌트에서 인터페이스를 작성하지 않고 ServiceImpl 클래스만 사용하는 경우

  • 유지보수 과정에서 서비스 구현 클래스를 다른 클래스로 변경하지 않겠다는 것을 의미.

  • 인터페이스 미작성 , 서비스 구현 클래스를 다른 클래스로 변경할 때 마다때마다 비즈니스 컴포넌트를 사용하는 모든 컨트롤러를 수정해야

  • 컴포넌트를 유연하게 변경하기 위해 반드시 인터페이스를 작성하자.

[비즈니스 컴포넌트 개발하기]

엔티티 클래스 만들기

Board.class

package com.studyboot.domain;

import java.util.Date;

import javax.persistence.Column;
import javax.persistence.Entity;
import javax.persistence.GeneratedValue;
import javax.persistence.Id;
import javax.persistence.JoinColumn;
import javax.persistence.ManyToOne;
import javax.persistence.Temporal;
import javax.persistence.TemporalType;

import lombok.Getter;
import lombok.Setter;
import lombok.ToString;

@Getter
@Setter
@ToString(exclude = "member")//순환 참조를 막기 위함
@Entity
public class Board {//게시판 테이블과 매핑할 변수들을 멤버로 선언
	@Id
	@GeneratedValue
	private Long seq;
	private String title;
	private String content;
	
	//기본값 가지도록 설정.
	//JPA가 수정 처리할 때 JPA의 구현체인 하이버네이트가 수정 SQL에 해당칼럼을 포함하지 않도록 설정
	@Temporal(TemporalType.TIMESTAMP)
	@Column(updatable = false)
	private Date createDate = new Date();
	@Column(updatable = false)
	private Long cnt = 0L;
	
	//다대일 연관관계 매핑
	@ManyToOne  //양방향 연관관계에서 다(N)에 해당 & 외래키(FK)를 소유한 Board 엔티티가 연관관계의 주인.
	@JoinColumn(name = "MEMBER_ID"  //MEMBER_ID 칼럼을 통해서 외래키를 관리
			,nullable = false //외부조인이 아닌 내부조인으로 처리하기 위해 nullable 속성 사용
			,updatable = false)
	private Member member;
	
	public void setMember(Member member) {
		this.member = member;
		member.getBoardList().add(this);
	}
}

Member.class

package com.studyboot.domain;

import java.util.ArrayList;
import java.util.List;

import javax.persistence.Column;
import javax.persistence.Entity;
import javax.persistence.EnumType;
import javax.persistence.Enumerated;
import javax.persistence.FetchType;
import javax.persistence.Id;
import javax.persistence.OneToMany;

import lombok.Getter;
import lombok.Setter;
import lombok.ToString;

@Getter
@Setter
@ToString(exclude = "boardList")
@Entity
public class Member {
	@Id
	@Column(name = "MEMBER_ID")
	private String id;
	private String password;
	private String name;
	@Enumerated(EnumType.STRING) //권한 정보를 문자열로 저장
	private Role role;
	private boolean enabled; // 사용자 사용여부 제어하는 변수
	
	//일대다 관계 매핑. Member가 일에 해당하므로 컬렉션 타입으로 해당하는 객체를 가지고 있어야 함.
	@OneToMany(mappedBy = "member",	 // member가 연관관계의 주인이 아님을 표시
			fetch = FetchType.EAGER) // 즉시 로딩
	private List<Board> boardList = new ArrayList<Board>();
}

Role

package com.studyboot.domain;

public enum Role {
	//회원의 권한 enum 클래스
	ROLE_MEMBER, ROLE_ADMIN
}

 

리포지터리 인터페이스 작성하기

MemberRepository

package com.studyboot.persistence;

import org.springframework.data.repository.CrudRepository;

import com.studyboot.domain.Member;

public interface MemberRepository extends CrudRepository<Member, String>{

}

BoardRepository

package com.studyboot.persistence;

import org.springframework.data.domain.Page;
import org.springframework.data.domain.Pageable;
import org.springframework.data.jpa.repository.Query;
import org.springframework.data.repository.CrudRepository;

import com.studyboot.domain.Board;

public interface BoardRepository extends CrudRepository<Board, Long>{
	//글 목록 검색
	@Query("SELECT b FROM Board b")
	public Page<Board> getBoardList(Pageable pageable); // 페이징 처리를 위해 Pageable 매개변수 사용
}

 

JPA 테스트하기

BoardRepositoryTest

@RunWith(SpringRunner.class)
@SpringBootTest
public class BoardRepositoryTest {

	@Autowired
	private BoardRepository boardRepo;

	@Autowired
	private MemberRepository memberRepo;
	
	@Test //입력테스트
	public void testInsert() {
		
		Member mem1 = new Member();
		mem1.setId("mem1");
		mem1.setName("hello");
		mem1.setPassword("123");
		mem1.setRole(Role.ROLE_MEMBER);
		memberRepo.save(mem1); //회원정보를 먼저 영속성 컨텍스트에 저장
		
		Member mem2 = new Member();
		mem2.setId("mem2");
		mem2.setName("bye");
		mem2.setPassword("456");
		mem2.setRole(Role.ROLE_ADMIN);
		memberRepo.save(mem2);
		
		for (int i=1; i<=13; i++) {
			Board b = new Board();
			b.setMember(mem1);
			b.setTitle("hello가 작성한 글" + i);
			b.setContent("내용"+i);
			boardRepo.save(b); //게시글 엔티티 저장
		}
		
		for (int i=1; i<=3; i++) {
			Board b = new Board();
			b.setMember(mem2);
			b.setTitle("bye가 작성한 글" + i);
			b.setContent("내용"+i);
			boardRepo.save(b);
		}
	}
	
	@Test //상세 조회 테스트
	public void testGetBoard() {
		Board board = boardRepo.findById(1L).get();
		
		System.out.println("[ "+board.getSeq() + "번 게시글 ]");
		System.out.println("제목\t :"+ board.getTitle());
		System.out.println("작성자\t :"+ board.getMember().getName());
		System.out.println("내용\t :" + board.getContent());
		System.out.println("등록일\t :"+ board.getCreateDate());
		System.out.println("조회수\t :"+ board.getCnt());
	}
	
	@Test //목록 조회 테스트
	public void testGetBoardList() {
		Member member = memberRepo.findById("mem1").get();

		System.out.println("[ "+member.getName() + "의 게시글 ]");
		for(Board b : member.getBoardList()){
			System.out.println(">>>>>" + b.toString());
		}
	}
}

 

서비스 인터페이스와 클래스 만들기

BoardService

클라이언트에 제공할 서비스 인터페이스 작성.

컨트롤러가 비즈니스 컴포넌트를 사용할 때는 반드시 컴포넌트가 제공하는 인터페이스를 사용해야만 한다.

package com.studyboot.service;

import org.springframework.data.domain.Page;

import com.studyboot.domain.Board;

public interface BoardService {
	public void insertBoard(Board board);
	public void updateBoard(Board board);
	public void deleteBoard(Board board);
	public Board getBoard(Board board);
	public Page<Board> getBoardList(Board board);
}

BoardServiceImpl 클래스는 BoardRepository 이용하여 실질적인 데이터베이스 연동을 처리한다.

package com.studyboot.service;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.domain.Page;
import org.springframework.data.domain.PageRequest;
import org.springframework.data.domain.Pageable;
import org.springframework.data.domain.Sort;

import com.studyboot.domain.Board;
import com.studyboot.persistence.BoardRepository;

public class BoardServiceImpl implements BoardService{

	@Autowired
	private BoardRepository boardRepo;
	
	public void insertBoard(Board board) {
		boardRepo.save(board);
	}

	public void updateBoard(Board board) {
		Board findBoard = boardRepo.findById(board.getSeq()).get();
		findBoard.setTitle(board.getTitle());
		findBoard.setContent(board.getContent());
		boardRepo.save(findBoard);
	}

	public void deleteBoard(Board board) {
		boardRepo.deleteById(board.getSeq());
	}

	public Board getBoard(Board board) {
		return boardRepo.findById(board.getSeq()).get();
	}

	public Page<Board> getBoardList(Board board) {
		Pageable pageable = PageRequest.of(0, 10, Sort.Direction.DESC, "seq");
		return boardRepo.getBoardList(pageable);
	}

}

2 프레젠테이션 레이어 개발

[프레젠테이션 개발 준비하기]

프레젠테이션 설정 기본 화면 만들기

application.properties
############################################
###			Presentation Layer		###
############################################
# Thymeleaf Cache Setting
spring.thymeleaf.cache=false

# Security log level Setting
logging.level.org.springframework.web=debug
logging.level.org.springframwork.security=debug
pom.xml
	<dependencies>
    <!-- 중간 생략 -->
		<dependency>
			<groupId>org.springframework.boot</groupId>
			<artifactId>spring-boot-starter-security</artifactId>
		</dependency>
		<dependency>
			<groupId>org.springframework.security</groupId>
			<artifactId>spring-security-test</artifactId>
			<scope>test</scope>
		</dependency>
		<!-- https://mvnrepository.com/artifact/org.thymeleaf.extras/thymeleaf-extras-springsecurity5 -->
		<dependency>
		    <groupId>org.thymeleaf.extras</groupId>
		    <artifactId>thymeleaf-extras-springsecurity5</artifactId>
		</dependency>
		<!-- https://mvnrepository.com/artifact/com.querydsl/querydsl-jpa -->
		<dependency>
		    <groupId>com.querydsl</groupId>
		    <artifactId>querydsl-jpa</artifactId>
		</dependency>
		<!-- https://mvnrepository.com/artifact/com.querydsl/querydsl-apt -->
		<dependency>
		    <groupId>com.querydsl</groupId>
		    <artifactId>querydsl-apt</artifactId>
		</dependency>
	</dependencies>

	<build>
		<plugins>
			<plugin>
				<groupId>org.springframework.boot</groupId>
				<artifactId>spring-boot-maven-plugin</artifactId>
			</plugin>
			<plugin>
				<groupId>com.mysema.maven</groupId>
				<artifactId>apt-maven-plugin</artifactId>
				<version>1.1.3</version>
				<executions>
					<execution>
						<goals>
							<goal>process</goal>
						</goals>
						<configuration>
							<outputDirectory>src/main/querydsl</outputDirectory>
							<processor>com.querydsl.apt.jpa.JPAAnnotationProcessor</processor>
						</configuration>
					</execution>
				</executions>
			</plugin>
		</plugins>
	</build>

</project>

기본 화면으로 사용할 index 페이지를 생성한다. 시스템에 접근한 사용자에게 가장 먼저 제공하는 메인 페이지 개념.

index.html
<!DOCTYPE html>
<html>
<head>
<meta http-equiv="Content-Type" content="text/html; charset=UTF-8">
<title>메인페이지</title>
</head>
<body>
<br>
<h3>게시판 프로그램</h3>
<hr>
<p align="center"><a href="board/getBoardList">글 목록 바로가기</a></p>
<p align="center"><a href="system/login">로그인</a></p>
<br>
<hr>
</body>
</html>

[게시판 기능 구현하기]

컨트롤러 작성

@Controller
@RequestMapping("/board/")
public class BoardController {
	
	@Autowired
	private BoardService boardService;
	
	@RequestMapping("/getBoardList")
	public String getBoardList(Model model, Search search ) {
		if (search.getSearchCondition() == null) {
			search.setSearchCondition("TITLE");
		} 
		if (search.getSearchKeyword() == null) {
			search.setSearchKeyword("");
		}
		Page<Board> boardList = boardService.getBoardList(search);
		//Page<Board> boardList = boardService.getBoardList(board);
		model.addAttribute("boardList", boardList);
		return "board/getBoardList"; 
	}
	@RequestMapping("/getBoard")
	public String getBoard(Model model, Board board) {
		model.addAttribute("board", boardService.getBoard(board));
		return "board/getBoard";
	}
	@GetMapping("/insertBoard")
	public void insertBoardView(Board board
			, @AuthenticationPrincipal SecurityUser principal) {
		board.setMember(principal.getMember());
	}
	@PostMapping("/insertBoard")
	public String insertBoard(Board board
			, @AuthenticationPrincipal SecurityUser principal) {
		board.setMember(principal.getMember());
		boardService.insertBoard(board);
		return "redirect:getBoardList";
	}
	@RequestMapping("/updateBoard")
	public String updateBoard(Board board) {
		boardService.updateBoard(board);
		return "forward:getBoardList";
	}
	@RequestMapping("/deleteBoard")
	public String deleteBoard(Board board) {
		boardService.deleteBoard(board);
		return "forward:getBoardList";
	}
}

 목록 기능

xmlns:sec="http://www.thymeleaf.org/thymeleaf-extras-springsecurity5" 를 루트 엘리먼트에 네임스페이스를 추가하면 HTML에서  스프링 시큐리티를 이용할 수 있다.

getBoardList.html
<html xmlns:th="http://www.thymeleaf.org"
	xmlns:sec="http://www.thymeleaf.org/thymeleaf-extras-springsecurity5">
<head>
	<title>리스트 페이지</title>
	<meta http-equiv="Content-Type" content="text/html; charset=UTF-8">
</head>
<body th:align="center">
<h3>게시글 목록</h3>
<!-- 타임리프에서 로그인정보 이용 isAuthenticated() : 인증 성공 여부 체크-->
<span sec:authorize="isAuthenticated()">
	<b>
		<font color="red">
			<span sec:authentication="principal.member.name"></span>
		</font>
	</b>님 환영합니다...............................................
	<a th:href="@{/system/logout}">LOGOUT</a>&nbsp;&nbsp;
	<a th:href="@{/admin/adminPage}">게시판 관리</a>
</span>
<!-- 검색기능 -->
<form th:action="@{/board/getBoardList}" method="post" >
<table th:width="700" th:align="center">
	<tr>
		<td align="right">
			<select name="searchCondition">
				<option value="TITLE">제목</option>
				<option value="CONTENT">내용</option>
			</select>
			<input name="searchKeyword" type="text"/>
			<input type="submit" value="검색">
		</td>
	</tr>
</table>
</form>
<!-- 리스트 -->
<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="@{/board/getBoard(seq=${board.seq})}" th:text="${board.title}"></a></td>
		<td th:text="${board.member.name}"></td>
		<td th:text="${#dates.format(board.createDate, 'yyyy-MM-dd')}"></td><!-- 날짜 형식 지정 -->
		<td th:text="${board.cnt}"></td>
	</tr>
</table>
<a th:href="@{/board/insertBoard}">글 등록</a>&nbsp;&nbsp;&nbsp;&nbsp;
</body>
</html>

상세/수정/삭제 기능

getBoard.html
<html xmlns:th="http://www.thymeleaf.org"
	xmlns:sec="http://www.thymeleaf.org/thymeleaf-extras-springsecurity5">
<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="@{/board/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.member.name}"></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="@{/board/insertBoard}">글 등록</a>&nbsp;&nbsp;&nbsp;&nbsp;
<!-- 타임리프 로그인정보 이용, 어드인 권한 사용자만 삭제 링크 사용 -->
<a sec:authorize="hasRole('ROLE_ADMIN')" 
	th:href="@{/board/deleteBoard(seq=${board.seq })}">글 삭제</a>&nbsp;&nbsp;&nbsp;&nbsp; 
<a th:href="@{/board/getBoardList}">리스트</a>&nbsp;&nbsp;&nbsp;&nbsp;
</body>
</html>

글 등록 기능

insertBoard.html
<html xmlns:th="http://www.thymeleaf.org"
	xmlns:sec="http://www.thymeleaf.org/thymeleaf-extras-springsecurity5">
<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="@{/board/insertBoard}" method="post">
<table border="1" th:cellpadding="0" th:cellspacing="0" th:align="center">
	<tr>
		<td bgcolor="#e3e3e3" width="70" th:text="제목"></td>
		<td align="left"><input type="text" name="title"/></td>
	</tr>
	<tr>
		<td bgcolor="#e3e3e3" width="70" th:text="작성자"></td>
		<td align="left">
			<!-- 로그인한 사용자의 이름 출력 -->
			<span sec:authentication="principal.member.name"></span>
		</td>
	</tr>
	<tr>
		<td bgcolor="#e3e3e3" width="70" th:text="내용"></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>
<hr>
<a th:href="@{/board/getBoardList}">리스트</a>
</body>
</html>

[시큐리티 적용하기]

시큐리티 커스터마이징하기

① UserDetail 클래스 생성

package com.studyboot.domain;

import org.springframework.security.core.authority.AuthorityUtils;
import org.springframework.security.core.userdetails.User;

public class SecurityUser extends User{

	private static final long serialVersionUID = 1L;
	private Member member;//회원 객체를 멤버 변수인 member에 할당. 인증된 회원정보를 html에서 사용하기 위함
	
	public SecurityUser(Member member) {
		//JPA에서 검색한 회원정보로 부모 클래스의 변수들을 초기화 
		super(member.getId(), member.getPassword(), //암호화 진행안하려면 {"noop"}+member.getPassword(),
				AuthorityUtils.createAuthorityList(member.getRole().toString()));
		this.member = member;
	}
	
	public Member getMember() {
		return member;
	}
}

② UserDetailsService 클래스 - 검색한 회원 정보를 설정 

@Service
public class SecurityUserDetailService implements UserDetailsService{
	@Autowired
	private MemberRepository memberRepo;

	@Override
	public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
		Optional<Member> optional = memberRepo.findById(username); //사용자 정보 조회
		if(!optional.isPresent()) {
			throw new UsernameNotFoundException(username+" 사용자 없음");
		} else {
			Member member = optional.get();
			return new SecurityUser(member); //정보 있을 경우 SecurityUser객체로 리턴 
		}
	}
}

③ 시큐리티 설정 클래스 생성

@EnableWebSecurity //이 클래스로 생성된 객체는 시큐리티 설정 파일을 의미. 동시에 시큐리티에 필요한 객체들을 생성
public class SecurityConfig extends WebSecurityConfigurerAdapter{
	//WebSecurityConfigurerAdapter 클래스를 빈으로 설정하기만 해도 애플리케이션은 로그인을 강제하지 않음

	@Autowired
	private SecurityUserDetailService userDetailService;

	@Bean //비밀번호 암호화
	public PasswordEncoder passwordEncoder() {
		return PasswordEncoderFactories.createDelegatingPasswordEncoder();
	}
	
	@Override //시큐리티와 관련된 설정시 configure 메소드 사용
	protected void configure(HttpSecurity security) throws Exception {
		
		// 애플리케이션 자원에 대한 인증과 인가 제어 가능.
		// authorizeRequests 사용자 인증과 권한 설정 
		// antMatchers AuthorizedUrl 반환	
		security.authorizeRequests() 		// 빌더 패턴을 사용
					.antMatchers("/","/system/**").permitAll()
					.antMatchers("/board/**").authenticated()
					.antMatchers("/admin/**").hasRole("ADMIN");
		
		security.csrf().disable();
		
		security.formLogin(); // 사용자에게 form 태그 기반의 로그인 화면 표시 

		security.formLogin()
					.loginPage("/system/login") // 로그인시 사용할 화면 별도 지정. 
					.defaultSuccessUrl("/board/getBoardList", true); //로그인 성공시 이동할 url 지정
		
		security.exceptionHandling() //ExceptionHandlingConfigurer 객체 리턴
					.accessDeniedPage("/system/accessDenied"); // 인증되지 않은 사용자에게 제공할 url 지정
		
		security.logout().logoutUrl("/system/logout")
					.invalidateHttpSession(true)		// 현재 브라우저와 연관된 세션을 강제종료
					.deleteCookies() 					// 쿠키 삭제 
					.logoutSuccessUrl("/");				// 로그아웃 후 이동할 화면 리다이렉트
		
		security.userDetailsService(userDetailService);
	}
}

시큐리티 화면 개발

① 컨트롤러 작성

@Controller
public class SecurityController {
	
	@GetMapping("/system/login")
	public void login() {
	}
	@GetMapping("/system/accessDenied")
	public void accessDenied() {
	}
	@GetMapping("/system/logout")
	public void logout() {
	}
	@GetMapping("/admin/adminPage")
	public void adminPage() {
	}
}

② 로그인 페이지

<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">
<h3 align="center">로그인</h3>
<form method="post">
<table th:align="center" border="1" th:cellpadding="0" th:cellspacing="0">
	<tr>
		<td bgcolor="#e4e4e4">아이디</td>
		<td><input type="text" name="username"/></td>
	</tr>
	<tr>
		<td bgcolor="#e4e4e4">비밀번호</td>
		<td><input type="password" name="password"/></td>
	</tr>
	<tr>
		<td colspan="2" align="center"><input type="submit" value="로그인"/></td>
	</tr>
</table>
</form>
</body>
</html>

③ 접근 권한 없음 페이지

<html xmlns:th="http://www.thymeleaf.org">
<head>
	<title>접근권한 에러</title>
	<meta http-equiv="Content-Type" content="text/html; charset=UTF-8">
</head>
<body>
<h3 align="center">페이지에 대한 접근권한이 없습니다...</h3>
<h3 align="center">관리자로 다시 로그인하세요</h3>
<a th:href="@{/system/login}">다시 로그인 시도하기</a>
</body>
</html>

④ 관리자 페이지 

<html xmlns:th="http://www.thymeleaf.org">
<head>
	<title>admin page</title>
	<meta http-equiv="Content-Type" content="text/html; charset=UTF-8">
</head>
<body>
<h3 align="center">관리자 페이지 입니다.</h3>
<a th:href="@{/board/getBoardList}">게시판 목록으로 이동</a>
</body>
</html>

로그인 사용자 - 등록 기능 구현

@AuthenticationPrincipal가 붙어야 인증정보를 가지고 있는 SecurityUser 객체가 할당된다.

BoardController

	@PostMapping("/insertBoard")
	public String insertBoard(Board board, @AuthenticationPrincipal SecurityUser principal) {
		board.setMember(principal.getMember());
		boardService.insertBoard(board);
		return "redirect:getBoardList";
	}

[기타 기능 추가하기]

검색 기능 추가하기

동적 쿼리를 사용해야 하므로 QueryDSL를 활용한다. 

BoardController
	@RequestMapping("/getBoardList")
	public String getBoardList(Model model, Search search ) {
		if (search.getSearchCondition() == null) {
			search.setSearchCondition("TITLE");
		} 
		if (search.getSearchKeyword() == null) {
			search.setSearchKeyword("");
		}
		Page<Board> boardList = boardService.getBoardList(search);
		model.addAttribute("boardList", boardList);
		return "board/getBoardList"; 
	}

BoardRepository
public interface BoardRepository extends CrudRepository<Board, Long>
	,QuerydslPredicateExecutor<Board>{
	//글 목록 검색
	@Query("SELECT b FROM Board b")
	public Page<Board> getBoardList(Pageable pageable); // 페이징 처리를 위해 Pageable 매개변수 사용
}

BoardService
public interface BoardService {
	public void insertBoard(Board board);
	public void updateBoard(Board board);
	public void deleteBoard(Board board);
	public Board getBoard(Board board);
	//public Page<Board> getBoardList(Board board);
	public Page<Board> getBoardList(Search search);
}

BoardServiceImpl
	public Page<Board> getBoardList(Search search) {
		BooleanBuilder builder = new BooleanBuilder();
		QBoard qBoard = QBoard.board;
		if(search.getSearchCondition().equalsIgnoreCase("TITLE")) {
			builder.and(qBoard.title.like("%"+search.getSearchKeyword()+"%"));
		} else if(search.getSearchCondition().equalsIgnoreCase("CONTENT")) {
			builder.and(qBoard.content.like("%"+search.getSearchKeyword()+"%"));
		}
		Pageable pageable = PageRequest.of(0, 10, Sort.Direction.DESC, "seq");
		return boardRepo.findAll(builder, pageable);
	}

1장부터 8장까지 정독한 후 실제 예제 소스를 작성하여 진행해보았다. 스프링 시큐리티나 JPA는 공부해보고 싶었기에 맛보기로 하기에 좋은 책이었다. 스프링 부트에 대한 기본기를 어렵지 않게 잡게 해 준 책에 감사하다! 짬짬이 회사가 끝난 뒤 공부했는데 사이드 프로젝트를 해볼까.. 정보보안기사를 시작할까.. 공부할 게 많구먼

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