728x90
반응형
📒 Servlet / JSP / JDBC
📕 개념
- SSR (Server Side Rendering)
- Servlet / JSP 사용
- JDBC (Java DataBase Connectivity) : 자바 프로그램안에서 DB와 연결하는 라이브러리 (J2SE)
- J2SE (standard edition) : local에서 단독으로 동작 (stand alone program)
- 내부에 인터페이스만 존재 (표준 스펙)
- 그 안에 구현체는 DB vendor가 작성
- MyBaits : JDBC의 확장판 (개발자는 XML 파일에 SQL문만 작성)
- JPA (Java Persistence Api) : (J2EE)
- J2EE (enterprise edition) : Dynamic 웹 등이 포함되어 서버가 필요로 할 때 사용 (server based program)
- 내부에 인터페이스만 존재 (표준 스펙)
- 객체를 메모리 뿐만 아니라 DB에 저장
- Hibernate(하이버네이트)가 JPA의 구현체
- Spring Data JPA (Hibernate를 쉽게 사용하기 위함)
- Static Web (정적으로 html만 동작) / Dynamic Web (동적으로 DB와 연결되어 동작)
- Maven / Gradle
- MVC (Model View Controller 패턴)
- Seperation of Responsibility(Concerns) 책임의 분리 (관심사의 분리)
- View : 사용자가 보는 화면 (HTML, CSS, JSP)
- Model : 데이터 (JAVA - value object, service object, data access object 포함)
- Controller : 화면에서 넘어오는 데이터를 받아 넘겨주는 역할 (Servlet - view와 model 간의 연결)
- Servlet : Java 코드 내부에 HMTL 코드를 포함시킬 수 있음 (java 메인)
- JSP : HTML 코드 내부에 Java 코드를 포함시킬 수 있음 (HTML 메인)
- 내부 동작 원리는 JSP를 Servlet으로 변환시켜 동작
- 설치
- STS
- JDK8 / STS(Spring Tool Suite) / Tomcat8.5
- c:\sts3.9-bundle 폴더명이 생성되도록 unzip
- STS.ini로 설정 경로 확인
- STS.exe 실행
- MariaDB 10.3 install
- root 패스워드 maria/maria
- utf-8 charset 체크 박스(Use UTF8 as default server's character set) 반드시 선택
- Location : C:\Program Files\MariaDB 10.3\
- TCP Port : 3306
- 내pc -> 관리 -> 서비스 및 응용 프로그램
- MySQL 실행 확인 가능
- MySQL 실행 확인 가능
- STS
- 기능적인 요구 사항 (Functional Requirements)
- 업무
- 비기능적인 요구 사항 (Non-Functional Requirements)
- 인증, 로깅, 트랜잭션 처리, thread pooling, connection pooling
📕 10/05
- import > general > Existing Project into WorkSpaces
Select archive file > ServeletJSPProject.zip - Spring Starter Project : Spring Boot
- Java Project : Java (MariaDB를 사용하기 위해 .jar 파일 첨부 필요)
- Maven Project : npm같은 역할 (POM.xml파일에 원하는 라이브러리 dependency를 작성하면 자동 다운로드)
- Dynamic Web Project : Web
- 내장된 Tomcat 사용
- Dynamic web module version : Servlet 버전
- Generate web.xml deployment descriptor 체크
- sql 추가
- package 추가
- jdbc -> DBConn.java
- java 코드 상단에 package jdbc; 추가
# root 계정으로 접속하여 사용자 계정과 DB 생성
mysql -u root –p
maria 입력 // password 입력
MariaDB [(none)]> show databases; // 데이터베이스 목록 확인
MariaDB [(none)]> use mysql; // mysql DB 사용
MariaDB [mysql]> create database boot_db; // boot_db DB 생성
MariaDB [mysql]> CREATE USER 'boot'@'%' IDENTIFIED BY 'boot'; // boot user 생성, boot password 지정
MariaDB [mysql]> GRANT ALL PRIVILEGES ON boot_db.* TO 'boot'@'%'; // boot DB의 권한 허용
MariaDB [mysql]> flush privileges; // grant 사용시 권한 적용을 위한 명령어
MariaDB [mysql]> select user, host from user; // 계정 조회, user는 system table
MariaDB [mysql]> exit; // 접속 종료
# boot 사용자 계정으로 접속한다.
mysql -u boot –p
boot 입력 // password 입력
use boot_db;
- MySQL Client 실행
- 자동으로 root로 접속됨
- boot 사용자로 접속
- 자동으로 root로 접속됨
- path 설정
- 내PC -> 속성 -> 고급 시스템 설정 -> 시스템 속성(고급) -> 환경변수(편집) -> 새로만들기 -> 경로 추가 -> 맨 위로 경로 이동
- C:\Program Files\MariaDB 10.3\bin
- user.sql
- open with -> text editor
- 한글 깨질 경우 : properties 이동해서 Text file encoding을 UTF-8로 변경
create table users(
id int(10) not null auto_increment primary key, // auto-increment : 자동으로 sequence한 값 증가, primary key : 기본키
userid varchar(100) not null ,
name varchar(100) not null ,
gender varchar(10),
city varchar(100)
);
alter table users add unique index users_userid_idx(userid); // unique : 중복 안됨
show index from users;
insert into users(userid,name,gender,city) values ('gildong','홍길동','남','서울');
commit;
insert into users(userid,name,gender,city) values ('dooly','둘리','여','부산');
commit; // mariaDB는 자동 commit
- DBConn.java
package jdbc;
import java.sql.Connection;
import java.sql.DriverManager;
import java.sql.PreparedStatement;
import java.sql.ResultSet;
import java.sql.SQLException;
public class DBConn {
public static void main(String[] args) {
final String driver = "org.mariadb.jdbc.Driver"; // java.sql의 driver를 구현한 것 (mariadb jar 안에 존재)
final String DB_IP = "localhost";
final String DB_PORT = "3306";
final String DB_NAME = "boot_db"; // DB이름으로 변경
final String DB_URL =
"jdbc:mariadb://" + DB_IP + ":" + DB_PORT + "/" + DB_NAME;
Connection conn = null;
PreparedStatement pstmt = null;
ResultSet rs = null;
try {
// mariaDB의 경우 new Driver(), oracle의 경우 newOracleDriver()로 사용
// new를 사용할 경우 벤더 중립적이 되지 못함
// 기존 코드는 따로 설정 파일로 뺄 수도 있음
// 1. Driver class Loading
Class.forName(driver);
System.out.println("DB_URL = " + DB_URL);
// 2. DB와 연결을 담당하는 Connection 객체 생성
conn = DriverManager.getConnection(DB_URL, "boot", "boot");
System.out.println("Connection className = " + conn.getClass().getName());
// Connection className = org.mariadb.jdbc.MariaDbConnection
if (conn != null) {
System.out.println("DB 접속 성공");
}
} catch (ClassNotFoundException e) { // class 예외 처리
System.out.println("드라이버 로드 실패");
e.printStackTrace();
} catch (SQLException e) { // getConnection 예외 처리
System.out.println("DB 접속 실패");
e.printStackTrace();
}
try {
// String sql = "select * from users"; // 테이블명 변경
String sql = "select * from users where userid = ?"; // where문 추가
// 3. SQL문을 DB에게 전달해주는 역할을 하는 Statement 생성
pstmt = conn.prepareStatement(sql);
System.out.println("Statement Class Name = " + pstmt.getClass().getName());
// Statement Class Name = org.mariadb.jdbc.ClientSidePreparedStatement
pstmt.setString(1, "dooly"); // parameter index 1부터 시작 (? 처리 : preapareStatement의 set변수타입 설정)
// 4. SQL문 실행결과를 담는 역할을 하는 ResultSet 생성
rs = pstmt.executeQuery();
System.out.println("ResultSet Class Name = " + rs.getClass().getName());
// ResultSet Class Name = org.mariadb.jdbc.internal.com.read.resultset.SelectResultSet
String userId = null;
String name = null;
String gender = null;
String city = null;
while (rs.next()) { // 메모리의 ResultSet 접근 (다 읽으면 true -> false 출력 )
userId = rs.getString("userid"); // 컬럼명 변경 (컬럼 index도 가능)
name = rs.getString("name"); // getString : 해당 컬럼의 값 가져오기
gender = rs.getString("gender");
city = rs.getString("city");
System.out.print(userId);
System.out.print(name);
System.out.print(gender);
System.out.print(city);
System.out.println();
}
} catch (SQLException e) {
System.out.println("error: " + e);
} finally {
try {
if (rs != null) {
rs.close();
}
if (pstmt != null) {
pstmt.close();
}
if (conn != null && !conn.isClosed()) {
conn.close();
}
} catch (SQLException e) {
e.printStackTrace();
}
}
}
}
- customer 테이블 생성
- id (auto-increment)
- name
- email (unique)
- age
- address
- entryDate
create table customer(
id int(10) not null auto_increment primary key,
name varchar(100) not null,
email varchar(100) not null,
age int(10),
addr varchar(100),
entryDate date,
UNIQUE KEY uk_name (email)
);
// alter table customer add unique index customer_email_idx(email);
alter table customer add unique(id);
- java8 api document
- java.sql [driver] : Class.forName(driver);(https://docs.oracle.com/javase/8/docs/api/)
- ➡️ Class.forName(driver);
- java.sql connection
- Connection n = new Oracle Connection() 할 수도 있지만 특정 DB에 종속됨
- ➡️ Connection conn = DriverManager.getConnection(DB_URL, "boot", "boot");
- javax.sql : extension
- java.lang class
- java.sql driver manager
- java.sql statement
- ➡️ PreparedStatement pstmt = conn.prepareStatement(sql);
- [resultset]
- ➡️ ResultSet rs = pstmt.executeQuery();
- java.sql [driver] : Class.forName(driver);(https://docs.oracle.com/javase/8/docs/api/)
- JDBC
- Driver (interface)
- Connection
- Statement
- ResultSet
- DB vendor 구현
- Driver / Oracle Driver
- MySQL Connection / Oracle Connection
- MariaDB Statement / Oracle Statement
- MariaDB ResultSet / Oracle ResultSet
- Mybatis 사용시 해당 쿼리문만 xml에 작성해도 됨
- MemberDAO.java
package chap10.dao;
import chap10.entity.Member;
import common.DBService;
import java.sql.*;
public class MemberDAO {
public boolean insertMember(Member member) {
boolean result = false;
int rowcnt = 0;
Connection conn = null;
Statement stmt = null;
// 1. Connection Pool에서 connection 얻기
conn = DBService.getConnection();
try {
//2. Statememt 객체 생성하기
stmt = conn.createStatement();
//3. Query 작성
String query = "INSERT INTO CS_MEMBER VALUES('" +
member.getTfMemberID() + "','" +
member.getTfName() + "','" +
member.getTfPassword() + "','" +
member.getTfAddress() + "','" +
member.getTfPhone() + "','" +
member.getSelPasswordQuestion() + "','" +
member.getTfPasswordAnswer() + "','" +
member.getRdMarriage() + "','" +
member.getChkHobby() + "','" +
member.getEtc() +"')";
//4. SQL문전송하기
rowcnt = stmt.executeUpdate( query );
//5. 결과 처리
if ( rowcnt > 0 )
result = true;
else
System.out.println ( "insert시 에러발생" );
}
catch( Exception e ) {
System.out.println( e );
}
finally {
// 6. Statement close
if ( stmt != null ) {
try {
stmt.close();
} catch ( SQLException e ) {
e.printStackTrace();
}
}
//7. DBService에게 connection 반환하기
DBService.releaseConnection( conn );
}
return result;
}
}
// Connection Pool : 미리 필요한 만큼 Connection을 미리 만들어 둠
// public void startServer() {
// System.out.println("2. Connection Pool Init");
//
// try {
// connectionPool = new ConnectionPool(
// jdbcUrl, jdbcUserID, jdbcPassword,
// initNumConnection, maxNumConnection,
// true, 10000
// );
// } catch(Exception e) {
// System.out.println("__ Pool Init Fail: " + e.getMessage());
// }
// }
- vo package 생성
- UserVO.java 생성
- 기본 생성자 생성
- argument 인자 받는 생성자 Source -> Generate Constructor Using Fields
- getters만 생성
- UserVO.java 생성
package vo;
public class UserVO {
private int id;
private String UserId;
private String name;
private String gender;
private String city;
public UserVO() { // 기본 생성자
}
public UserVO(int id, String userId, String name, String gender, String city) { // argument 인자 받는 생성자
super();
this.id = id;
UserId = userId;
this.name = name;
this.gender = gender;
this.city = city;
}
public int getId() {
return id;
}
public String getUserId() {
return UserId;
}
public String getName() {
return name;
}
public String getGender() {
return gender;
}
public String getCity() {
return city;
}
}
- dao package 생성
- main 상태로는 웹 브라우저상에 뿌려줄 수 없어서 DAO 객체로 변환
- DB를 읽어 VO의 객체에 담아서 서블렛에 전달
- UserDAO.java 생성
- 생성자에서 받아서 쓰기 때문에 객체 선언 및 생성자 필요X
- Extract Local Variable (구문 선택 + alt + shift + L)
- connection.prepareStatement(sql); ➡️ PreparedStatement pStmt = connection.prepareStatement(sql);
- pStmt.executeQuery(); ➡️ ResultSet rs = pStmt.executeQuery();
package dao;
import java.sql.Connection;
import java.sql.DriverManager;
import java.sql.PreparedStatement;
import java.sql.ResultSet;
import java.sql.SQLException;
import java.util.ArrayList;
import java.util.List;
import vo.UserVO;
public class UserDAO {
private Connection connection;
public UserDAO(String driverClass, String url, String username, String password) {
// 1. Driver class Loading (1번만 실행 필요)
try {
Class.forName(driverClass);
// 2. DB와 연결을 담당하는 Connection 객체 생성
connection = DriverManager.getConnection(url, username, password);
} catch (Exception e) {
e.printStackTrace();
}
}
public void connectionClose() { // // connection은 close 필요
try {
if (connection != null) connection.close();
} catch (SQLException e) {
e.printStackTrace();
}
}
// where 조건 조회
public UserVO getUser(String userId) {
PreparedStatement pStmt = null;
UserVO userVO = null;
String sql = "select * from users where userid = ?";
// 3. SQL문을 DB에게 전달해주는 역할을 하는 Statement 생성
try {
pStmt = connection.prepareStatement(sql);
pStmt.setString(1, userId);
ResultSet rs = pStmt.executeQuery();
if (rs.next()) {
userVO = new UserVO(rs.getInt("id"),
rs.getString("userId"),
rs.getString("name"),
rs.getString("gender"),
rs.getString("city"));
}
} catch (SQLException e) {
e.printStackTrace();
} finally {
try {
if (pStmt != null) pStmt.close();
} catch (SQLException e) {
e.printStackTrace();
}
}
return userVO;
}
// 목록 조회
public List<UserVO> getUserList() {
PreparedStatement pStmt = null;
List<UserVO> userList = new ArrayList<>();
String sql = "select * from users order by id";
try {
pStmt = connection.prepareStatement(sql);
ResultSet rs = pStmt.executeQuery();
while (rs.next()) {
UserVO userVO = new UserVO(rs.getInt("id"),
rs.getString("userId"),
rs.getString("name"),
rs.getString("gender"),
rs.getString("city"));
userList.add(userVO);
}
} catch (SQLException e) {
e.printStackTrace();
} finally {
try {
if (pStmt != null) pStmt.close();
} catch (SQLException e) {
e.printStackTrace();
}
}
return userList;
}
}
- Open Perspertive
- Java EE 선택 (Spring에서 바꾸기)
- Window -> Preferences -> Web -> JSP Files -> Encoding (ISO 10646/Unicode(UTF-8))
- index.jsp 생성
<%@ page language="java" contentType="text/html; charset=UTF-8"
pageEncoding="UTF-8"%>
// contentType : 브라우저상에 응답 결과에 대한 content type
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<title>Main Page</title>
</head>
<body>
<h1>사용자 관리</h1>
<ul>
<li><a href="userList.do" >사용자 목록</a></li> // a태그에 url-pattern 작성
</ul>
</body>
</html>
- 구조
- DB (users) ➡️ <Model> UserDAO (getUser, getUserList) ➡️ <Model> UserVO에 담아서 전달 ➡️ <Controller> UserListServlet ➡️ <View> userList.jsp 화면에 출력
- <View> index.jsp 사용자 목록 확인
- 웹 컨테이너가 객체 생성 (servlet 클래스와 패키지명을 web.xml에 세팅해서 서블릿에서 읽어감)
- @WebServlet("/userList") : 어노테이션 주석 처리
- 대신 xml로 세팅
- WebContent -> WEB-INF -> web.xml
- xml schema 역할 : servlet 정의할 때 xml 태그명이나 속성, 태그 순서 등 xml 규칙을 스키마로 사전 정의됨
- web.xml
<?xml version="1.0" encoding="UTF-8"?>
<web-app xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns="http://xmlns.jcp.org/xml/ns/javaee"
xsi:schemaLocation="http://xmlns.jcp.org/xml/ns/javaee http://xmlns.jcp.org/xml/ns/javaee/web-app_3_1.xsd"
id="WebApp_ID" version="3.1">
<display-name>MyDynamicWe</display-name>
<welcome-file-list>
<welcome-file>index.html</welcome-file>
<welcome-file>index.htm</welcome-file>
<welcome-file>index.jsp</welcome-file>
<welcome-file>default.html</welcome-file>
<welcome-file>default.htm</welcome-file>
<welcome-file>default.jsp</welcome-file>
</welcome-file-list>
<servlet>
<servlet-name>UserListServlet</servlet-name>
<servlet-class>controller.UserListServlet</servlet-class>
<init-param>
<param-name>driverClass</param-name>
<param-value>org.mariadb.jdbc.Driver</param-value>
</init-param>
<init-param>
<param-name>dbUrl</param-name>
<param-value>jdbc:mariadb://localhost:3306/boot_db</param-value>
</init-param>
<init-param>
<param-name>dbUsername</param-name>
<param-value>boot</param-value>
</init-param>
<init-param>
<param-name>dbPassword</param-name>
<param-value>boot</param-value>
</init-param>
</servlet>
<servlet-mapping>
<servlet-name>UserListServlet</servlet-name>
<url-pattern>*.do</url-pattern>
</servlet-mapping>
</web-app>
- 실행
- tomcat -> Add and Remove -> MyDynamicWe를 configured로 이동 -> start the server
- index.jsp -> Run As -> Run On Server (ctrl + f11)
- window -> web browser -> chrome 선택
- properties -> Web Project Settings -> myweb으로 변경
- controller 패키지 생성
- UserListServelet.java 생성
- 규칙이 HTTPServlet을 반드시 상속받음
- URL mapping : /userList
package controller;
import java.io.IOException;
import javax.servlet.ServletConfig;
import javax.servlet.ServletException;
import javax.servlet.annotation.WebServlet;
import javax.servlet.http.HttpServlet;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
/**
* Servlet implementation class UserListServlet
*/
// @WebServlet("/userList")
public class UserListServlet extends HttpServlet {
private static final long serialVersionUID = 1L;
/**
* @see HttpServlet#HttpServlet()
*/
public UserListServlet() {
super();
}
@Override
public void init(ServletConfig config) throws ServletException {
System.out.println(">> init");
String driver = config.getInitParameter("driverClass"); // org.mariadb.jdbc.Driver (web.xml의 param.value)
String url = config.getInitParameter("dbUrl");
String username = config.getInitParameter("dbUsername");
String password = config.getInitParameter("dbPassword");
System.out.println(driver);
System.out.println(url);
System.out.println(username);
System.out.println(password);
}
/**
* @see HttpServlet#doGet(HttpServletRequest request, HttpServletResponse response)
*/
protected void doGet(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
System.out.println(">> doGet");
// response.getWriter() : 응답에 대한 string 생성
response.getWriter().append("Served at: ").append(request.getContextPath());
}
@Override
public void destroy() {
System.out.println(">> destroy");
super.destroy();
}
}
- UserDAO 객체 사용
package controller;
import java.io.IOException;
import java.util.List;
import javax.servlet.ServletConfig;
import javax.servlet.ServletException;
import javax.servlet.annotation.WebServlet;
import javax.servlet.http.HttpServlet;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import dao.UserDAO;
import vo.UserVO;
/**
* Servlet implementation class UserListServlet
*/
// @WebServlet("/userList")
public class UserListServlet extends HttpServlet {
private static final long serialVersionUID = 1L;
UserDAO userDao;
/**
* @see HttpServlet#HttpServlet()
*/
public UserListServlet() {
super();
}
@Override
public void init(ServletConfig config) throws ServletException {
System.out.println(">> init");
String driver = config.getInitParameter("driverClass"); // org.mariadb.jdbc.Driver (web.xml의 param.value)
String url = config.getInitParameter("dbUrl");
String username = config.getInitParameter("dbUsername");
String password = config.getInitParameter("dbPassword");
System.out.println(driver);
System.out.println(url);
System.out.println(username);
System.out.println(password);
userDao = new UserDAO(driver, url, username, password); // 객체 대입
}
/**
* @see HttpServlet#doGet(HttpServletRequest request, HttpServletResponse response)
*/
protected void doGet(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
System.out.println(">> doGet");
// response.getWriter() : 응답에 대한 string 생성
response.getWriter().append("Served at: ").append(request.getContextPath());
List<UserVO> userList = userDao.getUserList(); // 객체 값 부르기
System.out.println(userList);
}
@Override
public void destroy() {
System.out.println(">> destroy");
super.destroy();
}
}
- UserVO.java 수정
- Generate to String() 추가
@Override public String toString() { return "UserVO [id=" + id + ", UserId=" + UserId + ", name=" + name + ", gender=" + gender + ", city=" + city + "]"; }
- 화면에 사용자 목록 뿌려줄 jsp 파일 생성
- userList.jsp
- DB에서 불러온 userList 객체를 넘겨줘야하는데 쿼리 ? 형식으로 userList.jsp로 넘겨줄 수 없음
- Request 요청을 통해 setAttribute/getAttribute 처리 필요
- RequestDispatcher의 forward(req, res) 메서드를 통해 포워딩되도록 함
- Servlet Request : setAttribute/getAttribute 메서드 (key/value를 string으로 가지고, object 객체 저장)
- Request Dispatcher : forward, include 메서드 (resource를 보내줌)
- Request Dispatcher는 인터페이스라 생성X
- Request Dispatcher를 생성해서 반환해주는 Servlet의 getRequestDispatcher 메서드 사용
- UserListServlet.java 수정
protected void doGet(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
System.out.println(">> doGet");
// response(응답) 데이터를 utf-8로 인코딩
response.setContentType("text/html; charset=UTF-8");
// response.getWriter() : 응답에 대한 string 생성
response.getWriter().append("Served at: ").append(request.getContextPath());
// UserDAO를 호출해서 DB 데이터를 가져오기
List<UserVO> userList = userDao.getUserList();
// Request 객체에 userList를 저장하기
request.setAttribute("users", userList); // key : users, value : userList
// RequestDispatcher 생성하기
RequestDispatcher dispatcher = request.getRequestDispatcher("userList.jsp"); //userList.jsp 포워딩 필요
// userList.jsp 페이지로 포워딩하기
dispatcher.forward(request, response); // 전달 받은 인자값 그대로 forward
}
- userList.jsp 수정
<%@ page language="java" contentType="text/html; charset=UTF-8"
pageEncoding="UTF-8"%>
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<title>Insert title here</title>
</head>
<body>
<h2>사용자 리스트</h2>
${users}
<!-- ${users}는 key값이고, 하단과 동일-->
<!-- (List<UserVO> request.getAttribute("users"); -->
</body>
</html>
- JSTL (Java Standard Tag Library)
- 기존 코드 방식
<table>
< % for(UserVO user:userList) { %>
<tr>
<td><%=user.getName()%></td>
</tr>
<% } % > - script tag와 expression tag를 java 코드로 표현
- JSTL.jar 필요 (WebContent/WEB-INF/lib에 위치)
- JSTL 반복문
- user.jsp 수정
- 기존 코드 방식
<%@ page language="java" contentType="text/html; charset=UTF-8"
pageEncoding="UTF-8"%>
<%@ taglib prefix="c" uri="http://java.sun.com/jsp/jstl/core" %> // corlibrary 사용을 위해 상단에 코드 추가
사용자 리스트
<%-- ${users} --%>
<%-- user는 UserVO를 의미 --%>
ID | UserId | Name | Gender | City |
---|---|---|---|---|
${user.id} | ${user.userId} | ${user.name} | ${user.gender} | ${user.city} |
- 전체 구조 정리
- Data Access Layer
- userDAO (Data Access Object) : DB에 접근하기 위해 JDBC 사용하여 DB 연동
- userVO (View Object) : 객체에 담아서 웹 상에 전달 (객체가 많아 ArrayList에 담음)
- Presentation Layer
- index.jsp : Entry 역할
- UserListServlet : 객체를 view로 화면에 보여주기 위한 Controller
- userList.jsp
- web.xml : DB, user, password 정보 등 환경별 설정 파일 (개발과 운영환경별로 설정 파일 분리 가능)
- init.param의 API는 세팅할 설정들이 ServletConfig 객체에 저장됨
- 해당 정보를 DAO에 넘겨줌
📒 Spring
📕 DI / IoC 개념 (10/09)
- springfwxml.zip
- Spring Configuration XML + Anntation 혼용
- springfwconfi.zip (No XML)
- Java config + Annotation
- myspringfw.zip
- Maven Project Template
- Maven Project Template
- mavem / gradle : 빌드 라이브러리
- xml 형식으로 표현
- Spring framework beans factory
- spring framework github
- bean : 컨테이너가 생성/관리해주는 객체
- beanfactory : 가장 최상위 인터페이스 (컨테이너 역할)
- bean 설정 정보 (=xml)
- getBean : 생성한 bean 가져오는 메서드
- Setter Injection : <property> 태그
- property 태그는 set*을 의미함
- (java파일) setPrinter == (xml파일) property name="printer"
- set제외 + 소문자 변환
- pom.xml
- 버전 바꾸고 싶을 경우 dependency에 설정
- Maven Dependencies 라이브러리 안에 내부적으로 불러다 사용 가능
org.springframework
spring-context
${spring.version}
- SpringFWXml 프로젝트의 src/main/java의 myspring.di.xml 파일 복사
- 개발자가 생성할 필요 없는데 언제 생성되는지 확인하기 위함
- 구체적인 클래스 이름 적지 않고, 인터페이스만 작성 (Printer printer)
- Printer.java
package myspring.di.xml;
public interface Printer {
public void print(String message);
}
- ConsolePrinter.java
package myspring.di.xml;
public class ConsolePrinter implements Printer {
public ConsolePrinter() {
System.out.println(this.getClass().getName() + "생성자 호출됨");
}
public void print(String message) {
System.out.println(message);
}
}
- StringPrinter.java
package myspring.di.xml;
public class StringPrinter implements Printer {
private StringBuffer buffer = new StringBuffer();
public StringPrinter() {
System.out.println(this.getClass().getName() + "생성자 호출됨");
}
public void print(String message) {
this.buffer.append(message);
}
public String toString() {
return this.buffer.toString();
}
}
- src/main/resources 폴더 안에 bean.xml 파일 생성 (Spring Bean Configuration
- beans, context 선택
- 인터페이스 제외하고 3개 선언
- setter 메서드 반드시 필요 (없을 경우 property 에러 발생)
- spring-beans.xml
- BeanFactory
- xml 파일에 어떤 기능을 해야하는지 적어져 있어서 xml 파일 경로 명시해줘야함
BeanFactory factory = new GenericXmlApplicationContext(“classpath:spring-beans.xml “);
ApplicationContext context = new GenericXmlApplicationContext(“classpath:spring-beans.xml “);
UserDao userDao = (UserDao) context.getBean(“userDao”); // getBean의 "userDao"는 Bean 등록할 때의 id값
- jUnit
- 단위 테스트 용도
- 테스트를 지원하는 어노테이션 사용 (@Test, @Before, @After)
- junit jupiter api
- Build Path > Configure Build Path > Libraries 탭 > Add Library > JUnit 선택 (JUnit5 버전)
- src/test/java 폴더에 myspring.di.xml 패키지 생성
- 테스트를 위한 HelloBeansJunitTest.java 클래스 생성
- 반드시 return 타입 void
- 싱글톤인지 확인하기 위해서 reference를 비교하여 동일한지 확인
- assertions 메서드 사용
- HelloBeansJunitTest.java
package myspring.di.xml;
import static org.junit.jupiter.api.Assertions.*; // static 메서드를 테스트케이스에 많이 사용하여 static import를 사용하여 Assertions.생략 가능
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.BeanFactory;
import org.springframework.context.support.GenericXmlApplicationContext;
public class HelloBeansJunitTest {
@Test
void hello() {
// 1. Spring Bean Container 객체 생성
// classpath에 xml 파일 경로 지정
BeanFactory factory = new GenericXmlApplicationContext("classpath:spring-beans.xml");
// 2. Container가 생성한 Bean을 요청하기
Hello hello1 = (Hello) factory.getBean("hello");
Hello hello2 = factory.getBean("hello", Hello.class); // 해당 방법 권장
// 3. HelloBean의 레퍼런스 비교하기
System.out.println(hello1 == hello2); // 싱글톤인지 아닌지 확인 목적
assertSame(hello1, hello2); // 주소가 같으면 테스트 성공으로 표시
assertEquals("Hello 스프링", hello2.sayHello()); // 예상한 값과 일치한지 확인
hello2.print();
Printer printer = factory.getBean("strPrinter", Printer.class);
assertEquals("Hello 스프링", printer.toString());
- bean 객체에 scope 속성 추가
- < bean id="hello" class="myspring.di.xml.Hello" scope="prototype">
-> 객체 항상 생성하여 주소값 다르기 때문에 테스트 false 나옴
- < bean id="hello" class="myspring.di.xml.Hello" scope="prototype">
- bean 객체를 < bean id="hello" class="myspring.di.xml.Hello" scope="singleton"> 하면 테스트 성공
- default가 singleton임
📕 DI / IoC (10/10)
- Construction Injection
- Hello.java 수정
public class Hello {
String name;
Printer printer;
List<String> names;
public Hello() {
System.out.println(this.getClass().getName() + "생성자 호출됨");
}
public Hello(String name, Printer printer) {
System.out.println(this.getClass().getName() + "오버로딩된 생성자 호출됨");
this.name = name;
this.printer = printer;
}
...
}
- spring-beans.xml 수정
<bean id="helloC" class="myspring.di.xml.Hello">
<!-- Constructor Injection -->
<constructor-arg index="0" value="생성자"/>
<constructor-arg index="1" ref="conPrinter"/>
</bean>
- HelloBeanJunitTest.java
package myspring.di.xml;
import static org.junit.jupiter.api.Assertions.*; // static 메서드를 테스트케이스에 많이 사용하여 static import를 사용하여 Assertions.생략 가능
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Disabled;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.BeanFactory;
import org.springframework.context.support.GenericXmlApplicationContext;
public class HelloBeansJunitTest {
BeanFactory factory;
@BeforeEach
void init() {
// 1. Spring Bean Container 객체 생성
// classpath에 xml 파일 경로 지정
factory = new GenericXmlApplicationContext("classpath:spring-beans.xml");
}
@Test
void 생성자주입테스트() {
Hello bean = factory.getBean("helloC", Hello.class);
assertEquals("Hello 생성자", bean.sayHello());
bean.print();
}
@Test @Disabled
void hello() {
// 2. Container가 생성한 Bean을 요청하기
Hello hello1 = (Hello) factory.getBean("hello");
Hello hello2 = factory.getBean("hello", Hello.class); // 해당 방법 권장
// 3. HelloBean의 레퍼런스 비교하기
System.out.println(hello1 == hello2); // 싱글톤인지 아닌지 확인 목적
assertSame(hello1, hello2); // 주소가 같으면 테스트 성공으로 표시
assertEquals("Hello 스프링", hello2.sayHello()); // 예상한 값과 일치한지 확인
hello2.print();
Printer printer = factory.getBean("strPrinter", Printer.class);
assertEquals("Hello 스프링", printer.toString());
}
}
- Collection Injection
- Hello.java 수정
public List<String> getNames() {
return this.names;
}
public void setNames(List<String> list) {
System.out.println("Hello setNames() " + list);
this.names = list;
}
- spring-beans.xml 수정
<bean id="helloC" class="myspring.di.xml.Hello">
<!-- Constructor Injection -->
<constructor-arg index="0" value="생성자"/>
<constructor-arg index="1" ref="conPrinter"/>
<property name="names">
<list>
<value>Spring Framework</value>
<value>Spring Boot</value>
<value>Spring Cloud</value>
</list>
</property>
</bean>
- HelloBeanJunitTest.java
@Test
void 생성자주입테스트() {
Hello bean = factory.getBean("helloC", Hello.class);
assertEquals("Hello 생성자", bean.sayHello());
bean.print();
List<String> names = bean.getNames();
assertEquals(3, names.size());
assertEquals("Spring Boot", names.get(1));
}
- Spring-Test
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-test</artifactId>
<version>${spring.version}</version>
<scope>test</scope>
<!-- scope : test라고 이름 지어진 폴더에만 적용 -->
</dependency>
- HelloBeanSpringTest.java 생성
- @ExtendWith
- Spring Bean Container 객체 생성 대신 @ExtendWith 사용
- 싱글톤의 Application Context 보장
- @ExtendWith(SpringExtension.class)
- @ContextConfiguration
- classpath로 경로 설정 대신 @ContextConfiguration
- 스프링 빈 설정 파일의 위치
- @ContextConfiguration(locations = "classpath:spring-beans.xml")
- @Autowired
- getBean 대신에 @Autowired 의존 받고 싶은 타입을 자동 연결
- type이 동일할 경우 Bean의 변수명과 Bean의 id가 동일한 것을 찾아옴
- 동일한게 없을 경우 찾을 수 없다고 표시
- 변수, setter메서드, 생성자, 일반메서드 적용 가능
- Retention(runtime) : 실행시간에 동작 보장
- @Autowired 없을 경우 객체가 생성되지 않아 NullPointerException 에러 발생
- @Qualifier
- 범위 한정자 (Autowired와 함께 기재)
- @Qualifier("helloC")
- @Resource
- 원하는 Bean의 id 값으로 가져옴
- 변수, setter 메서드에 적용 가능 (type(클래스), field(변수), method)
- Retention(runtime) : 실행시간에 동작 보장
- @Resource(name = "helloC")
- @Override
- 부모의 메서드를 재정의 하는데, 메서드 선언부가 동일한지 체크하는 어노테이션
- @ExtendWith
package myspring.di.xml;
import static org.junit.jupiter.api.Assertions.*;
import javax.annotation.Resource;
import org.junit.jupiter.api.Disabled;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.test.context.ContextConfiguration;
import org.springframework.test.context.junit.jupiter.SpringExtension;
// Spring Bean Container 객체 생성 대신 @ExtendWith 사용 (싱글톤의 Application Context 보장)
// classpath로 경로 설정 대신 @ContextConfiguration (스프링 빈 설정 파일의 위치)
@ExtendWith(SpringExtension.class)
@ContextConfiguration(locations = "classpath:spring-beans.xml")
public class HelloBeanSpringTest {
// 1/ getBean 대신에 @Autowired 의존 받고 싶은 타입을 자동 연결
// type이 동일할 경우 Bean의 변수명과 Bean의 id가 동일한 것을 찾아옴 (동일한게 없을 경우 찾을 수 없다고 표시)
@Autowired
Hello hello;
@Autowired
// 2. Qualifier : 범위 한정자 (Autowired와 함께 기재)
@Qualifier("helloC")
Hello hello2;
// 3. 원하는 Bean의 id 값으로 가져옴
@Resource(name = "helloC")
Hello hello3;
@Autowired
@Qualifier("strPrinter")
Printer printer;
//@Autowired
//StringPrinter printer;
@Test
void helloC() {
assertEquals("Hello 생성자", hello2.sayHello());
assertEquals("Hello 생성자", hello3.sayHello());
}
@Test // @Disabled
void hello() {
assertEquals("Hello 스프링", hello.sayHello()); // import static으로 인해 Assertions.assertEquals와 동일
hello.print();
assertEquals("Hello 스프링", printer.toString());
}
}
- XML 설정 단독 사용
- 어노테이션과 XML 설정 혼합 사용
- XML 설정을 줄이기 위해서 사용 (XML소스 안에다 작성)
- XML 존재 이유
- 프레임워크가 대신 객체를 생성하기 때문에, 해당 인스턴스를 알려줌
- 빈 컨테이너 (GenericXmlApplicationContext)가 객체 생성
- @Value
- @Component : pacakage의 위치 알려줌
- @Component와 @Bean의 차이점
- myspring.di.xml 복사하여 mysping.di.annot로 붙여넣기
- StringPritner.java 수정 : @Component 어노테이션 추가
package myspring.di.annot;
import org.springframework.stereotype.Component;
// target : 클래스 위에 선언할 수 있음
// retention : runtime에 실행됨을 보장
// id 설정 안하면 소문자 클래스명으로 자동 설정됨
@Component("stringPrinter")
public class StringPrinter implements Printer {
private StringBuffer buffer = new StringBuffer();
public StringPrinter() {
System.out.println(this.getClass().getName() + "생성자 호출됨");
}
public void print(String message) {
this.buffer.append(message);
}
public String toString() {
return this.buffer.toString();
}
}
- ConsolePritner.java 수정 : @Component 어노테이션 추가
package myspring.di.annot;
import org.springframework.stereotype.Component;
@Component("consolePrinter")
public class ConsolePrinter implements Printer {
public ConsolePrinter() {
System.out.println(this.getClass().getName() + "생성자 호출됨");
}
public void print(String message) {
System.out.println(message);
}
}
- 어노테이션 추가 후 xml 설정 필요함 (파일에 s가 붙지 않은 것은 아직 spring bean이 되지 않은 것)
- Hello.java 수정
package myspring.di.annot;
import java.util.List;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Component;
// xml파일의 bean 태그와 동일
@Component("helloBean")
public class Hello {
// property 속성의 value와 동일
@Value("어노테이션")
String name;
// preoverty 속성의 ref와 동일
@Autowired
@Qualifier("strPrinter")
Printer printer;
List<String> names;
public Hello() {
System.out.println(this.getClass().getName() + "생성자 호출됨");
}
public Hello(String name, Printer printer) {
System.out.println(this.getClass().getName() + "오버로딩된 생성자 호출됨");
this.name = name;
this.printer = printer;
}
public List<String> getNames() {
return this.names;
}
public void setNames(List<String> list) {
System.out.println("Hello setNames() " + list);
this.names = list;
}
// 어노테이션 사용시 없어도 됨
// public void setName(String name) {
// System.out.println("Hello setName() " + name);
// this.name = name;
// }
//
// public void setPrinter(Printer printer) {
// System.out.println("Hello setPrinter " + printer.getClass().getName());
// this.printer = printer;
// }
public String sayHello() {
return "Hello " + name;
}
public void print() {
this.printer.print(sayHello());
}
}
- src/main/resources -> Spring Bean Definition file -> spring-beans-annot.xml 이름 설정 -> beans + beans.xsd 선택 -> context + context.xsd 선택
- 어노테이션 적용하기 위해 패키지 설정
- 어노테이션 적용하기 위해 패키지 설정
<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns:context="http://www.springframework.org/schema/context"
xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd
http://www.springframework.org/schema/context http://www.springframework.org/schema/context/spring-context.xsd">
<!-- 어느테이션이 선언된 클래스들을 스캔하기 위한 설정 -->
<!-- context라는 prefix 설정하는 이유
- namespace : 기능별로 xml의 태그명이 똑같더라도 namespace가 다르면 구분 가능
- beans는 default namespace라서 prefix 없이 사용
- context는 :context라는 prefix가 있어서 태그명에 작성해야함-->
<context:component-scan base-package="myspring.di.annot"/>
</beans>
- src/test/java -> myspring.di.annot 패키지 생성 -> AnnotatedHelloBeanTest 파일 생성
package myspring.di.annot;
import javax.annotation.Resource;
import org.junit.jupiter.api.Assertions;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.test.context.ContextConfiguration;
import org.springframework.test.context.junit.jupiter.SpringExtension;
// file:src/main/resources/spring-beans-annot.xml와 classpath:spring-beans-annot.xml 동일
@ExtendWith(SpringExtension.class)
@ContextConfiguration(locations = "classpath:spring-beans-annot.xml")
public class AnnotatedHelloBeanTest {
// Hello가 종류가 1개밖에 없어서 Bean의 id와 일치하지 않아도 상관 없음 (type으로 찾기 때문에)
@Autowired
Hello hello;
@Resource(name="stringPrinter")
Printer printer;
@Test
public void helloBean() {
Assertions.assertEquals("Hello 어노테이션", hello.sayHello());
hello.print();
Assertions.assertEquals("Hello 어노테이션", printer.toString());
}
}
- Construction Injection
- xml+annotation 형태
- HelloCons.java 생성
package myspring.di.annot;
import java.util.List;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Component;
@Component("helloCons")
public class HelloCons {
// @Value("어노테이션")
String name;
// @Autowired
// @Qualifier("stringPrinter")
Printer printer;
List<String> names;
public HelloCons() {
System.out.println(this.getClass().getName() + "생성자 호출됨");
}
// 생성자 통해서 injection 받기
// 생성자를 argument에 적용
@Autowired
public HelloCons(@Value("annot생성자") String name, @Qualifier("consolePrinter") Printer printer) {
System.out.println(this.getClass().getName() + "오버로딩된 생성자 호출됨");
this.name = name;
this.printer = printer;
}
public List<String> getNames() {
return this.names;
}
public void setNames(List<String> list) {
System.out.println("Hello setNames() " + list);
this.names = list;
}
// 어노테이션 사용시 없어도 됨
// public void setName(String name) {
// System.out.println("Hello setName() " + name);
// this.name = name;
// }
//
// public void setPrinter(Printer printer) {
// System.out.println("Hello setPrinter " + printer.getClass().getName());
// this.printer = printer;
// }
public String sayHello() {
return "Hello " + name;
}
public void print() {
this.printer.print(sayHello());
}
}
- @Component
- 범용적인 general한 컴포넌트
- spring에게 대신 객체 생성해달라는 표시
- repository, service, controller 포함
- @Repository
- 데이터 연동하는 역할
- @Service
- 비즈니스 로직을 가짐
- @Controller
- 웹 쪽에서 연결하는 역할
- no XML
- 어노테이션 설정 단독 사용
- Spring JavaConfig 프로젝트는 XML이 아닌 자바 클래스에서 함 (configuration class)
- @Configuration
- 기존 xml 역할 대체
- @Bean
- @component와 기능은 동일하지만, 선언 위치가 다름
- @ComponentScan
- <context:component-scan base-package="myspring.di.annot"/>의 기능과 동일
- 설정 역할을 하는 자바 클래스임을 알려줌
- 메모리 상에 먼저 로딩됨
- src/main/java -> myspring.di.annot.config 패키지 생성 -> AnnotatedHelloConfig 파일 생성
package myspring.di.annot.config;
import org.springframework.context.annotation.ComponentScan;
import org.springframework.context.annotation.Configuration;
//xml을 대신해서 설정 역할을 하는 클래스
@Configuration
@ComponentScan(basePackages = {"myspring.di.annot"})
public class AnnotatedHelloConfig {
}
- xml 대신에 configuration class로 대체
- Bean Facotry
- GenericXmlApplicationContext -> AnnotationsConfigurationApplicationContext
- src/test/java -> myspring.di.annot.config 패키지 생성 -> AnnotatedHelloConfigTest 생성
package myspring.di.annot.config;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.test.context.ContextConfiguration;
import org.springframework.test.context.junit.jupiter.SpringExtension;
import org.springframework.test.context.support.AnnotationConfigContextLoader;
import myspring.di.annot.Hello;
// Bean 컨테이너 종류가 바뀌어서 loader를 통해 가져옴
// AnnotationConfigContextLoader.class는 AnnotationConfigApplicationContext 라는 Spring Bean Container를 로딩해주는 Loader 클래스
@ExtendWith(SpringExtension.class)
@ContextConfiguration(classes = AnnotatedHelloConfig.class, loader = AnnotationConfigContextLoader.class)
public class AnnotatedHelloConfigTest {
@Autowired
Hello hello;
@Test
public void hello() {
System.out.println(hello.sayHello()); // Hello 어노테이션
}
}
- @Configuration과 @Bean 차이점
- Spring Bean을 나타내는 어노테이션
- @Component는 클래스 위에 선언하고, @Bean은 메소드 위에 선언함
- 외부라이브러리에서 제공하는 클래스를 SpringBean으로 설정하는 경우에는 @Bean 어노테이션을 사용
- 외부 라이브러리를 가져다가 재사용하고 싶을 경우
- 일단 객체 생성하고, 메서드 생성한 후 메서드 위에 Bean이라고 작성
- docket이라는 객체가 spring bean이 됨
- 메서드 이름이 id가 됨
- 해당 코드에 Qualifier 없어도 됨
@Bean public Docket api() { return new Docket(DocumentationType.SWAGGER_2) .select() .apis(RequestHandlerSelectors.any()) .paths(PathSelectors.any()) .build(); }
- src/main/java -> myspring.di.xml.config 패키지 생성 -> XmlHelloConfig 생성
package myspring.di.xml.config;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import myspring.di.xml.ConsolePrinter;
import myspring.di.xml.Hello;
import myspring.di.xml.Printer;
import myspring.di.xml.StringPrinter;
@Configuration
public class XmlHelloConfig {
/*
* <bean id="strPrinter" class="myspring.di.xml.StringPrinter" />
*/
// qualifier 하지 않아도 메서드 이름이 bean의 id값
@Bean
public Printer strPrinter() {
return new StringPrinter();
}
@Bean
public Printer conPrinter() {
return new ConsolePrinter();
}
@Bean
public Hello hello() {
Hello hello = new Hello();
hello.setName("Java컨피그");
hello.setPrinter(strPrinter());
return hello;
}
}
- src/test/java -> myspring.di.xml.config 패키지 생성 -> XmlHelloConfigTest 생성
package myspring.di.xml.config;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.test.context.ContextConfiguration;
import org.springframework.test.context.junit.jupiter.SpringExtension;
import myspring.di.xml.Hello;
@ExtendWith(SpringExtension.class)
@ContextConfiguration(classes = XmlHelloConfig.class)
public class XmlHelloConfigTest {
@Autowired
Hello hello;
@Test
public void hello() {
System.out.println(hello.sayHello()); // Hello Java컨피그
}
}
📘 미션 (10/10)
- userService(hello)
- useDao (printer)
- 전략 1 : XML에 때려박아서 설정
- 전략 2 : xml의 공유의 문제가 생김 -> XML 설정 줄임
- 전략 3 : 스프링부트와 동일 (XML 아예 사용x)
- package : myspring.dl.strategy2로 따로 생성
📕 MyBatis (10/11)
- xml 파일 안에 sql + VO 객체 정의
- DB 연결하기 위한 과정
1) mariadb
2) HikariCP
3) SpringJDBC - 1) javax.sql의 DataSource
- connection을 가져오는 객체 지원
- JNDI(Java Naming and Directory API)는 DI와 유사
- driver vendor (연결 객체 제공/ connection pooling 제공/dirtributed transaction 제공)
- 2) org.springframework.jdbc의 SimpleDriverDataSource
- 개발에만 사용하고, 운영에 사용 X
- 실제 connection pool이 아니고, pool Connections이 실제가 아님
- WAS가 없어서 opensource로 사용
- Apache Commons DBCP 오픈소스 사용
- hikaricp에서 hikaricp 의존성 필요해 artifact -> pom.xml 추가
- HikariDataSource(이전 예제에서 Printer같은 존재)를 Spring Bean으로 설정
- setter 메서드가 있는지 확인 필요
- hikari api doc
- setDriverClassName, setJdbcUrl, setUsername, setPassword
- 3) Spring JDBC -> pom.xml 추가
- spring bean configurantion 파일 -> spring-beans-user.xml -> beans(.xsd) + context(.xsd) + p 선택
- SpringFWXml 프로젝트 -> resources/value.properties 복사 -> MySpringFW 프로젝트의 resources 폴더 하단에 붙여넣기
- value.properties : 환경설정 파일 (현재는 local이지만 운영으로 바뀌면 url의 127.0.0.1이나 유저 정보가 변동되기 때문에 환경 별로 다른 정보를 작성)
- value.properties 수정
db.driverClass=org.mariadb.jdbc.Driver
db.url=jdbc:mariadb://127.0.0.1:3306/boot_db?useUnicode=true&charaterEncoding=utf-8&useSSL=false&serverTimezone=UTC
db.username=boot
db.password=boot
myname=Spring
myprinter=printer
value1=JUnit
value2=AOP
value3=DI
printer1=stringPrinter
printer2=consolePrinter
- 프로젝트 이름 변경 (MySpringFW -> MySpringUser)
- myspringfw.zip을 import
- 프로젝트 이름 변경 (MySpringFW -> MySpringCustomer)
- spring-beans-user.xml
- Properties file 정보 설정
- property 하위 태그 대신 bean 태그의 attribute로 setter 메서드 지정
- DataSource 구현체인 HikariDataSource를 SpringBean으로 등록
- p:drvierClassName : setdriverClassName이 있고, 연결한다는 의미
<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns:context="http://www.springframework.org/schema/context"
xmlns:p="http://www.springframework.org/schema/p"
xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd
http://www.springframework.org/schema/context http://www.springframework.org/schema/context/spring-context.xsd">
<!-- Properties file 정보 설정 -->
<context:property-placeholder location="classpath:value.properties"/>
<!-- DataSource 구현체인 HikariDataSource를 SpringBean으로 등록 -->
<bean id="dataSource" class="com.zaxxer.hikari.HikariDataSource"
p:driverClassName="${db.driverClass}"
p:jdbcUrl="${db.url}"
p:username="${db.username}"
p:password="${db.password}"
/>
</beans>
- test 폴더에 myspring.user 패키지 생성 -> UserDBTest 클래스 생성
- DataSource import시 javax.sql 선택
package myspring.user;
import java.sql.Connection;
import java.sql.DatabaseMetaData;
import java.sql.SQLException;
import javax.sql.DataSource;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.test.context.ContextConfiguration;
import org.springframework.test.context.junit.jupiter.SpringExtension;
@ExtendWith(SpringExtension.class)
@ContextConfiguration(locations = "classpath:spring-beans-user.xml")
public class UserDBTest {
@Autowired
DataSource dataSource;
@Test
public void conn() {
try {
Connection connection = dataSource.getConnection();
DatabaseMetaData metaData = connection.getMetaData();
System.out.println("DB Product Name : " + metaData.getDatabaseProductName()); //MariaDB
System.out.println("DB Driver : " + metaData.getDriverName()); // MariaDB Connector/J
System.out.println("DB URL : " + metaData.getURL()); // jdbc:mariadb://127.0.0.1/boot_db?user=boot&password=***&...
System.out.println("DB Username : " + metaData.getUserName()); // boot
} catch (SQLException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
}
}
- mybatis -> pom.xml 추가
- mybatis api
- org.apache.ibatis.session -> Interfaces -> SqlSession
- create
- read
- insert
- select
- List<E>
- selectOne
- T
- update
- mybatis-spring doc -> pom.xml 추가
- 구조 (mybatis-spring를 쓰지 않고 mybtis만 썼을 때의 구조)
- DB
- jdbc.properties
- mapper.xml : SQL 쿼리문 작성 (mybatis-spring 없을 경우 DataSource(현재 spring-beans-user.xml)가 이곳에 작성)
- sqlMapConfig.xml : MyBatis configuration 파일
- SqlSessionFactory가 SqlSession 생성 : mapper.xml에 있는 sql 실행시켜주는 역할
- dao에서 sqlsession 사용
- mybatis-spring 사용하지 않으면 sqlsession을 멤버 변수에 선언해놓고 공유하는게 안됨 -> dao 메서드 별로 sqlsession을 새로 접속해서 가져와야함 -> mybaits-spring으로 해결
- 구조 (mybatis-spring 사용)
- datasource injection 받기
- mybatis config file
- mapping file
- myspring.student.vo와 myspring.user.vo 가져오기
- student는 참조 관계 있음
- sources 폴더에서 파일 3개 복사
- sqlMapConfig
- mybatis 설정 파일
- alias 이름을 mapper에서 사용
- settings : sql쿼리문을 로그의 개발모드 상에 보내주면, 로그를 볼 수 있는 설정 (log4J 필요)
- logging 사용 이유 : sysout으로 로그 사용할 경우 관리하기 어렵기 때문에, log level에 따라 환경에 따라 사용 가능
- StudnetMapper
- mapper 들어있는 것은 sql의 설정을 의미
- UserMapper
- mapper 들어있는 것은 sql의 설정을 의미
- sqlMapConfig
- folder -> mybatis 폴더 생성
- log4j2.xml 복사
- appender를 이용해서 console에도 찍을 수 있고 file에 로그 정보 저장 가능
- UserMapper
- selectOne : <T> T selectOne(String statement(select의 id), Object parameter(값을 파라미터로 넘김))
-> 해당 parameter 값이 value로 들어감 - dao에 있었던 쿼리 대신 xml에 분리되어 있으니 xml의 쿼리문으로 실행하는 것을 호출
- uservo가 뭔지만 알려주면 getter/setter 했던 과정 필요 없이 mybatis가 대신 해줌 (단 UserVO의 getters/setter 메서드와 이름이 동일해야 함)
- mybatis가 query를 조회하여 결과를 uservo에 저장하기 위해서 setter 호출 -> 저장된 uservo를 가져와서 화면에 뿌리는 방식으로 사용
- selectOne : <T> T selectOne(String statement(select의 id), Object parameter(값을 파라미터로 넘김))
- sqlsession 만드는 과정
- sqlsessionfactorybean을 이용하여 sqlsessionfactory 만들기
- datasource 의존 필요
- setDataSource() 메서드의 인자로 hikaridatasource 들어옴
- setConfigLocation() 으로 MyBatisConfig(sqlMapConfig) 파일 연결
- setMapperLocations() mapping (UserMapper) 파일 연결
- sqlsessionfactorybean을 이용하여 sqlsessionfactory 만들기
- spring-bean-user.xml 수정
- Mybatis-spring의 SqlSessionFactoryBean을 SpringBean으로 등록
- setdatasource : hikari datasource를 연결
- setconfigulation : mybatis/sqlmapconfig.xml 파일 연결
- setmapperlacations :
<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns:context="http://www.springframework.org/schema/context"
xmlns:p="http://www.springframework.org/schema/p"
xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd
http://www.springframework.org/schema/context http://www.springframework.org/schema/context/spring-context.xsd">
<!-- Properties file 정보 설정 -->
<context:property-placeholder location="classpath:value.properties"/>
<!-- DataSource 구현체인 HikariDataSource를 SpringBean으로 등록 -->
<bean id="dataSource" class="com.zaxxer.hikari.HikariDataSource"
p:driverClassName="${db.driverClass}"
p:jdbcUrl="${db.url}"
p:username="${db.username}"
p:password="${db.password}"
/>
<!-- Mybatis-spring의 SqlSessionFactoryBean을 SpringBean으로 등록 -->
<bean id="sqlSessionFactory" class="org.mybatis.spring.SqlSessionFactoryBean">
<property name="dataSource" ref="dataSource"/>
<property name="configLocation" value="classpath:mybatis/SqlMapConfig.xml" />
<property name="mapperLocations">
<list>
<value>classpath:mybatis/*Mapper.xml</value>
</list>
</property>
</bean>
</beans>
- sqlsessionfactory를 @autowired 이용해서 injection 받기
- UserDBTest.java
- sqlsession
- sqlsessiontemplate을 bean으로 등록해야 sql 실행 가능 (customerDaoImpl에서 사용)
- class session template은 thread safe (thread마다 생성하지 않아도 됨)
- 반드시 construct-arg 를 써야함
- 기본 생성자가 없기 때문에 construct injection이 필수임
- spring-beans-user.xml 수정
- argumet없는 기본 생성자가 없어서 setter를 설정할 때 property 사용X
- sqlsessionfactory를 주입받아야함 (factory를 의존하기 때문에)
<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns:context="http://www.springframework.org/schema/context"
xmlns:p="http://www.springframework.org/schema/p"
xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd
http://www.springframework.org/schema/context http://www.springframework.org/schema/context/spring-context.xsd">
<!-- Properties file 정보 설정 -->
<context:property-placeholder location="classpath:value.properties"/>
<!-- DataSource 구현체인 HikariDataSource를 SpringBean으로 등록 -->
<bean id="dataSource" class="com.zaxxer.hikari.HikariDataSource"
p:driverClassName="${db.driverClass}"
p:jdbcUrl="${db.url}"
p:username="${db.username}"
p:password="${db.password}"
/>
<!-- Mybatis-spring의 SqlSessionFactoryBean을 SpringBean으로 등록 -->
<bean id="sqlSessionFactory" class="org.mybatis.spring.SqlSessionFactoryBean">
<property name="dataSource" ref="dataSource"/>
<property name="configLocation" value="classpath:mybatis/SqlMapConfig.xml" />
<property name="mapperLocations">
<list>
<value>classpath:mybatis/*Mapper.xml</value>
</list>
</property>
</bean>
<!-- SqlSessionTemplate -->
<bean id="sqlSession" class="org.mybatis.spring.SqlSessionTemplate">
<constructor-arg ref="sqlSessionFactory"/>
</bean>
</beans>
- test 진행
- UserDBTest.java
package myspring.user;
import java.sql.Connection;
import java.sql.DatabaseMetaData;
import java.sql.SQLException;
import javax.sql.DataSource;
import org.apache.ibatis.session.SqlSession;
import org.apache.ibatis.session.SqlSessionFactory;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.test.context.ContextConfiguration;
import org.springframework.test.context.junit.jupiter.SpringExtension;
import myspring.user.vo.UserVO;
@ExtendWith(SpringExtension.class)
@ContextConfiguration(locations = "classpath:spring-beans-user.xml")
public class UserDBTest {
@Autowired
DataSource dataSource;
@Autowired
SqlSessionFactory sessionFactory;
@Autowired
SqlSession sqlSession;
@Test
public void session() {
UserVO user = sqlSession.selectOne("userNS.selectUserById", "dooly");
System.out.println(user);
}
@Test
public void sessionFactory() {
System.out.println(sessionFactory.getClass().getName()); // injection 잘 되었는지 확인 org.apache.ibatis.session.defaults.DefaultSqlSessionFactory
}
@Test
public void conn() {
try {
Connection connection = dataSource.getConnection();
DatabaseMetaData metaData = connection.getMetaData();
System.out.println("DB Product Name : " + metaData.getDatabaseProductName()); //MariaDB
System.out.println("DB Driver : " + metaData.getDriverName()); // MariaDB Connector/J
System.out.println("DB URL : " + metaData.getURL()); // jdbc:mariadb://127.0.0.1/boot_db?user=boot&password=***&...
System.out.println("DB Username : " + metaData.getUserName()); // boot
} catch (SQLException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
} // conn
}
- Mapper 인터페이스
- sql을 호출하기 위한 인터페이스
- sql을 호출하기 위한 인터페이스
- 쿼리문의 id와 mapper 인터페이스의 메서드 이름을 똑같이 맞추기
- mysprin.user.dao.mapper 복사 해서 main/java 폴더에 붙여넣기
- UserMapper.java
- UserMapper.xml과 UserMapper 인터페이스의 메서드명 일치시키기
- UserMapper 인터페이스가 있을 경우 xml이 수정될 때마다 업데이트 필요
- UserMapeer과 SqlSession 부르기 위해 연결하기 위한 설정 필요
-> mybatis-spring의 bean 추가 필요
- UserMapper.java
- UserMapperXml 수정
- < mapper namespace="myspring.user.dao.mapper.UserMapper"> -> < mapper namespace="userNS"> 변경
package myspring.user.dao.mapper;
import java.util.List;
import org.apache.ibatis.annotations.Param;
import org.apache.ibatis.annotations.Select;
import myspring.user.vo.UserVO;
public interface UserMapper {
//@Select("select * from users where userid=#{id}")
//UserVO selectUserById(@Param("id") String id);
UserVO selectUserById(String id);
List<UserVO> selectUserList();
void insertUser(UserVO userVO);
void updateUser(UserVO userVO);
void deleteUser(String id);
}
- UserMapper와 SqlSession 연결 설정
- MapperScannerConfigurer
- setBasePackage() : 패키지 위치 알려줌
- setSqlSessionTemplateBeanName()
- spring-beans-user.xml 수정
<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns:context="http://www.springframework.org/schema/context"
xmlns:p="http://www.springframework.org/schema/p"
xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd
http://www.springframework.org/schema/context http://www.springframework.org/schema/context/spring-context.xsd">
<!-- Properties file 정보 설정 -->
<context:property-placeholder location="classpath:value.properties"/>
<!-- DataSource 구현체인 HikariDataSource를 SpringBean으로 등록 -->
<bean id="dataSource" class="com.zaxxer.hikari.HikariDataSource"
p:driverClassName="${db.driverClass}"
p:jdbcUrl="${db.url}"
p:username="${db.username}"
p:password="${db.password}"
/>
<!-- Mybatis-spring의 SqlSessionFactoryBean을 SpringBean으로 등록 -->
<bean id="sqlSessionFactory" class="org.mybatis.spring.SqlSessionFactoryBean">
<property name="dataSource" ref="dataSource"/>
<property name="configLocation" value="classpath:mybatis/SqlMapConfig.xml" />
<property name="mapperLocations">
<list>
<value>classpath:mybatis/*Mapper.xml</value>
</list>
</property>
</bean>
<!-- SqlSessionTemplate -->
<bean id="sqlSession" class="org.mybatis.spring.SqlSessionTemplate">
<constructor-arg ref="sqlSessionFactory"/>
</bean>
<!-- Mybatis-Spring의 MapperScannerConfigurer을 SpringBean으로 등록 -->
<bean class="org.mybatis.spring.mapper.MapperScannerConfigurer">
<!-- 참조하는 것 없어서 bean id 없어도 됨 -->
<property name="basePackage" value="myspring.user.dao.mapper" />
<property name="sqlSessionTemplateBeanName" value="sqlSession"/>
</bean>
</beans>
- UserDBTest.java 수정
- userMapper test
package myspring.user;
import java.sql.Connection;
import java.sql.DatabaseMetaData;
import java.sql.SQLException;
import javax.sql.DataSource;
import org.apache.ibatis.session.SqlSession;
import org.apache.ibatis.session.SqlSessionFactory;
import org.junit.jupiter.api.Disabled;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.test.context.ContextConfiguration;
import org.springframework.test.context.junit.jupiter.SpringExtension;
import myspring.user.dao.mapper.UserMapper;
import myspring.user.vo.UserVO;
@ExtendWith(SpringExtension.class)
@ContextConfiguration(locations = "classpath:spring-beans-user.xml")
public class UserDBTest {
@Autowired
DataSource dataSource;
@Autowired
SqlSessionFactory sessionFactory;
@Autowired
SqlSession sqlSession;
@Autowired
UserMapper userMapper;
@Test
public void mapper() {
// id가 메서드 이름이 되어 argument 전달
UserVO user = userMapper.selectUserById("dooly");
System.out.println(user);
}
@Test @Disabled
public void session() {
// mapper인터페이스를 사용하지 않아 sqlsession의 메서드의 인자에 문자열로 namespace.SQL id 입력
UserVO user = sqlSession.selectOne("userNS.selectUserById", "dooly");
System.out.println(user);
}
@Test
public void sessionFactory() {
System.out.println(sessionFactory.getClass().getName()); // injection 잘 되었는지 확인 org.apache.ibatis.session.defaults.DefaultSqlSessionFactory
}
@Test
public void conn() {
try {
Connection connection = dataSource.getConnection();
DatabaseMetaData metaData = connection.getMetaData();
System.out.println("DB Product Name : " + metaData.getDatabaseProductName()); //MariaDB
System.out.println("DB Driver : " + metaData.getDriverName()); // MariaDB Connector/J
System.out.println("DB URL : " + metaData.getURL()); // jdbc:mariadb://127.0.0.1/boot_db?user=boot&password=***&...
System.out.println("DB Username : " + metaData.getUserName()); // boot
} catch (SQLException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
} // conn
}
- customerDAO
- myspring.user.dao와 service 패키지 복사 -> main/java 폴더에 붙여넣기
- 기존의 SqlSession을 injection 받음 (UserDaoImpl.java) -> 변경된 UserMapper을 injectiion 받음 (UserDaoImplMapper.java)
- xml에서 component scan 필요
- UserDBTest.java수정
- customerService test
package myspring.user;
import java.sql.Connection;
import java.sql.DatabaseMetaData;
import java.sql.SQLException;
import javax.sql.DataSource;
import org.apache.ibatis.session.SqlSession;
import org.apache.ibatis.session.SqlSessionFactory;
import org.junit.jupiter.api.Disabled;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.test.context.ContextConfiguration;
import org.springframework.test.context.junit.jupiter.SpringExtension;
import myspring.user.dao.mapper.UserMapper;
import myspring.user.service.UserService;
import myspring.user.vo.UserVO;
@ExtendWith(SpringExtension.class)
@ContextConfiguration(locations = "classpath:spring-beans-user.xml")
public class UserDBTest {
@Autowired
DataSource dataSource;
@Autowired
SqlSessionFactory sessionFactory;
@Autowired
SqlSession sqlSession;
@Autowired
UserMapper userMapper;
@Autowired
UserService userService;
@Test
public void service() {
UserVO user = userService.getUser("dooly");
System.out.println(user);
}
@Test @Disabled
public void mapper() {
// id가 메서드 이름이 되어 argument 전달
UserVO user = userMapper.selectUserById("dooly");
System.out.println(user);
}
@Test @Disabled
public void session() {
// mapper인터페이스를 사용하지 않아 sqlsession의 메서드의 인자에 문자열로 namespace.SQL id 입력
UserVO user = sqlSession.selectOne("userNS.selectUserById", "dooly");
System.out.println(user);
}
@Test
public void sessionFactory() {
System.out.println(sessionFactory.getClass().getName()); // injection 잘 되었는지 확인 org.apache.ibatis.session.defaults.DefaultSqlSessionFactory
}
@Test
public void conn() {
try {
Connection connection = dataSource.getConnection();
DatabaseMetaData metaData = connection.getMetaData();
System.out.println("DB Product Name : " + metaData.getDatabaseProductName()); //MariaDB
System.out.println("DB Driver : " + metaData.getDriverName()); // MariaDB Connector/J
System.out.println("DB URL : " + metaData.getURL()); // jdbc:mariadb://127.0.0.1/boot_db?user=boot&password=***&...
System.out.println("DB Username : " + metaData.getUserName()); // boot
} catch (SQLException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
} // conn
}
- 요청 받는 controller를 Front Controller 패턴 사용
- spring에서 제공하는 DispatcherServlet 사용 가능 (구현할 필요X)
-> web.xml에 세팅 필요 - 일반적인 자바 객체로 controller 생성
- View template을 JSP로 생성
- tomcat에 올려서 실행
- spring에서 제공하는 DispatcherServlet 사용 가능 (구현할 필요X)
- web.xml 설정
- = tomcat 설정 파일
- web.xml에 DispatcherServlet 맵핑
- web.xml에 spring configurtion 설정 필요
- tomcat이 spring web bean container 구동시켜주는 역할
- GenericWebApplicationContext가 spring web bean container 역할을 함
- (기존) JVM -> Spring FrameWork -> App (+ 설정 파일)
- (변경) JVM -> tomcat -> Spring FrameWork -> App
(spring 설정 파일을 tomcat에 알려주면, 해석해서 처리해줌) - spring web mvc -> pom.xml 추가
- 1) web.xml (src/main/webapp/WEB-INF 하단)
- contextLoadListener 추가
- listener : tomcat 서버가 start/stop 이벤트를 계속 확인하는 역할
- tomcat 메모리 상에 application context를 로드하는 것
- param location 태그에 spring-beans-user.xml을 알려줘야함
- web에 servlet context 자리에 해당 설정값이 저장될 것임
- dispatcherservlet 추가
- servlet의 param location : controller 쪽에만 적용되는 xml 파일 있을 경우 적용
<?xml version="1.0" encoding="UTF-8"?>
<web-app xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns="http://xmlns.jcp.org/xml/ns/javaee"
xsi:schemaLocation="http://xmlns.jcp.org/xml/ns/javaee http://xmlns.jcp.org/xml/ns/javaee/web-app_3_1.xsd"
id="WebApp_ID" version="3.1">
<display-name>CustomerSpringWeb</display-name>
<welcome-file-list>
<welcome-file>index.html</welcome-file>
<welcome-file>index.htm</welcome-file>
<welcome-file>index.jsp</welcome-file>
<welcome-file>default.html</welcome-file>
<welcome-file>default.htm</welcome-file>
<welcome-file>default.jsp</welcome-file>
</welcome-file-list>
<!-- needed for ContextLoaderListener -->
<context-param>
<param-name>contextConfigLocation</param-name>
<param-value>classpath:spring-beans-user.xml</param-value>
</context-param>
<!-- Bootstraps the root web application context before servlet initialization -->
<listener>
<listener-class>org.springframework.web.context.ContextLoaderListener</listener-class>
</listener>
<!-- The front controller of this Spring Web application, responsible for handling all application requests -->
<servlet>
<servlet-name>springDispatcherServlet</servlet-name>
<servlet-class>org.springframework.web.servlet.DispatcherServlet</servlet-class>
<init-param>
<param-name>contextConfigLocation</param-name>
<param-value>classpath:spring-beans-user.xml</param-value>
</init-param>
<load-on-startup>1</load-on-startup>
</servlet>
<!-- Map all requests to the DispatcherServlet for handling -->
<servlet-mapping>
<servlet-name>springDispatcherServlet</servlet-name>
<url-pattern>*.do</url-pattern>
</servlet-mapping>
</web-app>
- tomcat에 구동
- add and remove -> MySpringUser
- 해당 구문처럼 초기화 되어야 정상 실행 확인
- 2) UserController 작성
- @Controller : controller가 앞서 배운 component와 유사
- @RequestMapping : url 요청이 오면 controller 안의 어떤 메서드를 실행할 것인지 지정 (요청과 메서드 연결)
- index.jsp에서 *.do가 되어 있기 때문에 dispatcherservlet이 자동 호출됨
- myspring.user.controller 패키지 생성
- UserController 생성
- userService injection 받기
- 생성자 만들어서 객체 생성이 2번 될 것인데 생성 되는 것 로그 찍어보기
- 동일한 객체 2번 호출하는 이유
- web.xml에 listenr와 servlet의 loaction을 동일한 xml 요청 했기 때문에 2번 호출됨
-> 현재는 service+data acccess layer의 설정 파일과 presentation layer이 동일한 xml에 설정되어 있음
-> servlet과 web에 관련된 xml을 분리하여 생성해야함
- web.xml에 listenr와 servlet의 loaction을 동일한 xml 요청 했기 때문에 2번 호출됨
- resources 폴더에 beans-web.xml 복사 -> resources 폴더에 붙여넣기
- 순수 servlet은 jsp 파일을 포워딩 해줬는데, controller에서 포워딩 되는 .jsp 확장자 생략 가능
- beans-web.xml
- component-scan 추가
<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns:context="http://www.springframework.org/schema/context"
xmlns:mvc="http://www.springframework.org/schema/mvc"
xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans-3.2.xsd
http://www.springframework.org/schema/context http://www.springframework.org/schema/context/spring-context-3.2.xsd
http://www.springframework.org/schema/mvc http://www.springframework.org/schema/mvc/spring-mvc-3.2.xsd">
<context:component-scan base-package="myspring.user">
<!-- 상단 코드에서 myspring.user 패키지를 모두 불러왔지만, 해당 presentation layer를 위한 xml 설정파일이기 때문에 myspring.user 하단의 .controller만 include -->
<context:include-filter type="annotation" expression="org.springframework.stereotype.Controller"/>
</context:component-scan>
<!-- Spring MVC에 필요한 Bean들을 자동으로 등록해주는 태그-->
<mvc:annotation-driven />
<!-- DispatcherServlet의 변경된 url-pattern 때문에 필요한 태그 설정 -->
<mvc:default-servlet-handler/>
<!-- 아래 주석은 Controller에서 포워딩 되는 .jsp 확장자를 생략할 수 있다. -->
<bean id="viewResolver"
class="org.springframework.web.servlet.view.InternalResourceViewResolver">
<property name="prefix" value="/" />
<property name="suffix" value=".jsp" />
</bean>
<!-- annotation-driven 태그에서 내부적으로 처리해주는 설정 -->
<!-- <bean id="jsonHttpMessageConverter" -->
<!-- class="org.springframework.http.converter.json.MappingJacksonHttpMessageConverter" /> -->
<!-- <bean class="org.springframework.web.servlet.mvc.annotation.AnnotationMethodHandlerAdapter"> -->
<!-- <property name="messageConverters"> -->
<!-- <list> -->
<!-- <ref bean="jsonHttpMessageConverter"/> -->
<!-- </list> -->
<!-- </property> -->
<!-- </bean> -->
</beans>
- spring-beans-user.xml 수정
<context:component-scan base-package="myspring.user" >
<!-- 하단의 myspring.user에서 controller만 제외하고 scan -->
<context:exclude-filter type="annotation" expression="org.springframework.stereotype.Controller"/>
</context:component-scan>
- web.xml 수정
<servlet>
<servlet-name>springDispatcherServlet</servlet-name>
<servlet-class>org.springframework.web.servlet.DispatcherServlet</servlet-class>
<init-param>
<param-name>contextConfigLocation</param-name>
<param-value>classpath:beans-web.xml</param-value>
</init-param>
<load-on-startup>1</load-on-startup>
</servlet>
- 이제 객체 생성 1번만 발생한 것을 확인
- index.jsp 수정
- 목록 보여고 상세 정보 확인 구조
<%@ page language="java" contentType="text/html; charset=UTF-8"
pageEncoding="UTF-8"%>
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<title>user</title>
</head>
<body>
<h1>사용자 관리 메인</h1>
<ul>
<li><a href="userList.do">User 리스트</a></li>
</ul>
</body>
</html>
- UserController.java 수정
- @requestmapping을 사용하여 설정
- service 불러와서 리스트로 받아오기
package myspring.user.controller;
import java.util.List;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.servlet.ModelAndView;
import myspring.user.service.UserService;
import myspring.user.vo.UserVO;
@Controller
public class UserController {
@Autowired
private UserService userService;
public UserController() {
System.out.println(this.getClass().getName() + "생성자 호출됨");
}
// db에서 가져오고 화면에 보이는 것도 함께 설정
@RequestMapping("/userList.do")
public ModelAndView userList() {
// service 불러와서 리스트로 받기
// 뿌려줄 jsp 페이지를 ModelAndView 객체에 담음 (viewName=jsp파일 이름(jsp확장자 없이 이름만 기재), modelName=키값(forEach구문의 items), modelList=서비스에서 받아온 list 기재)
// key 값(userList)과 일치하여 list 변수명 바꾸기
List<UserVO> userVOList = userService.getUserList();
// ModelAndView(viewName, keyName, valueObject)
return new ModelAndView("userList", "userList", userVOList);
}
}
- jstl 의존성 추가 -> pom.xml 추가
- userinfo.jsp와 userList.jsp 복사 -> src/main/webapp 붙여넣기
- UserController.java 수정
- 상세 정보 보여주기
package myspring.user.controller;
import java.util.List;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Controller;
import org.springframework.ui.Model;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.servlet.ModelAndView;
import myspring.user.service.UserService;
import myspring.user.vo.UserVO;
@Controller
public class UserController {
@Autowired
private UserService userService;
public UserController() {
System.out.println(this.getClass().getName() + "생성자 호출됨");
}
// db에서 가져오고 화면에 보이는 것도 함께 설정
// View와 Model을 한꺼번에 전달하는 방법
@RequestMapping("/userList.do")
public ModelAndView userList() {
// service 불러와서 리스트로 받기
// 뿌려줄 jsp 페이지를 ModelAndView 객체에 담음 (viewName=jsp파일 이름(jsp확장자 없이 이름만 기재), modelName=키값(forEach구문의 items), modelList=서비스에서 받아온 list 기재)
// key 값(userList)과 일치하여 list 변수명 바꾸기
List<UserVO> userVOList = userService.getUserList();
// ModelAndView(viewName, keyName, valueObject)
return new ModelAndView("userList", "userList", userVOList);
}
//getUser.do?id=dooly
// View와 Model을 분리해서 전달하는 방법
@RequestMapping("/getUser.do")
public String getUser(@RequestParam("id") String userId, Model model) {
// @requestparam을 이용하여 ?(쿼리 스트링) 다음의 id 값 가져올 수 있음
// 받아온 userVO를 model에 담아줌
UserVO userVO = userService.getUser(userId);
model.addAttribute("user", userVO);
// 페이지 이름 return
return "userInfo";
}
}
📕 Spring Boot 개념 + 설치
- 설치
- staruml
- 다이어그램 작성
- graalVM
- 기존 jdk - JIT(just in time) 컴파일러, AOT(Ahead Of Time) 컴파일러
- 기존 mybatis 스프링은 톰캣 서버 + jsp -> Web Application Archive 파일로 압축 배포
- 3.x버전 사용하면서 spring을 native 이미지로 만들 수 있음 (보통 .jar/war로 배포하는데 exe파일로 가능)
- 조건은 c++ 컴파일러, springboot 3.x 버전, graalVM이 있어야함
- java17 선택
- intellij
- DataGrip : sql의 db를 연결해주는 것 (자동으로 uml다이어그램 생성)
- staruml
- StarUML
- association (클래스간 양방향 관계)
- mdj 확장자로 저장
- 아이콘 변경시 : format -> stereo type display
- 메서드 추가시 : Suppress Operations 끄기 + Add -> Operation
- (동등한 인스턴스 레벨에서) 참조시 : Directed Association (점선)
- (실행) 참조시 : Dependency
- UseCase Diagram
- SequenceDiagram
- IntelliJ
- SpringBoot Initializer
- boot + jsp 사용할 경우 .war만 가능
- artifact의 id가 디렉터리명 (zip 파일명)
- 의존성 설치 (Web MVC 사용 가능하기 위해)
- 기존 spring JDBC에서 Data JPA로 변경해서 사용 (JDBC도 함께 적용됨)
- mariaDB로 DB사용
- 메모리 DB인 h2 사용 (잠깐 메모리 상에 올려두어 개발시 편의성 제공)
- lombok
- 자바 라이브러리
- boilerplate code (개발시 반복적으로 사용해야 하는 코드)
- 🗒️ 예시 : getter/setter, toString, Equals, constructor 등 제공
- spring actuator
- 모니터링 기능 지원 (관리자 admin)
- 모니터링 기능 지원 (관리자 admin)
- validation (검증)
- 서버사이드 쪽에서 검증을 위함
- 크롬 개발자 도구에 작성한 검증하는 자바스크립트 코드가 노출되어 버림 (해당 검증 코드가 노출되면 안됨)
- 서비스에서 또다른 호출되는 서비스 로직이 될 수 있기 때문에, 호출 당하는 쪽에 검증 코드 추가 필요
- security는 나중에 추가 예정
- dev.tools 추가
- Application.class에 main을 이용해서 재컴파일등을 줄여주기 위한 기능
- start and end 지점 설정 가능
// pom.xml에 dependency 추가
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-devtools</artifactId>
<scope>runtime</scope>
<optional>true</optional>
</dependency>
- explore
- 예상 소스 구조 보여줌
- 예상 소스 구조 보여줌
- download
- IntelliJ
- file -> project structure -> graalvm sdk 추가
- file -> project structure -> graalvm sdk 추가
- Setting 설정
- Settings -> Compiler -> Annotation Processors
- lombok 사용할 경우 Enable annotation processing 활성화
- lombok 사용할 경우 Enable annotation processing 활성화
- Settings -> Compliler
- Build Project automatically dev tools 자동 실행
- Settings -> Advanced Settings
- Allow auto-make to start ...
- Allow auto-make to start ...
- Settting -> Editor -> File Encodings
- project encoding/properties files도 UTF-8 적용
- project encoding/properties files도 UTF-8 적용
- Settings -> Editor -> General -> Auto Import
- Add unambiguous imports on the fly
- Add unambiguous imports on the fly
- Settings -> Compiler -> Annotation Processors
- com.basic.myspringboot : basic 패키지
- resources
- .jar를 선택했기 때문에 thymeleaf 사용 (templates/.html)
- resources/static에 index.html 생성
- index.html
<!DOCTYPE html>
<html lang="ko">
<head>
<meta charset="UTF-8">
<title>Title</title>
</head>
<body>
<h2>Hello 스프링부트</h2>
</body>
</html>
- 실행 (http://localhost:8080/index.html 입력)
- spring boot
- jackson
- spring-boot-starter-web 안에 기재
- java와 json간 자동 변환 역할
- java -> json (직렬화), json -> java (역직렬화)
- Automatically configure Spring and 3rd party libraries whenever possible (XML 설정 아예 하지 않음)
- hikaridatasource 등의 xml에 bean객체를 생성하지 않아도 자동으로 의존성 설정됨
-> configuration.class를 만들어서 제공 (hikaridatasource 등 설정에 대한 파일) - org.springframework.boot.autoconfigue.data.jdbc
- 기존의 xml 파일을 대체하는 configuration 클래스를 생성할 수 있기 때문에, api에 장착되는 방식이 생김
- 실제 예시 ()
- jackson
- @SpringBootApplication = @SpringBootConfiguration + @EnableAutoConfiguration + @ComponentScan
- 스프링 부트의 가장 기본적인 설정을 선언해줌
- @SpringBootConfiguration
- @SpringBootConfiguration의 자체가 configuration 클래스
- configuration 클래스 역할
- @EnableAutoConfiguration
- 스프링 부트가 제공하는 configuration 클래스를 자동으로 활성화
- Maven:org.springframework.boot:spring-boot-autoconfigure -> spring-autoconfigure-metadata.properties 파일에서 configuration 클래스 설정되어있는 것 확인
- 기존의 beans-web.xml(controller 관련 설정 파일) 기능 대체
- @ComponentScan
- 스프링 부트 프로젝트를 생성할 때 설정한 base package부터 컴포넌트 스캐닝 역할
- componentscan은 package기반으로 적용되기 때문에, 별도의 다른 패키지일경우 spring bean으로 인식이 안됨
-> @SpringBootApplication 기준 하위 패키지를 생성 - @SpringBootApplication 컴포넌트는 프로젝트당 1개만 존재
- CompononeScan 하위에 configuration이 존재하기 때문에 @Bean 어노테이션 사용하여 bean 객체 생성 가능
<context:component-scan base-package="myspring.customer">
<context:include-filter type="annotation" expression="org.springframework.stereotype.Controller"/>
</context:component-scan>
<!-- Spring MVC에 필요한 Bean들을 자동으로 등록해주는 태그-->
<mvc:annotation-driven />
<!-- DispatcherServlet의 변경된 url-pattern 때문에 필요한 태그 설정 -->
<mvc:default-servlet-handler/>
<!-- 아래 주석은 Controller에서 포워딩 되는 .jsp 확장자를 생략할 수 있다. -->
<bean id="viewResolver"
class="org.springframework.web.servlet.view.InternalResourceViewResolver">
<property name="prefix" value="/" />
<property name="suffix" value=".jsp" />
</bean>
- 스프링 부트가 제공해주는 기존의 버전을 pom.xml에서 원하는 버전으로 변경 가능
- 기존의 spring-boot-starter-parent/spring-boot-dependencies의 코드 복사 -> pom.xml에 추가
- <properties> <java.version>17</java.version> <!-- spring framework 버전 바꾸기 가능 (spring-boot-dependencies에서 추가)--> <spring-framework.version>6.0.11</spring-framework.version> </properties>
- MySpringBoot3Application.java 파일 수정
package com.basic.myspringboot;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.context.annotation.Bean;
@SpringBootApplication
public class MySpringBoot3Application {
public static void main(String[] args) {
SpringApplication.run(MySpringBoot3Application.class, args);
}
@Bean
public String hello() {
return new String("Hello 스프링부트");
}
}
- server.port 설정
- application.properties 파일 수정
- 포트번호 변경 가능
server.port=8087
- WebApplicationType (일반 프로젝트 용도로 변경하고자 할 경우)
- WebApplicationType.NONE
- 톰캣도 구동 안되고, 프로세스 종료됨
- Webapp이 아니니까 presentation 계층이 없는 data acess + business layer만 동작 구조가 됨
- WebApplicationType.REACTIVE
- WebApplicationType.SERVLET
- 톰캣 구동되고, 프로세스 종료되지 않음
package com.basic.myspringboot; import org.springframework.boot.SpringApplication; import org.springframework.boot.WebApplicationType; import org.springframework.boot.autoconfigure.SpringBootApplication; import org.springframework.context.annotation.Bean; import javax.swing.*; @SpringBootApplication public class MySpringBoot3Application { public static void main(String[] args) { // SpringApplication.run(MySpringBoot3Application.class, args); SpringApplication application = new SpringApplication(MySpringBoot3Application.class); // WebApplication Type을 변경하기 위한 목적 application.setWebApplicationType(WebApplicationType.SERVLET); // None : 더이상 WebApplication이 아님 application.run(args); } }
- WebApplicationType.NONE
- resources/banner.txt (배너 변경)
- 배너 설정 : 하단 코드 복사해서 banner.txt에 붙여넣기
- 소스 안에 자바 코드로 배너 설정 가능하지만 banner.txt가 우선순위가 더 높아 우선 적용됨
,--. ,--. ,-----. ,--. | `.' |,--. ,--.| |) /_ ,---. ,---. ,-' '-. | |'.'| | \ ' / | .-. \| .-. | .-. |'-. .-' | | | | \ ' | '--' /' '-' ' '-' ' | | `--' `--'.-' / `------' `---' `---' `--' `---' Application Info : ${application.title} ${application.version} Powered by Spring Boot ${spring-boot.version} // application.title = pom.xml 파일의 <name>MySpringBoot3</name> // application.version = pom.xml 파일의 <version>0.0.1-SNAPSHOT</version> (개발 프로젝트이 버전 설정 가능)
- java home과 path 설정
- intellij의 terminal에서 설정이 안되어서 시스템 환경변수로 설정

// 경로 복사 후 설정
set JAVA_HOME = C:\Users\Administrator\Desktop\ucamp\springboot\graalvm-jdk-17_windows-x64_bin\graalvm-jdk-17.0.8+9.1
// path 설정 (%는 기존 path 유지 후 새로 추가 목적)
set PATH = %PATH%;%JAVA_HOME%\bin;
// 자바 버전 확인
java -version
- 변경된 설정 방법
- .jar 생성
- maven -> MySpringBoot3 -> Lifecycle -> package
- 레포지터리 받고 실행 파일 만들어서 배포를 위한 jar 생성해줌
- target 폴더에 .jar 파일 저장
// terminal에 실행 mvnw package
- 현재 폴더에서 jar 실행 (Application info에 해당 버전 확인)
java -jar .\target\MySpringBoot3-0.0.1-SNAPSHOT.jar
- ApplicationRuneer 인터페이스
- ApplicationRuneer는 org.springframework.boot 패키지 하단에 존재
- application이 실행되는지 인지하는 역할 (tomcat 구동 확인하는 listner와 유사)
- SpringApplication이 시작되면 run메서드가 실행됨
-> 인터페이스 implement 받고, run 메서드 오버라이딩 해서 사용 (오버라이딩시 람다식 사용 가능)
-> run 메서드를 만들고, @Order 사용하여 여러개의 run의 순서 설정 가능 (순서가 낮을 수록 우선순위 높음)
- runner 패키지 생성
- 반드시 componene 어노테이션 붙이기
- MyRunner.java 생성



package com.basic.myspringboot.runner;
import org.springframework.boot.ApplicationArguments;
import org.springframework.boot.ApplicationRunner;
import org.springframework.core.annotation.Order;
import org.springframework.stereotype.Component;
@Component
@Order(1)
public class MyRunner implements ApplicationRunner {
@Override
public void run(ApplicationArguments args) throws Exception {
// ApplicationArguments는 main메서드의 (String[] args) argument를 전달 받음
System.out.println("===> MyRunner.run");
}
}
- argument 전달 확인
- vm argument
- program argument
- argument 전달
- intellij 안에서 설정
- edit configurations
- add vm options, program arguments 선택
- program argumet(--bar) = true, vm arguments(-Dfoo) = false 결과 출력
- .jar 파일 생성하여 command 상에서 설정도 가능
- intellij 안에서 설정
- 환경 변수 get/set 방식
- 1️⃣ : @Value
- 2️⃣ : enviroment 이용
- value
- properties 의 값은 @Value 어노테이션을 통해 읽어옴
- 해당 값도 계속 변동되기 때문에 환경변수를 이용해 application.properties에 저장
- application.properties 수정
- 한글 입력하고자 할 경우 그냥 적용시 깨지기 때문에, 유니코드로 변환하여 설정 필요
- 유니코드 변환 도구
#server.port=8087
#스프링
myboot.name=\uc2a4\ud504\ub9c1
myboot.age=${random.int(1,100)}
myboot.fullName=${myboot.name} Boot
- MyRunner.java 수정
package com.basic.myspringboot.runner;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.boot.ApplicationArguments;
import org.springframework.boot.ApplicationRunner;
import org.springframework.core.annotation.Order;
import org.springframework.stereotype.Component;
@Component
@Order(1)
public class MyRunner implements ApplicationRunner {
@Value("${myboot.name}")
private String name;
@Value("${myboot.age}")
private int age;
@Value("${myboot.fullName}")
private String fullName;
@Override
public void run(ApplicationArguments args) throws Exception {
System.out.println("myboot.name = " + name);
System.out.println("myboot.age = " + age);
System.out.println("myboot.fullName = " + fullName);
// ApplicationArguments는 main메서드의 (String[] args) argument를 전달 받음
System.out.println("VM Argument foo = " + args.containsOption("foo"));
System.out.println("Program argument bar = " + args.containsOption("bar"));
}
}
- Spring Environment
- 환경변수 키 값을 받아와 environmet의 getProperty 메소드 이용하여 환경변수 값 얻어옴
package com.basic.myspringboot.runner;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.boot.ApplicationArguments;
import org.springframework.boot.ApplicationRunner;
import org.springframework.core.annotation.Order;
import org.springframework.core.env.Environment;
import org.springframework.stereotype.Component;
@Component
@Order(1)
public class MyRunner implements ApplicationRunner {
@Value("${myboot.name}")
private String name;
@Value("${myboot.age}")
private int age;
@Value("${myboot.fullName}")
private String fullName;
@Autowired
private Environment environment;
@Override
public void run(ApplicationArguments args) throws Exception {
// 포트 번호 받아오기
System.out.println("Port Number = " + environment.getProperty("local.server.port"));
// 환경변수 받아오기
System.out.println("myboot.name = " + name);
System.out.println("myboot.age = " + age);
System.out.println("myboot.fullName = " + fullName);
// ApplicationArguments는 main메서드의 (String[] args) argument를 전달 받음
System.out.println("VM Argument foo = " + args.containsOption("foo"));
System.out.println("Program argument bar = " + args.containsOption("bar"));
}
}
- java -jar jar파일 --myboot.name=이름 4위
- IP 등 다른 설정이 변동되었을 경우 소스를 수정하지 않고 command line 인자로 주기
-> 해당 인자값이 소스 안에 있는 코드보다 우선순위가 높아서 해당 값이 적용됨
-> 인자가 변동되었을 경우 반복적으로 jar 파일을 수정해야하는 번거로움 해소 - 외부 (ex. github 저장소)에 있는 properties 설정 파일을 인자값으로 전달하여 적용하는 것도 가능 (spring cloud config 이용)
java -jar .\target\MySpringBoot3-0.0.1-SNAPSHOT.jar --myboot.name=Spring
- IP 등 다른 설정이 변동되었을 경우 소스를 수정하지 않고 command line 인자로 주기
- github 연결
- 토큰 인증 방식
- 최초 등록 : git remote add origin https://github.com/24tngus/MySpringBoot3.0.git
- 재등록 : git remote set-url origin https://토큰값@github.com/24tngus/MySpringBoot3.0.git
- 확인 : git config remote.origin.url
📕 Spring Boot
- 개발/스테이징/테스트/운영 등 상황에 맞게 환경설정 파일 다르게 구분 가능
- com.basic.myspringboot.dto 패키지 생성
- customer.java 생성
- 빌더 패턴
- 객체를 생성하는 방법과 표현하는 방법을 분리해서 동일한 생성 절차에서 다른 결과 생성
- 📄 예시
- Customer (name, email, age, entryDate) 생성되었을 경우
- new Custoemr("aa", "aa@naver.com", null, null)
- new Customer ("bb", null, 20, null)
- 입력을 요구하는 인자를 모두 채우지 않고, 객체 생성시 필요한 데이터만 넣어 유연성 제공
- com.basic.myspringboot.config 패키지 생성
- TestConfig.java 생성
package com.basic.myspringboot.config;
import com.basic.myspringboot.dto.Customer;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.Profile;
@Profile("test") // 현재 어떤 환경인지 properties 파일에 설정 필요
@Configuration
public class TestConfig {
@Bean
public Customer customer() {
return Customer.builder() // CustomerBuilder inner class
.name("테스트모드")
.age(10)
.build(); // customer로 바꿔주는 기능
}
}
- ProdConfig.java 생성
package com.basic.myspringboot.config;
import com.basic.myspringboot.dto.Customer;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.Profile;
@Profile("prod") // 현재 어떤 환경인지 properties 파일에 설정 필요
@Configuration
public class ProdConfig {
@Bean
public Customer customer() {
return Customer.builder() // CustomerBuilder inner class
.name("운영모드")
.age(50)
.build(); // customer로 바꿔주는 기능
}
}
- application.properties 파일 수정
#server.port=8087
# 스프링 유니코드 작성
myboot.name=\uc2a4\ud504\ub9c1
myboot.age=${random.int(1,50)}
myboot.fullName=${myboot.name} Boot
# 현재 활성화 중인 환경 설정
spring.profiles.active=test
- 테스트
- MyRunner.java 수정
package com.basic.myspringboot.runner;
import com.basic.myspringboot.dto.Customer;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.boot.ApplicationArguments;
import org.springframework.boot.ApplicationRunner;
import org.springframework.core.annotation.Order;
import org.springframework.core.env.Environment;
import org.springframework.stereotype.Component;
@Component
@Order(1)
public class MyRunner implements ApplicationRunner {
@Value("${myboot.name}")
private String name;
@Value("${myboot.age}")
private int age;
@Value("${myboot.fullName}")
private String fullName;
@Autowired
private Environment environment;
@Autowired
private Customer customer;
@Override
public void run(ApplicationArguments args) throws Exception {
System.out.println("Customer 현재 모드 = " + customer.getName());
// 포트 번호 받아오기
System.out.println("Port Number = " + environment.getProperty("local.server.port"));
// 환경변수 받아오기
System.out.println("myboot.name = " + name);
System.out.println("myboot.age = " + age);
System.out.println("myboot.fullName = " + fullName);
// ApplicationArguments는 main메서드의 (String[] args) argument를 전달 받음
System.out.println("VM Argument foo = " + args.containsOption("foo"));
System.out.println("Program argument bar = " + args.containsOption("bar"));
}
}
- jar로 만들어서 실행
// 커맨드 라인이 우선순위 높아서 prod로 적용 확인
java -jar .\target\MySpringBoot3-0.0.1-SANPSHOT.jar --spring.profiles.active=prod
- DB에 따라 모드 구분
- 개발 모드 : h2DB (application-test.properteis)
- 운영 모드 : mariaDB (application.properties)
- application-test.properties 생성
myboot.name=\uc2a4\ud504\ub9c1 TEST Mode
- application-prod.properties 생성
myboot.name=\uc2a4\ud504\ub9c1 PROD Mode
- jar로 테스트
mvnw package
java -jar .\target\MySpringBoot3-0.0.1-SANPSHOT.jar --spring.profiles.active=prod
- 로깅 퍼사드
- Commons Loggin, SLF4j (simple loggin facade)
- 로깅 퍼사드를 통해서 Logger를 사용하여 로깅 구현체를 교체하기 쉽도록 함
- 로깅
- JUL (java.util.loggin), Log4j2, Logback
- 로그 퍼사드 구현체들
- JUL (java.util.loggin), Log4j2, Logback
- 스트링 부트 로깅
- logger.debug("")
- 로그 레벨 높이면 낮은 레벨을 사용해서 작성한 메서드는 볼 수 없음
- --debug (일부 핵심 라이브러리만 디버깅 모드로)
- --trace (전부 다 디버깅 모드로)
- 로그 파일 출력 : logging.file.path
- 로그 레벨 조정 : logging.level.패키지명 = 로그 레벨
- 로그 레벨 종류
- Error
- Warn
- Info (default) -> 운영 환경
- Debug -> 개발 환경
- Trace
- application-test.properties
myboot.name=\uc2a4\ud504\ub9c1 TEST Mode
# 개발 log level = debug
logging.level.com.basic.myspringboot=debug
- application-prod.properties
myboot.name=\uc2a4\ud504\ub9c1 PROD Mode
# 운영 log level = info
logging.level.com.basic.myspringboot=info
- TEST (MyRunner.java)
- logger.info
- logger.debug
package com.basic.myspringboot.runner;
import com.basic.myspringboot.dto.Customer;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.boot.ApplicationArguments;
import org.springframework.boot.ApplicationRunner;
import org.springframework.core.annotation.Order;
import org.springframework.core.env.Environment;
import org.springframework.stereotype.Component;
@Component
@Order(1)
public class MyRunner implements ApplicationRunner {
@Value("${myboot.name}")
private String name;
@Value("${myboot.age}")
private int age;
@Value("${myboot.fullName}")
private String fullName;
@Autowired
private Environment environment;
@Autowired
private Customer customer;
//로거 생성
Logger logger = LoggerFactory.getLogger(MyRunner.class);
@Override
public void run(ApplicationArguments args) throws Exception {
// info
logger.info("Logger 클래스 이름 {}", logger.getClass().getName()); // ch.qos.logback.classic.Logger
logger.info("Customer 현재 모드 = {}", customer.getName());
logger.info("Port Number = {}", environment.getProperty("local.server.port"));
// 환경변수 받아오기
logger.info("myboot.name = {}", name);
logger.info("myboot.age = {}", age);
logger.info("myboot.fullName = {}", fullName);
// debug
// ApplicationArguments는 main메서드의 (String[] args) argument를 전달 받음
logger.debug("VM Argument foo = {} Program argument bar = {}",
args.containsOption("foo")
, args.containsOption("bar")
);
}
}
- 로거를 Log4j2로 변경
- logback을 빼고 log42j 로 변경
- pom.xml 수정
// 하단에 추가
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter</artifactId>
<exclusions>
<exclusion>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-logging</artifactId>
</exclusion>
</exclusions>
</dependency>
<!-- log4j2 -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-log4j2</artifactId>
</dependency>
- ORM(Object Relational Mapping)
- Mapping Rule
- Entity Class ⇔ Table
- Entity Object ⇔ Row(Record)
- Entity Variables ⇔ Columns
- Mapping Rule
- JPA(Java Persistence API)
- ORM 기능을 위한 Jakarta EE 표준 스펙
- Hibernate
- JPA 표준스펙을 구현한 ORM 구현체
- Spring Data JPA
- Hibernate implements JPA
- DAO 인터페이스만 만들면 (안에 메서드만 정의하면) 그 안에 access logic을 자동적으로 구현해줌
- Spring Boot 개발자가 Hibernate를 좀 더 쉽게 사용할 수 있도록 Hibernate 구현체를 더 추상화 한 라이브러리
- Spring JPA와 Spring Boot JAP 차이점
- Spring Data JPA는 Spring Framework 개발자를 위한 라이브러리
- Spring Boot Starter Data JPA는 Spring Boot 개발자를 위한 라이브러리
- H2
- 지원되는 In-Memory 데이터베이스
- 추천, 콘솔기능 제공
- application-test.properties (H2 설정)
myboot.name=\uc2a4\ud504\ub9c1 TEST Mode
# 개발 log level = debug
logging.level.com.basic.myspringboot=debug
spring.datasource.url=jdbc:h2:mem:testdb
spring.datasource.driver-class
-name=org.h2.Driver
spring.datasource.username=sa
- 테스트
- DatabaseRunner.java 생성
package com.basic.myspringboot.runner;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.ApplicationArguments;
import org.springframework.boot.ApplicationRunner;
import org.springframework.core.annotation.Order;
import org.springframework.stereotype.Component;
import javax.sql.DataSource;
import java.sql.Connection;
import java.sql.DatabaseMetaData;
@Component
@Order(1)
@Slf4j // lombok에서 제공하고, 로깅퍼사드 기능 (로거 객체 만들지 않고 log로 사용 가능)
public class DatabaseRunner implements ApplicationRunner {
@Autowired
DataSource dataSource;
@Override
public void run(ApplicationArguments args) throws Exception {
log.info("DataSource 구현 클래스명 {}",dataSource.getClass().getName());
try (Connection connection = dataSource.getConnection()) {
DatabaseMetaData metaData = connection.getMetaData();
log.info("DB Product Name = {}", metaData.getDatabaseProductName());
log.info("DB URL = {}",metaData.getURL());
log.info("DB Username = {}",metaData.getUserName());
}
}
}
- localhost:8080/h2-console 입력
- JDBC URL : jdbc:h2:mem:testdb
- JDBC URL : jdbc:h2:mem:testdb
- Spring Boot 데이터_Entity 클래스
- @Entity
- Entity 클래스임을 지정하고, DB 테이블과 매핑하는 객체를 나타냄
- @Id : entity 기본키
- @GenerateValue : 기본키 값을 자동 생성됨을 나타낼 때 사용
- 자동 생성 전략 : (AUTO, IDENTITY, SEQUENCE, TABLE)
- @Column
- Column은 안줘도 되지만 설정할게 있을 경우 사용
- name 지정하지 않을 경우 변수명이 컬럼명 자동 지정
- @Entity
- com.basic.myspringboot.entity 클래스 생성
- Account.java 생성
package com.basic.myspringboot.entity;
import jakarta.persistence.*;
import lombok.Getter;
import lombok.Setter;
@Getter
@Setter
@Entity
public class Account {
@Id @GeneratedValue(strategy = GenerationType.AUTO)
private Long id;
@Column(unique = true, nullable = false)
private String username;
@Column(nullable = false)
private String password;
}
- ACCOUNT 테이블 확인
- MariaDB 연결 설정
- application-prod.properties 수정
myboot.name=\uc2a4\ud504\ub9c1 PROD Mode
# 운영 log level = info
logging.level.com.basic.myspringboot=info
# MariaDB 접속 정보
spring.datasource.url=jdbc:mariadb://127.0.0.1:3306/boot_db
spring.datasource.username=boot
spring.datasource.password=boot
spring.datasource.driverClassName=org.mariadb.jdbc.Driver
- Data Sources and Drivers
- spring.jpa.hibernate.ddl-auto
- create/create-drop/update는 개발모드에만 사용
- create
- JPA가 DB와 상호작용할 때 기존에 있던 스키마(테이블)을 삭제하고 새로 만드는 것
- create-drop
- JPA 종료 시점에 기존에 있었던 테이블을 삭제함
- update
- 기존 스키마는 유지 + 새로운 것만 추가 (변경된 부분만 반영)
- validate
- 엔티티와 테이블이 정상 매핑되어 있는지 검증
- none
- spring.jpa.show-sql=true
- JPA가 생성한 SQL문을 보여줄 지에 대한 여부를 알려주는 속성
- application-prod.properties 수정
- JPA가 생성한 SQL문을 보여줄 지에 대한 여부를 알려주는 속성
myboot.name=\uc2a4\ud504\ub9c1 PROD Mode
# 운영 log level = info
logging.level.com.basic.myspringboot=info
# MariaDB 접속 정보
spring.datasource.url=jdbc:mariadb://127.0.0.1:3306/boot_db
spring.datasource.username=boot
spring.datasource.password=boot
spring.datasource.driverClassName=org.mariadb.jdbc.Driver
# JPA를 사용한 데이터베이스 초기화
spring.jpa.hibernate.ddl-auto=create
spring.jpa.show-sql=true
- dialect 설정
- 특정 DB벤더의 기능을 추가하기 위해서 만든 것으로 사용하는 특정 벤더의 DBMS 사용이 가능함
- hibernamte에 사용 DB를 알려주면 그 DB의 특징에 맞춰서 최적화 하는 용도
- JPA에 어떤 DBMS를 사용하는지를 알려주는 방법
- PA에 Dialect를 설정할 수 있는 추상화 방언 클래스를 제공하고 설정된 방언으로 각 DBMS에 맞는 구현체를
제공 - hibernate.dialect=org.hibernate.dialect.MariaDBDialect
- application-prod.properties 수정
myboot.name=\uc2a4\ud504\ub9c1 PROD Mode
# 운영 log level = info
logging.level.com.basic.myspringboot=info
# MariaDB 접속 정보
spring.datasource.url=jdbc:mariadb://127.0.0.1:3306/boot_db
spring.datasource.username=boot
spring.datasource.password=boot
spring.datasource.driverClassName=org.mariadb.jdbc.Driver
# JPA를 사용한 데이터베이스 초기화
spring.jpa.hibernate.ddl-auto=create
spring.jpa.show-sql=true
# DB Dialect 설정
spring.jpa.database-platform=org.hibernate.dialect.MariaDBDialect
//Primary Key의 값 자동 증가 전략 4가지
public enum GenerationType {
/**
* Indicates that the persistence provider must assign
* primary keys for the entity using an underlying
* database table to ensure uniqueness.
*/
TABLE,
/**
* Indicates that the persistence provider must assign
* primary keys for the entity using a database sequence.
* Sequence 를 제공하는 Oracle
*/
SEQUENCE,
/**
* Indicates that the persistence provider must assign
* primary keys for the entity using a database identity column.
* Column Auto Increment를 제공하는 MySql, MariaDB
*/
IDENTITY,
/**
* Indicates that the persistence provider should pick an
* appropriate strategy for the particular database. The
* AUTO generation strategy may expect a database * resource to exist, or it may attempt to create one. A vendor * may provide documentation on how to create such resources * in the event that it does not support schema generation * or cannot create the schema resource at runtime. */ AUTO (default) } Dialect(방언, 사투리) spring.jpa.properties.hibernate.dialect=org.hibernate.dialect.MariaDB103Dialect Hibernate Dialect Api doc https://docs.jboss.org/hibernate/orm/6.2/javadocs/org/hibernate/dialect/package-summary.html GenerationType.Auto MariaDB [boot_db]> desc account; +----------+--------------+------+-----+---------+-------+ | Field | Type | Null | Key | Default | Extra | +----------+--------------+------+-----+---------+-------+ | id | bigint(20) | NO | PRI | NULL | | | password | varchar(255) | YES | | NULL | | | username | varchar(255) | YES | UNI | NULL | | +----------+--------------+------+-----+---------+-------+ account_seq Sequence 추가로 생성된다. MariaDB [boot_db]> desc account_seq; +-----------------------+---------------------+------+-----+---------+-------+ | Field | Type | Null | Key | Default | Extra | +-----------------------+---------------------+------+-----+---------+-------+ | next_not_cached_value | bigint(21) | NO | | NULL | | | minimum_value | bigint(21) | NO | | NULL | | | maximum_value | bigint(21) | NO | | NULL | | | start_value | bigint(21) | NO | | NULL | | | increment | bigint(21) | NO | | NULL | | | cache_size | bigint(21) unsigned | NO | | NULL | | | cycle_option | tinyint(1) unsigned | NO | | NULL | | | cycle_count | bigint(21) | NO | | NULL | | +-----------------------+---------------------+------+-----+---------+-------+ GenerationType.Identity ( auto increment column ) +----------+--------------+------+-----+---------+----------------+ | Field | Type | Null | Key | Default | Extra | +----------+--------------+------+-----+---------+----------------+ | id | bigint(20) | NO | PRI | NULL | auto_increment | | password | varchar(255) | YES | | NULL | | | username | varchar(255) | YES | UNI | NULL | | +----------+--------------+------+-----+---------+----------------+ @Table(“user”)
- Repository 인터페이스
- Account findByUsername(String username); 안해도됨
- repository 패키지 생성
- AccountRepostiry 인터페이스 생성
- Optional : null일수도 있고 아닐수도 있는 값을 위함
- pk를 사용해서만 조회 가능 (다른 컬럼으로 조회하고 싶을 경우 따로 sql문 작성 필요)
- update
- entity 객체 생성 -> setter 메서드 호출하여 값 변경하고 save
CrudRepository의 메소드에 대한 설명
T 는 Entity 클래스의 Type
등록: S save(S entity);
리스트 조회: Iterable findAll(); (CrudRepository)
List findAll(); (JpaRepository)
PK로 조회: Optional findById(ID id);
삭제: void delete(T entity);
void deleteById(ID id);
PK존재여부: boolean existsById(ID id);
select * from accounts where id = 3L
select * from accounts where username = ‘spring’
select * from users where email=’aa@a.com’ 1개의 entity
select * from users where name=’spring’ 여러개의 entity
Iterable
https://docs.oracle.com/javase/8/docs/api/java/lang/Iterable.html
Optional
https://docs.oracle.com/en/java/javase/17/docs/api/java.base/java/util/Optional.html
JPA Repository query(finder) method 메소드 명 규칙
https://docs.spring.io/spring-data/jpa/docs/current/reference/html/#jpa.query-methods
- Query Method 규칙
- JPQL
- select u from User u where u.emailAddress = ?1 and u.lastname = ?2
- Select u : *
- From User : User는 Table이 아닌 Entity - AccountRepository.java 수정
package com.basic.myspringboot.repository;
import com.basic.myspringboot.entity.Account;
import org.springframework.data.jpa.repository.JpaRepository;
import java.util.Optional;
public interface AccountRepository extends JpaRepository<Account, Long> {
// < Entity 클래스, PK값 >
// Insert, Delete, Select만 존재
// select * from account where username = 'spring'
Optional<Account> findByUsername(String username);
}
- spring framework
- 기존 웹 접근 방식
- 글 읽기/삭제 : GET
- 글 등록/수정 : POST
- 서버에 보내는 방식은 쿼리 스트링 사용 (key=value 형식)
- 변경된 Restful API 방식
- 글 읽기 : GET
- 글 등록 : POST
- 글 삭제 : DELETE
- 글 수정 : PUT
- URI(자원 명시, 명사형) + METHOD(자원 제어 명령, GET/POST/PUT/DELETE)
- 기존 웹 접근 방식
- REST 사용 이유
- 분산 시스템 설계 목적
- Web 브라우저 이외의 클라이언트를 위해 사용
- REST 제약 조건
- Stateless : 세션 쿠키 저장X
- 캐시 처리 가능
- Spring Boot Web MVC
- LocalDateTime : 현재 시각 설정 가능
- @CreationTimeStamp : 객체를 생성하면서 자동적으로 현재 시간 적용
- account.java Entity 생성
package com.basic.myspringboot.entity;
import jakarta.persistence.*;
import lombok.Getter;
import lombok.Setter;
import org.hibernate.annotations.CreationTimestamp;
import java.time.LocalDateTime;
@Entity
@Table(name = "users")
@Getter @Setter
public class User {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
@Column(nullable = false)
private String name;
@Column(unique = true, nullable = false)
private String email;
@Column(nullable = false, updatable = false)
@CreationTimestamp
private LocalDateTime createdAt = LocalDateTime.now();
}
- UserRepository.java Repository 생성
package com.basic.myspringboot.repository;
import com.basic.myspringboot.entity.User;
import org.springframework.data.jpa.repository.JpaRepository;
import java.util.Optional;
public interface UserRepository extends JpaRepository<User, Long> {
Optional<User> findByName(String name);
}
- Controller
- @Controller (Spring Framework) -> @RestController (Spring Boot)
- @RestController = @Controller + @ResponseBody
- @ResponseBody
- 변환된 데이터를 응답(response) body에 담아주는 역할
- Java Object -> JSON 변환 처리는 Jackson이 담당
WebMVC 관련 어노테이션 설명
@RestController = @Controller + @ResponseBody
Response (응답) : @ResponseBody
: Java Object -> JSON 변환 처리는 Jackson이 담당 함
: 변환된 데이터를 응답(response) body에 담아 주는 역할
Request (요청) : @RequestBody
: JSON -> Java Object 변환 처리는 Jackson이 담당함
: 변환된 데이터를 요청(request)에 담아서 컨트롤러의 메서드의 아규먼트로 매핑 해주는 역할
실질적인 변환 처리가 누가 할까요? Jackson
Java => Json : Serialization (직렬화)
Json => Java : DeSerialization (역직렬화)
ResponseEntity
: Body + Http Status Code + Header 한번에 담아서 응답을 주는 객체
https://www.baeldung.com/spring-response-entity
@PathVariable
Put과 Patch 차이점
: put - 모든 항목 전체수정
: patch - 부분 항목 수정
등록
POST
http://localhost:8080/users
header
content-type:application/json
body
{
"name":"스프링",
"email":"spring@a.com"
}
Id로 조회 GET
http://localhost:8080/users/{id}
email로 조회 GET
http://localhost:8080/users/email/spring@a.com/
목록조회 GET
http://localhost:8080/users
수정 PATCH
header
content-type:application/json
body
http://localhost:8080/users/{email}/
{
"name":"Spring"
}
수정 PUT
header
content-type:application/json
body
http://localhost:8080/users/1
{
"name":"Spring",
"email":"spring@a.com"
}
삭제 DELETE
http://localhost:8080/users/1
ResponseEntity 클래스 https://docs.spring.io/spring-framework/docs/current/javadoc-api/org/springframework/http/ResponseEntity.html
=> Body + Status Code + Header
http://localhost:8080/users/xml
com.fasterxml.jackson.dataformat
jackson-dataformat-xml
${jackson-bom.version}
Thymeleaf Docs
https://www.thymeleaf.org/doc/tutorials/3.1/usingthymeleaf.html
Thymeleaf-Spring Docs
https://www.thymeleaf.org/doc/tutorials/3.1/thymeleafspring.html
- Controller 패키지 생성
- UserBasicRestController.java 클래스 생성
- @PostMapping 테이블이 모두 동일한 경우 상단에 설정 가능
- 등록
- 조회
- 개별 조회
- 에러 처리를 위해 람다식 사용
- exception (에러 처리)
- 500 에러 대비 코드
- 500 에러 대비 코드
- advice
- @RestControllerAdvice : 각 클래스별로 공통적으로 처리해야할 경우 추가
- @BusinessHandler : exception의 메시지와 상태코드 확인하여 메시지를 키로 사용하여 BusinessException의 맞는 메시지 출력
- Optional
- get() : 값 가져오는 메소드 (값이 없을 경우 NoSuchElementException 발생해서 isPresent() 필요)
- isPresent() : 값이 있는지 확인 메소드
- UserBasicRestController.java 수정

package com.basic.myspringboot.controller;
import com.basic.myspringboot.entity.User;
import com.basic.myspringboot.repository.UserRepository;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.*;
import java.util.List;
import java.util.Optional;
import static org.springframework.util.ClassUtils.isPresent;
@RestController
@RequestMapping("/users")
public class UserBasicRestController {
@Autowired
private UserRepository userRepository;
@PostMapping
public User create(@RequestBody User user) {
return userRepository.save(user);
}
@GetMapping
public List<User> getUsers() {
return userRepository.findAll();
}
@GetMapping("/{id}")
public User getUser(@PathVariable Long id) {
Optional<User> optionalUser = userRepository.findById(id);
// if(Optional<User>.isPresent()) {
// User user = optionalUser.get();
// return user;
// }
// orElseThrow(Supplier) Supplier의 추상메서드가 T get()
User user = optionalUser.orElseThrow();
return user;
}
}
- 상세 정보 조회
- 없을 경우 에러 처리
- UserRepository 수정
- 중복을 허용할 경우 -> List 반환
- 중복 허용하지 않을 경우 (unique key) -> Optional로 반환
package com.basic.myspringboot.repository;
import com.basic.myspringboot.entity.User;
import org.springframework.data.jpa.repository.JpaRepository;
import java.util.List;
import java.util.Optional;
public interface UserRepository extends JpaRepository<User, Long> {
Optional<User> findByEmail(String email);
List<User> findByName(String name);
}
- 특정 컬럼으로 조회
- UserBasicRestController.java 수정
package com.basic.myspringboot.controller;
import com.basic.myspringboot.entity.User;
import com.basic.myspringboot.exception.BusinessException;
import com.basic.myspringboot.repository.UserRepository;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.http.HttpStatus;
import org.springframework.web.bind.annotation.*;
import java.util.List;
import java.util.Optional;
import static org.springframework.util.ClassUtils.isPresent;
@RestController
@RequestMapping("/users")
public class UserBasicRestController {
@Autowired
private UserRepository userRepository;
@PostMapping
public User create(@RequestBody User user) {
return userRepository.save(user);
}
@GetMapping
public List<User> getUsers() {
return userRepository.findAll();
}
@GetMapping("/{id}")
public User getUser(@PathVariable Long id) {
Optional<User> optionalUser = userRepository.findById(id);
// 하단과 동일 코드
// if(optionalUser.isPresent()) {
// User user = optionalUser.get();
// return user;
// }
// orElseThrow(Supplier) Supplier의 추상메서드가 T get()
User user = optionalUser.orElseThrow(() -> new BusinessException("User Not Found", HttpStatus.NOT_FOUND));
return user;
}
// 그냥 (/{email}) 할 경우 숫자인지 문자열인지 인식 못함
@GetMapping("/email/{email}")
public User getUserByEmail(@PathVariable String email) {
return userRepository.findByEmail(email)
.orElseThrow(() -> new BusinessException("요청하신 email에 해당하는 User가 없습니다", HttpStatus.NOT_FOUND));
}
}
- ResponseEntity
- Body + Http Status Code + Header 한번에 담아서 응답을 주는 객체
- 서버가 웹 토큰등을 요청받았을 때 응답 헤더에 토큰을 추가해서 전달 -> 클라이언트는 전달받은 응답 헤더를 확인하여 토큰 사용
- 삭제
- 삭제하기 전에 존재하는지 확인 필요
- 조회 먼저 수행
- UserBasicRestController.java 수정
Controller 클래스 -> Repository 인터페이스
Controller 클래스 -> Service 클래스 -> Repository 인터페이스
IntelliJ IDEA 단축키
ctrl + shift + f10 : Java run
ctrl + shift + t : 테스트 케이스 추가
ctrl + alt + o(오우) : auto import
ctrl + alt + v : return type 자동 생성 ( eclipse 는 alt + shift + l(엘) )
ctrl + alt + shift + l(대문자엘) : code format
alt + insert : generate constructor, getter, setter
alt + enter : import , create new class(interface, enum)
alt + shift + insert : column selection mode ( eclipse alt + shift + a )
ctrl + alt + m : extract method
ctrl + alt + n : inline method
ctrl + o (오우) : override method
ctrl + i (아이) : implements method
📕 JPA
- UPDATE
- UseRestController.java 수정
package com.basic.myspringboot.controller;
import com.basic.myspringboot.entity.User;
import com.basic.myspringboot.exception.BusinessException;
import com.basic.myspringboot.repository.UserRepository;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.*;
import java.util.List;
import java.util.Optional;
import static org.springframework.util.ClassUtils.isPresent;
@RestController
@RequestMapping("/users")
public class UserBasicRestController {
@Autowired
private UserRepository userRepository;
@PostMapping
public User create(@RequestBody User user) {
return userRepository.save(user);
}
@GetMapping
public List<User> getUsers() {
return userRepository.findAll();
}
@GetMapping("/{id}")
public User getUser(@PathVariable Long id) {
Optional<User> optionalUser = userRepository.findById(id);
// 하단과 동일 코드
// if(optionalUser.isPresent()) {
// User user = optionalUser.get();
// return user;
// }
// orElseThrow(Supplier) Supplier의 추상메서드가 T get()
User user = optionalUser.orElseThrow(() -> new BusinessException("User Not Found", HttpStatus.NOT_FOUND));
return user;
}
// 그냥 (/{email}) 할 경우 숫자인지 문자열인지 인식 못함
@GetMapping("/email/{email}")
public User getUserByEmail(@PathVariable String email) {
return userRepository.findByEmail(email)
.orElseThrow(() -> new BusinessException("요청하신 email에 해당하는 User가 없습니다", HttpStatus.NOT_FOUND));
}
@GetMapping("/name/{name}")
public List<User> getUserByName(@PathVariable String name) {
return userRepository.findByName(name);
}
@DeleteMapping("/{id}")
public ResponseEntity<?> deleteUser(@PathVariable Long id) {
User user = userRepository.findById(id)
.orElseThrow(() -> new BusinessException("User Not Found", HttpStatus.NOT_FOUND));
userRepository.delete(user);
// user를 삭제해도 가능
//return ResponseEntity.ok(user);
return ResponseEntity.ok(id + " User가 삭제 되었습니다");
}
@PutMapping("/{userId}")
public User updateUser(@PathVariable("userId") Long id, @RequestBody User userDetail) {
User user = userRepository.findById(id)
.orElseThrow(() -> new BusinessException("User Not Found", HttpStatus.NOT_FOUND));
// 수정하려는 값을 저장
user.setName(userDetail.getName());
user.setEmail(userDetail.getEmail());
// set 한 값을 DB에 반영
User updatedUser = userRepository.save(user);
return updatedUser;
}
}
- Service 역할
- DTO -> Entity
- Entity -> DTO
- Response와 Request VO객체 생성
- name, email 입력 필요
- 출력은 모두 출력
- UserReqDTO.java 생성
- name, email
package com.basic.myspringboot.dto;
import lombok.AllArgsConstructor;
import lombok.Getter;
import lombok.NoArgsConstructor;
import lombok.Setter;
@NoArgsConstructor // 기본 생성자 생성
@AllArgsConstructor
@Getter @Setter
public class UserReqDTO {
private String name;
private String email;
}
- UserResDTO.java 생성
package com.basic.myspringboot.dto;
import lombok.AllArgsConstructor;
import lombok.Getter;
import lombok.NoArgsConstructor;
import lombok.Setter;
import java.time.LocalDateTime;
@NoArgsConstructor // 기본 생성자 생성
@AllArgsConstructor
@Getter @Setter
public class UserResDTO {
private Long id;
private String name;
private String email;
private LocalDateTime createdAt = LocalDateTime.now();
}
- modelmapper
- object mapping 역할
- pom.xml 의존성 추가 필요
// 하단에 추가
<dependency>
<groupId>org.modelmapper</groupId>
<artifactId>modelmapper</artifactId>
<version>3.1.1</version>
</dependency>
- @Bean 객체 생성을 반복적으로 하지 않도록 configuration 파일에 설정
// 하단처럼 객체 생성하지 않고 @Bean 사용
ModelMapper modelMapper = new ModelMapper();
OrderDTO orderDTO = modelMapper.map(order, OrderDTO.class);
- MySpringBoot3Application.java
package com.basic.myspringboot;
import org.modelmapper.ModelMapper;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.WebApplicationType;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.context.annotation.Bean;
import javax.swing.*;
@SpringBootApplication
public class MySpringBoot3Application {
public static void main(String[] args) {
// SpringApplication.run(MySpringBoot3Application.class, args);
SpringApplication application = new SpringApplication(MySpringBoot3Application.class);
// WebApplication Type을 변경하기 위한 목적
application.setWebApplicationType(WebApplicationType.SERVLET);
// None : 더이상 WebApplication이 아님
application.run(args);
}
@Bean
public ModelMapper modelMapper() {
ModelMapper modelMapper = new ModelMapper();
return modelMapper;
}
}
- UserService.java
- @Autowired 한개도 사용되지 않음
- saveUser
- 등록 (UserReqDto)에 데이터 입력이 들어오면 reqDto -> entity 변환
- DB 저장 후 entity -> resDto 변환
- getUsers
- User -> UserResDto 변환 후 반환 필요
@Transactional(readOnly = true)
public List<UserResDto> getUsers() {
List<User> userList = userRepository.findAll(); //DB에서 읽어옴
List<UserResDto> userResDtoList = userList.stream() //Stream<User> //Stream에 담음
.map(user -> modelMapper.map(user, UserResDto.class))//Stream<UserResDto> // Entity를 Dto로 담음
.collect(toList());//List<UserResDto> // Stream을 List로 변경
return userResDtoList;
}
- service 패키지 생성
- UserService.java 생성
- @Autowired 쓰지 않고, 변수 생성한 후 생성자 생성해서 injection 가능
- mock 객체도 주입하여 사용 목적
- Mock Object
- 가짜 객체, 실제 객체가 아님
- 실제 db에 연동하지 않고 개발한 서비스가 제대로 동작하는지 테스트 역할
- 새로운 변수를 추가할 경우 생성자에 추가해줘야함
-> lobok의 @RequiredArgsConstructor 사용 - 트랜잭션의 4가지 특징 (ACID)
- Atomicity (원자성) : 모든 작업이 반영되거나 롤백
- Consistenty (일관성) : 데이터는 미리 정의된 규칙에서만 수정 가능
- Isolation (고립성) : A의 트랜잭션이 실행되고 있을 때, B가 작업할 수 있는 정도 (nothing / update만 가능 등)
- Durability (영구성) : 1번 반영(커밋)된 트랜잭션의 내용은 영원히 적용되는 특성
- 트랜잭션
- 여러 작업을 하나로 묶은 단위
- all or nothing (모두 실행되거나 실행되지 않음)
- transactional 사용할 경우 PUT 처리시 setter만 호출하고 save는 하지 않아도됨 (자동 데이터 변경)
Transactional Propagation(트랜잭션 전파속성)
Transaction Propagation
https://helloino.tistory.com/127
Required
None ===> T1
T1 ===> T1 (기존 트랜잭션이 유지됨)
Requires_new
None ===> T2
T1 ===> T2 (항상 새로운 트랜잭션이 시작됨)
Mandatory
None ===> Exception이 발생함
T1 ===> T1 (기존 트랜잭션이 유지됨)
-----------------------------------------------
- UserService.java
- Biz Logic(Business Logic) 수행 위치
- DB 접근 외에 이자율 계산 등 필요한 서비스 로직 처리
package com.basic.myspringboot.service;
import com.basic.myspringboot.dto.UserReqDTO;
import com.basic.myspringboot.dto.UserResDTO;
import com.basic.myspringboot.entity.User;
import com.basic.myspringboot.repository.UserRepository;
import lombok.RequiredArgsConstructor;
import org.modelmapper.ModelMapper;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
@Service
@RequiredArgsConstructor // lombok에 final로 선언된 변수들의 생성자 만들어줌
@Transactional
public class UserService {
// 해당 방식은 Setter Injection
// @Autowired
// private UserRepository userRepository;
// Constructor Injection 방식
// @Autowired 사용하지 않고 injection
// final은 선언과 동시에 초기화 필요 (생성자를 통해 초기화해도 됨)
private final UserRepository userRepository;
private final ModelMapper modelMapper;
// injection 받는 객체 늘어날 경우 개발자가 계속 추가 필요
// -> lobok의 @RequiredArgsConstructor 사용
// public UserService(UserRepository userRepository, ModelMapper modelMapper) {
// this.userRepository = userRepository;
// this.modelMapper = modelMapper;
// }
public UserResDTO saveUser(UserReqDTO userReqDto) {
//reqDto => entity 매핑
User user = modelMapper.map(userReqDto, User.class);
// DB에 저장
User savedUser = userRepository.save(user);
//entity => resDto 매핑
return modelMapper.map(savedUser, UserResDTO.class);
}
}
- UserRestController.java 생성
package com.basic.myspringboot.controller;
import com.basic.myspringboot.dto.UserReqDTO;
import com.basic.myspringboot.dto.UserResDTO;
import com.basic.myspringboot.service.UserService;
import lombok.RequiredArgsConstructor;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
@RestController
@RequestMapping("/api/users") // url 똑같으면 겹치니까 다르게 작성
@RequiredArgsConstructor
public class UserRestController {
private final UserService userService;
// 등록
@PostMapping
public UserResDTO saveUser(@RequestBody UserReqDTO userReqDTO) {
return userService.saveUser(userReqDTO); // service에서 모두 처리되어 controller에서는 호출만 수행
}
}
- 등록 test
- 조회
- transactional을 걸면 성능에 반비례하기 때문에 readOnly를 성능에 도움을 주기 위해 사용
- UserService.java
package com.basic.myspringboot.service;
import com.basic.myspringboot.dto.UserReqDTO;
import com.basic.myspringboot.dto.UserResDTO;
import com.basic.myspringboot.entity.User;
import com.basic.myspringboot.exception.BusinessException;
import com.basic.myspringboot.repository.UserRepository;
import lombok.RequiredArgsConstructor;
import org.modelmapper.ModelMapper;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.http.HttpStatus;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
@Service
@RequiredArgsConstructor // lombok에 final로 선언된 변수들의 생성자 만들어줌
@Transactional
public class UserService {
// 해당 방식은 Setter Injection
// @Autowired
// private UserRepository userRepository;
// Constructor Injection 방식
// @Autowired 사용하지 않고 injection
// final은 선언과 동시에 초기화 필요 (생성자를 통해 초기화해도 됨)
private final UserRepository userRepository;
private final ModelMapper modelMapper;
// injection 받는 객체 늘어날 경우 개발자가 계속 추가 필요
// -> lobok의 @RequiredArgsConstructor 사용
// public UserService(UserRepository userRepository, ModelMapper modelMapper) {
// this.userRepository = userRepository;
// this.modelMapper = modelMapper;
// }
// 등록
public UserResDTO saveUser(UserReqDTO userReqDto) {
//reqDto => entity 매핑
User user = modelMapper.map(userReqDto, User.class);
// DB에 저장
User savedUser = userRepository.save(user);
//entity => resDto 매핑
return modelMapper.map(savedUser, UserResDTO.class);
}
// 조회
public UserResDTO getUserById(Long id) {
User userEntity = userRepository.findById(id) // return type : Optional<User>
.orElseThrow(() -> new BusinessException(id + "User Not Found", HttpStatus.NOT_FOUND));
// Entity -> ResDTO로 변환
UserResDTO userResDTO = modelMapper.map(userEntity, UserResDTO.class);
return userResDTO;
}
}
- UserRestController.java
package com.basic.myspringboot.controller;
import com.basic.myspringboot.dto.UserReqDTO;
import com.basic.myspringboot.dto.UserResDTO;
import com.basic.myspringboot.service.UserService;
import lombok.RequiredArgsConstructor;
import org.springframework.web.bind.annotation.*;
@RestController
@RequestMapping("/api/users") // url 똑같으면 겹치니까 다르게 작성
@RequiredArgsConstructor
public class UserRestController {
private final UserService userService;
// 등록
@PostMapping
public UserResDTO saveUser(@RequestBody UserReqDTO userReqDTO) {
return userService.saveUser(userReqDTO); // service에서 모두 처리되어 controller에서는 호출만 수행
}
// 조회
@GetMapping("/{id}")
public UserResDTO getUserById(@PathVariable Long id) {
return userService.getUserById(id);
}
}
- 조회 test
- java.util.stream
- 기존 collection : 단순히 데이터 담아주는 역할
- stream : collection 기능 + DB 집계 기능
- averagingInt()
- counting()
- maxBy(), minBy()
- reducing() : 합쳐줌
- summingInt () : 합계 계산
- RDB 사용할 경우 sql 집계해서 결과를 전달하니까 굳이 필요없지만, 집계 기능이 없는 DB를 사용할 경우를 대비
- Application 단에서 sql 집계 결과 생성
- java.util.List 안에 stream Collection 있음
- collection을 stream으로 변환 필요
- List의 entity를 변경하기 위해 map() 메서드 사용
@Transactional(readOnly = true) public List<UserResDto> getUsers() { List<User> userList = userRepository.findAll(); List<UserResDto> userResDtoList = userList.stream() //Stream<User> .map(user -> modelMapper.map(user, UserResDto.class))//Stream<UserResDto> 변환 .collect(toList());//List<UserResDto> 변환 return userResDtoList; }
- 목록 조회
- UserService.java
- collect
- stream 요소를 집계하여 반환
- Collectors를 반환
- toList() 메서드 : List로 변환되어 Collector로 반환
package com.basic.myspringboot.service;
import com.basic.myspringboot.dto.UserReqDTO;
import com.basic.myspringboot.dto.UserResDTO;
import com.basic.myspringboot.entity.User;
import com.basic.myspringboot.exception.BusinessException;
import com.basic.myspringboot.repository.UserRepository;
import lombok.RequiredArgsConstructor;
import org.modelmapper.ModelMapper;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.http.HttpStatus;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import java.util.List;
import java.util.stream.Collectors;
import static java.util.stream.Collectors.toList; // Collectors.toList()에서 Collectors 생략 가능
@Service
@RequiredArgsConstructor // lombok에 final로 선언된 변수들의 생성자 만들어줌
@Transactional
public class UserService {
// 해당 방식은 Setter Injection
// @Autowired
// private UserRepository userRepository;
// Constructor Injection 방식
// @Autowired 사용하지 않고 injection
// final은 선언과 동시에 초기화 필요 (생성자를 통해 초기화해도 됨)
private final UserRepository userRepository;
private final ModelMapper modelMapper;
// injection 받는 객체 늘어날 경우 개발자가 계속 추가 필요
// -> lobok의 @RequiredArgsConstructor 사용
// public UserService(UserRepository userRepository, ModelMapper modelMapper) {
// this.userRepository = userRepository;
// this.modelMapper = modelMapper;
// }
// 등록
public UserResDTO saveUser(UserReqDTO userReqDto) {
//reqDto => entity 매핑
User user = modelMapper.map(userReqDto, User.class);
// DB에 저장
User savedUser = userRepository.save(user);
//entity => resDto 매핑
return modelMapper.map(savedUser, UserResDTO.class);
}
// 조회
@Transactional(readOnly = true) // 조회 메서드인 경우에 readOnly=true를 설정하면 성능 향상에 도움
public UserResDTO getUserById(Long id) {
User userEntity = userRepository.findById(id) // return type : Optional<User>
.orElseThrow(() -> new BusinessException(id + "User Not Found", HttpStatus.NOT_FOUND));
// Entity -> ResDTO로 변환
UserResDTO userResDTO = modelMapper.map(userEntity, UserResDTO.class);
return userResDTO;
}
// 전체 목록 조회
@Transactional(readOnly = true)
public List<UserResDTO> getUsers() {
List<User> userList = userRepository.findAll(); // List<User>
// List<User> -> List<UserResDTO>
List<UserResDTO> userResDTOList = userList.stream() // List<User> -> Stream<User>
// map(Function) Function의 추상메서드 : R apply (T t)
.map(user -> modelMapper.map(user, UserResDTO.class)) // Stream<User> -> Stream<UserResDTO>
.collect(toList());// Stream<UserResDTO> -> List<UserResDTO>
return userResDTOList;
}
}
- UserRestController.java
package com.basic.myspringboot.controller;
import com.basic.myspringboot.dto.UserReqDTO;
import com.basic.myspringboot.dto.UserResDTO;
import com.basic.myspringboot.service.UserService;
import lombok.RequiredArgsConstructor;
import org.springframework.web.bind.annotation.*;
import java.util.List;
@RestController
@RequestMapping("/api/users") // url 똑같으면 겹치니까 다르게 작성
@RequiredArgsConstructor
public class UserRestController {
private final UserService userService;
// 등록
@PostMapping
public UserResDTO saveUser(@RequestBody UserReqDTO userReqDTO) {
return userService.saveUser(userReqDTO); // service에서 모두 처리되어 controller에서는 호출만 수행
}
// 조회
@GetMapping("/{id}")
public UserResDTO getUserById(@PathVariable Long id) {
return userService.getUserById(id);
}
// 전체 목록 조회
@GetMapping
public List<UserResDTO> getUsers() {
return userService.getUsers();
}
}
- 전체 목록 조회 test
- 람다식 : 추상메서드 인터페이스를 오버라이딩 할 때 사용 가능?
- 수정
- unique key인 email을 이용하여 수정
- 기존 : findById -> set -> save
- 변경 (@Transactional) : findByEmail -> set
public UserResDto updateUser(String email, UserReqDto userReqDto) {
User existUser = userRepository.findByEmail(email)
.orElseThrow(() ->
new BusinessException(email + " User Not Found", HttpStatus.NOT_FOUND));
//setter method 호출
existUser.setName(userReqDto.getName());
return modelMapper.map(existUser, UserResDto.class);
}
- UserService.java
package com.basic.myspringboot.service;
import com.basic.myspringboot.dto.UserReqDTO;
import com.basic.myspringboot.dto.UserResDTO;
import com.basic.myspringboot.entity.User;
import com.basic.myspringboot.exception.BusinessException;
import com.basic.myspringboot.repository.UserRepository;
import lombok.RequiredArgsConstructor;
import org.modelmapper.ModelMapper;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.http.HttpStatus;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import java.util.List;
import java.util.stream.Collectors;
import static java.util.stream.Collectors.toList; // Collectors.toList()에서 Collectors 생략 가능
@Service
@RequiredArgsConstructor // lombok에 final로 선언된 변수들의 생성자 만들어줌
@Transactional
public class UserService {
// 해당 방식은 Setter Injection
// @Autowired
// private UserRepository userRepository;
// Constructor Injection 방식
// @Autowired 사용하지 않고 injection
// final은 선언과 동시에 초기화 필요 (생성자를 통해 초기화해도 됨)
private final UserRepository userRepository;
private final ModelMapper modelMapper;
// injection 받는 객체 늘어날 경우 개발자가 계속 추가 필요
// -> lobok의 @RequiredArgsConstructor 사용
// public UserService(UserRepository userRepository, ModelMapper modelMapper) {
// this.userRepository = userRepository;
// this.modelMapper = modelMapper;
// }
// 등록
public UserResDTO saveUser(UserReqDTO userReqDto) {
//reqDto => entity 매핑
User user = modelMapper.map(userReqDto, User.class);
// DB에 저장
User savedUser = userRepository.save(user);
//entity => resDto 매핑
return modelMapper.map(savedUser, UserResDTO.class);
}
// 조회
@Transactional(readOnly = true) // 조회 메서드인 경우에 readOnly=true를 설정하면 성능 향상에 도움
public UserResDTO getUserById(Long id) {
User userEntity = userRepository.findById(id) // return type : Optional<User>
.orElseThrow(() -> new BusinessException(id + "User Not Found", HttpStatus.NOT_FOUND));
// Entity -> ResDTO로 변환
UserResDTO userResDTO = modelMapper.map(userEntity, UserResDTO.class);
return userResDTO;
}
// 전체 목록 조회
@Transactional(readOnly = true)
public List<UserResDTO> getUsers() {
List<User> userList = userRepository.findAll(); // List<User>
// List<User> -> List<UserResDTO>
List<UserResDTO> userResDTOList = userList.stream() // List<User> -> Stream<User>
// map(Function) Function의 추상메서드 : R apply (T t)
.map(user -> modelMapper.map(user, UserResDTO.class)) // Stream<User> -> Stream<UserResDTO>
.collect(toList());// Stream<UserResDTO> -> List<UserResDTO>
return userResDTOList;
}
// 수정
public UserResDTO updateUser(String email, UserReqDTO userReqDto) {
User existUser = userRepository.findByEmail(email)
.orElseThrow(() ->
new BusinessException(email + " User Not Found", HttpStatus.NOT_FOUND));
// Dirty Checking 변경 감지를 해서 setter method만 호출해도 update query가 실행됨
existUser.setName(userReqDto.getName());
return modelMapper.map(existUser, UserResDTO.class); // User -> UserResDTO
}
}
- UserRestController.java
package com.basic.myspringboot.controller;
import com.basic.myspringboot.dto.UserReqDTO;
import com.basic.myspringboot.dto.UserResDTO;
import com.basic.myspringboot.service.UserService;
import lombok.RequiredArgsConstructor;
import org.springframework.web.bind.annotation.*;
import java.util.List;
@RestController
@RequestMapping("/api/users") // url 똑같으면 겹치니까 다르게 작성
@RequiredArgsConstructor
public class UserRestController {
private final UserService userService;
// 등록
@PostMapping
public UserResDTO saveUser(@RequestBody UserReqDTO userReqDTO) {
return userService.saveUser(userReqDTO); // service에서 모두 처리되어 controller에서는 호출만 수행
}
// 조회
@GetMapping("/{id}")
public UserResDTO getUserById(@PathVariable Long id) {
return userService.getUserById(id);
}
// 전체 목록 조회
@GetMapping
public List<UserResDTO> getUsers() {
return userService.getUsers();
}
// 수정
@PatchMapping("/{email}")
public UserResDTO updateUser(@PathVariable String email, @RequestBody UserReqDTO userReqDTO) {
return userService.updateUser(email, userReqDTO);
}
}
- @DynamicUpdate
- name만 수정했는데 모든 컬럼 수정되는 문제 해결
- 수정하고자 하는 값만 변경 가능
- User.java 수정
package com.basic.myspringboot.entity;
import jakarta.persistence.*;
import lombok.Getter;
import lombok.Setter;
import org.hibernate.annotations.CreationTimestamp;
import org.hibernate.annotations.DynamicUpdate;
import java.time.LocalDateTime;
@Entity
@Table(name = "users")
@Getter @Setter
@DynamicUpdate
public class User {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
@Column(nullable = false)
private String name;
@Column(unique = true, nullable = false)
private String email;
@Column(nullable = false, updatable = false)
@CreationTimestamp
private LocalDateTime createdAt = LocalDateTime.now();
}
- 변경 감지 (Dirty Checking)
- setter만 호출해도 update 수행 (변경 감지 기능)
- 데이터 변동되는 행위를 dirty라 표현
- transaction 관리 하에 동작해야 dirty checking 사용 가능
- @Transactional
- transaction.begin() + transaction.commit() 기능
- 삭제
- UserService.java
package com.basic.myspringboot.service;
import com.basic.myspringboot.dto.UserReqDTO;
import com.basic.myspringboot.dto.UserResDTO;
import com.basic.myspringboot.entity.User;
import com.basic.myspringboot.exception.BusinessException;
import com.basic.myspringboot.repository.UserRepository;
import lombok.RequiredArgsConstructor;
import org.modelmapper.ModelMapper;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.http.HttpStatus;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import java.util.List;
import java.util.stream.Collectors;
import static java.util.stream.Collectors.toList; // Collectors.toList()에서 Collectors 생략 가능
@Service
@RequiredArgsConstructor // lombok에 final로 선언된 변수들의 생성자 만들어줌
@Transactional
public class UserService {
// 해당 방식은 Setter Injection
// @Autowired
// private UserRepository userRepository;
// Constructor Injection 방식
// @Autowired 사용하지 않고 injection
// final은 선언과 동시에 초기화 필요 (생성자를 통해 초기화해도 됨)
private final UserRepository userRepository;
private final ModelMapper modelMapper;
// injection 받는 객체 늘어날 경우 개발자가 계속 추가 필요
// -> lobok의 @RequiredArgsConstructor 사용
// public UserService(UserRepository userRepository, ModelMapper modelMapper) {
// this.userRepository = userRepository;
// this.modelMapper = modelMapper;
// }
// 등록
public UserResDTO saveUser(UserReqDTO userReqDto) {
//reqDto => entity 매핑
User user = modelMapper.map(userReqDto, User.class);
// DB에 저장
User savedUser = userRepository.save(user);
//entity => resDto 매핑
return modelMapper.map(savedUser, UserResDTO.class);
}
// 조회
@Transactional(readOnly = true) // 조회 메서드인 경우에 readOnly=true를 설정하면 성능 향상에 도움
public UserResDTO getUserById(Long id) {
User userEntity = userRepository.findById(id) // return type : Optional<User>
.orElseThrow(() -> new BusinessException(id + "User Not Found", HttpStatus.NOT_FOUND));
// Entity -> ResDTO로 변환
UserResDTO userResDTO = modelMapper.map(userEntity, UserResDTO.class);
return userResDTO;
}
// 전체 목록 조회
@Transactional(readOnly = true)
public List<UserResDTO> getUsers() {
List<User> userList = userRepository.findAll(); // List<User>
// List<User> -> List<UserResDTO>
List<UserResDTO> userResDTOList = userList.stream() // List<User> -> Stream<User>
// map(Function) Function의 추상메서드 : R apply (T t)
.map(user -> modelMapper.map(user, UserResDTO.class)) // Stream<User> -> Stream<UserResDTO>
.collect(toList());// Stream<UserResDTO> -> List<UserResDTO>
return userResDTOList;
}
// 수정
public UserResDTO updateUser(String email, UserReqDTO userReqDto) {
User existUser = userRepository.findByEmail(email)
.orElseThrow(() ->
new BusinessException(email + " User Not Found", HttpStatus.NOT_FOUND));
// Dirty Checking 변경 감지를 해서 setter method만 호출해도 update query가 실행됨
existUser.setName(userReqDto.getName());
return modelMapper.map(existUser, UserResDTO.class); // User -> UserResDTO
}
// 삭제
public void deleteUser(Long id) {
User user = userRepository.findById(id) //Optional<User>
.orElseThrow(() ->
new BusinessException(id + " User Not Found", HttpStatus.NOT_FOUND));
userRepository.delete(user);
}
}
- UserRestController.java
package com.basic.myspringboot.controller;
import com.basic.myspringboot.dto.UserReqDTO;
import com.basic.myspringboot.dto.UserResDTO;
import com.basic.myspringboot.service.UserService;
import lombok.RequiredArgsConstructor;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.*;
import java.util.List;
@RestController
@RequestMapping("/api/users") // url 똑같으면 겹치니까 다르게 작성
@RequiredArgsConstructor
public class UserRestController {
private final UserService userService;
// 등록
@PostMapping
public UserResDTO saveUser(@RequestBody UserReqDTO userReqDTO) {
return userService.saveUser(userReqDTO); // service에서 모두 처리되어 controller에서는 호출만 수행
}
// 조회
@GetMapping("/{id}")
public UserResDTO getUserById(@PathVariable Long id) {
return userService.getUserById(id);
}
// 전체 목록 조회
@GetMapping
public List<UserResDTO> getUsers() {
return userService.getUsers();
}
// 수정
@PatchMapping("/{email}")
public UserResDTO updateUser(@PathVariable String email, @RequestBody UserReqDTO userReqDTO) {
return userService.updateUser(email, userReqDTO);
}
// 삭제
@DeleteMapping("/{id}")
public ResponseEntity<?> deleteUser(@PathVariable Long id) {
userService.deleteUser(id);
return ResponseEntity.ok(id + " User가 삭제처리 되었습니다.");
}
}
- resources 하단에 mobile 폴더 생성
- index.html을 mobile 폴더에 붙여넣기
- 그냥 localhost:8080/mobile/index.html 입력할 경우 404 에러 발생
- Customizing Error Page
- 원하는 페이지로 에러 처리
- static 하단에 error 폴더 생성
- 404 HTML 파일 생성
- 404.html
<!DOCTYPE html>
<html lang="en" >
<head>
<meta charset="UTF-8">
<title>market-wp.com</title>
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/normalize/5.0.0/normalize.min.css">
<style>
#oopss {
background: linear-gradient(-45deg, #fff300, #efe400);
position: fixed;
left: 0px;
top: 0;
width: 100%;
height: 100%;
line-height: 1.5em;
z-index: 9999;
}
#oopss #error-text {
font-size: 40px;
display: flex;
flex-direction: column;
align-items: center;
font-family: 'Shabnam', Tahoma, sans-serif;
color: #000;
direction: rtl;
}
#oopss #error-text img {
margin: 85px auto 20px;
height: 342px;
}
#oopss #error-text span {
position: relative;
font-size: 3.3em;
font-weight: 900;
margin-bottom: 50px;
}
#oopss #error-text p.p-a {
font-size: 19px;
margin: 30px 0 15px 0;
}
#oopss #error-text p.p-b {
font-size: 15px;
}
#oopss #error-text .back {
background: #fff;
color: #000;
font-size: 30px;
text-decoration: none;
margin: 2em auto 0;
padding: .7em 2em;
border-radius: 500px;
box-shadow: 0 20px 70px 4px rgba(0, 0, 0, 0.1), inset 7px 33px 0 0px #fff300;
font-weight: 900;
transition: all 300ms ease;
}
#oopss #error-text .back:hover {
-webkit-transform: translateY(-13px);
transform: translateY(-13px);
box-shadow: 0 35px 90px 4px rgba(0, 0, 0, 0.3), inset 0px 0 0 3px #000;
}
@font-face {
font-family: Shabnam;
src: url("https://cdn.rawgit.com/ahmedhosna95/upload/ba6564f8/fonts/Shabnam/Shabnam-Bold.eot");
src: url("https://cdn.rawgit.com/ahmedhosna95/upload/ba6564f8/fonts/Shabnam/Shabnam-Bold.eot?#iefix") format("embedded-opentype"), url("https://cdn.rawgit.com/ahmedhosna95/upload/ba6564f8/fonts/Shabnam/Shabnam-Bold.woff") format("woff"), url("https://cdn.rawgit.com/ahmedhosna95/upload/ba6564f8/fonts/Shabnam/Shabnam-Bold.woff2") format("woff2"), url("https://cdn.rawgit.com/ahmedhosna95/upload/ba6564f8/fonts/Shabnam/Shabnam-Bold.ttf") format("truetype");
font-weight: bold;
}
@font-face {
font-family: Shabnam;
src: url("https://cdn.rawgit.com/ahmedhosna95/upload/ba6564f8/fonts/Shabnam/Shabnam.eot");
src: url("https://cdn.rawgit.com/ahmedhosna95/upload/ba6564f8/fonts/Shabnam/Shabnam.eot?#iefix") format("embedded-opentype"), url("https://cdn.rawgit.com/ahmedhosna95/upload/ba6564f8/fonts/Shabnam/Shabnam.woff") format("woff"), url("https://cdn.rawgit.com/ahmedhosna95/upload/ba6564f8/fonts/Shabnam/Shabnam.woff2") format("woff2"), url("https://cdn.rawgit.com/ahmedhosna95/upload/ba6564f8/fonts/Shabnam/Shabnam.ttf") format("truetype");
font-weight: normal;
}
</style>
</head>
<body>
<div id='oopss'>
<div id='error-text'>
<img src="https://cdn.rawgit.com/ahmedhosna95/upload/1731955f/sad404.svg" alt="404">
<span>404 PAGE</span>
<p class="p-a">
. The page you were looking for could not be found</p>
<p class="p-b">
... Back to previous page
</p>
<a href='#' class="back">... Back to previous page</a>
</div>
</div>
</body>
</html>
- ("/m/**") : 웹 상의 경로
- target 폴더 -> classes 하단에 mobile 생성됨
- WebConfig.java 생성
- config에 추가
package com.basic.myspringboot.config;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.servlet.config.annotation.ResourceHandlerRegistry;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;
@Configuration
public class WebConfig implements WebMvcConfigurer {
@Override
public void addResourceHandlers(ResourceHandlerRegistry registry) {
registry.addResourceHandler("/mobile/**")
//반드시 mobile 다음에 / 을 주어야 한다.
.addResourceLocations("classpath:/mobile/")
.setCachePeriod(20);//20초
}
}
- http://localhost:8080/mobile/index.html
- webjar
- backend jar 안에 포함되어 배포
- backend jar 안에 포함되어 배포
- favicon
- favocon.ico 파일을 static 하단에 붙여넣기
- favocon.ico 파일을 static 하단에 붙여넣기
- gradle
- Gradle-Groovy 선택
- 소스 코드는 동일
- application.properties -> application.yaml
- pom.xml ->build.gradle
- yaml 문법
- # : 주석
- --- : 문서의 시작
- ... : 문서의 끝
- key: value : 키-값 형식 (: 이후 공백 필수)
- Thymeleaf
- 의존성 추가 pom.xml 수정
- 서버사이드 자바 템플릿 엔진
- Thymeleaf 템플릿 페이지 위치
- /src/main/resources/templates/

