스프링 부트(Spring Boot)

JOY🌱·2023년 3월 6일
0

🍃 Spring

목록 보기
3/3
post-thumbnail

💁‍♀️ 스프링 부트(Spring Boot)란,
자바 기반의 오픈 소스 프레임워크인 스프링(Spring)의 하위 프로젝트로, 웹 애플리케이션 및 마이크로서비스를 쉽게 개발하고 설정할 수 있도록 도와주는 도구

📌 OGNL(Object Graph Navigation Language) 참고
Thymeleaf

👀 타임리프 (Thymeleaf)

💁‍♀️ 타임리프 (Thymeleaf)란,
스프링 부트에서 공식적으로 지원하는 뷰 템플릿으로 JSP와 달리 HTML 확장자를 가지고 있어 JSP처럼 Servlet이 문서를 표현하는 방식이 아니므로 서버없이 동작 가능

📌 기본 설정

◼ main/resources/application.yml

server:
  port: 8001

◼ main/java/com/greedy/thymeleaf/config/Chap01ThymeleafApplication.java

@SpringBootApplication
public class Chap01ThymeleafApplication {

	public static void main(String[] args) {
		SpringApplication.run(Chap01ThymeleafApplication.class, args);
	}
}

◼ main/java/com/greedy/thymeleaf/config/ContextConfiguration.java

/* Chap01ThymeleafApplication의 위치가 config 폴더 내에 존재하여 config 폴더 내에서만 ComponentScan이 되기 때문에
 * config의 상위폴더인 thymeleaf 안의 LectureController까지 ComponentScan하기 위해 이 Class 생성 */
@Configuration
@ComponentScan(basePackages="com.greedy.thymeleaf")
public class ContextConfiguration {

}

👉 표현식

◼ main/resources/static/index.html

<button onclick="location.href='/lecture/expression?title=표현식&no=5&no=6'">표현식</button>

◼ main/java/com/greedy/thymeleaf/controller/LectureController.java

@Controller
@RequestMapping("/lecture")
public class LectureController {

	@GetMapping("/expression")
	public String expression(Model model) {
		
		model.addAttribute("member", new MemberDTO("신짱구", 5, '남', "서울시 떡잎유치원"));
		model.addAttribute("hello", "hello!<h3>Thymeleaf🌿</h3>");
		
		
		return "lecture/expression"; 
		/* 디폴트 경로는 templates 폴더 
		 * 따라서 templates/lecture/expression를 의미 */
	}
}

◼ main/resources/messages.properties

message.first=hello spring boot
message.second=hello {0}

◼ main/resources/templates/lecture/expression.html

<html xmlns:th="http://www.thymeleaf.org">

<!DOCTYPE html>
<!-- 타임리프의 th 속성을 사용하기 위한 네임스페이스. html의 속성으로 작성 -->
<html xmlns:th="http://www.thymeleaf.org">
<head>
<meta charset="UTF-8">
<title>expression</title>
<style>
h2 { background : #fdefe6; }
h3 { background : #f5fee1; }
</style>
</head>
<body>

	<h1 align='center'>표현식</h1>

/* 블라블라 */ /*/ 블라블라 /*/

	<h2>주석</h2>
	<!-- 
		주석의 종류
		parser-level 주석
		: thymeleaf가 처리 될 때 제거되어 클라이언트에게 노출되지 않는 주석
		
		prototype-level 주석
		: thymeleaf 처리 후에 화면에 보여지게 되는 주석
	 -->
	 
	 <ul>
	 	<li>parser-level 주석</li>
	 	<!--/* 주석 내용 */-->
	 	<li>prototype-level 주석</li>
	 	<!--/*/ 주석 내용 /*/-->
	 </ul>

${...} th:text

	 <h2>표현식1 - 변수 표현식 ${...}</h2>
	 <!-- index.html에 쿼리스트링으로 작성한 값들 가져옴 -->
	 <p th:text="${ param.title }"></p>
	 <p th:text="${ param.no[0] }"></p>
	 <p th:text="${ param.no[1] }"></p>
	 <!--
	 	[ 브라우저 콘솔 출력문 ] 
	 	<p>표현식</p> 
	 	<p>5</p> 
	 	<p>6</p> 
	 -->
	 
	 <!-- 
	 	parameter로 넘어온 경우 => param
	 	session attribute인 경우 => session
	 	request attribute(model)인 경우는 따로 명시 X (디폴트라서 작성할 필요X). request라고 명시할 시, 오류 발생
	 	파라미터가 존재하지 않을 경우 항상 오류 발생
	  -->

#{...}

	  <h2>표현식2 - 메세지 표현식 #{...}</h2>
	  <!-- 
	  	resources/messages.properties 파일로부터 값을 읽어옴
	   -->
	   <p th:text="#{ message.first }"></p>
	   <p th:text="#{ message.second(Joy) }"></p>

@{...}

	   <h2>표현식3 - 링크 표현식 @{...}</h2>
	   <!-- 
	   	소괄호 안에만 표현하면 링크의 파라미터로 기능하며, 중괄호 안에 변수명을 작성해주면 path variable로 기능
	    -->
	    <a th:href="@{/}">메인으로</a>
	    <a th:href="@{/(name=${member.name},age=${member.age})}">Test1</a> <!-- ?name=신짱구&age=5 -->
	    <a th:href="@{/{name}/{age}(name=${member.name},age=${member.age})}">Test2</a> <!-- 신짱구/5 -->
	    <a th:href="@{/{name}(name=${member.name},age=${member.age})}">Test3</a> <!-- 신짱구?age=5 -->

*{...} th:object

	   <h2>표현식4 - 선택 변수 표현식 *{...}</h2>
	   <p th:text="${ member.name }"></p>
	   <p th:object="${ member }" th:text="*{ age }"></p>
	   <div th:object="${ member }">
	   		<p th:text="*{ name }"></p>
	   		<p th:text="*{ age }"></p>
	   		<p th:text="*{ gender }"></p>
	   </div>

th:text th:utext th:value

	   <h2>HTML 출력 th:text, th:utext, th:value</h2>
	   
	   <ul>
	   		<li th:text="${ hello }"></li>
	   		<li th:utext="${ hello }"></li>
	   		<li><input type="text" th:value="${ member.name }"></li>
	   </ul>

[[...]] [(...)] th:inline="text" th:inline="none" th:inline="javascript"

	   <h2>표현식5 - 인라인 표현식</h2>
	   <p th:inline="none"> <!-- 설명을 위한 글에 존재하는 [[]]와 [()]도 생략되어 브라우저에 나타나기 때문에 none 속성 부여 -->
	   		변수 표현식의 값을 html에 직접 표현하기위해서 th:text와 같은 [[...]]를 사용하고
	   		th:utext와 같은 [(...)]를 사용 가능
	   		대괄호로 묶어 이와 같이 변수 표현식의 값을 가져오는 것을 인라인 모드라고 하며 인라인 모드는 text모드와 자바스크립트 모드가 있음
	   		
	   		변수 표현식의 값을 html에서 사용하려고 할 때 th:inline="text"를 태그에 속성으로 주고 사용하는데 이는 기본 값.
	   		반면 인라인 모드를 적용하지 않으려면 th:inline="none"을 속성 값으로 주면 단순 문자열로 처리 되는 것을 볼 수 있음.
	   		자바스크립트에서 사용하려면 th:inline="javascript"를 태그에 속성 값으로 주는데 이 역시 기본 값.
	   </p>
	   
	   <ul>
	   		<li th:inline="text">[[${hello}]]</li> <!-- 대괄호 -->
	   		<li>[(${hello})]</li> <!-- 소괄호, th:inline="text"는 기본 값이기 때문에 생략해도 정상 동작, 주로 사용 !! -->
	   		<li th:inline="none">[[${hello}]]</li>
	   		<li th:inline="none">[(${hello})]</li>
	   </ul>
	   
	   <script th:inline="javascript">
	   	
	   		window.onload = function() {
	   			
	   			/* 동적 페이지에서는 정상 동작 하지만 정적 페이지에서는 자바스크립트 문법 오류 발생 */
	   			// const hello = [[${hello}]];
	   			
	   			/* 정적 페이지에서는 정상 동작 하지만 동적 페이지에서는 자바스크립트 오류 발생 */
	   			// const hello = "[[${hello}]]"
	   			
	   			const hello = '[[${hello}]]';
	   			
	   			alert(hello);
	   		}
	   
	   </script>

||

	   <h2>리터럴 치환 ||</h2>
	   <h3>'+'를 쓰지 않고 문자열 합치기</h3>
	   <p th:object="${ member }" th:text="|name = *{ name }|"></p>
	   <p th:object="${ member }" th:text="|age = *{ age }|"></p>
	   <p th:object="${ member }" th:text="|gender = *{ gender }|"></p>
	   <p th:object="${ member }" th:text="|address = *{ address }|"></p>

th:block

	   <h2>th:block</h2>
	   <h3>범위를 지정하고 싶을 때 사용. th:block을 통해 해당 범위에 변수나 객체를 적용하거나
	   조건에 해당되는지에 따라 해당 범위를 보여주거나 보여주지 않을 때 사용 가능</h3>
	   <th:block th:object="${ member }">
	   		<p th:text="*{ age }"></p>
	   </th:block>
	   <!-- div를 사용했을 때와 차이점 : div는 태그로써 남지만 th:block는 타임리프에서 해석되고나서 사라짐 -->
</body>
</html>

👉 제어문

◼ main/java/com/greedy/thymeleaf/model/dto/MemberDTO.java

💁‍♀️ Lombok이란,
DTO/VO 클래스의 constructor, getter/setter, toSting 등을 어노테이션을 통해 자동 작성해주는 기능을 포함한 라이브러리.
필드 수정 후 해당 코드에 대한 수정 작업이 별도로 필요하지 않다는 장점.
팀 프로젝트에서 활용 시 모든 팀원이 해당 라이브러리를 설치한 환경에서 사용해야함.
구조와 무관하게 남용 될 수 있어 프로젝트에 따라서는 사용하지 않는 경우도 있음에 유의
Lombok 설치 방법

//@NoArgsConstructor	/* 기본 생성자 생성됨 */
//@AllArgsConstructor	/* 모든 매개변수 생성자 생성됨 */
//@Getter
//@Setter
//@ToString
//@EqualsAndHashCode
@Data /* 모든 매개변수 생성자를 제외하고 위에 있는 어노테이션을 한 번에 처리할 수 있는 어노테이션 */
@AllArgsConstructor
public class MemberDTO {

	private String name;
	private int age;
	private char gender;
	private String address;

}

◼ main/resources/static/index.html

<button onclick="location.href='/lecture/conditional'">제어문</button>

◼ main/java/com/greedy/thymeleaf/controller/LectureController.java

@Controller
@RequestMapping("/lecture")
public class LectureController {

	@GetMapping("/conditional")
	public String conditional(Model model) {
		
		/* 제어문 */
		model.addAttribute("num", 1);
		model.addAttribute("str", "바나나");
		
		/* 반복문을 위한 리스트 */
		List<MemberDTO> memberList = new ArrayList<>();
		memberList.add(new MemberDTO("흰둥이", 2, '남', "서울시 떡잎마을"));
		memberList.add(new MemberDTO("짱구", 5, '남', "서울시 떡잎마을"));
		memberList.add(new MemberDTO("철수", 5, '남', "서울시 떡잎마을"));
		memberList.add(new MemberDTO("유리", 5, '여', "서울시 떡잎마을"));
		
		model.addAttribute("memberList", memberList);
		
		return "lecture/conditional";
	}

}

◼ main/resources/templates/lecture/conditional.html

<!DOCTYPE html>
<html xmlns:th="http://www.thymeleaf.org">
<head>
<meta charset="UTF-8">
<title>conditional</title>
<style>
h2 { background : #fdefe6; }
h3 { background : #f5fee1; }
</style>
</head>
<body>
	<h1 align='center'>제어문</h1>

th:if th:unless #strings isEmpty()

	<h2>th:if, th:unless</h2>
	<h3>
		th:if는 변수 표현식의 OGNL(Object Graph Navigation Language)를 활용한 조건식으로
		조건문을 작성하면 결과가 true일 대 해당 태그의 범위가 처리 됨 (JSTL에서도 사용 된 표현식)
		th:unless는 표현식의 OGNL을 활용한 결과가 false일 때 해당 태그 범위가 처리 됨
	</h3>
	
	<p th:if="${ num > 0 }">넘어온 값은 0보다 크당!</p>
	<p th:if="${ num < 0 }">넘어온 값은 0보다 작당!</p>
	<<p th:unless="${ num > 0 }">넘어온 값은 0보다 크지 않당!</p>
	<p th:unless="${ num < 0 }">넘어온 값은 0보다 작지 않당!</p>
	<!--
		ex) num이 1일 경우,
		넘어온 값은 0보다 크당!
		넘어온 값은 0보다 작지 않당!
		만 출력
	 -->
	 
	 <th:block th:if="${ str == '사과' }">
	 	<p>사과 조아🍎</p>
	 </th:block>
	 <th:block th:if="${ str == '바나나' }">
	 	<p>바나나 조아🍌</p>
	 </th:block>
	 <!-- 
	 	ex) str이 바나나일 경우,
	 	바나나 조아🍌
	 	출력
	  -->
	  
	  <p th:if="${ num > 0 and num <= 10 }">1부터 10까지의 양수</p>
	  <p th:if="${ str != null and str == '바나나' }">바나나 좋다구!🍌🍌</p>
	  
	  <!-- #strings 라는 타임리프에서 제공되는 Utility Objects에서 제공하는 메소드를 통해서도 다양한 처리 가능 -->
	  <p th:if="${ !#strings.isEmpty(str) and str == '바나나' }">바나나 좋다구 세 번째 말하는 중🍌🍌🍌</p>

th:switch th:case

	 <h2>th:switch, th:case</h2>
	 <h3>th:switch와 th:case를 통해 해당 조건의 값이 어떤 case에 해당되는지에 따라 태그를 선택 가능</h3>
	 <th:block th:switch="${ str }">
	 	<span th:case="사과">사과🍎 선택 :)</span>
	 	<span th:case="바나나">바나나🍌 선택 :)</span>
	 </th:block>

