BE - 수업 실습

수현·2023년 10월 6일
0

Ucamp

목록 보기
14/19

📒 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 실행 확인 가능
  • 기능적인 요구 사항 (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 사용자로 접속
  • 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();
  • 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만 생성
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 사용을 위해 상단에 코드 추가 

<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<title>Insert title here</title>
</head>
<body>
	<h2>사용자 리스트</h2>
	<%-- ${users} --%>
	<table border="1">
		<tr>
			<th>ID</th>
			<th>UserId</th>
			<th>Name</th>
			<th>Gender</th>
			<th>City</th>
		</tr>
		<c:forEach var="user" items="${users}">
			<%-- user는  UserVO를 의미 --%>
			<tr>
				<td>${user.id}</td>
				<td>${user.userId}</td>
				<td>${user.name}</td>
				<td>${user.gender}</td>
				<td>${user.city}</td>
			</tr>
		</c:forEach>
	</table>
</body>
</html>

  • 전체 구조 정리
  • 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
  • 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 라이브러리 안에 내부적으로 불러다 사용 가능
<dependencies>
		<!-- https://mvnrepository.com/artifact/org.springframework/spring-context -->
		<dependency>
			<groupId>org.springframework</groupId>
			<artifactId>spring-context</artifactId>
			<version>${spring.version}</version>
		</dependency>

</dependencies>
  • 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
<?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-4.3.xsd">

	<!-- StringPrinter 클래스를 Spring Bean으로 등록 -->
	<!-- getBean() 메서드 사용시 bean의 id 사용  -->
	<bean id="strPrinter" class="myspring.di.xml.StringPrinter" />
	
	<!-- ConsolePrinter 클래스를 Spring Bean으로 등록 -->
	<!-- 인터페이스는 작성할 필요X -->
	<bean id="conPrinter" class="myspring.di.xml.ConsolePrinter" />

	<!-- Hello 클래스를 Spring Bean으로 등록 -->
	<bean id="hello" class="myspring.di.xml.Hello">
		<property name="name" value="스프링"/>
		<!-- value는 setName의 값으로 들어감 -->
		<property name="printer" ref="strPrinter" />
	</bean>
</beans>
  • 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 객체를 < 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));
	}

<!-- https://mvnrepository.com/artifact/org.springframework/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
      • 부모의 메서드를 재정의 하는데, 메서드 선언부가 동일한지 체크하는 어노테이션
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-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의 설정을 의미
  • 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를 가져와서 화면에 뿌리는 방식으로 사용

  • sqlsession 만드는 과정
    • sqlsessionfactorybean을 이용하여 sqlsessionfactory 만들기
      • datasource 의존 필요
      • setDataSource() 메서드의 인자로 hikaridatasource 들어옴
      • setConfigLocation() 으로 MyBatisConfig(sqlMapConfig) 파일 연결
      • setMapperLocations() mapping (UserMapper) 파일 연결
  • 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을 호출하기 위한 인터페이스

  • 쿼리문의 id와 mapper 인터페이스의 메서드 이름을 똑같이 맞추기
  • mysprin.user.dao.mapper 복사 해서 main/java 폴더에 붙여넣기
    • UserMapper.java
      • UserMapper.xml과 UserMapper 인터페이스의 메서드명 일치시키기
      • UserMapper 인터페이스가 있을 경우 xml이 수정될 때마다 업데이트 필요
        - UserMapeer과 SqlSession 부르기 위해 연결하기 위한 설정 필요
        -> mybatis-spring의 bean 추가 필요
  • 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에 올려서 실행
  • 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을 분리하여 생성해야함
  • 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

    • 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)
    • 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 추가
  • Setting 설정
    • Settings -> Compiler -> Annotation Processors
      • lombok 사용할 경우 Enable annotation processing 활성화
    • Settings -> Compliler
      • Build Project automatically dev tools 자동 실행
    • Settings -> Advanced Settings
      • Allow auto-make to start ...
    • Settting -> Editor -> File Encodings
      • project encoding/properties files도 UTF-8 적용
    • Settings -> Editor -> General -> Auto Import
      • Add unambiguous imports on the fly

  • 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에 장착되는 방식이 생김
    • 실제 예시 ()
  • @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);
        }
    }
    

  • 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 상에서 설정도 가능
  • 환경 변수 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

  • github 연결

📕 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
      • 로그 퍼사드 구현체들
  • 스트링 부트 로깅

    • 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
  • 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

  • Spring Boot 데이터_Entity 클래스

    • @Entity
      • Entity 클래스임을 지정하고, DB 테이블과 매핑하는 객체를 나타냄
    • @Id : entity 기본키
    • @GenerateValue : 기본키 값을 자동 생성됨을 나타낼 때 사용
      • 자동 생성 전략 : (AUTO, IDENTITY, SEQUENCE, TABLE)
    • @Column
      • Column은 안줘도 되지만 설정할게 있을 경우 사용
      • name 지정하지 않을 경우 변수명이 컬럼명 자동 지정
  • 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 수정
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
    * <code>AUTO</code> 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 extends T> S save(S entity);
리스트 조회: Iterable<T> findAll();  (CrudRepository)
          List<T> findAll(); (JpaRepository)
PK로 조회: Optional<T> 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

<dependency>
  <groupId>com.fasterxml.jackson.dataformat</groupId>
  <artifactId>jackson-dataformat-xml</artifactId>
  <version>${jackson-bom.version}</version>
</dependency>

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 에러 대비 코드
  • 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 의존성 추가 필요
// 하단에 추가
<!-- https://mvnrepository.com/artifact/org.modelmapper/modelmapper -->
		<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 안에 포함되어 배포
  • favicon

    • 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.firstName"=th:object="{session.user.firstName}" = th:object = "{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 연속적인 수로 화면에 뿌리기
  • 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

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 파일 작성
  • 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
  • 공개 옵션 조정

    • 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의 쿠키를 지워주는 등 동작 필요

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

📕 인증

  • 인증

  • 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가 삭제처리 되었습니다.");
    }
}

  • 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 관계

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

}

profile
Notion으로 이동 (https://24tngus.notion.site/3a6883f0f47041fe8045ef330a147da3?v=973a0b5ec78a4462bac8010e3b4cd5c0&pvs=4)

0개의 댓글