<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-thymeleaf</artifactId>
</dependency>
- 다국어 언어 지원
- {...} 사용
- {...} 사용
- Vriable
- ${ } 사용
- Selection Variable
- th:object 정의된 변수가 있으면 그 변수에 포함된 값 나타냄
- *{ } 사용
- th:text = "{session.user}"
+ th:text = "*{firstName}"
- Link URL
- @{ } 사용
- Value 연결 방식
- th:text="${name}"
- span th:text="${name}"
- [[${name}]]
- UserController.java 생성
package com.basic.myspringboot.controller;
import org.springframework.stereotype.Controller;
import org.springframework.ui.Model;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
@Controller
@RequestMapping("/userspage")
public class UserController {
@GetMapping("/first")
public String leaf(Model model) {
model.addAttribute("name","스프링부트");
return "leaf";
}
}
- templates/leaf.html 생성
<!DOCTYPE html>
<html xmlns:th="http://www.thymeleaf.org">
<head>
<meta charset="UTF-8">
<title>Spring Boot Thymeleaf</title>
</head>
<body>
<h1 th:text="${name}">네임</h1>
<h1>Hello, <span th:text="${name}">스팬태그</span></h1>
<h1>Hello, [[${name}]]</h1>
</body>
</html>
- 실행
- http://localhost:8080/first
- 톰캣 상에 동작하는 것이 아닌 local에서 실행
- templates/leaf.html 실행
- 경로 : C:\Users\Administrator\Desktop\ucamp\springboot\workspace\MySpringBoot3\src\main\resources\templates
- 컨트롤러 거치지 않아서 name의 value를 인식하지 못함
- jsp와 차이점은 thymeleaf는 변수를 읽지 않아도 error 나지 않고, 실행 가능
- Thymeleaf 수동으로 설정하기 (기존 자동 설정)
- spring.thymeleaf.prefix=classpath:templates/
- spring.thymeleaf.check-template-location=true
- spring.thymeleaf.suffix=.html
- spring.thymeleaf.mode=HTML5
- spring.thymeleaf.cache=false
- spring.thymeleaf.order=0
- localhost:8080은 static 하단의 index.html 실행
- 정적 페이지 거치지 않고 컨트롤러가 지정한 페이지 지정 가능
- redirect: 사용
- IndexController.java 생성
package com.basic.myspringboot.controller;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.GetMapping;
@Controller
public class IndexController {
@GetMapping("/")
public String index() {
return "redirect:/userspage/first"; // leaf.html 요청되어 출력
}
}
- index.html 생성하지 않고 실습
- UserController.java 수정
package com.basic.myspringboot.controller;
import com.basic.myspringboot.dto.UserResDTO;
import com.basic.myspringboot.service.UserService;
import lombok.RequiredArgsConstructor;
import org.springframework.stereotype.Controller;
import org.springframework.ui.Model;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.servlet.ModelAndView;
import java.util.List;
@Controller
@RequestMapping("/userspage")
@RequiredArgsConstructor
public class UserController {
private final UserService userService;
@GetMapping("/first")
public String leaf(Model model) {
model.addAttribute("name","스프링부트");
return "leaf";
}
@GetMapping("/index")
public ModelAndView index() {
List<UserResDTO> userResDTOList = userService.getUsers();
return new ModelAndView("index", "users", userResDTOList); // viewName, modelName, modelObject
}
}
- templates/index.html
<!DOCTYPE html>
<html xmlns:th="http://www.thymeleaf.org">
<meta charset="UTF-8">
<body>
<table>
<tr>
<th>Name</th>
<th>Email</th>
</tr>
<tr th:each="user : ${users}">
<td th:text="${user.name}"></td>
<td th:text="${user.email}"></td>
</tr>
</table>
</body>
</html>
- IndexController.java 위치 수정
package com.basic.myspringboot.controller;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.GetMapping;
@Controller
public class IndexController {
@GetMapping("/")
public String index() {
return "redirect:/userspage/index";
// return "redirect:/userspage/first"; // leaf.html 요청되어 출력
}
}
- thymeleaf each index
- id 연속적인 수로 화면에 뿌리기
- id 연속적인 수로 화면에 뿌리기
- index.html 수정
- index() : 0부터 시작
- count() : 1부터 시작
- thymeleaf date format
- dates 내장 객체 사용
- dates.format()
- th:text="${#temporals.format(notice.date, 'dd-MM-YY')}
<!DOCTYPE html>
<html xmlns:th="http://www.thymeleaf.org">
<meta charset="UTF-8">
<body>
<table>
<tr>
<th>Seq</th>
<th>Name</th>
<th>Email</th>
<th>EntryDate</th>
</tr>
<tr th:each="user, userStat : ${users}">
<td th:text="${userStat.count}"></td> <!-- id 연속적인 수로 출력 -->
<td th:text="${user.name}"></td>
<td th:text="${user.email}"></td>
<!-- <td th:text="${user.createdAt}"></td>-->
<td th:text="${#temporals.format(user.createdAt, 'yyyy-MM-dd hh:mm')}"></td>
</tr>
</table>
</body>
</html>
ctrl+alt+v (변수명 설정)
ctrl+alt+shift+L (자동 정렬)
📕 thymeleaf
- time.temperal
- JPA의 구현체 hibernate
- validation의 구현체 hibernate-validation
Validation(입력항목 검증) 표준 스펙
Java Bean Validation
jakarta.validation.*
Jakarta API Docs
https://jakarta.ee/specifications/platform/8/apidocs/
Java Bean Validation의 구현체
Hibernate Validator
https://github.com/hibernate/hibernate-validator
https://hibernate.org/validator/
Hibernate Validator API
https://docs.jboss.org/hibernate/validator/6.2/api/
Spring Framework5 API Docs
https://docs.spring.io/spring-framework/docs/5.3.24/javadoc-api/
jakarta validation의 어노테이션
@NotEmpty vs @NotBlank
@NotEmpty : white space(“ “) 는 허용
@NotBlank : 문자열의 공백을 제거(trim)하고 체크 하므로
white space(“ “) 허용하지 않음 trim() + notEmpty()
UserDto ${userDto}
- javax.validation.constraints
- 데이터 검증을 위한 로직을 도메인 모델 자체에 묶어서 표현
- email, digit, null 등 형식이 맞는지 체크해주는 기능
- @Valid
- 데이터 검증을 위한 어노테이션
- @NotEmpty : white space(“ “) 허용
- @NotBlank : 문자열의 공백을 제거(trim)하고 체크
- white space(“ “) 허용하지 않음
- trim() + notEmpty()
- Entity 대신 ReqDTO에 구현
- UserReqDTO.java
package com.basic.myspringboot.dto;
import jakarta.validation.constraints.NotBlank;
import jakarta.validation.constraints.NotEmpty;
import lombok.AllArgsConstructor;
import lombok.Getter;
import lombok.NoArgsConstructor;
import lombok.Setter;
@NoArgsConstructor // 기본 생성자 생성
@AllArgsConstructor
@Getter @Setter
public class UserReqDTO {
@NotEmpty(message = "Name은 필수 입력 항목입니다") // " " 허용
private String name;
@NotBlank(message = "Email은 필수 입력 항목입니다") // " " 허용하지 않음
private String email;
}
- 왜 GetMapping에서 User user(UserReqDTO) 인자로 받아야하는지
- @Valid는 입력 조건에 맞게 체크
- @NotBlank/NotEmpty는 조건 명시
- 만약 에러가 났을 경우 BindingResult에 정보를 저장
- org.springframework.validation Errors
- Errors 클래스의 하위 클래스가 BindingResult
- getFieldError : 객체에 담아서 전송
- getFieldErrors : 여러 에러를 객체에 담음
- getGlobalError : 개별 항목이 아니라 다중 필드의 에러를 체크해야할 경우 사용
- getObjectName : 담고 있는 객체 (UserReqDTO)의 이름
- hasErrors() : 에러 객체에 정보가 저장했는지(에러가 발생했는지) 확인
- #fields : 에러에 대한 객체를 thymeleaf 쪽에서 만들어줌 (=bindresult)
- templates/index.html 수정 (타임리프 하단에 작성)
<!DOCTYPE html>
<html xmlns:th="http://www.thymeleaf.org">
<meta charset="UTF-8">
<body>
<table>
<tr>
<th>Seq</th>
<th>Name</th>
<th>Email</th>
<th>EntryDate</th>
</tr>
<tr th:each="user, userStat : ${users}">
<td th:text="${userStat.count}"></td> <!-- id 연속적인 수로 출력 -->
<td th:text="${user.name}"></td>
<td th:text="${user.email}"></td>
<!-- <td th:text="${user.createdAt}"></td>-->
<td th:text="${#temporals.format(user.createdAt, 'yyyy-MM-dd hh:mm')}"></td>
</tr>
</table>
<p>
<a herf="/userspage/signup">Insert</a> <!-- 추가 -->
</p>
</body>
</html>
- UserController.java 수정
package com.basic.myspringboot.controller;
import com.basic.myspringboot.dto.UserReqDTO;
import com.basic.myspringboot.dto.UserResDTO;
import com.basic.myspringboot.service.UserService;
import jakarta.validation.Valid;
import lombok.RequiredArgsConstructor;
import org.springframework.stereotype.Controller;
import org.springframework.ui.Model;
import org.springframework.validation.BindingResult;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.servlet.ModelAndView;
import java.util.List;
@Controller
@RequestMapping("/userspage")
@RequiredArgsConstructor
public class UserController {
private final UserService userService;
@GetMapping("/first")
public String leaf(Model model) {
model.addAttribute("name","스프링부트");
return "leaf";
}
@GetMapping("/index")
public ModelAndView index() {
List<UserResDTO> userResDTOList = userService.getUsers();
return new ModelAndView("index", "users", userResDTOList); // viewName, modelName, modelObject
}
// 등록 페이지를 호출 해주는 메서드
@GetMapping("/signup")
public String showSignUpForm(UserReqDTO user) {
return "add-user";
}
// 입력 항목 검증을 한 후 등록 처리 메서드
@PostMapping("/adduser")
public String addUser(@Valid UserReqDTO user, BindingResult result, Model model) {
// 입력 항목 검증 오류가 발생했는지 확인
if (result.hasErrors()) {
return "add-user";
}
// 등록 요청
userService.saveUser(user);
model.addAttribute("users", userService.getUsers());
return "index";
}
}
- add-user.html 생성
- templates 하단에 생성
<!DOCTYPE html>
<html xmlns:th="http://www.thymeleaf.org">
<meta charset="UTF-8">
<body>
<!-- th:action="@{/adduser}"를 th:action="@{/userspage/adduser}"로 변경 -->
<!-- th:object="${user}" 의 user는 user객체의 클래스 이름 (소문자 변환 필요)-->
<form action="#" th:action="@{/userspage/adduser}" th:object="${userReqDTO}" method="post">
<label for="name">Name</label>
<input type="text" th:field="*{name}" id="name">
<span th:if="${#fields.hasErrors('name')}" th:errors="*{name}"></span>
<br/>
<label for="email">Email</label>
<input type="text" th:field="*{email}" id="email">
<span th:if="${#fields.hasErrors('email')}" th:errors="*{email}"></span>
<br/>
<input type="submit" value="Add User">
</form>
</body>
</html>
- UserReqDTO 수정
package com.basic.myspringboot.dto;
import jakarta.validation.constraints.Email;
import jakarta.validation.constraints.NotBlank;
import jakarta.validation.constraints.NotEmpty;
import lombok.AllArgsConstructor;
import lombok.Getter;
import lombok.NoArgsConstructor;
import lombok.Setter;
@NoArgsConstructor // 기본 생성자 생성
@AllArgsConstructor
@Getter @Setter
public class UserReqDTO {
@NotEmpty(message = "Name은 필수 입력 항목입니다") // " " 허용
private String name;
@NotBlank(message = "Email은 필수 입력 항목입니다") // " " 허용하지 않음
@Email(message = "Email 형식이 아닙니다")
private String email;
}
- lable for="name" 구문으로 인해 id를 지정함
- field
- id, name, value 속성을 제공
- thymeleaf + spring
- 한글 인코딩 설정 필요 없음
- UPDATE
- UserController.java 수정
package com.basic.myspringboot.controller;
import com.basic.myspringboot.dto.UserReqDTO;
import com.basic.myspringboot.dto.UserResDTO;
import com.basic.myspringboot.service.UserService;
import jakarta.validation.Valid;
import lombok.RequiredArgsConstructor;
import org.springframework.stereotype.Controller;
import org.springframework.ui.Model;
import org.springframework.validation.BindingResult;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.servlet.ModelAndView;
import java.util.List;
@Controller
@RequestMapping("/userspage")
@RequiredArgsConstructor
public class UserController {
private final UserService userService;
@GetMapping("/first")
public String leaf(Model model) {
model.addAttribute("name", "스프링부트");
return "leaf";
}
@GetMapping("/index")
public ModelAndView index() {
List<UserResDTO> userResDTOList = userService.getUsers();
return new ModelAndView("index", "users", userResDTOList); // viewName, modelName, modelObject
}
// 등록 페이지를 호출 해주는 메서드
@GetMapping("/signup")
public String showSignUpForm(UserReqDTO user) {
return "add-user";
}
// 입력 항목 검증을 한 후 등록 처리 메서드
@PostMapping("/adduser")
public String addUser(@Valid UserReqDTO user, BindingResult result, Model model) {
// 입력 항목 검증 오류가 발생했는지 확인
if (result.hasErrors()) {
return "add-user";
}
// 등록 요청
userService.saveUser(user);
// model.addAttribute("users", userService.getUsers());
return "redirect:/userspage/index";
}
// 수정 페이지를 호출해주는 메서드
@GetMapping("/edit/{id}")
public String showUpdateForm(@PathVariable Long id, Model model) {
UserResDTO userResDTO = userService.getUserById(id);
model.addAttribute("user", userResDTO);
return "update-user";
}
}
- update-user.html 생성
<!DOCTYPE html>
<html xmlns:th="http://www.thymeleaf.org">
<meta charset="UTF-8">
<body>
<!-- th:object="${user}" DTO로 변환하면 에러남-->
<form action="#" th:action="@{/update/{id}(id=${user.id})}" th:object="${user}" method="post">
<label for="name">Name</label>
<input type="text" th:field="*{name}" id="name">
<span th:if="${#fields.hasErrors('name')}" th:errors="*{name}"></span> <br />
<label for="email">Email</label>
<input type="text" th:field="*{email}" id="email">
<span th:if="${#fields.hasErrors('email')}" th:errors="*{email}"></span> <br />
<input type="submit" value="Update User">
</form>
</body>
</html>
- index.html 수정
<!DOCTYPE html>
<html xmlns:th="http://www.thymeleaf.org">
<meta charset="UTF-8">
<body>
<table>
<tr>
<th>Seq</th>
<th>Name</th>
<th>Email</th>
<th>EntryDate</th>
<th>Update</th>
</tr>
<tr th:each="user, userStat : ${users}">
<td th:text="${userStat.count}"></td> <!-- id 연속적인 수로 출력 -->
<td th:text="${user.name}"></td>
<td th:text="${user.email}"></td>
<!-- <td th:text="${user.createdAt}"></td>-->
<td th:text="${#temporals.format(user.createdAt, 'yyyy-MM-dd hh:mm')}"></td>
<td><a th:href="@{/userspage/edit/{id}(id=${user.id})}">Update</a></td>
</tr>
</table>
<p><a href="/userspage/signup">Insert</a></p>
</body>
</html>
- 기존에 UserReqDTO에 id 값이 없어서 새로 UserReqForm 생성
- UserReqForm.java
package com.basic.myspringboot.dto;
import jakarta.validation.constraints.Email;
import jakarta.validation.constraints.NotBlank;
import jakarta.validation.constraints.NotEmpty;
import lombok.AllArgsConstructor;
import lombok.Getter;
import lombok.NoArgsConstructor;
import lombok.Setter;
@NoArgsConstructor // 기본 생성자 생성
@AllArgsConstructor
@Getter @Setter
public class UserReqForm {
private Long id;
@NotEmpty(message = "Name은 필수 입력 항목입니다") // " " 허용
private String name;
@NotBlank(message = "Email은 필수 입력 항목입니다") // " " 허용하지 않음
@Email(message = "Email 형식이 아닙니다")
private String email;
}
- UserService.java 수정
package com.basic.myspringboot.service;
import com.basic.myspringboot.dto.UserReqDTO;
import com.basic.myspringboot.dto.UserReqForm;
import com.basic.myspringboot.dto.UserResDTO;
import com.basic.myspringboot.entity.User;
import com.basic.myspringboot.exception.BusinessException;
import com.basic.myspringboot.repository.UserRepository;
import lombok.RequiredArgsConstructor;
import org.modelmapper.ModelMapper;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.http.HttpStatus;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import java.util.List;
import java.util.stream.Collectors;
import static java.util.stream.Collectors.toList; // Collectors.toList()에서 Collectors 생략 가능
@Service
@RequiredArgsConstructor // lombok에 final로 선언된 변수들의 생성자 만들어줌
@Transactional
public class UserService {
// 해당 방식은 Setter Injection
// @Autowired
// private UserRepository userRepository;
// Constructor Injection 방식
// @Autowired 사용하지 않고 injection
// final은 선언과 동시에 초기화 필요 (생성자를 통해 초기화해도 됨)
private final UserRepository userRepository;
private final ModelMapper modelMapper;
// injection 받는 객체 늘어날 경우 개발자가 계속 추가 필요
// -> lobok의 @RequiredArgsConstructor 사용
// public UserService(UserRepository userRepository, ModelMapper modelMapper) {
// this.userRepository = userRepository;
// this.modelMapper = modelMapper;
// }
// 등록
public UserResDTO saveUser(UserReqDTO userReqDto) {
//reqDto => entity 매핑
User user = modelMapper.map(userReqDto, User.class);
// DB에 저장
User savedUser = userRepository.save(user);
//entity => resDto 매핑
return modelMapper.map(savedUser, UserResDTO.class);
}
// 조회
@Transactional(readOnly = true) // 조회 메서드인 경우에 readOnly=true를 설정하면 성능 향상에 도움
public UserResDTO getUserById(Long id) {
User userEntity = userRepository.findById(id) // return type : Optional<User>
.orElseThrow(() -> new BusinessException(id + "User Not Found", HttpStatus.NOT_FOUND));
// Entity -> ResDTO로 변환
UserResDTO userResDTO = modelMapper.map(userEntity, UserResDTO.class);
return userResDTO;
}
// 전체 목록 조회
@Transactional(readOnly = true)
public List<UserResDTO> getUsers() {
List<User> userList = userRepository.findAll(); // List<User>
// List<User> -> List<UserResDTO>
List<UserResDTO> userResDTOList = userList.stream() // List<User> -> Stream<User>
// map(Function) Function의 추상메서드 : R apply (T t)
.map(user -> modelMapper.map(user, UserResDTO.class)) // Stream<User> -> Stream<UserResDTO>
.collect(toList());// Stream<UserResDTO> -> List<UserResDTO>
return userResDTOList;
}
// 수정
public UserResDTO updateUser(String email, UserReqDTO userReqDto) {
User existUser = userRepository.findByEmail(email)
.orElseThrow(() ->
new BusinessException(email + " User Not Found", HttpStatus.NOT_FOUND));
// Dirty Checking 변경 감지를 해서 setter method만 호출해도 update query가 실행됨
existUser.setName(userReqDto.getName());
return modelMapper.map(existUser, UserResDTO.class); // User -> UserResDTO
}
public void updateUserForm(UserReqForm userReqForm) {
User existUser = userRepository.findById(userReqForm.getId())
.orElseThrow(() ->
new BusinessException(userReqForm.getId() + " User Not Found", HttpStatus.NOT_FOUND));
existUser.setName(userReqForm.getName());
// 반환하지 않고 update만 진행
}
// 삭제
public void deleteUser(Long id) {
User user = userRepository.findById(id) //Optional<User>
.orElseThrow(() ->
new BusinessException(id + " User Not Found", HttpStatus.NOT_FOUND));
userRepository.delete(user);
}
}
- UserController.java 수정
package com.basic.myspringboot.controller;
import com.basic.myspringboot.dto.UserReqDTO;
import com.basic.myspringboot.dto.UserReqForm;
import com.basic.myspringboot.dto.UserResDTO;
import com.basic.myspringboot.service.UserService;
import jakarta.validation.Valid;
import lombok.RequiredArgsConstructor;
import org.springframework.stereotype.Controller;
import org.springframework.ui.Model;
import org.springframework.validation.BindingResult;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.servlet.ModelAndView;
import java.util.List;
@Controller
@RequestMapping("/userspage")
@RequiredArgsConstructor
public class UserController {
private final UserService userService;
@GetMapping("/first")
public String leaf(Model model) {
model.addAttribute("name", "스프링부트");
return "leaf";
}
@GetMapping("/index")
public ModelAndView index() {
List<UserResDTO> userResDTOList = userService.getUsers();
return new ModelAndView("index", "users", userResDTOList); // viewName, modelName, modelObject
}
// 등록 페이지를 호출 해주는 메서드
@GetMapping("/signup")
public String showSignUpForm(UserReqDTO user) {
return "add-user";
}
// 입력 항목 검증을 한 후 등록 처리 메서드
@PostMapping("/adduser")
public String addUser(@Valid UserReqDTO user, BindingResult result, Model model) {
// 입력 항목 검증 오류가 발생했는지 확인
if (result.hasErrors()) {
return "add-user";
}
// 등록 요청
userService.saveUser(user);
return "redirect:/userspage/index";
}
// 수정 페이지를 호출해주는 메서드
@GetMapping("/edit/{id}")
public String showUpdateForm(@PathVariable Long id, Model model) {
UserResDTO userResDTO = userService.getUserById(id);
model.addAttribute("user", userResDTO);
return "update-user";
}
@PostMapping("/update/{id}")
public String updateUser(@PathVariable("id") long id, @Valid UserReqForm user, BindingResult result, Model model) {
if (result.hasErrors()) {
user.setId(id);
return "update-user";
}
userService.updateUserForm(user);
return "redirect:/userspage/index";
}
}
- update-user.html
- path 추가
- email readonly 추가
<!DOCTYPE html>
<html xmlns:th="http://www.thymeleaf.org">
<meta charset="UTF-8">
<body>
<!-- th:action="@{/update}를 th:action="@{/userspage/update}"로 수정-->
<!-- th:object="${user}" DTO로 변환하면 에러남-->
<form action="#" th:action="@{/userspage/update/{id}(id=${user.id})}" th:object="${user}" method="post">
<label for="name">Name</label>
<input type="text" th:field="*{name}" id="name">
<span th:if="${#fields.hasErrors('name')}" th:errors="*{name}"></span> <br />
<label for="email">Email</label>
<input type="text" th:field="*{email}" id="email" readonly>
<span th:if="${#fields.hasErrors('email')}" th:errors="*{email}"></span> <br />
<input type="submit" value="Update User">
</form>
</body>
</html>
- DELETE
- UserController.java 수정
package com.basic.myspringboot.controller;
import com.basic.myspringboot.dto.UserReqDTO;
import com.basic.myspringboot.dto.UserReqForm;
import com.basic.myspringboot.dto.UserResDTO;
import com.basic.myspringboot.service.UserService;
import jakarta.validation.Valid;
import lombok.RequiredArgsConstructor;
import org.springframework.stereotype.Controller;
import org.springframework.ui.Model;
import org.springframework.validation.BindingResult;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.servlet.ModelAndView;
import java.util.List;
@Controller
@RequestMapping("/userspage")
@RequiredArgsConstructor
public class UserController {
private final UserService userService;
@GetMapping("/first")
public String leaf(Model model) {
model.addAttribute("name", "스프링부트");
return "leaf";
}
@GetMapping("/index")
public ModelAndView index() {
List<UserResDTO> userResDTOList = userService.getUsers();
return new ModelAndView("index", "users", userResDTOList); // viewName, modelName, modelObject
}
// 등록 페이지를 호출 해주는 메서드
@GetMapping("/signup")
public String showSignUpForm(UserReqDTO user) {
return "add-user";
}
// 입력 항목 검증을 한 후 등록 처리 메서드
@PostMapping("/adduser")
public String addUser(@Valid UserReqDTO user, BindingResult result, Model model) {
// 입력 항목 검증 오류가 발생했는지 확인
if (result.hasErrors()) {
return "add-user";
}
// 등록 요청
userService.saveUser(user);
return "redirect:/userspage/index";
}
// 수정 페이지를 호출해주는 메서드
@GetMapping("/edit/{id}")
public String showUpdateForm(@PathVariable Long id, Model model) {
UserResDTO userResDTO = userService.getUserById(id);
model.addAttribute("user", userResDTO);
return "update-user";
}
@PostMapping("/update/{id}")
public String updateUser(@PathVariable("id") long id, @Valid UserReqForm user, BindingResult result, Model model) {
if (result.hasErrors()) {
System.out.println(">> hasErrors user " + user);
// user.setId(id);
model.addAttribute("user", user);
return "update-user";
// return "redirect:/userspage/edit/{id}(id=${user.id})";
}
userService.updateUserForm(user);
return "redirect:/userspage/index";
}
// 삭제
@GetMapping("/delete/{id}")
public String deleteUser(@PathVariable("id") long id) {
userService.deleteUser(id);
return "redirect:/userspage/index";
}
}
- index.html 수정
<!DOCTYPE html>
<html xmlns:th="http://www.thymeleaf.org">
<meta charset="UTF-8">
<body>
<table>
<tr>
<th>Seq</th>
<th>Name</th>
<th>Email</th>
<th>EntryDate</th>
<th>Update</th>
<th>Delete</th>
</tr>
<tr th:each="user, userStat : ${users}">
<td th:text="${userStat.count}"></td> <!-- id 연속적인 수로 출력 -->
<td th:text="${user.name}"></td>
<td th:text="${user.email}"></td>
<!-- <td th:text="${user.createdAt}"></td>-->
<td th:text="${#temporals.format(user.createdAt, 'yyyy-MM-dd hh:mm')}"></td>
<td><a th:href="@{/userspage/edit/{id}(id=${user.id})}">Update</a></td>
<td><a th:href="@{/userspag/delete/{id}(id=${user.id})}">Delete</a></td>
</tr>
</table>
<p><a href="/userspage/signup">Insert</a></p>
</body>
</html>
- CORS
- signle Origin Policy를 우회하기 위한 기법
- 방법
- @Controller 위에 @CrossOrigin 작성
- 단점 : 컨트롤러마다 어노테이션 작성 필요
-> 한꺼번에 설정하는 방법 : Config 파일 작성
- @Controller 위에 @CrossOrigin 작성
- Vue와 붙일 경우에는 RestAPI만 사용할 수 있음
- WebConfig.java
package com.basic.myspringboot.config;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.servlet.config.annotation.CorsRegistry;
import org.springframework.web.servlet.config.annotation.ResourceHandlerRegistry;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;
@Configuration
public class WebConfig implements WebMvcConfigurer {
@Override
public void addResourceHandlers(ResourceHandlerRegistry registry) {
registry.addResourceHandler("/mobile/**")
//반드시 mobile 다음에 / 을 주어야 한다.
.addResourceLocations("classpath:/mobile/")
.setCachePeriod(20);//20초
}
@Override
public void addCorsMappings(CorsRegistry registry) {
registry.addMapping("/**")
.allowedOrigins("http://localhost:3000")
.allowedMethods("*");;
}
}
- Actuator
- 모니터링 기능 (admin을 위한 기능)
- 관리자가 메모리 상태, Bean 개수, 컨트롤러별 매핑 정보 등 확인
- health와 info를 제외한 대부분의 Endpoint가 기본적으로 비공개 상태