th:each stat #numbers .sequence()

	 <h2>th:each</h2>
	 <table border="1">
	 	<tr>
	 		<th>이름</th>
	 		<th>나이</th>
	 		<th>성별</th>
	 		<th>주소</th>
	 	<</tr>
	 	<tr th:each="member : ${ memberList }"> <!-- th:each="변수 : 반복하고자 하는 대상 객체" -->
	 		<td th:text="${ member.name }"></td>
	 		<td th:text="${ member.age }"></td>
	 		<td th:text="${ member.gender }"></td>
	 		<td th:text="${ member.address }"></td>
	 	</tr>
	 </table>
	 
	 <h3>th:each에 stat(status)을 추가해서 반복 상태 확인 가능</h3>
	 <p>
	 	index : 0부터 시작하는 인덱스, count : 1부터 시작하는 수, current : 현재 객체의 정보,
	 	even : 짝수 데이터 여부, odd : 홀수 데이터 여부, first : 첫 번째 데이터 여부, last : 마지막 데이터 여부
	 </p>
	 <table border="1">
	 	<tr>
	 		<th>이름</th>
	 		<th>나이</th>
	 		<th>성별</th>
	 		<th>주소</th>
	 		<th>index</th>
	 		<th>count</th>
	 		<th>current</th>
	 		<th>even</th>
	 		<th>odd</th>
	 		<th>first</th>
	 		<th>last</th>
	 	</tr>
	 	<tr th:each="member, stat : ${ memberList }"> <!-- th:each="변수 : 반복하고자 하는 대상 객체" -->
	 		<td th:text="${ member.name }"></td>
	 		<td th:text="${ member.age }"></td>
	 		<td th:text="${ member.gender }"></td>
	 		<td th:text="${ member.address }"></td>
	 		<td th:text="${ stat.index }"></td>
	 		<td th:text="${ stat.count }"></td>
	 		<td th:text="${ stat.current }"></td>
	 		<td th:text="${ stat.even }"></td>
	 		<td th:text="${ stat.odd }"></td>
	 		<td th:text="${ stat.first }"></td>
	 		<td th:text="${ stat.last }"></td>
	 	</tr>
	 </table>
	 
	 <h3>th:each에 stat을 변수로 선언하지 않으면 '변수명+Stat'으로 반복 상태 확인 가능</h3>
	 <table border="1">
	 	<tr>
	 		<th>이름</th>
	 		<th>나이</th>
	 		<th>성별</th>
	 		<th>주소</th>
	 		<th>index</th>
	 		<th>count</th>
	 		<th>current</th>
	 		<th>even</th>
	 		<th>odd</th>
	 		<th>first</th>
	 		<th>last</th>
	 	</tr>
	 	<tr th:each="member : ${ memberList }"> <!-- th:each="변수 : 반복하고자 하는 대상 객체" -->
	 		<td th:text="${ member.name }"></td>
	 		<td th:text="${ member.age }"></td>
	 		<td th:text="${ member.gender }"></td>
	 		<td th:text="${ member.address }"></td>
	 		<td th:text="${ memberStat.index }"></td>
	 		<td th:text="${ memberStat.count }"></td>
	 		<td th:text="${ memberStat.current }"></td>
	 		<td th:text="${ memberStat.even }"></td>
	 		<td th:text="${ memberStat.odd }"></td>
	 		<td th:text="${ memberStat.first }"></td>
	 		<td th:text="${ memberStat.last }"></td>
	 	</tr>
	 </table>
	 
	 <h3>#numbers Utility Objects를 이용하여 반복문 작성하기</h3>
	 <th:block th:each="num : ${ #numbers.sequence(5, 10) }">
	 	<a th:href="@{|numbering/${ numStat.count }|}" th:text="번호 + ${ num }"></a>	
	 </th:block>
	 
	
</body>
</html>

👉 기타

◼ main/java/com/greedy/thymeleaf/model/dto/SelectCriteria.java

@Data
@AllArgsConstructor
public class SelectCriteria {
	
	private int startPage;
	private int endPage;
	private int pageNo;
	
}

◼ main/resources/static/index.html

<button onclick="location.href='/lecture/etc'">기타</button>

◼ main/java/com/greedy/thymeleaf/controller/LectureController.java

@Controller
@RequestMapping("/lecture")
public class LectureController {

	@GetMapping("/etc")
	public String etc(Model model) {
		
		SelectCriteria selectCriteria = new SelectCriteria(1, 10, 3);
		model.addAttribute("selectCriteria", selectCriteria);
		
		/* Object */
		model.addAttribute("member", new MemberDTO("치즈", 2, '남', "서울시 갱얼쥐마을"));
		
		/* List */
		List<MemberDTO> memberList = new ArrayList<>();
		memberList.add(new MemberDTO("흰둥이", 2, '남', "서울시 떡잎마을"));
		memberList.add(new MemberDTO("짱구", 5, '남', "서울시 떡잎마을"));
		memberList.add(new MemberDTO("철수", 5, '남', "서울시 떡잎마을"));
		memberList.add(new MemberDTO("유리", 5, '여', "서울시 떡잎마을"));
		
		model.addAttribute("memberList", memberList);
		
		/* Map */
		Map<String, MemberDTO> memberMap = new HashMap<>();
		memberMap.put("m01", new MemberDTO("흰둥이", 2, '남', "서울시 떡잎마을"));
		memberMap.put("m02", new MemberDTO("짱구", 5, '남', "서울시 떡잎마을"));
		memberMap.put("m03", new MemberDTO("철수", 5, '남', "서울시 떡잎마을"));
		memberMap.put("m04", new MemberDTO("유리", 5, '여', "서울시 떡잎마을"));
		
		model.addAttribute("memberMap", memberMap);
		
		return "lecture/etc";
	}

}

◼ main/resources/static/css/common.css

@charset "UTF-8";

h2 { color : salmon; }

.bold { font-weight : bolder; }

◼ main/resources/templates/lecture/etc.html