- actuator.endpoints
- localhost:8080/actuator
- http://localhost:8087/actuator/health
- localhost:8080/actuator
- 공개 옵션 조정
- appication.properties 수정
# 서버 포트 설정
server.port=8087
# 스프링 유니코드 작성
#myboot.name=\uc2a4\ud504\ub9c1 (applcation-prod/test.properties에서 설정)
myboot.age=${random.int(1,50)}
myboot.fullName=${myboot.name} Boot
# 현재 활성화 중인 환경 설정
spring.profiles.active=prod
#actuator의 모든 endpoint 공개하기
management.endpoints.web.exposure.include=*

- mappings
- http://localhost:8087/actuator/mappings
- controller에서 메소드 경로 적어준 것

- spring boot admin
- 관리자를 위한 admin UI 제공
- actuator를 위한 서버 springbootadmin 실행
- @EnableAdminServer
- localhost:8090
- admin client 생성
- applicatoin.properties 수정
# 서버 포트 설정
server.port=8087
# 스프링 유니코드 작성
#myboot.name=\uc2a4\ud504\ub9c1 (applcation-prod/test.properties에서 설정)
myboot.age=${random.int(1,50)}
myboot.fullName=${myboot.name} Boot
# 현재 활성화 중인 환경 설정
spring.profiles.active=prod
#actuator의 모든 endpoint 공개하기
management.endpoints.web.exposure.include=*
# admin 페이지를 위해 서버 포트 설정
spring.boot.admin.client.url=http://localhost:8090





- 시큐리티 (폼 인증)
- 메서드 시큐리티
- 메서드마다 권한을 줄 수 있어 사용자마다 다르게 설정 가능
- index.html 요청 – 로그인 창이 자동으로 뜬다
- Username : user
- Password : 애플리케이션을 실행할 때 마다 랜덤 값 생성
- 콘솔에 출력된 비밀번호로 로그인
- password는 반드시 DB에 암호화 하여 저장
- 개발모드에서는 password 테스트 용으로만 사용
- 운영모드에서는 사용x
- jsessionid 쿠키를 내려줌
- user/d53b84a7-d981-4c93-8a27-6927858e3942 로그인
- 로그아웃시 jsessionid의 쿠키를 지워주는 등 동작 필요

-
- 요청이 들어오면 Autentication Filter가 동작함 (servlet filter)
-
- 필터가 UsernamePasswordAuthneticationToken 호출하여 입력한 user/password를 AuthenticationToken에 담음
-
- AuthenticationToken을 AuthenticationManager에 전달
-
- AutenticationProvider에 토큰에서 꺼낸 id/password를 꺼내 UserDetailsService에 전달
-
- UserDetailsService은 사용자가 입력한 정보가 기존에 가진 정보와 동일한지 확인
- InMemory가 아닌 DB와 연동하면, UserDetailsService를 implement한 InMemoryUserDetailes이 동작
-> username과 매칭되는 DB테이블이 있는지 확인 (있을 경우 password 읽어옴)
-
- Security API가 만들어놓은 User에서 확인
- InMemoryUserDetailsManager가 UserDetailsService 생성함
-
- 인증한 정보를 Authentication 객체에 저장