<!DOCTYPE html>
<html xmlns:th="http://www.thymeleaf.org">
<head>
<meta charset="UTF-8">
<title>etc</title>
<link rel="stylesheet" type="text/css" href="/css/common.css">
<style>
h2 { background : #fdefe6; }
h3 { background : #f5fee1; }
p { font-weight : bolder; background : #ecfbfd; }
span { background : #e6e7fd; padding : 10px; }
</style>
</head>
<body>

	<h1 align='center'>기타</h1>

th:with

	<h2>th:with</h2>
	<h3>지역 변수를 지정해서 사용 가능</h3>
	<th:block th:with="start = ${ selectCriteria.startPage }, last = ${ selectCriteria.endPage }">
		<th:block th:each="p : ${ #numbers.sequence(start, last) }"> <!-- start 값인 1부터 last 값인 10까지 반복 -->
			<th:block th:if="${ selectCriteria.pageNo eq p }">
				<button th:text="${ p } + 꾹" disabled></button> <!-- 현재 페이지라면, 버튼 비활성화 -->
			</th:block>
			<th:block th:if="${ selectCriteria.pageNo ne p }">
				<button th:text="${ p } + 꾹"></button>
			</th:block>
		</th:block>
	</th:block>

${...} get()

	<h2>Spring EL</h2>
	<h3>변수 표현식 ${...}은 SpringEL이라는 스프링에서 제공하는 표현식을 사용 가능
		단순한 변수가 아닌 Object, List, Map 같은 객체를 사용하기 위해 활용 가능 </h3>
		
	<p>Object</p>
	<ul>
		<li th:text="${ member.name }"></li> 		<!-- 기본 형식 (주로 사용) -->
		<li th:text="${ member['age'] }"></li> 		<!-- 대괄호 형식 -->
		<li th:text="${ member.getGender() }"></li> <!-- getter 형식 -->
	</ul>
		
	<p>List</p>
	<ul>
		<li th:text="${ memberList[1].name }"></li>
		<li th:text="${ memberList[1]['age'] }"></li>
		<li th:text="${ memberList[1].getGender() }"></li>
		<li th:text="${ memberList.get(2).name }"></li>
		<li th:text="${ memberList.get(2)['age'] }"></li>
		<li th:text="${ memberList.get(2).getGender() }"></li>
	</ul>
		
	<p>Map</p>
	<ul>
		<li th:text="${ memberMap['m04'].name }"></li>
		<li th:text="${ memberMap['m04']['age'] }"></li>
		<li th:text="${ memberMap['m04'].getGender() }"></li>
	</ul>

th:attrappend th:classappend

	<h2>속성 값 설정</h2>
	<span class="text" th:attrappend="class='bold '">class 속성 확인</span> <!-- 기존의 text라는 클래스가 이미 있는 상태에서 class를 띄어쓰기 없이 추가하면 textclass라고 인식하여 bold 속성 확인 X -->
	<span class="text" th:attrappend="class=' bold'">class 속성 확인</span> <!-- 띄어쓰기로 인해 bold 속성 확인 O -->
	<span class="text" th:classappend="bold">class 속성 확인</span> 		  <!-- attrappend 대신 classappend를 활용하여 class를 추가하면 위와 같은 문제 X -->
		
		
</body>
</html>


👀 CRUD & Test

📌 기본 설정

◼ main/resources/application.yml

# oracle driver config
spring:
  datasource:
    url: jdbc:oracle:thin:@localhost:1521:xe
    driver-class-name: oracle.jdbc.driver.OracleDriver
    username: C##GREEDY
    password: GREEDY
    
# mybatis config
mybatis:
  mapper-locations: mappers/**/*.xml # mappers 폴더의 하위 디렉토리 중 xml로 끝나는 모든 파일들을 mapper로 등록

◼ main/java/com/greedy/crud/config/Chap02CrudApplication.java

/* 기본 형태 */

◼ main/java/com/greedy/crud/config/ContextConfiguration.java

/* Chap02CrudApplication의 위치가 config 폴더 내에 존재하여 config 폴더 내에서만 ComponentScan이 되기 때문에
 * config의 상위폴더인 crud 안의 LectureController까지 ComponentScan하기 위해 이 Class 생성 */
@Configuration
@ComponentScan("com.greedy.crud") /* basePackages 생략하고 경로만 작성하는 것 가능 */
public class ContextConfiguration {

	/* 전체 컨텍스트에서 사용할 빈 추가 등록 (MessageSource를 활용하기 위함)*/
	@Bean
	public MessageSource messageSource() {
		
		ReloadableResourceBundleMessageSource messageSource
			= new ReloadableResourceBundleMessageSource();
		
		/* resources/messages/message들을 이 빈이 읽어올 것 */
		messageSource.setBasename("classpath:messages/message");
		
		return messageSource;
	}
}

◼ main/java/com/greedy/crud/config/MybatisConfiguration.java

@Configuration
@MapperScan(basePackages = "com.greedy.crud", annotationClass = Mapper.class) 
/* : annotation이 Mapper타입인 것들만 스캔할 것 */
public class MybatisConfiguration {

}

1) 모든 메뉴 조회

◼ MenuDTO.java

@Data
public class MenuDTO {

	private int code;
	private String name;
	private int price;
	private int categoryCode;
	private String orderableStatus;
	
}

◼ MenuMapper.java

/* MybatisConfiguration에서 Mapper 어노테이션만을 스캔하기로 설정했으므로 이 인터페이스가 스캐닝될 것 */
@Mapper
public interface MenuMapper {

	/* 1. 모든 메뉴 조회 */
	List<MenuDTO> findAllMenu();
    
}

◼ MenuMapper.xml

<resultMap id="menuResultMap" type="com.greedy.crud.model.dto.MenuDTO">
	<id property="code" column="MENU_CODE"/>
	<result property="name" column="MENU_NAME"/>
	<result property="price" column="MENU_PRICE"/>
	<result property="categoryCode" column="CATEGORY_CODE"/>
	<result property="orderableStatus" column="ORDERABLE_STATUS"/>
</resultMap>

<select id="findAllMenu" resultMap="menuResultMap">
	SELECT
			MENU_CODE
		,	MENU_NAME
		,	MENU_PRICE
		,	CATEGORY_CODE
		,	ORDERABLE_STATUS
		FROM TBL_MENU
		WHERE ORDERABLE_STATUS ='Y'
		ORDER BY MENU_CODE
</select>

◼ MenuMapperTests.java

@Disabled assertNotNull

/* test를 진행하기 위해 테스트할 클래스와 동일한 패키지를 가진 tests 클래스 생성 */

@SpringBootTest
/* 어플리케이션 설정을 해당 설정 파일에서 가져오겠다는 의미 */
/* Chap02CrudApplication에 있는 빈 객체를 사용하기 위해 가져옴 */
@ContextConfiguration(classes = {Chap02CrudApplication.class})
public class MenuMapperTests {
	
	@Autowired
	private MenuMapper menuMapper;
    
    @Test
	@Disabled /* @Disabled : 테스트를 실행하지 않고 싶을 때 사용 */
	public void 전체_메뉴_조회용_매퍼_테스트() {
		
		// given (주어져야하는 input 데이터)
		
		// when
		List<MenuDTO> menuList = menuMapper.findAllMenu();
		
		// then
		assertNotNull(menuList);
		System.out.println(menuList);
		
	}
    
}

◼ MenuService.java

public interface MenuService {
	
    /* 1. 모든 메뉴 조회 */
	List<MenuDTO> findAllMenu();
    
}

◼ MenuServiceImpl.java

@Transactional

/* A. 빈 등록 */
@Service("menuService")
@Transactional /* @Transactional 어노테이션이 붙은 클래스 내부의 메소드는 모두 transction 관리 됨
 				  @Transactional 어노테이션은 메소드 레벨로 분리해서 작성할 수도 있음
 				  메소드 동작 시 Exception이 발생하면 전체 트랜잭션을 rollback하고 정상 수행 시에는 commit 하는 동작이 일어남 */
public class MenuServiceImpl implements MenuService {

	/* B. MenuMapper의 bean 객체로부터 의존성을 주입받는 코드 */
	private final MenuMapper menuMapper;
	
	@Autowired
	public MenuServiceImpl(MenuMapper menuMapper) {
		this.menuMapper = menuMapper;
	}
	
	/* C. MenuService 인터페이스로부터 메소드 오버라이드 */
    
    /* 1. 모든 메뉴 조회 */
	@Override
	public List<MenuDTO> findAllMenu() {
		return menuMapper.findAllMenu();
	}
    
}

◼ MenuServiceTests.java

@SpringBootTest
/* Chap02CrudApplication에 있는 빈 객체를 사용하기 위해 가져옴 */
@ContextConfiguration(classes = {Chap02CrudApplication.class}) 
public class MenuServiceTests {

	@Autowired
	private MenuService menuService;
    
    @Test
	public void 전체_메뉴_조회용_서비스_메소드_테스트() {
		
		// given (주어져야하는 input 데이터)
		
		// when
		List<MenuDTO> menuList = menuService.findAllMenu();
				
		// then
		assertNotNull(menuList);
		System.out.println(menuList);
	}
}

◼ MenuController.java

/* 빈 등록 */
@Controller
@RequestMapping("/menu")
public class MenuController {

	/* 의존성 주입 */
	private MenuService menuService;
	
	@Autowired
	public MenuController(MenuService menuService) {
		this.menuService = menuService;
	}
    
    @GetMapping("/list")
	public String findMenuList(Model model) {
		
		/* menuService를 호출하여 받아온 값을 menuList에 저장 */
		List<MenuDTO> menuList = menuService.findAllMenu();
		model.addAttribute("menuList", menuList);
		
		return "menu/list";
	}
}

◼ MenuControllerTests.java

@BeforeEach MockMvc get() status() isOk() forwardedUrl() print()

@SpringBootTest
/* Chap02CrudApplication에 있는 빈 객체를 사용하기 위해 가져옴 */
@ContextConfiguration(classes = {Chap02CrudApplication.class}) 
public class MenuControllerTests {

	/* 의존성 주입 */
	@Autowired
	private MenuController menuController;
	
	/* Controller는 테스트할 때 사용자의 요청이 필요 */
	/* MockMvc : 웹 어플리케이션을 어플리케이션 서버에 배포하지 않고도 Spring Web의 동작을 재현할 수 있는 클래스
	 * 즉, WAS의 구동 없이 Controller에 request를 만들어 날리는 테스트 로직 수행 가능 
	 * (사용자의 요청을 가상으로 만들어주는 역할 - view가 없어도 코드 확인 가능) */
	private MockMvc mockMvc;
	
	@BeforeEach /* @BeforeEach : test들이 수행되기 전 이 메소드를 호출하고 수행 */
	public void setUp() {
		mockMvc = MockMvcBuilders.standaloneSetup(menuController).build();
	}
    
    /* 실제 수행할 테스트 코드들 작성 */
    
    @Test
	public void 전체_메뉴_조회용_컨트롤러_테스트() throws Exception {
		
		// given
		
		// when & then
		mockMvc.perform(MockMvcRequestBuilders.get("/menu/list"))
			.andExpect(MockMvcResultMatchers.status().isOk()) /* status() : http 상태코드(정상응답 : 200)*/
			.andExpect(MockMvcResultMatchers.forwardedUrl("menu/list")) /* forwardedUrl : forward되는 url이 이게 맞는지 확인 */
			.andDo(MockMvcResultHandlers.print()); /* print() : 수행 결과를 전체적으로 출력하여 확인 */
	}
}

◼ index.html

<button onclick="location.href='/menu/list'">전체 메뉴 조회</button>

◼ list.html

<h1 align="center">메뉴 목록 페이지</h1>
	
<table align="center" border="1">
	<tr>
		<th>메뉴번호</th>
		<th>메뉴이름</th>
		<th>메뉴가격</th>
		<th>카테고리코드</th>
		<th>판매상태</th>
	</tr>
	<tr th:each="menu : ${ menuList }">
		<td th:text="${ menu.code }"></td>
		<td th:text="${ menu.name }"></td>
		<td th:text="${ menu.price }"></td>
		<td th:text="${ menu.categoryCode }"></td>
		<td th:text="${ menu.orderableStatus }"></td>	
	</tr>
</table>

2-1) 모든 카테고리 조회

💁‍♀️ 2-2) 새로운 메뉴 등록을 할 때,
DB에 존재하는 카테고리를 비동기적인 통신(ajax)을 활용하여 조회해올 수 있도록 하기 위한 기능

◼ CategoryDTO.java

@Data
public class CategoryDTO {

	private int code;
	private String name;
	private int refCategoryCode;
	
}

◼ MenuMapper.java

@Mapper
public interface MenuMapper {

	/* 2-1. 모든 카테고리 조회 */
	List<CategoryDTO> findAllCategory();
    
}

◼ MenuMapper.xml

<resultMap id="categoryResultMap" type="com.greedy.crud.model.dto.CategoryDTO">
	<id property="code" column="CATEGORY_CODE"/>
	<result property="name" column="CATEGORY_NAME"/>
	<result property="refCategoryCode" column="REF_CATEGORY_CODE"/>
</resultMap>

<select id="findAllCategory" resultMap="categoryResultMap">
	SELECT
			CATEGORY_CODE
		,	CATEGORY_NAME
		,	REF_CATEGORY_CODE
		FROM TBL_CATEGORY
		WHERE REF_CATEGORY_CODE IS NOT NULL
</select>

◼ MenuMapperTests.java

@SpringBootTest
/* Chap02CrudApplication에 있는 빈 객체를 사용하기 위해 가져옴 */
@ContextConfiguration(classes = {Chap02CrudApplication.class})
public class MenuMapperTests {
	
	@Autowired
	private MenuMapper menuMapper;
    
    @Test
	public void 전체_카테고리_조회용_매퍼_테스트() {
		
		// given
		
		// when
		List<CategoryDTO> categoryList = menuMapper.findAllCategory();
		
		// then
		assertNotNull(categoryList);
		System.out.println(categoryList);
		
	}
    
}

◼ MenuService.java

public interface MenuService {
	
    /* 2-1. 모든 카테고리 조회 */
	List<CategoryDTO> findAllCategory();
    
}

◼ MenuServiceImpl.java

/* A. 빈 등록 */
@Service("menuService")
@Transactional
public class MenuServiceImpl implements MenuService {

	/* B. MenuMapper의 bean 객체로부터 의존성을 주입받는 코드 */
	private final MenuMapper menuMapper;
	
	@Autowired
	public MenuServiceImpl(MenuMapper menuMapper) {
		this.menuMapper = menuMapper;
	}
	
	/* C. MenuService 인터페이스로부터 메소드 오버라이드 */
    /* 2-1. 모든 카테고리 조회 */
	@Override
	public List<CategoryDTO> findAllCategory() {

		return menuMapper.findAllCategory();
	}
    
}

◼ MenuServiceTests.java

@SpringBootTest
/* Chap02CrudApplication에 있는 빈 객체를 사용하기 위해 가져옴 */
@ContextConfiguration(classes = {Chap02CrudApplication.class}) 
public class MenuServiceTests {

	@Autowired
	private MenuService menuService;
    
    @Test
	public void 전체_카테고리_조회용_서비스_메소드_테스트() {
		
		// given
		
		// when
		List<CategoryDTO> categoryList = menuService.findAllCategory();
						
		// then
		assertNotNull(categoryList);
		System.out.println(categoryList);
		
	}
    
}

◼ MenuController.java

@ResponseBody

/* 빈 등록 */
@Controller
@RequestMapping("/menu")
public class MenuController {

	/* 의존성 주입 */
	private MenuService menuService;
	
	@Autowired
	public MenuController(MenuService menuService) {
		this.menuService = menuService;
	}
    
    /* Spring web에는 jackson databind 등의 의존성이 모두 추가되어 있으므로 비동기 통신에서
	 * JSON 타입의 응답을 하기 위해서는 @ResponseBody 어노테이션만 추가하면 자동으로 변환 됨 */
	@GetMapping(value = "/category", produces = "application/json; charset=UTF-8")
	public @ResponseBody List<CategoryDTO> findCategoryList() {
		return menuService.findAllCategory();
	}
    
}

◼ MenuControllerTests.java

content() contentType()

@SpringBootTest
/* Chap02CrudApplication에 있는 빈 객체를 사용하기 위해 가져옴 */
@ContextConfiguration(classes = {Chap02CrudApplication.class}) 
public class MenuControllerTests {

	/* 의존성 주입 */
	@Autowired
	private MenuController menuController;
	
	private MockMvc mockMvc;
	
	@BeforeEach
	public void setUp() {
		mockMvc = MockMvcBuilders.standaloneSetup(menuController).build();
	}
    
    /* 실제 수행할 테스트 코드들 작성 */
    
    @Test
	public void 전체_카테고리_조회용_컨트롤러_테스트 () throws Exception {
		
		// given
		
		// when & then
		mockMvc.perform(MockMvcRequestBuilders.get("/menu/category")) /* : /menu/category라는 url요청을 get방식으로 */
			.andExpect(MockMvcResultMatchers.status().isOk())
			.andExpect(MockMvcResultMatchers.content().contentType("application/json; charset=UTF-8")) /* contentype이 입력한 것과 동일하게 설정되어 있는가 */
			.andDo(MockMvcResultHandlers.print());
		/* MockHttpServletResponse: Body에 카테고리가 자바스크립트에서 사용할 수 있는 문자열로 출력되는 것을 확인 가능 */
	}
    
}

◼ index.html

<button onclick="location.href='/menu/regist'">신규 메뉴 등록</button>

◼ regist.html

$.ajax({}) append() val() text()

<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<title>menu regist</title>
<!-- 비동기적 통신(ajax)을 위해 jQuery에서 스니펫 복사하여 추가 -->
<script src="https://ajax.googleapis.com/ajax/libs/jquery/3.6.3/jquery.min.js"></script>
</head>
<body>

	<h3 align="center">신규 메뉴 등록</h3>
	<form action="/menu/regist" method="post" align="center">
		<label>메뉴 이름 : </label>
		<input type="text" name="name"><br>
		<label>메뉴 가격 : </label>
		<input type="number" name="price"><br>
		<label>카테고리 : </label>
		<select name="categoryCode" id="categoryCode"></select><br>
		<label>판매 상태 : </label>
		<select name="orderableStatus">
			<option value="Y">Y</option>
			<option value="N">N</option>
		</select><br>
		<input type="submit" value="전송">
	</form>
	
	<script>
		$(function(){
			
			/* 비동기적인 통신 호출할 것 */
			/* 기본값이 get방식이므로 method 생략, 넘기는 데이터가 없기 때문에 data 생략 */
			$.ajax({
				url : "/menu/category",
				success : function(cateListArray) {  /* MenuController의 findCategoryList()메소드로부터 List<CategoryDTO>형태의 응답 데이터가 그대로 넘어옴 */
					console.log(cateListArray);
				
					const $categoryCode = $("#categoryCode");
					
					for(let index in cateListArray) {
						$categoryCode.append($("<option>").val(cateListArray[index].code).text(cateListArray[index].name));
					}
					/* 반복문 돌면서 index 0번째부터 <option>이 붙은 새로운 노드들을 categoryCode에 생성 */
				
				},
				error : function(xhr) { console.log(xhr); }
				
			});
			
		})
	</script>
</body>
</html>

2-2) 새로운 메뉴 등록