- id와 pw 설정
- application.properties 수정
# 서버 포트 설정
server.port=8087
# 스프링 유니코드 작성
#myboot.name=\uc2a4\ud504\ub9c1 (applcation-prod/test.properties에서 설정)
myboot.age=${random.int(1,50)}
myboot.fullName=${myboot.name} Boot
# 현재 활성화 중인 환경 설정
spring.profiles.active=prod
#actuator의 모든 endpoint 공개하기
management.endpoints.web.exposure.include=*
# admin 페이지를 위해 서버 포트 설정
#spring.boot.admin.client.url=http://localhost:8090
# 인증을 위한 user_name과 user_password 설정
spring.security.user.name=boot
spring.security.user.password=test1234

- configuration class로 user 계정 설정
- SecurityConfig.java 생성
- 메모리에 새로운 유저를 만들고 InMemoryUserDetailes 객체 사용
- UserDetailsService가 사용자 값 가져와서 확인
package com.basic.myspringboot.config;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
import org.springframework.security.core.userdetails.User;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.security.provisioning.InMemoryUserDetailsManager;
@Configuration
@EnableWebSecurity
public class SecurityConfig {
@Bean
public PasswordEncoder passwordEncoder() {
return new BCryptPasswordEncoder();
}
@Bean
//authentication
public UserDetailsService userDetailsService(PasswordEncoder encoder) {
UserDetails admin = User.withUsername("adminboot")
.password(encoder.encode("pwd1"))
.roles("ADMIN") // Admin 권한 부여
.build();
UserDetails user = User.withUsername("userboot")
.password(encoder.encode("pwd2"))
.roles("USER") // User 권한 부여
.build();
return new InMemoryUserDetailsManager(admin, user);
}
}