◼ MenuMapper.java

@Mapper
public interface MenuMapper {
	
    /* 2-2. 새로운 메뉴 등록 */
	int registMenu(MenuDTO menu);
    
}

◼ MenuMapper.xml

<insert id="registMenu">
	INSERT
		INTO TBL_MENU
	(
		MENU_CODE
	,	MENU_NAME
	,	MENU_PRICE
	,	CATEGORY_CODE
	,	ORDERABLE_STATUS
	)
	VALUES
	(
		SEQ_MENU_CODE.NEXTVAL
	,	#{ name }
	,	#{ price }
	, 	#{ categoryCode }
	, 	#{ orderableStatus }
	)
</insert>

◼ MenuMapperTests.java

@DisplayName assertEquals()

@SpringBootTest
/* Chap02CrudApplication에 있는 빈 객체를 사용하기 위해 가져옴 */
@ContextConfiguration(classes = {Chap02CrudApplication.class})
public class MenuMapperTests {
	
	@Autowired
	private MenuMapper menuMapper;
    
    @Test
	@DisplayName("신규 메뉴가 잘 추가 되는지 매퍼 인터페이스의 메소드 확인") /* @DisplayName : 설명 붙일 수 있음 */
	public void testRegistMenu() {
		
		// given
		MenuDTO menu = new MenuDTO();
		menu.setName("짜장바나나스무디");
		menu.setPrice(20000);
		menu.setCategoryCode(7);
		menu.setOrderableStatus("Y");
		
		// when
		/* 주어진 데이터 등록 */
		int result = menuMapper.registMenu(menu);
		
		// then
		/* assertEquals : 예상한 결과와 같은지 체크 */
		assertEquals(1, result); 

	}
}

◼ MenuService.java

public interface MenuService {
	
    /* 2-2. 새로운 메뉴 등록 */
	boolean registMenu(MenuDTO menu) throws Exception ; /* int일 때는 삽입된 행의 갯수 / boolean은 삽입 유무 판단 */
    
}

◼ MenuServiceImpl.java

/* A. 빈 등록 */
@Service("menuService")
@Transactional
public class MenuServiceImpl implements MenuService {

	/* B. MenuMapper의 bean 객체로부터 의존성을 주입받는 코드 */
	private final MenuMapper menuMapper;
	
	@Autowired
	public MenuServiceImpl(MenuMapper menuMapper) {
		this.menuMapper = menuMapper;
	}
	
	/* C. MenuService 인터페이스로부터 메소드 오버라이드 */
    
    /* 2-2. 새로운 메뉴 등록 */
	@Override
	/* @Transactional : 이 메소드가 호출되는 동안 발생하는 트랜잭션을 관리 -> 모두 잘 완료되면 메소드 종료시 커밋 */
	public boolean registMenu(MenuDTO menu) throws Exception {
		
		int result = menuMapper.registMenu(menu);
		
		/* 메뉴 등록에 실패하면 어떤 일이 일어나는지 테스트 하기 위해 의도적으로 Exception 발생시킴 */
		if(result <= 0) {
			throw new Exception("메뉴 등록에 실패 😵");
		}
		
		return result > 0 ? true : false;
	}
    
}

◼ MenuServiceTests.java

assertTrue() assertThrows()

@SpringBootTest
/* Chap02CrudApplication에 있는 빈 객체를 사용하기 위해 가져옴 */
@ContextConfiguration(classes = {Chap02CrudApplication.class}) 
public class MenuServiceTests {

	@Autowired
	private MenuService menuService;
    
    @Test
	public void 신규_메뉴_등록용_서비스_성공_테스트() throws Exception {
		
		// given
		/* 테스트를 위한 데이터 임의로 작성 */
		MenuDTO menu = new MenuDTO();
		menu.setName("치즈똥아이스크림");
		menu.setPrice(7000);
		menu.setCategoryCode(7);
		menu.setOrderableStatus("Y");
		
		// when
		boolean result = menuService.registMenu(menu);
		
		// then
		assertTrue(result); /* assertTrue : 입력된 값이 true인지 확인 */
	}
	@Test
	public void 신규_메뉴_등록용_서비스_실패_테스트() {
		
		// given
		/* 테스트를 위한 데이터 임의로 작성 */
		MenuDTO menu = new MenuDTO();
		menu.setName("과메기메론빙수");
		menu.setPrice(7000);
		menu.setCategoryCode(100); /* 없는 카테고리를 입력하여 의도적으로 예외발생 시키기 */
		menu.setOrderableStatus("Y");
				
		// when
//		boolean result = menuService.registMenu(menu); /* 아래에 람다식을 입력했으므로 주석 */
				
		// then
		assertThrows(Exception.class, () -> menuService.registMenu(menu)); 
		/* assertThrows : 입력된 값이 오류인지 확인 */
		/* : 'menuService.registMenu(menu)을 실행 했을 때 이러한 타입의 Exception이 발생하는가?'를 판별 */
		
        /* 테스트가 정상적으로 실행되었다면, 과메기메론빙수는 DB에 삽입되지않음 (Exception 발생했기때문) */
		
	}
}

◼ MenuController.java

MessageSource @ModelAttribute RedirectAttributes Locale addFlashAttribute() getMessage

/* 빈 등록 */
@Controller
@RequestMapping("/menu")
public class MenuController {

	/* 의존성 주입 */
	private MenuService menuService;
	private MessageSource messageSource; /* 메세지를 읽어올 수 있도록 의존성 주입 */
	
	@Autowired
	public MenuController(MenuService menuService, MessageSource messageSource) {
		this.menuService = menuService;
		this.messageSource = messageSource;
	}
    
    /* 신규 메뉴 등록용 화면 이동 */
	/* 이 메소드의 역할 : GetMapping으로 요청한 주소를 forwarding -> 신규 메뉴 등록용 화면이 나타남 */
	@GetMapping("/regist")
	public void registPage() {}
	