- csrf (cross-site request forgery)
- 악성 웹 사이트 공격 유형
- 사용자가 자신의 의지와 무관하게 공격자가 의도한 행위(수정/삭제/등록 등) 특정 웹사이트에 요청하게 하는 공격
- filter chain
- SecurityConfig.java 수정
package com.basic.myspringboot.config;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
import org.springframework.security.core.userdetails.User;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.security.provisioning.InMemoryUserDetailsManager;
import org.springframework.security.web.SecurityFilterChain;
import static org.springframework.security.config.Customizer.withDefaults;
@Configuration
@EnableWebSecurity
public class SecurityConfig {
@GetMapping("/welcome")
public String welcome() {
return "Welcome this endpoint is not secure";
}
@Bean
//authentication
public UserDetailsService userDetailsService(PasswordEncoder encoder) {
UserDetails admin = User.withUsername("adminboot")
.password(encoder.encode("pwd1"))
.roles("ADMIN") // Admin 권한 부여
.build();
UserDetails user = User.withUsername("userboot")
.password(encoder.encode("pwd2"))
.roles("USER") // User 권한 부여
.build();
return new InMemoryUserDetailsManager(admin, user);
}
@Bean
public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
return http.csrf(csrf -> csrf.disable())
.authorizeHttpRequests(auth -> {
auth.requestMatchers("/users/welcome").permitAll() // 해당 경로라면 모든 권한 허용
.requestMatchers("/users/**").authenticated(); // 그 외 인증 필요
})
.formLogin(withDefaults())
.build();
}
}
- UserController.java
package com.basic.myspringboot.controller;
import com.basic.myspringboot.dto.UserReqDTO;
import com.basic.myspringboot.dto.UserReqForm;
import com.basic.myspringboot.dto.UserResDTO;
import com.basic.myspringboot.service.UserService;
import jakarta.validation.Valid;
import lombok.RequiredArgsConstructor;
import org.springframework.stereotype.Controller;
import org.springframework.ui.Model;
import org.springframework.validation.BindingResult;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.servlet.ModelAndView;
import java.util.List;
@Controller
@RequestMapping("/userspage")
@RequiredArgsConstructor
public class UserController {
private final UserService userService;
@GetMapping("/welcome")
public String welcome() {
return "Welcome this endpoint is not secure";
}
@GetMapping("/first")
public String leaf(Model model) {
model.addAttribute("name", "스프링부트");
return "leaf";
}
// 등록 전에 목록 출력
@GetMapping("/index")
public ModelAndView index() {
List<UserResDTO> userResDTOList = userService.getUsers();
return new ModelAndView("index", "users", userResDTOList); // viewName, modelName, modelObject
}
// 등록 페이지를 호출 해주는 메서드
@GetMapping("/signup")
public String showSignUpForm(UserReqDTO user) {
return "add-user";
}
// 입력 항목 검증을 한 후 등록 처리 메서드
@PostMapping("/adduser")
public String addUser(@Valid UserReqDTO user, BindingResult result, Model model) {
// 입력 항목 검증 오류가 발생했는지 확인
if (result.hasErrors()) {
return "add-user";
}
// 등록 요청
userService.saveUser(user);
return "redirect:/userspage/index";
}
// 수정 페이지를 호출해주는 메서드
@GetMapping("/edit/{id}")
public String showUpdateForm(@PathVariable Long id, Model model) {
UserResDTO userResDTO = userService.getUserById(id);
model.addAttribute("user", userResDTO);
return "update-user";
}
@PostMapping("/update/{id}")
public String updateUser(@PathVariable("id") long id, @Valid UserReqForm user, BindingResult result, Model model) {
if (result.hasErrors()) {
System.out.println(">> hasErrors user " + user);
// user.setId(id);
model.addAttribute("user", user);
return "update-user";
// return "redirect:/userspage/edit/{id}(id=${user.id})";
}
userService.updateUserForm(user);
return "redirect:/userspage/index";
}
// 삭제
@GetMapping("/delete/{id}")
public String deleteUser(@PathVariable("id") long id) {
userService.deleteUser(id);
return "redirect:/userspage/index";
}
}
- SeCurityConfig.java
package com.basic.myspringboot.config;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
import org.springframework.security.core.userdetails.User;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.security.provisioning.InMemoryUserDetailsManager;
import org.springframework.security.web.SecurityFilterChain;
import static org.springframework.security.config.Customizer.withDefaults;
@Configuration
@EnableWebSecurity
public class SecurityConfig {
@Bean
public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
return http.csrf(csrf -> csrf.disable())
.authorizeHttpRequests(auth -> {
auth.requestMatchers("/api/users/welcome").permitAll() // 해당 경로라면 모든 권한 허용
.requestMatchers("/api/users/**").authenticated(); // 그 외 인증 필요
})
.formLogin(withDefaults())
.build();
}
@Bean
public PasswordEncoder passwordEncoder() {
return new BCryptPasswordEncoder();
}
@Bean
//authentication
public UserDetailsService userDetailsService(PasswordEncoder encoder) {
UserDetails admin = User.withUsername("adminboot")
.password(encoder.encode("pwd1"))
.roles("ADMIN") // Admin 권한 부여
.build();
UserDetails user = User.withUsername("userboot")
.password(encoder.encode("pwd2"))
.roles("USER") // User 권한 부여
.build();
return new InMemoryUserDetailsManager(admin, user);
}
}
📕 인증
- http://localhost:8080/api/users/welcome
- http://localhost:8080/userspage/index
- http://localhost:8080/api/users