	@PostMapping("/regist")
	public String registMenu(@ModelAttribute MenuDTO menu, RedirectAttributes rttr, Locale locale) throws Exception { 
	/* 전달 받는 값이 여러 개이므로 MenuDTO를 반환형으로 */
		
		menuService.registMenu(menu);
		
		/* RedirectAttributes를 활용하여 redirect를 해도 값을 보존하고 메뉴 등록에 성공했다는 성공 메세지를 노출 */
		/* 스프링 컨테이너에서 받아야할 값(message)이 있다면, 그것은 의존성을 주입받아 사용해야하기 때문에 의존성 주입시키는 코드 맨 위에 작성 */
		rttr.addFlashAttribute("successMessage", messageSource.getMessage("registMenu", null, locale)); /* locale : 어느 지역에서 왔는지 전달 */
		
		/* redirect를 해야 findMenuList() 메소드를 다시 호출하여 메뉴 리스트를 보여줌 
		 * (forwarding을 하면 이 메소드에는 menuList 객체 자체가 존재하지 않으므로 thymeleaf에서 오류 발생)*/
		return "redirect:/menu/list";
	}
    
}

◼ MenuControllerTests.java

post() params() is3xxRedirection() flash() attributeCount() redirectedUrl

@SpringBootTest
/* Chap02CrudApplication에 있는 빈 객체를 사용하기 위해 가져옴 */
@ContextConfiguration(classes = {Chap02CrudApplication.class}) 
public class MenuControllerTests {

	/* 의존성 주입 */
	@Autowired
	private MenuController menuController;
	
	private MockMvc mockMvc;
	
	@BeforeEach
	public void setUp() {
		mockMvc = MockMvcBuilders.standaloneSetup(menuController).build();
	}
    
    /* 실제 수행할 테스트 코드들 작성 */
    
    @Test
	public void 신규_메뉴_등록용_컨트롤러_테스트 () throws Exception {
		
		// given
		MultiValueMap<String, String> params = new LinkedMultiValueMap<>();
		params.add("name", "미공개계절메뉴");
		params.add("price", "10000");
		params.add("categoryCode", "5");
		params.add("orderableStatus", "Y");
				
			
		// when & then
		mockMvc.perform(MockMvcRequestBuilders.post("/menu/regist").params(params)) /* : /menu/regist라는 url요청을 post방식으로 */
			.andExpect(MockMvcResultMatchers.status().is3xxRedirection()) /* redirect한 뒤 응답 코드가 300번대인지 확인(성공을 의미) */
			.andExpect(MockMvcResultMatchers.flash().attributeCount(1)) /* : flashAttribute가 1개 있는지 확인 */
			.andExpect(MockMvcResultMatchers.redirectedUrl("/menu/list")) /* : redirect된 url이 /menu/list인지 확인 */
			.andDo(MockMvcResultHandlers.print());
		
	}
}

◼ index.html

<button onclick="location.href='/menu/regist'">신규 메뉴 등록</button>

◼ message_ko.properties

registMenu=\uC2E0\uADDC \uBA54\uB274 \uB4F1\uB85D\uC774 \uC644\uB8CC\uB418\uC5C8\uC5B4\uC694!\uD83E\uDD73
/* 신규 메뉴 등록이 완료되었어요!🥳 */

◼ list.html

<!-- list.html의 <head>에 alert창을 띄우는 자바스크립트 코드 추가 -->

<script>
	const successMessage = '[[${successMessage}]]';
	
	/* 해당 값이 존재할 때만 alert창 띄우기 */
	if(successMessage) alert(successMessage);
</script>

◼ regist.html

/* 위의 regist.html과 동일 */


👀 로그인 (Security Session Login)

👉 application.yml

# server config
server:
  port: 8001
  
# database config
spring:
  datasource:
    url: jdbc:oracle:thin:@localhost:1521:xe
    driver-class-name: oracle.jdbc.driver.OracleDriver
    username: C##LOGIN_AUTH
    password: LOGIN_AUTH
    
# mybatis config
mybatis:
  mapper-locations: mappers/**/*.xml # mappers 폴더의 하위 디렉토리 중 xml로 끝나는 모든 파일들을 mapper로 등록
  
# log level
logging: 
  level:
# root - 전역 설정
    root: info # 기본값
# package level - 지역 설정(원하는 상세 경로(패키지) 설정)
    '[com.greedy.security.member.service]': trace

👉 Chap03SecuritySessionLoginApplication.java

@SpringBootApplication @ComponentScan

@SpringBootApplication
/* 기존 ContextConfiguration 클래스에 작성했던 @ComponentScan 어노테이션을 직접 이 클래스에 작성하는 것도 가능 */
@ComponentScan("com.greedy.security")
public class Chap03SecuritySessionLoginApplication {

	public static void main(String[] args) {
		SpringApplication.run(Chap03SecuritySessionLoginApplication.class, args);
	}

}

👉 MybatisConfig.java

@Configuration @MapperScan

@Configuration
@MapperScan(basePackages = "com.greedy.security", annotationClass = Mapper.class)
public class MybatisConfig {

}

👉 AuthorityDTO.java

/* TBL_AUTHORITY */
@Data
public class AuthorityDTO {
	
	private int code;
	private String name;
	private String desc;
	
}

👉 MemberRoleDTO.java

/* TBL_MEMBER_ROLE */
@Data
public class MemberRoleDTO {
	
	private int memberNo;
	private int authorityCode;
	
	/* TBL_AUTHORITY - 권한 코드별로 가지는 권한을 나타냄 */
	private AuthorityDTO authority;
}

👉 MemberDTO.java

/* TBL_MEMBER */
@Data
public class MemberDTO {
	
	private int no;									// 회원번호
	private String id;								// 회원아이디
	private String pwd;								// 회원비밀번호
	private String tempPwdYn;						// 임시비밀번호여부
	private Date pwdChangedDatetime;				// 회원비밀번호변경일자
	private String pwdExpDate;						// 회원비밀번호만료일자
	private String name;							// 회원이름
	private Date registDatetime;					// 회원가입일시
	private int accumLoginCount;					// 누적로그인횟수
	private int loginFailedCount;					// 로그인연속실패횟수
	private String accLockYn;						// 계정잠금여부
	private String accInactiveYn;					// 계정비활성화여부
	private String accExpDate;						// 계정만료일자
	private String accExpYn;						// 계정만료여부
	private Date accSecessionDatetime;				// 계정탈퇴일시
	private String accSecessionYn;					// 계정탈퇴여부
	
	/* TBL_MEMBER_ROLE - 한 멤버는 여러 권한을 가질 수 있음
    	(관리자는 사용자&관리자의 권한을 가질 수 있음) */
	private List<MemberRoleDTO> memberRoleList;		// 권한 목록
	
}

👉 CustomUser.java

/* User를 상속받아 User의 값을 가지되 기능을 더 확장(추가)하기 위한 클래스 */
@Getter
@ToString // 이곳에 있는 정보들을 문자열로 확인할 수 있게끔
public class CustomUser extends User {
	
	/* 추가적으로 갖고 싶은 정보 */
	private int no;				// 회원번호
	private String name;		// 회원이름
	private Date registDate;	// 회원가입일시

	public CustomUser(MemberDTO member, Collection<? extends GrantedAuthority> authorities) {
		super(member.getId(), member.getPwd(), authorities); // 유저가 가져야 할 값
		setDetails(member);	// 우리가 필요한 값
	}
	
	/* 필요한 값을 생성자에 선언 */
	private void setDetails(MemberDTO member) {
		this.no = member.getNo();
		this.name = member.getName();
		this.registDate = member.getRegistDatetime();
	}

}

👉 SpringSecurityConfig.java

@EnableWebSecurity AuthenticationService PasswordEncoder BCryptPasswordEncoder() SecurityFilterChain AuthenticationManager

@EnableWebSecurity /* 시큐리티 설정 활성화 및 bean 등록 */
public class SpringSecurityConfig {
	
	/* 의존성 주입 */
	private final AuthenticationService authenticationService;
	
	public SpringSecurityConfig(AuthenticationService authenticationService) {
		this.authenticationService = authenticationService;
	}
	
	
	/* 1. 비밀번호 암호화에 사용할 객체 BCriptPasswordEncoder bean 등록 */
	@Bean
	public PasswordEncoder passwordEncoder() {
		
		return new BCryptPasswordEncoder();
	}

	/* 2. HTTP 요청에 대한 설정 */
	@Bean
	public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
		
		return http
				/* csrf는 기본적으로 활성화 되어 있으므로 비활성화 처리 */
				.csrf() 
					.disable()
				/* 요청에 대한 권한 체크 */
				.authorizeHttpRequests()
					/* hasRole 안에 전달하는 값은 "ROLE_"이 자동으로 붙음 (따라서 DB의 컬럼명이 ROLE_MEMBER라면 MEMBER만 작성) */
					.antMatchers("/order/**", "/member/mypage").hasRole("MEMBER") /* : 나열된 요청들은 MEMBER만 가능 */
					.antMatchers(HttpMethod.POST, "/menu/**").hasRole("ADMIN") /* : post방식으로 요청한 menu와 관련한 요청들은 ADMIN만 가능 */
					.antMatchers("/admin/**").hasRole("ADMIN")
					/* 위에 서술 된 내용 외의 모든 요청은 허가함 (인증 되지 않은 사용자도 요청 가능)*/
					.anyRequest().permitAll()
				.and()
					/* 로그인 설정 */
					.formLogin()
					/* 로그인 페이지 설정 */
					.loginPage("/member/login")
					/* 성공 시 랜딩 페이지 설정 */
					.successForwardUrl("/")
					/* 실패 시 랜딩 페이지 설정 */
					.failureForwardUrl("/error/login")
				.and()
					/* 로그아웃 설정 */
					.logout()
					/* 로그아웃 주소 */
					.logoutRequestMatcher(new AntPathRequestMatcher("/member/logout"))
					/* 세션 만료 */
					.invalidateHttpSession(true)
					/* JSESSIONID 쿠키 삭제 */
					.deleteCookies("JSESSIONID")
					/* 로그아웃 후 랜딩 페이지 */
					.logoutSuccessUrl("/") /* 메인으로 이동 */
				.and()
					/* 예외 처리 */
					.exceptionHandling()
					/* 인증이 필요한 때에는 로그인 페이지로 이동 
					 * 인가가 되지 않았을 때의 랜딩 페이지를 설정 
					 * (ex : 사용자가 로그인을 했어도 관리자 페이지는 접근 권한 X)*/
					.accessDeniedPage("/error/denied")
				.and()
					.build();
				/* 최종적으로 build한 객체를 SecurityFilterChain 형태로 등록 */
	}
	
	/* 3. 사용자 인증을 위해서 사용할 Service bean 등록 */
	@Bean
	public AuthenticationManager authManager(HttpSecurity http) throws Exception {
		
		/* ProviderManager의 상위 타입인 AuthenticationManager로 반환형 설정 */
		
		return http
				.getSharedObject(AuthenticationManagerBuilder.class)
				.userDetailsService(authenticationService) /* userDetailsService(인증 로직)에 어떤 설정을 할 것인지 입력 */
				.passwordEncoder(passwordEncoder()) /* passwordEncoder(암호화)에 어떤 설정을 할 것인지 입력 */
				.and()
				.build();
				
	}
}

🙋‍ 잠깐 ! 왜 System.out이 아닌 log를 사용하나요?
System.out을 사용하기 보다는 Log 출력을 사용하는 것이 성능상 좋음
Log4j <-> Logback (Slf4j - 추상체)
Logback이 스프링 부트의 기본 설정


log level

  • error : 요청을 처리하는 중 오류 발생
  • warn : 처리 가능한 문제, 향후 시스템 에러의 원인이 될 수 있는 경고성 메세지
  • info : 상태 변경과 같은 정보성 로그
  • debug : 프로그램을 디버깅 하기 위한 용도
  • trace : 디버그보다 훨씬 상세한 정보

기본 로그 레벨은 info 레벨로 설정 되어 있어 info 이하의 레벨은 출력 X
application.yml 파일에서 log level 변경 가능

👉 AuthenticationService.java

@Slf4j UserDetailsService

/* 사용자 인증과 관련된 객체를 UserDetailsService를 상속받아 다뤄줘야 스프링에서 읽어감 */

@Slf4j // Lombok에서 제공하는 어노테이션으로 log라는 이름으로 필드 선언을 제공
@Service
public class AuthenticationService implements UserDetailsService {
	
	/* MemberMapper로 부터 의존성 주입 */
	private final MemberMapper memberMapper;
	
	public AuthenticationService(MemberMapper memberMapper) {
		this.memberMapper = memberMapper;
	}

	/* @Slf4j라는 Lombok이 제공하는 어노테이션을 설정하기 때문에 따로 선언 필요 X */
//	private static final Logger log = LoggerFactory.getLogger(AuthenticationService.class);
	
	/* 로그인 페이지에서 아이디, 비밀번호를 입력 후, submit버튼을 누르게 되면 이 메소드가 호출될 것 */
	@Override
	public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
		/* DB에 접근하여 username으로 조회한 뒤, 조회한 데이터를 UserDetails 타입의 객체로 반환하기 */
		
//		System.out.println("username : " + username); /* 로그인 할 때, 입력한 아이디(username)가 정상적으로 넘어옴 */
		
		log.error("username : {}", username);
		log.warn("username : {}", username);
		log.info("username : {}", username);
		log.debug("username : {}", username);
		log.trace("username : {}", username);
		
		/* 사용자 정의 타입으로 유저 조회 */
		MemberDTO member = memberMapper.findMemberById(username);
		
		log.info("member : {}", member);
		
		/* 조회된 값이 없을 시 처리 (아이디가 데이터베이스에 존재하지 않음)*/
		if(member == null) throw new UsernameNotFoundException("username not found 😰");
		
		/* 권한 리스트 만들기 */
		List<GrantedAuthority> authorities = new ArrayList<>();
		for(MemberRoleDTO role : member.getMemberRoleList()) { /* getMemberRoleList() : List<MemberRoleDTO>를 반환*/
			authorities.add(new SimpleGrantedAuthority(role.getAuthority().getName())); /* role이 가지고 있는 getAuthority() 메소드를 활용하여 그것의 이름 받아오기 */
		} 
		
		/* 스프링 시큐리티 모듈에 넘겨줘야하는 User()에 담아서 반환 */
//		return new User(member.getId(), member.getPwd(), authorities);
		
		/* User 객체에 담기지 않는 추가 정보를 User를 extends 한 CustomUser에 담아 리턴 */
		return new CustomUser(member, authorities);
	}
}

👉 MemberMapper.java

/* MybatisConfig에 설정한대로 이 클래스가 스캐닝 될 것 */
@Mapper
public interface MemberMapper {

	MemberDTO findMemberById(String username);
	
}

👉 MemberMapper.xml

<resultMap id="loginMemberResultMap" type="com.greedy.security.member.model.dto.MemberDTO">
		<id property="no" column="MEMBER_NO"/>
		<result property="id" column="MEMBER_ID"/>
		<result property="pwd" column="MEMBER_PWD"/>
		<result property="tempPwdYn" column="TEMP_PWD_YN"/>
		<result property="pwdChangedDatetime" column="PWD_CHANGED_DATETIME"/>
		<result property="pwdExpDate" column="PWD_EXP_DATE"/>
		<result property="name" column="MEMBER_NAME"/>
		<result property="registDatetime" column="MEMBER_REGIST_DATETIME"/>
		<result property="accumLoginCount" column="ACCUM_LOGIN_COUNT"/>
		<result property="loginFailedCount" column="LOGIN_FAILED_COUNT"/>
		<result property="accLockYn" column="ACC_LOCK_YN"/>
		<result property="accInactiveYn" column="ACC_INACTIVE_YN"/>
		<result property="accExpDate" column="ACC_EXP_DATE"/>
		<result property="accExpYn" column="ACC_EXP_YN"/>
		<result property="accSecessionDatetime" column="ACC_SECESSION_DATETIME"/>
		<result property="accSecessionYn" column="ACC_SECESSION_YN"/>
		
		<!-- 1대 다의 관계에서는, collection을 사용 (미리 MemberDTO의 필드에 memberRoleList 선언) -->
		<collection property="memberRoleList" resultMap="memberRoleResultMap"/>
	</resultMap>
	
	<resultMap type="com.greedy.security.member.model.dto.MemberRoleDTO" id="memberRoleResultMap">
		<id property="memberNo" column="REF_MEMBER_NO"/>
		<id property="authorityCode" column="REF_AUTHORITY_CODE"/>
		
		<!-- 11의 관계에서는 association을 사용 -->
		<association property="authority" resultMap="authorityResultMap"/>
	</resultMap>
	
	<resultMap type="com.greedy.security.member.model.dto.AuthorityDTO" id="authorityResultMap">
		<id property="code" column="REF_AUTHORITY_CODE2"/>
		<result property="name" column="AUTHORITY_NAME"/>
		<result property="desc" column="AUTHORITY_DESC"/>
	</resultMap>

	<select id="findMemberById" resultMap="loginMemberResultMap">
		SELECT
		       A.MEMBER_NO
		     , A.MEMBER_ID
		     , A.MEMBER_PWD
		     , A.TEMP_PWD_YN
		     , A.PWD_CHANGED_DATETIME
		     , A.PWD_EXP_DATE
		     , A.MEMBER_NAME
		     , A.MEMBER_REGIST_DATETIME
		     , A.ACCUM_LOGIN_COUNT
		     , A.LOGIN_FAILED_COUNT
		     , A.ACC_LOCK_YN
		     , A.ACC_INACTIVE_YN
		     , A.ACC_EXP_DATE
		     , A.ACC_EXP_YN
		     , A.ACC_SECESSION_DATETIME
		     , A.ACC_SECESSION_YN
		     , B.MEMBER_NO REF_MEMBER_NO
		     , B.AUTHORITY_CODE REF_AUTHORITY_CODE
		     , C.AUTHORITY_CODE REF_AUTHORITY_CODE2
		     , C.AUTHORITY_NAME
		     , C.AUTHORITY_DESC
		  FROM TBL_MEMBER A
		  LEFT JOIN TBL_MEMBER_ROLE B ON (A.MEMBER_NO = B.MEMBER_NO)
		  LEFT JOIN TBL_AUTHORITY C ON (B.AUTHORITY_CODE = C.AUTHORITY_CODE)
		 WHERE A.MEMBER_ID = #{ username }
	</select>

👉 MessageConfig.java

MessageSource setBasename() setDefaultEncoding() MessageSourceAccessor

@Configuration
public class MessageConfig {
	
	/* MessageSource 
	 * : code, arguments를 통해 .properties에 저장 된 메세지를 읽어 동적으로 메세지를 만들어 주는 역할을 하는 클래스 
	 * 스프링에서는 다양한 구현체 클래스를 사용하는데 가장 자주 사용되는 것은 ReloadableResourceBundleMessageSource 클래스
	 * */
	