- 인증
- formlogin
//.formLogin(withDefaults())
.formLogin(login -> login
.loginPage("/login")
.loginProcessingUrl("/login-process")
.usernameParameter("username")
.passwordParameter("password")
.defaultSuccessUrl("/users/index", true)
.permitAll()
)
.logout((logout) -> logout.logoutUrl("/app-logout")
.deleteCookies("JSESSIONID")
.logoutSuccessUrl("/")
)
.build

- 인증하다 실페 에러코드 : 401
- 인증한 후 권한이 없는 에러코드 : 403
- 전체 사용자 정보는 admin만 볼 수 있음
- 자신의 정보를 보는 것은 사용자도 볼 수 있음
- SecurityConfig.java 수정
package com.basic.myspringboot.config;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.config.annotation.method.configuration.EnableMethodSecurity;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
import org.springframework.security.core.userdetails.User;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.security.provisioning.InMemoryUserDetailsManager;
import org.springframework.security.web.SecurityFilterChain;
import static org.springframework.security.config.Customizer.withDefaults;
@Configuration
@EnableWebSecurity
@EnableMethodSecurity // 인가
public class SecurityConfig {
@Bean
public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
return http.csrf(csrf -> csrf.disable())
.authorizeHttpRequests(auth -> {
auth.requestMatchers("/api/users/welcome", "/userspage/**").permitAll()
.requestMatchers("/api/users/**").authenticated();
})
.formLogin(withDefaults())
.build();
}
@Bean
public PasswordEncoder passwordEncoder () {
return new BCryptPasswordEncoder();
}
@Bean
//authentication
public UserDetailsService userDetailsService (PasswordEncoder encoder){
UserDetails admin = User.withUsername("adminboot")
.password(encoder.encode("pwd1"))
.roles("ADMIN")
.build();
UserDetails user = User.withUsername("userboot")
.password(encoder.encode("pwd2"))
.roles("USER")
.build();
return new InMemoryUserDetailsManager(admin, user);
}
}
- UserRestController.java 수정
package com.basic.myspringboot.controller;
import com.basic.myspringboot.dto.UserReqDTO;
import com.basic.myspringboot.dto.UserResDTO;
import com.basic.myspringboot.service.UserService;
import lombok.RequiredArgsConstructor;
import org.springframework.http.ResponseEntity;
import org.springframework.security.access.prepost.PreAuthorize;
import org.springframework.web.bind.annotation.*;
import java.util.List;
@RestController
@RequestMapping("/api/users") // url 똑같으면 겹치니까 다르게 작성
@RequiredArgsConstructor
public class UserRestController {
private final UserService userService;
@GetMapping("/welcome")
public String welcome() {
return "Welcome this endpoint is not secure";
} // UserController에서 UserRestController로 이동하니까 됨
// 등록
@PostMapping
public UserResDTO saveUser(@RequestBody UserReqDTO userReqDTO) {
return userService.saveUser(userReqDTO); // service에서 모두 처리되어 controller에서는 호출만 수행
}
// 조회
@GetMapping("/{id}")
@PreAuthorize("hasAuthority('ROLE_USER')")
public UserResDTO getUserById(@PathVariable Long id) {
return userService.getUserById(id);
}
// 전체 목록 조회
@GetMapping
@PreAuthorize("hasAuthority('ROLE_ADMIN')")
public List<UserResDTO> getUsers() {
return userService.getUsers();
}
// 수정
@PatchMapping("/{email}")
public UserResDTO updateUser(@PathVariable String email, @RequestBody UserReqDTO userReqDTO) {
return userService.updateUser(email, userReqDTO);
}
// 삭제
@DeleteMapping("/{id}")
public ResponseEntity<?> deleteUser(@PathVariable Long id) {
userService.deleteUser(id);
return ResponseEntity.ok(id + " User가 삭제처리 되었습니다.");
}
}
- roo 로그인시 전체 조회
- http://localhost:8087/api/users
- 로그인 한 후에는 다음 페이지가 연결이 안되어 403 에러가 뜨지만, api/users로 경로 입력할 경우 정상 작동
- user로그인시 개별 조회

- UserDetailsService가 username/pwd를 읽어서 DB에 있는지 확인한 후 결과를 User 객체에 저장
- 인증은 authentication manager가 수행
- security가 인증할 때 사용하는 User에 저장해야 security가 확인
- userDetails를 implement받은 userInfoUserDetails(User) 클래스에 DB에서 이메일 주소와 패스워드를 꺼내와 저장
- Entity 생성 -> Repository 생성 -> DB에 암호화 하여 저장
- securtiy 패키지 생성
- 하단에 config, controller, repository, vo, entity, service 패키지 생성
- UserInfo.java 생성
package com.basic.myspringboot.security.entity;
import jakarta.persistence.*;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;
@Entity
@Data
@AllArgsConstructor
@NoArgsConstructor
public class UserInfo {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private int id;
@Column(nullable = false)
private String name;
@Column(unique = true, nullable = false)
private String email;
private String password;
private String roles;
}
- UserInfoRepository.java 생성
package com.basic.myspringboot.security.repository;
import com.basic.myspringboot.security.entity.UserInfo;
import org.springframework.data.repository.ListCrudRepository;
import java.util.Optional;
public interface UserInfoRepository extends ListCrudRepository<UserInfo, Integer> {
Optional<UserInfo> findByEmail(String email);
}
- UserInfoUserDetails.java
- UserDetails를 implement받아 UserInfoUserDetails 생성
package com.basic.myspringboot.security.vo;
import com.basic.myspringboot.security.entity.UserInfo;
import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.core.authority.SimpleGrantedAuthority;
import org.springframework.security.core.userdetails.UserDetails;
import java.util.Arrays;
import java.util.Collection;
import java.util.List;
import java.util.stream.Collectors;
public class UserInfoUserDetails implements UserDetails {
private String email;
private String password;
private List<GrantedAuthority> authorities;
public UserInfoUserDetails(UserInfo userInfo) {
email=userInfo.getEmail(); // email을 인증할 때 사용 목적
password=userInfo.getPassword(); // 암호화된 password
// 권한
// userInfo.getRoles() : table에 저장된 role 정보
// split(",") : , 기준으로 파싱하여 ROLE_ADMIN, ROLE_USER 가져옴
authorities= Arrays.stream(userInfo.getRoles().split(",")) // Stream<String>
// Stream<String> -> Stream<SimpleGrantedAuthority)
// 람다식 .map(SimpleGrantedAuthority::new) = .map(roleName -> new SimpleGrantedAuthority(roleName))
.map(SimpleGrantedAuthority::new) // Stream<SimpleGrantedAuthority)
.collect(Collectors.toList());
}
@Override
public Collection<? extends GrantedAuthority> getAuthorities() {
return authorities;
}
// providemanger에서 인증 할 때 하단 메서드 호출
@Override
public String getPassword() {
return password;
}
@Override
public String getUsername() {
return email;
} // email을 이용하여 인증 처리
@Override
public boolean isAccountNonExpired() {
return true;
}
@Override
public boolean isAccountNonLocked() {
return true;
}
@Override
public boolean isCredentialsNonExpired() {
return true;
}
@Override
public boolean isEnabled() {
return true;
}
}
- UserInfoDetailsService.java
- username으로 email이 들어가서 name->email
package com.basic.myspringboot.security.service;
import com.basic.myspringboot.security.entity.UserInfo;
import com.basic.myspringboot.security.repository.UserInfoRepository;
import com.basic.myspringboot.security.vo.UserInfoUserDetails;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.core.userdetails.UsernameNotFoundException;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.stereotype.Service;
import java.util.Optional;
@Service
public class UserInfoUserDetailsService implements UserDetailsService {
@Autowired
private UserInfoRepository repository;
@Autowired
private PasswordEncoder passwordEncoder;
@Override
public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
Optional<UserInfo> optionalUserInfo = repository.findByEmail(username); // name -> email로 변경
return optionalUserInfo.map(userInfo -> new UserInfoUserDetails(userInfo))
//userInfo.map(UserInfoUserDetails::new)
.orElseThrow(() -> new UsernameNotFoundException("user not found " + username));
}
public String addUser(UserInfo userInfo) {
userInfo.setPassword(passwordEncoder.encode(userInfo.getPassword()));
UserInfo savedUserInfo = repository.save(userInfo);
return savedUserInfo.getName() + " user added!!";
}
}
- UserInfoController.java 생성
package com.basic.myspringboot.security.controller;
import com.basic.myspringboot.security.entity.UserInfo;
import com.basic.myspringboot.security.service.UserInfoUserDetailsService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
@RestController
@RequestMapping("/userinfos")
public class UserInfoController {
@Autowired
private UserInfoUserDetailsService service;
@PostMapping("/new")
public String addNewUser(@RequestBody UserInfo userInfo) {
return service.addUser(userInfo);
}
}
- SecurityConfig.java 수정
package com.basic.myspringboot.security.config;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.config.annotation.method.configuration.EnableMethodSecurity;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
import org.springframework.security.core.userdetails.User;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.security.provisioning.InMemoryUserDetailsManager;
import org.springframework.security.web.SecurityFilterChain;
import static org.springframework.security.config.Customizer.withDefaults;
@Configuration
@EnableWebSecurity
@EnableMethodSecurity // 인가
public class SecurityConfig {
@Bean
public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
return http.csrf(csrf -> csrf.disable())
.authorizeHttpRequests(auth -> {
auth.requestMatchers("/api/users/welcome",
"/userspage/**",
"/userinfos/new").permitAll() // 경로 추가
.requestMatchers("/api/users/**").authenticated();
})
.formLogin(withDefaults())
.build();
}
@Bean
public PasswordEncoder passwordEncoder () {
return new BCryptPasswordEncoder();
}
@Bean
//authentication
public UserDetailsService userDetailsService (PasswordEncoder encoder){
UserDetails admin = User.withUsername("adminboot")
.password(encoder.encode("pwd1"))
.roles("ADMIN")
.build();
UserDetails user = User.withUsername("userboot")
.password(encoder.encode("pwd2"))
.roles("USER")
.build();
return new InMemoryUserDetailsManager(admin, user);
}
}

- jwt 인증
- postman으로 이메일/비밀번호 POST
- 하단의 결과를 jwt에 입력하면 이메일 확인 가능
- postman -> Authorization -> Bearer Token 선택
- 토큰 발행
//// jwt (json web token) 처리 - 로그인 처리
/*
POST
http://localhost:8080/api/userinfos/login
{
"email":"admin@aa.com",
"password":"pwd1"
}
jwt : eyJhbGciOiJIUzI1NiJ9.eyJzdWIiOiJhZG1pbkBhYS5jb20iLCJpYXQiOjE2OTc2MTAzNTQsImV4cCI6MTY5NzYxMjE1NH0._JIKGkjyItb-rB5MPebopyG5UPitaNFhcfCyCkv8KLg
*/

- 토큰으로 데이터 조회
- 토큰 발행 코드
- 1) UserInfoController.java
- jwt가 유효한지 확인하기 위해서 JJWT API 사용
- 해당 조각 1개 ("alg")을 claim이라고 하고 claims() 메서드를 이용해서 클레임 가져옴
package com.basic.myspringboot.controller;
import com.basic.myspringboot.entity.UserInfo;
import com.basic.myspringboot.jwt.dto.AuthRequest;
import com.basic.myspringboot.jwt.service.JwtService;
import com.basic.myspringboot.repository.UserInfoRepository;
import com.basic.myspringboot.service.UserInfoUserDetailsService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.security.authentication.AuthenticationManager;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.userdetails.UsernameNotFoundException;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
@RestController
@RequestMapping("/api/userinfos")
public class UserInfoController {
// @Autowired
// private UserInfoUserDetailsService service;
//// jwt 처리 - 로그인 처리하기 위해서 Injection 받음
@Autowired
private AuthenticationManager authenticationManager;
@Autowired
private JwtService jwtService;
@Autowired
private UserInfoRepository repository;
@Autowired
private PasswordEncoder passwordEncoder;
// @PostMapping("/new")
// public String addNewUser(@RequestBody UserInfo userInfo) {
// return service.addUser(userInfo);
// }
@PostMapping("/new")
public String addNewUser(@RequestBody UserInfo userInfo){
userInfo.setPassword(passwordEncoder.encode(userInfo.getPassword()));
UserInfo savedUserInfo = repository.save(userInfo);
return savedUserInfo.getName() + " user added!!";
}
//// jwt (json web token) 처리 - 로그인 처리
/*
POST
http://localhost:8080/api/userinfos/login
{
"email":"admin@aa.com",
"password":"pwd1"
}
jwt : eyJhbGciOiJIUzI1NiJ9.eyJzdWIiOiJhZG1pbkBhYS5jb20iLCJpYXQiOjE2OTc2MDg1NzUsImV4cCI6MTY5NzYxMDM3NX0.e6TNoZBOliNRJT7o20itAFX3gcnN_D3VHHWmQRdu3GQ
*/
@PostMapping("/login")
public String authenticateAndGetToken(@RequestBody AuthRequest authRequest) {
// authenticationManager.authenticate: authenticate 호출하여 직접 인증 처리하는 구문
Authentication authentication = authenticationManager.authenticate(
new UsernamePasswordAuthenticationToken( // 토큰에 email과 password 포함
authRequest.getEmail(),
authRequest.getPassword()
));
// sec:authorize="isAuthenticated()" : 인증이 되어 있는지 확인 (navbar.html)
if (authentication.isAuthenticated()) {
return jwtService.generateToken(authRequest.getEmail());
} else {
// email이나 pwd 틀릴 경우 해당 구문에 걸려 서버500 에러 메시지 출력
throw new UsernameNotFoundException("invalid user request !");
}
}
}
- JwtService.java
package com.basic.myspringboot.jwt.service;
import io.jsonwebtoken.Claims;
import io.jsonwebtoken.Jwts;
import io.jsonwebtoken.SignatureAlgorithm;
import io.jsonwebtoken.io.Decoders;
import io.jsonwebtoken.security.Keys;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.stereotype.Component;
import java.security.Key;
import java.util.Date;
import java.util.HashMap;
import java.util.Map;
import java.util.function.Function;
@Component
public class JwtService {
public static final String SECRET = "5367566B59703373367639792F423F4528482B4D6251655468576D5A71347437";
private Key getSignKey() {
byte[] keyBytes = Decoders.BASE64.decode(SECRET);
return Keys.hmacShaKeyFor(keyBytes);
}
private Claims extractAllClaims(String token) {
return Jwts
.parserBuilder()
.setSigningKey(getSignKey())
.build()
.parseClaimsJws(token)
.getBody();
}
public <T> T extractClaim(String token, Function<Claims, T> claimsResolver) {
final Claims claims = extractAllClaims(token);
return claimsResolver.apply(claims);
}
public String extractUsername(String token) {
return extractClaim(token, Claims::getSubject);
}
public Date extractExpiration(String token) {
return extractClaim(token, Claims::getExpiration);
}
private Boolean isTokenExpired(String token) {
return extractExpiration(token).before(new Date());
}
public Boolean validateToken(String token, UserDetails userDetails) {
final String username = extractUsername(token);
return (username.equals(userDetails.getUsername()) && !isTokenExpired(token));
}
// 토큰 생성
public String generateToken(String userName){
Map<String,Object> claims=new HashMap<>();
return createToken(claims,userName);
}
private String createToken(Map<String, Object> claims, String userName) {
return Jwts.builder() //JwtBuilder
.setClaims(claims)
.setSubject(userName)
.setIssuedAt(new Date(System.currentTimeMillis()))
.setExpiration(new Date(System.currentTimeMillis()+1000*60*30)) // 만료시간 : 30분
.signWith(getSignKey(), SignatureAlgorithm.HS256) // 알고리즘 설정
.compact();
}
}


- JWTService.java
- 토큰 맞는지 검증해주는 메서드
public Boolean validateToken(String token, UserDetails userDetails) {
final String username = extractUsername(token);
return (username.equals(userDetails.getUsername()) && !isTokenExpired(token));
}
- JwtAuthenticationFilter.java
- 요청 들어오면 Bearer Token 유형의 해당 발급받은 토큰을 요
package com.basic.myspringboot.jwt.filter;
import com.basic.myspringboot.jwt.service.JwtService;
import com.basic.myspringboot.service.UserInfoUserDetailsService;
import jakarta.servlet.FilterChain;
import jakarta.servlet.ServletException;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.web.authentication.WebAuthenticationDetailsSource;
import org.springframework.stereotype.Component;
import org.springframework.web.filter.OncePerRequestFilter;
import java.io.IOException;
@Component
public class JwtAuthenticationFilter extends OncePerRequestFilter { // 요청마다 매번 실행되는 토큰
@Autowired
private JwtService jwtService;
@Autowired
private UserInfoUserDetailsService userDetailsService;
@Override
protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain)
throws ServletException, IOException {
String authHeader = request.getHeader("Authorization"); // 유형이 필수로 지정되어야 해서 확인
String token = null;
String username = null;
//Authorization 헤더의 값이 Bearer로 시작하는지를 체크
if (authHeader != null && authHeader.startsWith("Bearer ")) {
token = authHeader.substring(7); // Bearer + token값으로 넘어오니까 토큰만 파싱 처리
username = jwtService.extractUsername(token);
}
// SecurityContextHolder.getContext().getAuthentication() : Authentication 가져옴
if (username != null && SecurityContextHolder.getContext().getAuthentication() == null) {
// UserDetails : 인증 정보를 담고 있는 객체
UserDetails userDetails = userDetailsService.loadUserByUsername(username);
//Token의 유효성 검증
if (jwtService.validateToken(token, userDetails)) { // validateToken 호출
UsernamePasswordAuthenticationToken authToken =
new UsernamePasswordAuthenticationToken(userDetails,
null, userDetails.getAuthorities()); // 권한 정보 포함하여 토큰 생성
authToken.setDetails(new WebAuthenticationDetailsSource().buildDetails(request));
SecurityContextHolder.getContext().setAuthentication(authToken); // 검증 확인한 후 다시 Authentication에 저장
}
}
filterChain.doFilter(request, response);
}
}
- SecurityConfig.java
package com.basic.myspringboot.config;
import com.basic.myspringboot.jwt.filter.JwtAuthenticationFilter;
import com.basic.myspringboot.service.UserInfoUserDetailsService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.autoconfigure.security.servlet.PathRequest;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.core.annotation.Order;
import org.springframework.security.authentication.AuthenticationManager;
import org.springframework.security.authentication.AuthenticationProvider;
import org.springframework.security.authentication.dao.DaoAuthenticationProvider;
import org.springframework.security.config.annotation.authentication.configuration.AuthenticationConfiguration;
import org.springframework.security.config.annotation.method.configuration.EnableMethodSecurity;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
import org.springframework.security.config.annotation.web.configuration.WebSecurityCustomizer;
import org.springframework.security.config.http.SessionCreationPolicy;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.security.web.SecurityFilterChain;
import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter;
import org.springframework.security.config.annotation.web.configurers.AbstractHttpConfigurer;
import org.springframework.web.servlet.handler.HandlerMappingIntrospector;
import static org.springframework.security.config.Customizer.withDefaults;
@Configuration
@EnableWebSecurity
@EnableMethodSecurity
public class SecurityConfig {
//// jwt 처리 - 로그인 처리
@Autowired
private JwtAuthenticationFilter jwtAuthenticationFilter;
@Bean
public WebSecurityCustomizer webSecurityCustomizer() {
return (web) -> web.ignoring()
.requestMatchers(PathRequest.toStaticResources().atCommonLocations());
//.requestMatchers("/resources/static/**");
}
@Bean
@Order(1)
public SecurityFilterChain apiFilterChain(HttpSecurity http) throws Exception {
return http.csrf(csrf -> csrf.disable())
.formLogin(form -> form.disable()) // form로그인 안씀
.httpBasic(basic -> basic.disable())
.authorizeHttpRequests( auth -> {
auth.requestMatchers("/api/users/welcome", "/api/userinfos/new",
"/api/userinfos/login").permitAll()
.requestMatchers("/api/users/**").authenticated();
})
.sessionManagement(session -> session.sessionCreationPolicy(SessionCreationPolicy.STATELESS)) // 세션 정책
// .httpBasic(withDefaults())
.authenticationProvider(authenticationProvider())
// 해당 필터보다 jwtAuthenticationFilter 이게 먼저 동작하도록 지정
.addFilterBefore(jwtAuthenticationFilter, UsernamePasswordAuthenticationFilter.class)
.build();
}
@Bean
@Order(2)
public SecurityFilterChain formLoginFilterChain(HttpSecurity http, HandlerMappingIntrospector introspector)
throws Exception {
return http.csrf(csrf -> csrf.disable())
.authorizeHttpRequests(auth -> {
//auth.requestMatchers(PathRequest.toStaticResources().atCommonLocations()).permitAll()
auth.requestMatchers("/login").permitAll()
// .requestMatchers("/api/users/**").authenticated()
.requestMatchers("/users/**").authenticated();
//.requestMatchers("/api/users/**").authenticated();
//.requestMatchers("/**").denyAll();
})
//.formLogin(withDefaults())
.formLogin(login -> login
.loginPage("/login")
.loginProcessingUrl("/login-process")
.usernameParameter("username")
.passwordParameter("password")
.defaultSuccessUrl("/users/index", true)
.permitAll()
)
.logout((logout) -> logout.logoutUrl("/app-logout")
.deleteCookies("JSESSIONID")
.logoutSuccessUrl("/")
)
.build();
}
@Bean
public PasswordEncoder passwordEncoder() {
return new BCryptPasswordEncoder();
}
@Bean
public UserDetailsService userDetailsService() {
return new UserInfoUserDetailsService();
}
@Bean
public AuthenticationProvider authenticationProvider() {
DaoAuthenticationProvider authenticationProvider
= new DaoAuthenticationProvider();
authenticationProvider.setUserDetailsService(userDetailsService());
authenticationProvider.setPasswordEncoder(passwordEncoder());
return authenticationProvider;
}
//// jwt 처리 - 로그인 처리하기 위함
@Bean
public AuthenticationManager authenticationManager(AuthenticationConfiguration config)
throws Exception {
return config.getAuthenticationManager();
}
// @Bean
// //authentication
// public UserDetailsService userDetailsService(PasswordEncoder encoder) {
// UserDetails admin = User.withUsername("adminboot")
// .password(encoder.encode("pwd1"))
// .roles("ADMIN")
// .build();
// UserDetails user = User.withUsername("userboot")
// .password(encoder.encode("pwd2"))
// .roles("USER")
// .build();
// return new InMemoryUserDetailsManager(admin, user);
// }
}
- 1 : N 관계
- student.sql
CREATE TABLE DEPT
(
DEPT_ID bigint not null auto_increment primary key,
DEPT_CODE int(4) NOT NULL UNIQUE,
DEPT_NAME VARCHAR(30) NOT NULL
);
insert into DEPT (dept_code, dept_name) values (10,'경제학과');
insert into DEPT (dept_code, dept_name) values (20,'컴퓨터공학과');
insert into DEPT (dept_code, dept_name) values (30,'영어영문학과');
insert into DEPT (dept_code, dept_name) values (40,'건축공학과');
commit;
CREATE TABLE STUDENT
(
STU_ID bigint not null auto_increment primary key,
STU_CODE int(6) NOT NULL UNIQUE,
STU_NAME VARCHAR(100) NOT NULL,
STU_AGE int(3) NOT NULL,
STU_GRADE VARCHAR(50),
STU_DAYNIGHT VARCHAR(50),
DEPT_CODE int(4) NOT NULL,
FOREIGN KEY (DEPT_CODE) REFERENCES DEPT (DEPT_CODE)
);
insert into student(STU_CODE, STU_NAME, STU_AGE, STU_GRADE, STU_DAYNIGHT, DEPT_CODE) values (1002,'홍길동',20,'1학년','주간',30);
commit;
CREATE TABLE COURSE
(
COURSE_ID bigint not null auto_increment primary key,
COURSE_CODE int(4) NOT NULL UNIQUE,
COURSE_NAME VARCHAR(100),
COURSE_INSTRUCTOR VARCHAR(100)
);
insert into COURSE(COURSE_CODE, COURSE_NAME, COURSE_INSTRUCTOR) values (1000,'자바프로그래밍','김자바');
insert into COURSE(COURSE_CODE, COURSE_NAME, COURSE_INSTRUCTOR) values (2000,'파이썬프로그래밍','박파이썬');
commit;
CREATE TABLE COURSE_STATUS
(
STATUS_ID bigint not null auto_increment primary key,
STU_CODE int(6) NOT NULL,
COURSE_CODE int(4) NOT NULL,
COURSE_SCORE int(4) NOT NULL,
FOREIGN KEY (STU_CODE) REFERENCES STUDENT(STU_CODE),
FOREIGN KEY (COURSE_CODE) REFERENCES COURSE(COURSE_CODE)
);
insert into COURSE_STATUS(STU_CODE, COURSE_CODE, COURSE_SCORE) values (1002,1000,90);
insert into COURSE_STATUS(STU_CODE, COURSE_CODE, COURSE_SCORE) values (1002,2000,80);
commit;
- DEPT와 STUDENT
- 1:1 관계
- 1:1 관계
- COURSE
- COURSE_STATUS
- 학생 과목 수강 이력
- STUDENT와 1:N 관계, COURSE와 1:N 관계
- 학버과 학과 코드 참조
- StudentMapper.xml
- 1:N관계는 Collection 사용
<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
"http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<!-- <mapper namespace="studentNS"> -->
<mapper namespace="myspring.student.dao.mapper.StudentMapper">
<!-- UserMapper.xml : <select id="selectUserById" parameterType="string" resultType="User"> -->
<resultMap id="studentDeptResultMap" type="Student">
<id property="id" column="stu_id" javaType="Long" jdbcType="NUMERIC" />
<result property="code" column="stu_code" javaType="Integer" jdbcType="NUMERIC" />
<result property="name" column="stu_name" javaType="String" jdbcType="VARCHAR" />
<result property="age" column="stu_age" javaType="Integer" jdbcType="NUMERIC" />
<result property="grade" column="stu_grade" javaType="String" jdbcType="VARCHAR" />
<result property="daynight" column="stu_daynight" javaType="String" jdbcType="VARCHAR" />
<association property="dept" column="dept_code" javaType="Dept" resultMap="deptResultMap" />
</resultMap>
<resultMap id="studentCourseStatusResultMap" type="Student">
<id property="id" column="stu_id" javaType="Long" jdbcType="NUMERIC" />
<result property="code" column="stu_code" javaType="Integer" jdbcType="NUMERIC" />
<result property="name" column="stu_name" javaType="String" jdbcType="VARCHAR" />
<result property="age" column="stu_age" javaType="Integer" jdbcType="NUMERIC" />
<result property="grade" column="stu_grade" javaType="String" jdbcType="VARCHAR" />
<result property="daynight" column="stu_daynight" javaType="String" jdbcType="VARCHAR" />
<association property="dept" column="dept_code" javaType="Dept" resultMap="deptResultMap" />
<collection property="courseStatus" ofType="CourseStatus" resultMap="coursestatusResultMap" />
</resultMap>
<resultMap id="studentResultMap" type="Student">
<id property="id" column="stu_id" javaType="Long" jdbcType="NUMERIC" />
<result property="code" column="stu_code" javaType="Integer" jdbcType="NUMERIC" />
<result property="name" column="stu_name" javaType="String" jdbcType="VARCHAR" />
<result property="age" column="stu_age" javaType="Integer" jdbcType="NUMERIC" />
<result property="grade" column="stu_grade" javaType="String" jdbcType="VARCHAR" />
<result property="daynight" column="stu_daynight" javaType="String" jdbcType="VARCHAR" />
</resultMap>
<resultMap id="deptResultMap" type="Dept">
<id property="id" column="dept_id" javaType="Long" jdbcType="NUMERIC" />
<result property="code" column="dept_code" javaType="Integer" jdbcType="NUMERIC" />
<result property="name" column="dept_name" javaType="String" jdbcType="VARCHAR" />
</resultMap>
<resultMap id="courseResultMap" type="Course">
<id property="id" column="course_id" javaType="Long" jdbcType="NUMERIC" />
<result property="code" column="course_code" javaType="Integer" jdbcType="NUMERIC" />
<result property="name" column="course_name" javaType="String" jdbcType="VARCHAR" />
<result property="instructor" column="course_instructor" javaType="String" jdbcType="VARCHAR" />
</resultMap>
<resultMap id="coursestatusResultMap" type="CourseStatus">
<id property="id" column="status_id" javaType="Long" jdbcType="NUMERIC" />
<result property="score" column="course_score" javaType="Integer" jdbcType="NUMERIC" />
<association property="course" column="course_code" javaType="Course" resultMap="courseResultMap" />
</resultMap>
<select id="selectStudentDept" resultMap="studentDeptResultMap">
select
s.stu_id,
s.stu_code,
s.stu_name,
s.stu_age,
s.stu_grade,
s.stu_daynight,
d.dept_id,
d.dept_code,
d.dept_name
from student s, dept d
where s.dept_code = d.dept_code
</select>
<select id="selectStudentCourseStatus" resultMap="studentCourseStatusResultMap">
select s.stu_id,
s.stu_code,
s.stu_name,
s.stu_age,
s.stu_grade,
s.stu_daynight,
d.dept_id,
d.dept_code,
d.dept_name,
c.course_id,
c.course_code,
c.course_name,
c.course_instructor,
t.status_id,
t.COURSE_SCORE
from student s, dept d, course_status t, course c
where s.stu_code = t.stu_code
and s.dept_code = d.dept_code
and t.course_code = c.course_code
</select>
<select id="selectCourse" resultMap="courseResultMap">
select COURSE_ID,
COURSE_CODE,
COURSE_NAME,
COURSE_INSTRUCTOR
from COURSE
order by COURSE_ID
</select>
<sql id="selectStudent">
select * from student
</sql>
<select id="selectStudentByName" parameterType="String"
resultMap="studentResultMap">
<include refid="selectStudent" />
where stu_name like CONCAT('%',#{keyword},'%')
</select>
<select id="selectStudentByGradeOrDay" parameterType="Student"
resultMap="studentResultMap">
<include refid="selectStudent" />
<where>
<if test="grade != null">
stu_grade = #{grade}
</if>
<if test="daynight != null">
and stu_daynight = #{daynight}
</if>
</where>
</select>
<select id="selectStudentByGradeOrDayMap" parameterType="Map"
resultMap="studentResultMap">
<include refid="selectStudent" />
<where>
<if test="grade != null">
stu_grade = #{grade}
</if>
<if test="day != null">
or stu_daynight = #{daynight}
</if>
</where>
</select>
<select id="selectStudentGrade" resultType="integer">
select count(*) stu_cnt from STUDENT group by STU_GRADE
</select>
<insert id="insertCourse" parameterType="Course">
insert into course
(course_code,course_name,course_instructor)
values(#{code},#{name},#{instructor})
</insert>
<insert id="insertStudent" parameterType="Student">
insert into student
(stu_code,stu_name,stu_age,stu_grade,stu_daynight,dept_code)
values(
#{code},
#{name},
#{age},
#{grade},
#{daynight},#{dept.code} )
</insert>
<update id="updateStudent" parameterType="Student">
update student set
stu_name = #{name},
stu_age = #{age},
stu_grade = #{grade},
stu_daynight = #{daynight},
dept_code = #{dept.code}
where stu_id = #{id}
</update>
<insert id="insertCourseStatus" parameterType="CourseStatus">
insert into COURSE_STATUS (STU_CODE,COURSE_CODE,COURSE_SCORE)
values (#{student.code},#{course.code},#{score})
</insert>
<delete id="deleteStudent" parameterType="Integer">
delete from student where stu_id = #{value}
</delete>
</mapper>
- StudentEntity.java
package com.myboot.datajpa.entity;
import java.util.ArrayList;
import java.util.List;
import javax.persistence.Column;
import javax.persistence.Entity;
import javax.persistence.FetchType;
import javax.persistence.GeneratedValue;
import javax.persistence.GenerationType;
import javax.persistence.Id;
import javax.persistence.JoinColumn;
import javax.persistence.OneToMany;
import javax.persistence.OneToOne;
import javax.persistence.Table;
import com.fasterxml.jackson.annotation.JsonIgnore;
@Entity
@Table(name = "student")
public class StudentEntity {
@Id @GeneratedValue(strategy = GenerationType.IDENTITY)
@Column(name = "student_id")
private Long id;
@Column(unique = true, nullable = false)
private Integer code;
private String name;
private Integer age;
private String grade;
private String daynight;
// 학생 -> 학과 참조 (단방향)
@OneToOne(fetch = FetchType.LAZY) // 1:1 관계
// EAGER : 참조 관계에 있는 DEPT 까지 함께 Fetch됨 (1:1 관계일때는 무관하지만, 많을 경우 모든 참조 관계 Fetct)
// LAZY : 직접 사용하는 DEPT fetch
@JoinColumn(name = "dept_id") // 외래키 연결
private DeptEntity dept; // DEPT의 PK 작성 (해당 테이블 참조한다는 목적)
//@JsonIgnore
@OneToMany(mappedBy = "student")
private List<CourseStatusEntity> courseStatus = new ArrayList<>();;
public StudentEntity() {
}
public StudentEntity(Integer code, String name, Integer age, String grade, String daynight) {
this.code = code;
this.name = name;
this.age = age;
this.grade = grade;
this.daynight = daynight;
}
public Long getId() {
return id;
}
public void setId(Long id) {
this.id = id;
}
public Integer getCode() {
return code;
}
public void setCode(Integer code) {
this.code = code;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public Integer getAge() {
return age;
}
public void setAge(Integer age) {
this.age = age;
}
public String getGrade() {
return grade;
}
public void setGrade(String grade) {
this.grade = grade;
}
public String getDaynight() {
return daynight;
}
public void setDaynight(String daynight) {
this.daynight = daynight;
}
// 해당 메소드를 호출할 때 Student가 가진 Dept 참조 테이블 Fetch
// 성능 이슈로 FetchType은 Eager이 아닌 Lazy 사용 권장
public DeptEntity getDept() {
return dept;
}
public void setDept(DeptEntity dept) {
this.dept = dept;
}
public List<CourseStatusEntity> getCourseStatus() {
return courseStatus;
}
public void setCourseStatus(List<CourseStatusEntity> courseStatus) {
this.courseStatus = courseStatus;
}
@Override
public String toString() {
return "StudentEntity [id=" + id + ", code=" + code + ", name=" + name + ", age=" + age + ", grade=" + grade
+ ", daynight=" + daynight + ", dept=" + dept + "]";
}
}
- DeptEntity.java
package com.myboot.datajpa.entity;
import javax.persistence.Column;
import javax.persistence.Entity;
import javax.persistence.GeneratedValue;
import javax.persistence.GenerationType;
import javax.persistence.Id;
import javax.persistence.Table;
@Entity
@Table(name = "dept")
public class DeptEntity {
@Id @GeneratedValue(strategy = GenerationType.IDENTITY)
@Column(name = "dept_id") // 변수는 id이지만 컬럼명은 dept_id이기 때문에 연결 목적
private Long id;
@Column(unique = true, nullable = false)
private Integer code;
private String name;
public DeptEntity() {
}
public DeptEntity(Integer code, String name) {
this.code = code;
this.name = name;
}
public Long getId() {
return id;
}
public void setId(Long id) {
this.id = id;
}
public Integer getCode() {
return code;
}
public void setCode(Integer code) {
this.code = code;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
@Override
public String toString() {
return "DeptVO [id=" + id + ", code=" + code + ", name=" + name + "]";
}
}
- student와 course-status를 양방향 매핑 필요
- StudentEntity.java
package com.myboot.datajpa.entity;
import java.util.ArrayList;
import java.util.List;
import javax.persistence.Column;
import javax.persistence.Entity;
import javax.persistence.FetchType;
import javax.persistence.GeneratedValue;
import javax.persistence.GenerationType;
import javax.persistence.Id;
import javax.persistence.JoinColumn;
import javax.persistence.OneToMany;
import javax.persistence.OneToOne;
import javax.persistence.Table;
import com.fasterxml.jackson.annotation.JsonIgnore;
@Entity
@Table(name = "student")
public class StudentEntity {
@Id @GeneratedValue(strategy = GenerationType.IDENTITY)
@Column(name = "student_id")
private Long id;
@Column(unique = true, nullable = false)
private Integer code;
private String name;
private Integer age;
private String grade;
private String daynight;
// 학생 -> 학과 참조 (단방향)
@OneToOne(fetch = FetchType.LAZY) // 1:1 관계
// EAGER : 참조 관계에 있는 DEPT 까지 함께 Fetch됨 (1:1 관계일때는 무관하지만, 많을 경우 모든 참조 관계 Fetct)
// LAZY : 직접 사용하는 DEPT fetch
@JoinColumn(name = "dept_id") // 외래키 연결
private DeptEntity dept; // DEPT의 PK 작성 (해당 테이블 참조한다는 목적)
//@JsonIgnore
// 학생 -> 학생 이력을 보기 위해 해당 구문 필요 (없을 경우 학생 이력에서만 학생만 볼 수 있음)
// one : 학생 many : 학생 이력
@OneToMany(mappedBy = "student") // CourseStatusEntity의 @ManyToOne의 student 변수를 매핑
private List<CourseStatusEntity> courseStatus = new ArrayList<>();;
public StudentEntity() {
}
public StudentEntity(Integer code, String name, Integer age, String grade, String daynight) {
this.code = code;
this.name = name;
this.age = age;
this.grade = grade;
this.daynight = daynight;
}
public Long getId() {
return id;
}
public void setId(Long id) {
this.id = id;
}
public Integer getCode() {
return code;
}
public void setCode(Integer code) {
this.code = code;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public Integer getAge() {
return age;
}
public void setAge(Integer age) {
this.age = age;
}
public String getGrade() {
return grade;
}
public void setGrade(String grade) {
this.grade = grade;
}
public String getDaynight() {
return daynight;
}
public void setDaynight(String daynight) {
this.daynight = daynight;
}
// 해당 메소드를 호출할 때 Student가 가진 Dept 참조 테이블 Fetch
// 성능 이슈로 FetchType은 Eager이 아닌 Lazy 사용 권장
public DeptEntity getDept() {
return dept;
}
public void setDept(DeptEntity dept) {
this.dept = dept;
}
public List<CourseStatusEntity> getCourseStatus() {
return courseStatus;
}
public void setCourseStatus(List<CourseStatusEntity> courseStatus) {
this.courseStatus = courseStatus;
}
@Override
public String toString() {
return "StudentEntity [id=" + id + ", code=" + code + ", name=" + name + ", age=" + age + ", grade=" + grade
+ ", daynight=" + daynight + ", dept=" + dept + "]";
}
}
- CourseStatusEntity.java
package com.myboot.datajpa.entity;
import javax.persistence.CascadeType;
import javax.persistence.Column;
import javax.persistence.Entity;
import javax.persistence.FetchType;
import javax.persistence.GeneratedValue;
import javax.persistence.GenerationType;
import javax.persistence.Id;
import javax.persistence.JoinColumn;
import javax.persistence.ManyToOne;
import javax.persistence.OneToOne;
import javax.persistence.Table;
import com.fasterxml.jackson.annotation.JsonIgnore;
@Entity
@Table(name = "course_status")
public class CourseStatusEntity {
@Id @GeneratedValue(strategy = GenerationType.IDENTITY)
@Column(name = "status_id")
private Long id;
//@JsonIgnore
// 학생 이력 -> 학생을 봤을 때 학생에게 이력이 여러개 이기 때문에 ManyToOne
@ManyToOne(fetch = FetchType.LAZY)
@JoinColumn(name = "student_id")
private StudentEntity student;
@OneToOne(fetch = FetchType.LAZY, cascade = CascadeType.ALL)
@JoinColumn(name = "course_id")
private CourseEntity course;
private Integer score;
public Long getId() {
return id;
}
public void setId(Long id) {
this.id = id;
}
public StudentEntity getStudent() {
return student;
}
public void setStudent(StudentEntity student) {
this.student = student;
}
public CourseEntity getCourse() {
return course;
}
public void setCourse(CourseEntity course) {
this.course = course;
}
public Integer getScore() {
return score;
}
public void setScore(Integer score) {
this.score = score;
}
@Override
public String toString() {
return "CourseStatusEntity [id=" + id + ", score=" + score + "]";
}
}
- FetchType의 Default가 Eager이기 때문에, 모두 Lazy로 설정 필요
- StudentServiceImpl.java
package com.myboot.datajpa.service;
import java.util.List;
import java.util.Optional;
import java.util.stream.Collectors;
import org.modelmapper.ModelMapper;
import org.springframework.http.HttpStatus;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import com.myboot.datajpa.entity.CourseStatusEntity;
import com.myboot.datajpa.entity.StudentEntity;
import com.myboot.datajpa.exception.BusinessException;
import com.myboot.datajpa.repository.CourseStatusRepository;
import com.myboot.datajpa.repository.DeptRepository;
import com.myboot.datajpa.repository.StudentRepository;
import com.myboot.datajpa.vo.CourseStatusVO;
import com.myboot.datajpa.vo.DeptVO;
import com.myboot.datajpa.vo.StudentVO;
@Service("studentService")
@Transactional
public class SutdentServiceImpl implements StudentService {
private StudentRepository studentRepository;
private DeptRepository deptRepository;
private CourseStatusRepository statusRepository;
private ModelMapper modelMapper;
// modelMapper : Entity -> VO 변환 역할
public SutdentServiceImpl(StudentRepository studentRepository, DeptRepository deptRepository,
CourseStatusRepository statusRepository, ModelMapper modelMapper) {
this.studentRepository = studentRepository;
this.deptRepository = deptRepository;
this.statusRepository = statusRepository;
this.modelMapper = modelMapper;
}
@Override
public StudentVO getStudent(Integer code) throws Exception {
// 매핑을 할 때 변수명과 컬럼명이 일치하지 않을 수 있을 경우를 고려
modelMapper.getConfiguration().setAmbiguityIgnored(true);
Optional<StudentEntity> optional = studentRepository.findByCode(code); // 학번을 가져와 사용
// 학번이 없을 경우 에러 처리
if(!optional.isPresent())
throw new BusinessException(code + " Student가 존재하지 않습니다.", HttpStatus.NOT_FOUND);
StudentEntity student = optional.get();
StudentVO studentVO = modelMapper.map(student, StudentVO.class); //Entity -> VO 변환
// Fetch Type이 Lazy로 설정되어 있을 경우 getDept() 메서드를 호출할 때 데이터 가져옴
DeptVO deptVO = modelMapper.map(student.getDept(), DeptVO.class); // VO -> Entity 변환
studentVO.setDept(deptVO);
List<CourseStatusEntity> statusList = student.getCourseStatus();
System.out.println(statusList);
// Stream을 사용하여 Entity -> VO 변환
List<CourseStatusVO> statusVoList = statusList.stream()
.map(entity -> new CourseStatusVO(entity))
.collect(Collectors.toList());
studentVO.setCourseStatus(statusVoList); // VO 반환
return studentVO;
}
@Override
public List<CourseStatusVO> getCourseStatus(Integer code) throws Exception {
Optional<StudentEntity> optional = studentRepository.findByCode(code);
if(!optional.isPresent())
throw new BusinessException(code + " Student가 존재하지 않습니다.", HttpStatus.NOT_FOUND);
StudentEntity student = optional.get();
List<CourseStatusEntity> statusList = statusRepository.findByStudent(student);
List<CourseStatusVO> statusVoList = statusList.stream()
.map(status -> new CourseStatusVO(status, status.getStudent()))
.collect(Collectors.toList());
return statusVoList;
}
@Override
public StudentEntity getStudentEntity(Integer code) throws Exception {
Optional<StudentEntity> optional = studentRepository.findByCode(code);
if(!optional.isPresent())
throw new BusinessException(code + " Student가 존재하지 않습니다.", HttpStatus.NOT_FOUND);
StudentEntity student = optional.get();
return student;
}
}

728x90
반응형
'프로젝트 > U-CAMP' 카테고리의 다른 글
FE - JS, jQuery 미션 (0) | 2025.04.10 |
---|---|
톰캣, DB 환경설정 (0) | 2025.04.10 |
BE - 미션 (0) | 2025.04.10 |
BE - 시험 (0) | 2025.04.10 |
SQL 문제 (0) | 2025.04.10 |