	@Bean
	public MessageSource messageSource() {
		
		ReloadableResourceBundleMessageSource messageSource = new ReloadableResourceBundleMessageSource();
		messageSource.setBasename("classpath:/messages/message");
		messageSource.setDefaultEncoding("UTF-8");
		
		return messageSource;
	}
	
	/* MessageSourceAccessor 
	 * : 사용자가 MessageSource 기능을 편리하게 사용할 수 있도록 구현 된 클래스 
	 * 	DefaultLocale을 멤버 변수로 가지고 해당 Locale 값을 읽어옴 
	 * 	해당하는 코드의 메세지가 없을 경우 ""값을 전달하여 NullPointerException을 방지
	 * 	(null 값 대신 빈 문자열을 반환하기 때문)
	 * */
	@Bean
	public MessageSourceAccessor messageSourceAccessor() {
		return new MessageSourceAccessor(messageSource()); 
		/* 위의 messageSource() 메소드를 생성자로 전달 
		 * 결과적으로 직접 작성한 설정대로 MessageSourceAccessor 사용 가능 */
	}	
}

👉 Message_ko_KR.properties

error.login=\uB85C\uADF8\uC778\uC5D0 \uC2E4\uD328\uD588\uC2B5\uB2C8\uB2E4\uD83D\uDE13 \uC544\uC774\uB514\uC640 \uBE44\uBC00\uBC88\uD638\uB97C \uD655\uC778\uD574\uC8FC\uC138\uC694!
error.denied=\uD5C8\uC6A9 \uB418\uC9C0 \uC54A\uC740 \uC694\uCCAD\uC774\uC5D0\uC694\uD83E\uDD2F

👉 ErrorController.java

RedirectAttributes addFlashAttribute() getMessage()

@Controller
@RequestMapping("/error")
public class ErrorController {
	
	/* 의존성 주입 */
	private final MessageSourceAccessor messageSourceAccessor;
	
	public ErrorController(MessageSourceAccessor messageSourceAccessor) {
		this.messageSourceAccessor = messageSourceAccessor;
	}

	
	@PostMapping("/login")
	public String loginFailed(RedirectAttributes rttr) {
		
		/* 빈을 등록 후, alert로 띄울 메세지 properties로 부터 읽어와서 처리 */
		rttr.addFlashAttribute("message", messageSourceAccessor.getMessage("error.login"));
		
		return "redirect:/member/login";
	}
	
	@GetMapping("/denied")
	public String accessDenied(RedirectAttributes rttr) {
		
		/* 빈을 등록 후, alert로 띄울 메세지 properties로 부터 읽어와서 처리 */
		rttr.addFlashAttribute("message", messageSourceAccessor.getMessage("error.denied"));
		
		return "redirect:/";
	}
}

👉 MainController.java

@Controller
public class MainController {

	@GetMapping(value = {"/", "/main"})
	public String main() {
		return "main/main";
	}
	
	/* 리턴방식이 post방식일 경우, 
	 * '성공 시 랜딩 페이지 설정(successForwardUrl)', 
	 * '실패 시 랜딩 페이지 설정(failureForwardUrl)' 등을 위해 PostMapping 추가 */
	@PostMapping(value="/")
	public String redirectMain() {
		return "redirect:/"; /* redirect하여 main으로 이동할 수 있게끔 */
	}
	
}

👉 main.html

&& sec:authorize isAuthenticated() isAnonymous() hasRole() sec:authentication principal ``

<!DOCTYPE html>
<html xmlns:th="http://www.thymeleaf.org"
	  xmlns:sec="http://www.thymeleaf.org/thymeleaf-extras-springsecurity5">
<head>
<meta charset="UTF-8">
<title>main</title>
<style>
h1 { font-weight:bolder; color:salmon; }
button { background:salmon; color:white; font-weight:bolder; border:none; cursor:pointer; border-radius:10px; padding:10px 20px; }
button:hover { background:#fdf8be; color:salmon; }
</style>
<script>
	/* && 연산자 : 왼쪽 피연산자가 false면 오른쪽 피연산자도 false가 되기 때문에,
	전달되는 message가 없다면 alert창이 실행되지 않을 것 (message가 있을 때만 alert창 실행)*/
	const message = "[[${message}]]";
	message && alert(message);
</script>
</head>
<body>

	<h1 align="center">🎉 Welcome to Spring Security Project🕺 🎉</h1>
	<div align="right">
	
		<!-- isAuthenticated() : 인증(로그인) 되어 있는지 확인 -->
		<th:block sec:authorize="isAuthenticated()"> <!-- 로그인 했을 때만 보임 -->
			<h3><span sec:authentication="principal"></span>님 환영해요🥳</h3>
			<h3><span sec:authentication="principal.username"></span>님 환영해요🥳</h3>
			<h3><span sec:authentication="principal.name"></span>님 환영해요🥳</h3>
			<h3><span th:text="${ #authentication.principal.no }"></span>님 환영해요🥳</h3>
			<h3><span th:text="${ #dates.format(#authentication.principal.registDate, 'yyyy-MM-dd') }"></span>님 환영해요🥳</h3>
			<button onclick="location.href='/member/mypage'">마이페이지</button>
			<button onclick="location.href='/member/logout'">로그아웃</button>
		</th:block>
		<!-- isAnonymous() : 인증(로그인) 되어 있지 않은지 확인 -->
		<th:block sec:authorize="isAnonymous()">
			<h3>로그인 해주세요🐥</h3>
			<button onclick="location.href='/member/login'">로그인</button>
		</th:block>
	</div>
	
	<button onclick="location.href='/menu/list'">메뉴 보기</button> <!-- 모든 사람에게 보임 -->
	<th:block sec:authorize="hasRole('MEMBER')"> <!-- MEMBER일 때만 보임 -->
		<button onclick="location.href='/order'">주문 하기</button>
	</th:block>
	<th:block sec:authorize="hasRole('ADMIN')"> <!-- ADMIN일 때만 보임 -->
		<button onclick="location.href='/admin/dashboard'">관리자HOME</button>
	
	</th:block>
	
</body>
</html>

👉 MemberController.java

@AuthenticationPrincipal

@Slf4j
@Controller
@RequestMapping("/member")
public class MemberController {

	@GetMapping("/login")
	public void loginForm() {}
	
	/* @AuthenticationPrincipal : 세션 객체에 저장되어 있는 로그인한 사람의 대한 정보를 받아올 수 있음 !!! */
	@GetMapping("/mypage")
	public void mypage(@AuthenticationPrincipal CustomUser user) {
		
		log.info("로그인 유저 정보 : {}", user);
	}
}

👉 login.html

<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<title>Insert title here</title>
<style>
h1 { font-weight:bolder; color:salmon; }
button { background:salmon; color:white; font-weight:bolder; border:none; cursor:pointer; border-radius:10px; padding:10px 20px; }
button:hover { background:#fdf8be; color:salmon; }
</style>
<script>
	/* && 연산자 : 왼쪽 피연산자가 false면 오른쪽 피연산자도 false가 되기 때문에,
	전달되는 message가 없다면 alert창이 실행되지 않을 것 (message가 있을 때만 alert창 실행)*/
	const message = "[[${message}]]";
	message && alert(message);
</script>
</head>
<body>

	<!-- 
		"/member/login" 요청을 로그인 요청으로 설정해두었기 때문에 스프링 시큐리티 필터에서는 이 요청을 가로챔.
		userDetailsService의 용도로 AuthenticationService를 사용하겠다고 설정했으므로 loadUserByUserName 메소드가 호출됨
		스프링 시큐리티 필터 내에서 id, password를 다루는 이름이 "username", "password"로 설정되어 있으므로
		해당 값을 넘길 때, name 속성이 다른 명칭으로 설정되지 않도록 유의
	 -->

	<h1 align="center">로그인 페이지</h1>
	<div align="center">
		<form action="/member/login" method="post">
			<div>
				<span>아이디 : </span>
				<input type="text" name="username"> <!-- name 속성 변경 X -->
			</div>
			<div>
				<span>비밀번호 : </span>
				<input type="password" name="password"> <!-- name 속성 변경 X -->
			</div><br>
			<div>
				<button>로그인</button>
			</div>
		</form>
	</div>
	
</body>
</html>

👉 AdminController.java

@Controller
@RequestMapping("/admin")
public class AdminController {

	@GetMapping("/dashboard")
	public String getDashBoard() {
		
		return "admin/dashboard";
	}
}

👉 dashboard.html

<h1 align="center">관리자 대쉬보드 😎</h1>

👉 MenuController.java

@Controller
@RequestMapping("/menu")
public class MenuController {
	
	@GetMapping("/list")
	public String findMenuList() {
		
		return "menu/list";
	}
}

👉 list.html

<h1 align="center">메뉴 리스트📃</h1>

👉 OrderController.java

@Controller
public class OrderController {
	
	@GetMapping("/order")
	public String orderPage() {
		
		return "order/order";
	}
}

👉 order.html

<h1 align="center">주문 페이지🍑</h1>


👀 종합적인 기능

profile
Tiny little habits make me

0개의 댓글