728x90
300x250

[JSP] 21. Jsp/Servlet(MVC) Maven 기반의 다중 업로드, 다운로드, 삭제 구현(1)


이 글의 소스코드를 수정해서 삭제 기능까지 추가해보도록 하겠다.


1. [JSP] 12. Jsp/Servlet(MVC) Maven 기반의 다중 파일 업로드, 다운로드 구현(1)

https://yyman.tistory.com/1414

2. [JSP] 12. Jsp/Servlet(MVC) Maven 기반의 다중 파일 업로드, 다운로드 구현(2)

https://yyman.tistory.com/1415



1. 결과


입력부터 다운로드, 삭제에 이르는 전 과정을 담아보았다.



그림 1. 다중 업로드 작업




그림 2. 다중 업로드된 모습





그림 3. 파일을 찾을 수 없을 때 반응




그림 4. 다운로드 모습




그림 5. 삭제 모습



그림 6. 파일이 다 지워질 경우에 폴더 삭제까지 하기



그림 7. 프로젝트 구성도(변경됨)




2. 변경 - Controller (FrontController.java)


package com.fileWeb.controller;


import java.io.IOException;

import java.util.HashMap;

import java.util.Map;


import javax.servlet.ServletConfig;

import javax.servlet.ServletException;

import javax.servlet.http.HttpServlet;

import javax.servlet.http.HttpServletRequest;

import javax.servlet.http.HttpServletResponse;

import javax.servlet.http.HttpSession;


import com.fileWeb.util.HttpUtil;


/**

 * Servlet implementation class HomeController

 */

public class FrontController extends HttpServlet {

private static final long serialVersionUID = 1L;

       

private String charset = null;

    public FrontController() {

        super();

    }


protected void doGet(HttpServletRequest req, HttpServletResponse res) throws ServletException, IOException {

doAction(req, res);

}

protected void doPost(HttpServletRequest req, HttpServletResponse res) throws ServletException, IOException {

doAction(req, res);

}

// FrontController 패턴 & Command 패턴

protected void doAction(HttpServletRequest req, HttpServletResponse res) throws ServletException, IOException {


ServletConfig sc = this.getServletConfig();

charset = sc.getInitParameter("charset");

req.setAttribute("charset", charset);

req.setCharacterEncoding(charset);

res.setContentType("text/html; charset=" + charset);

System.out.println(charset);

String uri = req.getRequestURI();

System.out.println("uri : " + uri);

String conPath = req.getContextPath();

System.out.println("conPath : " + conPath);

String command = uri.substring(conPath.length());

System.out.println("command : " + command);

Controller subController = null;

System.out.println("reqMapSize : " + req.getParameterMap().size());


if(command.equals("/board/insert.do")){

System.out.println("insert");

System.out.println("----------------");


    subController = new BoardInsertController();

    subController.execute(req, res);

   

}else if (command.equals("/board/insertResult.do")) {

System.out.println("insertResult");

System.out.println("----------------");


    subController = new BoardInsertResultController();

subController.execute(req, res);

}

else if(command.equals("/board/insertMultiResult.do")){

System.out.println("insertResult");

System.out.println("----------------");


    subController = new BoardInsertMultiResultController();

subController.execute(req, res);

}else if(command.equals("/board/download.do")) {

System.out.println("download");

System.out.println("----------------");

HttpUtil.fileDownload(req, res, null);

}else if(command.equals("/board/fileRemove.do")) {

System.out.println("remove");

System.out.println("----------------");

    HttpUtil.removeFile(req, res);

}

}

}



파일명: FrontController.java


[첨부(Attachments)]

FrontController-updated.zip





3. 변경 - Controller (BoardInsertMultiResultController.java)


변경 내용은 BoardInsertMultiResultController에 구현된 업로드 관련 소스를 HttpUtil로 이동시켰다.

파일명이 다소 너무 길어서 한 가지 이야기하면, 작명도 조금 연습하면 좋을 듯 싶다. (너무 긴 건 좋은 건 아니다.)


package com.fileWeb.controller;


import java.io.IOException;


import javax.servlet.ServletException;

import javax.servlet.http.HttpServletRequest;

import javax.servlet.http.HttpServletResponse;


import com.fileWeb.util.HttpUtil;


public class BoardInsertMultiResultController implements Controller {


@Override

public void execute(HttpServletRequest req, HttpServletResponse res) throws

ServletException, IOException {

HttpUtil.uploadFile(req, res);

HttpUtil.forward(req, res, "/WEB-INF/view/board/insertResult.jsp");

}

}


파일명: BoardInsertMultiResultController.java


[첨부(Attachments)]

BoardInsertMultiResultController-updated.zip






4. 변경 - Util 패키지로 변경 (HttpUtil.java)


다소 코드에 변화가 있었다.


변경 전: com.fileWeb.controller.HttpUtil.java

변경 후: com.fileWeb.util.HttpUtil.java




package com.fileWeb.util;


import java.io.File;

import java.io.FileInputStream;

import java.io.FileNotFoundException;

import java.io.IOException;

import java.io.InputStream;

import java.io.OutputStream;

import java.io.PrintWriter;

import java.util.HashMap;

import java.util.Iterator;

import java.util.List;

import java.util.Map;


import javax.servlet.RequestDispatcher;

import javax.servlet.ServletConfig;

import javax.servlet.ServletException;

import javax.servlet.http.HttpServlet;

import javax.servlet.http.HttpServletRequest;

import javax.servlet.http.HttpServletResponse;


import org.apache.commons.fileupload.FileItem;

import org.apache.commons.fileupload.disk.DiskFileItemFactory;

import org.apache.commons.fileupload.servlet.ServletFileUpload;



public class HttpUtil extends HttpServlet {

private static final long serialVersionUID = 1L;

private static String charset = null; // 문자열


private static int SIZETHRESHOLD = 4096;

private static String UPLOAD_FOLDER = "upload";

private static String UPLOAD_TMP_FOLDER = File.separator + "WEB-INF" + File.separator + "temp";

private static long MAX_UPLOAD_SIZE = 3 * 1024 * 1024;

private static Map<String, Object> reqMap = null; // req 정보(MultiRequest)

private static Map<Integer, Map<String, Object>> fileMap = null; // 다중 파일 지원

private static int num = 0;


public static void forward(HttpServletRequest req, HttpServletResponse res,

String path) throws ServletException, IOException {

try {

RequestDispatcher dispatcher = req.getRequestDispatcher(path);

dispatcher.forward(req, res);

}catch(Exception e) {

e.printStackTrace();

}

}

public static void uploadFile(HttpServletRequest req, HttpServletResponse res) throws

ServletException, IOException {


fileMap = new HashMap<Integer, Map<String, Object>>(); 

reqMap = new HashMap<>();

num = 1;

        

        PrintWriter out = res.getWriter();

        out.println("<HTML><HEAD><TITLE>Multipart Test</TITLE></HEAD><BODY>");

try {

            

            //디스크상의 프로젝트 실제 경로얻기

            //String contextRootPath = "c:" + File.separator + "upload";

String dirName = UPLOAD_FOLDER ; 

// String dirName = "upload"; 

String contextRootPath = req.getSession().getServletContext().getRealPath("/") + dirName;

            System.out.println("실제경로:" + contextRootPath);

            

            //1. 메모리나 파일로 업로드 파일 보관하는 FileItem의 Factory 설정

            DiskFileItemFactory diskFactory = new DiskFileItemFactory(); // 디스크 파일 아이템 공장

            diskFactory.setSizeThreshold(SIZETHRESHOLD); // 업로드시 사용할 임시 메모리

            // diskFactory.setSizeThreshold(4096); // 업로드시 사용할 임시 메모리

            diskFactory.setRepository(new File(contextRootPath + UPLOAD_TMP_FOLDER)); // 임시저장폴더

            // diskFactory.setRepository(new File(contextRootPath + UPLOAD_TMP_FOLDER)); // 임시저장폴더

            

            //2. 업로드 요청을 처리하는 ServletFileUpload생성

            ServletFileUpload upload = new ServletFileUpload(diskFactory);


            // upload.setSizeMax(3 * 1024 * 1024); //3MB : 전체 최대 업로드 파일 크기

            upload.setSizeMax(MAX_UPLOAD_SIZE); // 전체 최대 업로드 파일 크기

            

            //3. 업로드 요청파싱해서 FileItem 목록구함​​

            List<FileItem> items = upload.parseRequest(req); 


            Iterator<FileItem> iter = items.iterator(); //반복자(Iterator)로 받기​            

            while(iter.hasNext()) { //반목문으로 처리​    

                FileItem item = (FileItem) iter.next(); //아이템 얻기

                 //4. FileItem이 폼 입력 항목인지 여부에 따라 알맞은 처리

                

                if(item.isFormField()){ 

                //파일이 아닌경우

                    processFormField(out, item);

                    

                } else {

                //파일인 경우

                System.out.println("오류:" + item.getName());

               

                // 버그 개선 item 이름값 비어있을 때

                if ( item.getName() != "") {

                processUploadFile(out, item, contextRootPath);

                }

                System.out.println("오류2:");

                }

            }

            

        } catch(Exception e) {

            out.println("<PRE>");

            e.printStackTrace(out);

            out.println("</PRE>");

        }

out.println( "usrID(Map): " + reqMap.get("usrID") );

out.println( "usrPasswd(Map):" + reqMap.get("usrPasswd") );

        

        out.println("</BODY></HTML>");

// req.setAttribute("usrID", reqMap.get("usrID"));

// req.setAttribute("login", 1);//Object Type으로 넘어감

        req.setAttribute("reqMap", reqMap);

        req.setAttribute("fileMap", fileMap);

        

        // 방법3

        for( Integer key : fileMap.keySet() ){


            Map<String, Object> fileMapNode = fileMap.get(key);

            System.out.println( String.format("키 : %s, 값: %s", key, fileMapNode.get("fileName") ));

            

        }

        

// System.out.println("오류3:" + reqMap.get("usrID"));

}


//업로드한 정보가 파일인경우 처리

private static void processUploadFile(PrintWriter out, FileItem item, String contextRootPath)

throws Exception {

Map<String, Object> fileNode = new HashMap<String, Object>();


String dirName = UPLOAD_FOLDER ; 

String name = item.getFieldName(); // 파일의 필드 이름 얻기

String fileName = item.getName(); // 파일명 얻기

// 임시 - 실제 원본 이름 추출

File originalFile = new File(fileName);

String originalFileName = originalFile.getName();

System.out.println("임시:" + originalFileName );

String contentType = item.getContentType(); // 컨텐츠 타입 얻기

long fileSize = item.getSize(); // 파일의 크기 얻기

// 업로드 파일명을 현재시간으로 변경후 저장

String fileExt = fileName.substring(fileName.lastIndexOf("."));

String uploadedFileName = System.currentTimeMillis() + ""; 

System.out.println(fileExt);

System.out.println(uploadedFileName);

// 저장할 절대 경로로 파일 객체 생성

String realUploadFile = File.separator + dirName + File.separator + uploadedFileName;

System.out.println("실제 저장직전폴더:" + contextRootPath + realUploadFile);

File uploadedFile = new File(contextRootPath + realUploadFile);

item.write(uploadedFile); //파일 저장

//========== 뷰단에 출력 =========//

out.println("<P>");

out.println("파라미터 이름:" + name + "<BR>");

out.println("파일 이름:" + fileName + "<BR>");

out.println("콘텐츠 타입:" + contentType + "<BR>");

out.println("파일 사이즈:" + fileSize + "<BR>");

//확장자가 이미지인겨우 이미지 출력

if(".jpg.jpeg.bmp.png.gif".contains(fileExt.toLowerCase())) {

out.println("<IMG SRC='upload/" 

+ uploadedFileName 

+ "' width='300'><BR>");

}

out.println("</P>");

out.println("<HR>");

out.println("실제저장경로 : "+uploadedFile.getPath()+"<BR>");

out.println("<HR>");

// 파일 정보

fileNode.put("name", name);

fileNode.put("fileName", originalFileName);

fileNode.put("contentType", contentType);

fileNode.put("fileSize", fileSize);

fileNode.put("fileExt", fileExt);

fileNode.put("uploadedFileName", uploadedFileName);

fileNode.put("realName", uploadedFile.getName());

fileNode.put("realPath", uploadedFile.getPath());

fileMap.put(num, fileNode);

num++;

}

private static void processFormField(PrintWriter out, FileItem item) 

throws Exception{

String name = item.getFieldName(); //필드명 얻기

Object value = item.getString("UTF-8"); //UTF-8형식으로 필드에 대한 값읽기

// out.println(name + ":" + value + "<BR>"); //출력

reqMap.put(name, value);

}

/*

* 다운로드(Download)

*/

public static void fileDownload(HttpServletRequest req, HttpServletResponse res,

String path) throws ServletException, IOException {


charset = (String) req.getAttribute("charset");

System.out.println(charset);

PrintWriter out = res.getWriter();

// 파일 업로드된 경로

String root = req.getSession().getServletContext().getRealPath("/");

String savePath = root + UPLOAD_FOLDER + File.separator + UPLOAD_FOLDER ;

// String savePath = root + "upload" + File.separator + "upload";

// 서버에 실제 저장된 파일명

String filename = "1601561525229" ;

System.out.println("파일 실제 폴더경로:" + savePath);

System.out.println("실제 파일명:" + filename);

// 실제 내보낼 파일명

String orgfilename = "license한글.txt" ;

req.setCharacterEncoding(charset);

res.setCharacterEncoding(charset);

InputStream in = null;

OutputStream os = null;

File file = null;

boolean skip = false;

String client = "";

try{

    // 파일을 읽어 스트림에 담기

    try{

        file = new File(savePath, filename);

        in = new FileInputStream(file);

    }catch(FileNotFoundException fe){

        skip = true;

    }

    client = req.getHeader("User-Agent");

    // 파일 다운로드 헤더 지정

    res.reset() ;

    res.setContentType("application/octet-stream");

    res.setHeader("Content-Description", "JSP Generated Data");

    if(!skip){

        // IE

        if(client.indexOf("MSIE") != -1){

            res.setHeader ("Content-Disposition", "attachment; filename="+new String(orgfilename.getBytes("KSC5601"),"ISO8859_1"));

        }else{

            // 한글 파일명 처리

            orgfilename = new String(orgfilename.getBytes("KSC5601"),"iso-8859-1");

            res.setHeader("Content-Disposition", "attachment; filename=\"" + orgfilename + "\"");

            res.setHeader("Content-Type", "application/octet-stream; charset=utf-8");

        }  

        res.setHeader ("Content-Length", ""+file.length() );

        os = res.getOutputStream();

        

        byte b[] = new byte[(int)file.length()];

        int leng = 0;

        while( (leng = in.read(b)) > 0 ){

            os.write(b,0,leng);

        }

    }else{

    // 한글 깨짐 - 해결

    res.setContentType("text/html;charset=" + charset);

        out.println("<html><head>");

        out.println("<script language='javascript'>alert('파일을 찾을 수 없습니다.');history.back();</script>");

        out.println("</head><body></body></html>");

    }

    in.close();

    os.close();

}catch(Exception e){

e.printStackTrace();

}

    

}


/*

*  파일 삭제, 폴더 삭제

*/

public static void removeFile(HttpServletRequest req, HttpServletResponse res) 

throws ServletException, IOException{


// 파일 업로드된 경로

String root = req.getSession().getServletContext().getRealPath("/");

String savePath = root + UPLOAD_FOLDER + File.separator + UPLOAD_FOLDER ;

String filename = "1601561525229" ;


        File file = new File(savePath, filename);

        // 파일이 존재할 떄

        if (file.exists()) {

        file.delete();

        }

        

        removeDirectory(req, res, savePath);

}

/*

*/

private static boolean removeDirectory(HttpServletRequest req, HttpServletResponse res,

String path) throws ServletException, IOException {

boolean result = false;

File usrDir = new File(path);

if(!usrDir.exists()) {                  // 경로 존재 여부

            result = false;     

        }

else {

        File[] lowFiles = usrDir.listFiles();      // 경로 내의 파일 리스트

        

        // 폴더 삭제

        if ( usrDir.isDirectory() 

        && lowFiles.length == 0 ) {

   

        System.out.println("폴더 삭제처리 완료");

        usrDir.delete();

       

        return true;

       

    }else{

    result = false;

    }

        

        }

        

        return result;

}


}



파일명: HttpUtil.java


[첨부(Attachments)]

HttpUtil-updated.zip




* 맺음글(Conclusion)


파일 업로드, 다운로드, 삭제에 대해서 살펴보았다.

반응형
728x90
300x250

[JSP] 20. MyBatis-3.5.5, HikariCP 3.4.2 연동 - Maven(Servlet) Spring 제거버전 (Oracle 19g) - Java 방식


JSP/Servlet 순수한 방식에서 HikariCP를 연동할 수 있는 방법에 대해서 고민을 하게 된 이유는 Spring Framework의 의존성을 줄일 수 있겠냐는 생각을 가지고 접근하게 되었다.


JSP/Servlet이 현재에도 의외로 많이 사용되고 있다. Spring-Framework 사용 안 한 형태도 동작하는 프로젝트가 많기 때문에 조금 생각해보면 효과적으로 기존 프로젝트도 개선할 수 있을 거 같아서 작성하게 되었다.


기존 프로젝트에도 MyBatis 프레임워크와 HikariCP를 적용할 수 있으니 가능성을 가지고 순수한 JSP에 대해서도 관심을 가져봤으면 하는 바람이다.


커넥션 풀은 현재의 웹 개발에서는 반드시 필요한 존재라고 본다. 

(중요성에 대해서는 별도로 시간이 나면, 조금 더 심화적인 방법으로 연재하도록 하겠다.)


* XML로 설정한 MyBatis에서는 가능한가요?

= 불가능하다. (미지원)



- IDE: Spring-Tool-Suite 4-4.7.2 Releases (2020-06 최신)

- DB: Oracle Databases 19g (2020-09 최신)

- Maven 3.6.3/1.16.0.20200610-1735 (2020-09 최신)

- JAR: javax.servlet-api.4.0.1.jar (2020-09 최신)

         ojdbc8-19.7.0.0.jar (2020-09 최신)

         MyBatis 3.5.5 (2020-09 최신)

         HikariCP 3.4.2 (2020-09 최신)



* 정말로 Spring-Framework는 사용하지 않았나요?

= 그렇다.



관련 글)

1. [JSP] 19. MyBatis-3.5.5 와 Maven / Servlet 연동하기 (Oracle 19g) - Java 방식, https://yyman.tistory.com/1434, 2020-10-01

-> 이 글의 소스 코드 이어서 계속(해당 게시글에서는 변형됨.)



1. 결과


이전의 게시글에서 동작 반응이 다소 변경되었다. (HikariConnection Pool 반응 상태를 추가하였음.



그림 1. HikariPool - 2 반응 추가


사소해보이지만, 무척 중요하다. 동시 접속했을 때, 서버가 죽느냐 사느냐를 결정할 수도 있다.

커넥션 풀은 데이터베이스 접속을 원할하게 해주는 중요한 것이라고 비유해주고 싶다.



그림 2. 프로젝트 구성 모습


* 이전의 프로젝트와 차이점: exmple 탈자를 정정하였음. (example로)

- 패키지명을 잘 조정해서 사용하면 크게 무리없이 동작할 것이다.


1. 폴더 추가

/src/main/resources 폴더가 생성되었다.

- db.properties가 추가되었다.



2. POM.xml - 설정하기


POM.xml의 변화는 HikariCP 3.4.2가 추가되었다.

바뀐 것은 없다. 그래서 잘 따라해야 하는 것이다.


<?xml version="1.0" encoding="UTF-8"?>


<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"

  xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">

  <modelVersion>4.0.0</modelVersion>


  <groupId>com.exmplebatis</groupId>

  <artifactId>web</artifactId>

  <version>0.0.1-SNAPSHOT</version>

  <packaging>war</packaging>


  <name>web Maven Webapp</name>

  <!-- FIXME change it to the project's website -->

  <url>http://www.example.com</url>


  <properties>

    <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>

    <maven.compiler.source>1.7</maven.compiler.source>

    <maven.compiler.target>1.7</maven.compiler.target>

  </properties>


  <dependencies>

    <dependency>

      <groupId>junit</groupId>

      <artifactId>junit</artifactId>

      <version>4.11</version>

      <scope>test</scope>

    </dependency>

    <!-- https://mvnrepository.com/artifact/javax.servlet/javax.servlet-api -->

<dependency>

    <groupId>javax.servlet</groupId>

    <artifactId>javax.servlet-api</artifactId>

    <version>4.0.1</version>

    <scope>provided</scope>

</dependency>

<!-- https://mvnrepository.com/artifact/org.mybatis/mybatis -->

<dependency>

    <groupId>org.mybatis</groupId>

    <artifactId>mybatis</artifactId>

    <version>3.5.5</version>

</dependency>

<!-- https://mvnrepository.com/artifact/com.zaxxer/HikariCP -->

<dependency>

    <groupId>com.zaxxer</groupId>

    <artifactId>HikariCP</artifactId>

    <version>3.4.2</version>

</dependency>

<dependency>

<groupId>com.oracle.database.jdbc</groupId>

<artifactId>ojdbc8</artifactId>

<version>19.7.0.0</version>

</dependency>

  </dependencies>


  <build>

    <finalName>web</finalName>

    <pluginManagement><!-- lock down plugins versions to avoid using Maven defaults (may be moved to parent pom) -->

      <plugins>

        <plugin>

          <artifactId>maven-clean-plugin</artifactId>

          <version>3.1.0</version>

        </plugin>

        <!-- see http://maven.apache.org/ref/current/maven-core/default-bindings.html#Plugin_bindings_for_war_packaging -->

        <plugin>

          <artifactId>maven-resources-plugin</artifactId>

          <version>3.0.2</version>

        </plugin>

        <plugin>

          <artifactId>maven-compiler-plugin</artifactId>

          <version>3.8.0</version>

        </plugin>

        <plugin>

          <artifactId>maven-surefire-plugin</artifactId>

          <version>2.22.1</version>

        </plugin>

        <plugin>

          <artifactId>maven-war-plugin</artifactId>

          <version>3.2.2</version>

        </plugin>

        <plugin>

          <artifactId>maven-install-plugin</artifactId>

          <version>2.5.2</version>

        </plugin>

        <plugin>

          <artifactId>maven-deploy-plugin</artifactId>

          <version>2.8.2</version>

        </plugin>

      </plugins>

    </pluginManagement>

  </build>

</project>



파일명: pom.xml


[첨부(Attachments)]

pom-update-hikaricp.zip




3. /resources/db.properties 설정하기


db.properties에 관한 것이다.



그림 3. db.properties 설정 내용


jdbcUrl=jdbc:oracle:thin:@localhost:1521:orcl

dataSourceClassName=oracle.jdbc.driver.OracleDriver

dataSource.user=UserName

dataSource.password=Password

cachePrepStmts=true

prepStmtCacheSize=250

prepStmtCacheSqlLimit=2048


파일명: db.properties


[첨부(Attachments)]

db.zip




4. SqlMapSessionFactory.java - com.example.web.db


DB 세션 영역이다.

일부 인터넷 게시글을 찾아보면, SqlSessionFactoryBeans ssfb = new SqlSessionFactoryBeans(); 객체 생성 후에 setDatasource()로 연결시키면 된다고 소개된 글들이 있다.


이 부분은 공식 메뉴얼을 찾아본 바로는 MyBatis에서 Spring을 지원하기 위해서 만든 부분이다.

JSP/Servlet 기반의 프로젝트에서는 지원하지 않는 부분이다. (Beans 처리를 미지원함.)


충분히 시간을 가지고 태스트가 완료된 코드이다.



그림 4. SqlMapSessionFactory.java - 작업 모습



package com.example.web.db;


import java.io.IOException;

import java.io.InputStream;

import java.sql.Connection;

import java.sql.PreparedStatement;

import java.sql.ResultSet;

import java.util.Properties;


import javax.sql.DataSource;


import org.apache.ibatis.mapping.Environment;

import org.apache.ibatis.session.Configuration;

import org.apache.ibatis.session.SqlSessionFactory;

import org.apache.ibatis.session.SqlSessionFactoryBuilder;

import org.apache.ibatis.transaction.TransactionFactory;

import org.apache.ibatis.transaction.jdbc.JdbcTransactionFactory;


import com.example.web.mapper.CompUsersMapper;

import com.zaxxer.hikari.HikariConfig;

import com.zaxxer.hikari.HikariDataSource;


import oracle.jdbc.pool.OracleDataSource;


public class SqlMapSessionFactory {


private static SqlMapSessionFactory factory = new SqlMapSessionFactory();


public static SqlMapSessionFactory getInstance() {

return factory;

}


public static SqlSessionFactory ssf;


    private static String CLASSNAME;

    private static String JDBC_URL;

    private static String USERNAME;

    private static String PASSWORD;

    private static String CACHE_PREP_STMTS;

    

    private HikariDataSource ds;

    private HikariConfig config;



private SqlMapSessionFactory() {

/* HikariCP 로드 */

    InputStream inputStream;

    config = new HikariConfig();

   

    String resource = "db.properties";

    Properties properties = new Properties();

    

    try {


    inputStream = getClass().getClassLoader().getResourceAsStream(resource);

        properties.load(inputStream);


        System.out.println("jdbcurl:" + properties.getProperty("jdbcUrl"));

        System.out.println("className" + properties.getProperty("dataSourceClassName"));


        CLASSNAME = properties.getProperty("dataSourceClassName");

        JDBC_URL = properties.getProperty("jdbcUrl");

        USERNAME = properties.getProperty("dataSource.user");

        PASSWORD = properties.getProperty("dataSource.password");

        CACHE_PREP_STMTS = properties.getProperty("cachePrepStmts");


        config.setDriverClassName(CLASSNAME);

        config.setJdbcUrl( JDBC_URL );

        config.setUsername( USERNAME );

        config.setPassword( PASSWORD );

        config.addDataSourceProperty( "cachePrepStmts" , CACHE_PREP_STMTS );

        config.addDataSourceProperty( "prepStmtCacheSize" , "250" );

        config.addDataSourceProperty( "prepStmtCacheSqlLimit" , "2048" );

        ds = new HikariDataSource( config );


    System.out.println("성공:" + ds);


    } catch (IOException e) {

    System.out.println("오류:" + e.getMessage());

        e.printStackTrace();

    }

    

}

   

// iBatis(MyBatis 반환)

public SqlSessionFactory getSqlSessionFactory() {

DataSource hDs = ds;

    // DataSource dataSource = getOracleDataSource();

System.out.println(hDs);

    TransactionFactory transactionFactory = new JdbcTransactionFactory();

    Environment environment = new Environment("development", transactionFactory, hDs);

    Configuration configuration = new Configuration(environment);


    configuration.addMapper(CompUsersMapper.class); // Mapper 클래스

   

//    System.out.println("성공2");

        return new SqlSessionFactoryBuilder().build(configuration);


    }


public void close(Connection conn, PreparedStatement ps, ResultSet rs) {


if ( rs != null ) {


try {

rs.close();

}

catch(Exception ex) {

System.out.println("오류 발생: " + ex);

}

close(conn, ps); // Recursive 구조 응용(재귀 함수)

} // end of if


}


public void close(Connection conn, PreparedStatement ps) {


if (ps != null ) {

try {

ps.close();

}

catch(Exception ex) {

System.out.println("오류 발생: " + ex);

}

} // end of if


if (conn != null ) {


try {

conn.close();

}

catch(Exception ex) {

System.out.println("오류 발생: " + ex);

}


} // end of if


}


}


파일명: SqlMapSessionFactory.java


[첨부(Attachments)]

SqlMapSessionFactory-update.zip




5. FrontController.java - com.example.web.controller


FrontController에 대한 변화이다.



그림 5. FrontController.java - 작업 모습



package com.example.web.controller;


import java.io.IOException;

import javax.servlet.ServletException;

import javax.servlet.http.HttpServlet;

import javax.servlet.http.HttpServletRequest;

import javax.servlet.http.HttpServletResponse;


import org.apache.ibatis.session.SqlSession;

import org.apache.ibatis.session.SqlSessionFactory;


import com.example.web.db.SqlMapSessionFactory;

import com.example.web.mapper.CompUsersMapper;

import com.example.web.model.CompUsers;


public class FrontController extends HttpServlet {

private static final long serialVersionUID = 1L;

    SqlSessionFactory factory = null;

    SqlMapSessionFactory sqlMapFactory = null;


/**

* @see HttpServlet#doGet(HttpServletRequest request, HttpServletResponse response)

*/

protected void doGet(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {

doAction(request, response);

}


/**

* @see HttpServlet#doPost(HttpServletRequest request, HttpServletResponse response)

*/

protected void doPost(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {

doAction(request, response);

}


protected void doAction(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {

sqlMapFactory = SqlMapSessionFactory.getInstance();

try {

factory = sqlMapFactory.getSqlSessionFactory();

} catch (Exception e) {

e.printStackTrace();

}

try (SqlSession session = factory.openSession()) {

  CompUsersMapper mapper = session.getMapper(CompUsersMapper.class);

  CompUsers user = mapper.findByUsername("user");

  

  System.out.println("계정명:" + user.getUsername());

  

}

}


}



파일명: FrontController.java


[첨부(Attachments)]

FrontController-update.zip





6. CompUsers.java - com.example.web.model (Model)


이전 게시글과는 변화는 없지만, 다시 재언급한다.


package com.example.web.model;


public class CompUsers {


private String username;

private String password;

private int enabled;

public String getUsername() {

return username;

}

public void setUsername(String username) {

this.username = username;

}

public String getPassword() {

return password;

}

public void setPassword(String password) {

this.password = password;

}

public int getEnabled() {

return enabled;

}

public void setEnabled(int enabled) {

this.enabled = enabled;

}

}



파일명: CompUsers.java


[첨부(Attachments)]

CompUsers-updated.zip


CREATE TABLE comp_users (

username VARCHAR(50) NOT NULL,

password VARCHAR(300) NOT NULL,

enabled INT NOT NULL,

PRIMARY KEY (username)

);


-- 계정

INSERT INTO comp_users (username, password, enabled) VALUES ('user', '$2a$10$x04djNV2e9rpcPPRyXoLk.rMm6iZe2/vYdzpqHQcLeNSYdt7kc30O', 1);

INSERT INTO comp_users (username, password, enabled) VALUES ('admin', '$2a$10$QUddY3O/6ZgkYCR6MFlv9.nqA501Fm0cc/ZxQHX5pwb1o0CYCTiIS', 1);


파일명: CompUsers.sql


[첨부(Attachments)]

comp_users_sql.zip




7. CompUsersMapper.java - com.example.web.mapper (Mapper - DAO)


이전 게시글과 비교했을 때 변화는 없었다.


package com.example.web.mapper;


import org.apache.ibatis.annotations.Mapper;

import org.apache.ibatis.annotations.Select;


import com.example.web.model.CompUsers;


@Mapper

public interface CompUsersMapper {

  @Select("SELECT * FROM comp_users WHERE username = #{username}")

  public CompUsers findByUsername(String username);

  

}



파일명: CompUsersMapper.sql


[첨부(Attachments)]

CompUsersMapper-updated.zip




* 맺음글(Conclusion)


HikariCP 구성과 MyBatis-3.5.5 셋팅에 대해서 JSP/Servlet 방식으로 다뤄보았다.

반응형
728x90
300x250

[JSP] 19. MyBatis-3.5.5 와 Maven / Servlet 연동하기 (Oracle 19g) - Java 방식


MyBatis 관련 이전의 글을 읽어보고 실습을 해본 다음에 경험해봐도 무방할 것으로 보인다.

이번에 소개할 내용은 조금 세련된 방식으로 MyBatis를 적용하는 방법에 대해서 소개하려고 한다.


이 글을 작성하게 된 계기는 MyBatis 공식 사이트에서 제시하는 방법론에 대해서 실질적으로 어떻게 구현하는지 소개해보고 싶어서 그렇다.


이전 스타일에 익숙하다면, 새로운 스타일을 경험하시어 훨씬 생산성이 있는 방법으로 개발을 시도해보면 좋지 않겠냐는 것이다.


1. [JSP] 8. 영속프레임워크 MyBatis를 활용한 CRUD 구현 - JSP와 Oracle (XML 방식), 2020-9-19
https://yyman.tistory.com/1390 

-> XML 방식으로 구현됨.


- IDE: Spring-Tool-Suite 4-4.7.2 Releases (2020-06 최신)

- DB: Oracle Databases 19g (2020-09 최신)

- Maven 3.6.3/1.16.0.20200610-1735 (2020-09 최신)

- JAR: javax.servlet-api.4.0.1.jar (2020-09 최신)

         ojdbc8-19.7.0.0.jar (2020-09 최신)

         MyBatis 3.5.5 (2020-09 최신)



1. 결과


결과를 바탕으로 살펴보면, 이렇게 출력되면 잘 된 것이다.



그림 1. 결과 - DB 연동 모습



그림 2. 결과 - 프로젝트 구성도


작업을 할 내용이다. 매우 간결한 프로젝트 구성이다.



2. 데이터베이스 설계


Spring Security 5의 사용자 계정 테이블을 인용하여 설계하였다.

테이블의 구조는 간단한 형태이다.



그림 3. Oracle SQL Developer - 데이터베이스 설계





그림 4. Oracle SQL Developer - (데이터 내용)



그림 5. Oracle SQL Developer - COMP_USERS (Model)


CREATE TABLE comp_users (

username VARCHAR(50) NOT NULL,

password VARCHAR(300) NOT NULL,

enabled INT NOT NULL,

PRIMARY KEY (username)

);


-- 계정

INSERT INTO comp_users (username, password, enabled) VALUES ('user', '$2a$10$x04djNV2e9rpcPPRyXoLk.rMm6iZe2/vYdzpqHQcLeNSYdt7kc30O', 1);

INSERT INTO comp_users (username, password, enabled) VALUES ('admin', '$2a$10$QUddY3O/6ZgkYCR6MFlv9.nqA501Fm0cc/ZxQHX5pwb1o0CYCTiIS', 1);


파일명: comp_users.sql


[첨부(Attachments)]

comp_users.zip





3. 프로젝트 생성하기


프로젝트 생성에 대해서 소개하겠다.



그림 6. Maven Project 생성하기(1)


File -> New -> Maven Project를 클릭한다.




그림 7. Maven Project 생성하기(2)


프로젝트를 생성할 때 "org.apache.maven.archetype", "maven-archetype-webapp"을 선택한다.

Next를 클릭한다.



그림 8. Maven Project 생성하기(3)


Group Id, Artifact Id를 입력해준다.

Finish를 누른다.




4. 자바 버전 - Build Path, Project Facets 설정하기


프로젝트를 선택한다. 그리고 Properties에 들어가서 설정할 것이다.



그림 9. 프로젝트의 마우스 오른쪽 버튼의 메뉴 모습


프로젝트를 선택한 후 마우스 오른쪽 버튼을 누른다.

Properties를 클릭한다.



그림 10. 프로젝트의 Build Path


Java Build Path를 클릭한다.

JRE System Library를 14버전으로 변경해준다.

Apply를 누른다.




그림 11. 프로젝트의 Project Factes


Project Factes를 클릭한다.

Java의 버전을 14로 변경한다.

Apply를 누른다.



4. MyBatis - 공식 사이트 메뉴얼 읽어보기


공식 사이트에서 지원하는 방식에 대해서 간단하게 소개되어 있다.

아래의 공식 링크를 참고해서 적용할 것이다.


https://mybatis.org/mybatis-3/ko/getting-started.html



그림 12. MyBatis - 시작하기 (Official Site)





5. pom.xml - 설정하기


http://mvnrepository.com에서 Java Servlet API 4.0.1과 MyBatis를 찾아서 추가한다.

Oracle은 Oracle 공식 사이트나 또는 Oracle Databases 19g가 설치되어 있다면, 간단하게 Add Dependency를 통해서 pom.xml에 추가할 수 있다.



그림 13. Servlet API 4.0.1 - Mvnrepository






그림 14. MyBatis 3.5.5 - Mvnrepository



그림 15. Pom.xml 마우스 오른쪽 버튼 메뉴 모습


pom.xml을 마우스 오른쪽 버튼으로 클릭한다.

Maven->Add Dependency를 클릭한다.


(참고로 Oracle Databases 19g가 설치된 경우에서만 가능한 작업이다.)

(설치가 되지 않은 경우라면, Oracle 공식 사이트에서 Oracle JDBC를 내려받기 바란다.

그리고 lib 폴더에 넣어주고 프로젝트에서 셋팅해줘야 한다.)



그림 16. Add-Dependency


Oracle을 검색한다.

com.oracle.database.jdbc  "ojdbc8"을 선택한다.

OK를 누른다.



그림 17. STS 4.4 - pom.xml 모습


<?xml version="1.0" encoding="UTF-8"?>


<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"

  xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">

  <modelVersion>4.0.0</modelVersion>


  <groupId>com.examplebatis</groupId>

  <artifactId>web</artifactId>

  <version>0.0.1-SNAPSHOT</version>

  <packaging>war</packaging>


  <name>web Maven Webapp</name>

  <!-- FIXME change it to the project's website -->

  <url>http://www.example.com</url>


  <properties>

    <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>

    <maven.compiler.source>1.7</maven.compiler.source>

    <maven.compiler.target>1.7</maven.compiler.target>

  </properties>


  <dependencies>

    <dependency>

      <groupId>junit</groupId>

      <artifactId>junit</artifactId>

      <version>4.11</version>

      <scope>test</scope>

    </dependency>

    <!-- https://mvnrepository.com/artifact/javax.servlet/javax.servlet-api -->

<dependency>

    <groupId>javax.servlet</groupId>

    <artifactId>javax.servlet-api</artifactId>

    <version>4.0.1</version>

    <scope>provided</scope>

</dependency>

<!-- https://mvnrepository.com/artifact/org.mybatis/mybatis -->

<dependency>

    <groupId>org.mybatis</groupId>

    <artifactId>mybatis</artifactId>

    <version>3.5.5</version>

</dependency>


<dependency>

<groupId>com.oracle.database.jdbc</groupId>

<artifactId>ojdbc8</artifactId>

<version>19.7.0.0</version>

</dependency>

  </dependencies>


  <build>

    <finalName>web</finalName>

    <pluginManagement><!-- lock down plugins versions to avoid using Maven defaults (may be moved to parent pom) -->

      <plugins>

        <plugin>

          <artifactId>maven-clean-plugin</artifactId>

          <version>3.1.0</version>

        </plugin>

        <!-- see http://maven.apache.org/ref/current/maven-core/default-bindings.html#Plugin_bindings_for_war_packaging -->

        <plugin>

          <artifactId>maven-resources-plugin</artifactId>

          <version>3.0.2</version>

        </plugin>

        <plugin>

          <artifactId>maven-compiler-plugin</artifactId>

          <version>3.8.0</version>

        </plugin>

        <plugin>

          <artifactId>maven-surefire-plugin</artifactId>

          <version>2.22.1</version>

        </plugin>

        <plugin>

          <artifactId>maven-war-plugin</artifactId>

          <version>3.2.2</version>

        </plugin>

        <plugin>

          <artifactId>maven-install-plugin</artifactId>

          <version>2.5.2</version>

        </plugin>

        <plugin>

          <artifactId>maven-deploy-plugin</artifactId>

          <version>2.8.2</version>

        </plugin>

      </plugins>

    </pluginManagement>

  </build>

</project>



파일명: pom.xml


[첨부(Attachments)]

pom.zip




6. SqlMapSessionFactory.java - 자바 방식 연결부


자바 방식 연결부를 작성하도록 하겠다.



그림 18. Java Resources의 마우스 오른쪽 클릭 메뉴 모습


Java Resources를 마우스 오른쪽 버튼으로 클릭한다.

New->Class를 클릭한다.




그림 19. SqlMapSessionFactory.java 만들기


Package명과 Name을 입력한다. 

(예: package: com.exmple.web.db)    //    탈자(exmple로 만들었으니 알아서 참고할 것.)

(예: Name: SqlMapSessionFactory)


Finish를 누른다.



그림 20. SqlMapSessionFactory.java


코드를 수정해준다.


package com.exmple.web.db;


import java.io.IOException;

import java.io.InputStream;

import java.sql.Connection;

import java.sql.DriverManager;

import java.sql.PreparedStatement;

import java.sql.ResultSet;

import java.sql.SQLException;


import javax.sql.DataSource;


import org.apache.ibatis.io.Resources;

import org.apache.ibatis.mapping.Environment;

import org.apache.ibatis.session.Configuration;

import org.apache.ibatis.session.SqlSessionFactory;

import org.apache.ibatis.session.SqlSessionFactoryBuilder;

import org.apache.ibatis.transaction.TransactionFactory;

import org.apache.ibatis.transaction.jdbc.JdbcTransactionFactory;


import com.exmple.web.mapper.CompUsersMapper;


import oracle.jdbc.pool.OracleDataSource;


public class SqlMapSessionFactory {


private static SqlMapSessionFactory factory = new SqlMapSessionFactory();


private SqlMapSessionFactory() {}


private final static String driverName = "oracle.jdbc.driver.OracleDriver";

private final static String dbUrl = "jdbc:oracle:thin:@127.0.0.1:1521:orcl";

private final static String userName = "사용자계정명";

private final static String userPassword = "비밀번호";


public static SqlMapSessionFactory getInstance() {

return factory;

}


public static SqlSessionFactory ssf;


    static{


    DataSource dataSource = getOracleDataSource();

    TransactionFactory transactionFactory = new JdbcTransactionFactory();

   

    Environment environment = new Environment("development", transactionFactory, dataSource);

    Configuration configuration = new Configuration(environment);

   

    configuration.addMapper(CompUsersMapper.class); // Mapper 클래스

   

        ssf = new SqlSessionFactoryBuilder().build(configuration);

        

    }

    

// iBatis(MyBatis 반환)

public static SqlSessionFactory getSqlSessionFactory(){

        return ssf;

    }


/*


*     public static DataSource getMySQLDataSource() {

        Properties props = new Properties();


        FileInputStream fis = null;

        MysqlDataSource mysqlDS = null;

        

        try {

            fis = new FileInputStream("db.properties");


            props.load(fis);

            mysqlDS = new MysqlDataSource();

            mysqlDS.setURL(props.getProperty("MYSQL_DB_URL"));

            mysqlDS.setUser(props.getProperty("MYSQL_DB_USERNAME"));

            mysqlDS.setPassword(props.getProperty("MYSQL_DB_PASSWORD"));

            

        } catch (IOException e) {

            e.printStackTrace();

        }

        return mysqlDS;

        

    }

    */

/*

* Description: 순정 오라클 데이터소스

*/

    private static DataSource getOracleDataSource(){

        

    OracleDataSource oracleDS = null;

        

    try {

            oracleDS = new OracleDataSource();

            oracleDS.setURL(dbUrl);

            oracleDS.setUser(userName);

            oracleDS.setPassword(userPassword);

            

        } catch (SQLException e) {

            e.printStackTrace();

        }

        return oracleDS;


    }


public Connection connect() {


Connection conn = null;


try {

Class.forName(driverName);

conn = DriverManager.getConnection(dbUrl, userName, userPassword);

}

catch(Exception ex) {

System.out.println("오류 발생: " + ex);

}

return conn;

}


public void close(Connection conn, PreparedStatement ps, ResultSet rs) {


if ( rs != null ) {


try {

rs.close();

}

catch(Exception ex) {

System.out.println("오류 발생: " + ex);

}

close(conn, ps); // Recursive 구조 응용(재귀 함수)

} // end of if


}


public void close(Connection conn, PreparedStatement ps) {


if (ps != null ) {

try {

ps.close();

}

catch(Exception ex) {

System.out.println("오류 발생: " + ex);

}

} // end of if


if (conn != null ) {


try {

conn.close();

}

catch(Exception ex) {

System.out.println("오류 발생: " + ex);

}


} // end of if


}


}


파일명: SqlMapSessionFactory.java


[첨부(Attachments)]

SqlMapSessionFactory.zip


비고: MySQL의 지원에 대해서 코드를 적어놓았다.




7. Servlet - 생성하기(Controller)


서블릿을 생성해줄 것이다.


그림 21. Java Resources의 오른쪽 버튼 메뉴 모습


Java Resources를 마우스 오른쪽 버튼으로 클릭한다.

New->Servlet을 클릭한다.



그림 22. Servlet 만들기


Package명은 "com.exmple.web.controller"로 한다.

Class Name은 "FrontController"로 한다.


Finish를 누른다.


(Exmple 오타 -> Example이 맞지만, 이미 생성했으니 그냥 따르는 걸로 하겠음.)



8. web.xml - 수정 작업


Servlet 경로를 살짝 수정해주겠다.



그림 23. web.xml - 수정하기



<!DOCTYPE web-app PUBLIC

 "-//Sun Microsystems, Inc.//DTD Web Application 2.3//EN"

 "http://java.sun.com/dtd/web-app_2_3.dtd" >


<web-app>

  <display-name>Archetype Created Web Application</display-name>

  <servlet>

  <servlet-name>FrontController</servlet-name>

  <display-name>FrontController</display-name>

  <description></description>

  <servlet-class>com.exmple.web.controller.FrontController</servlet-class>

  </servlet>

  <servlet-mapping>

  <servlet-name>FrontController</servlet-name>

  <url-pattern>/sample</url-pattern>

  </servlet-mapping>

</web-app>



파일명: web.xml


[첨부(Attachments)]

web.zip





9. CompUsers.java - Model(모델)


앞서 설계한 DB Model을 코드로 구현할 것이다.



그림 24. CompUsers.java


package com.exmple.web.model;


public class CompUsers {


private String username;

private String password;

private int enabled;

public String getUsername() {

return username;

}

public void setUsername(String username) {

this.username = username;

}

public String getPassword() {

return password;

}

public void setPassword(String password) {

this.password = password;

}

public int getEnabled() {

return enabled;

}

public void setEnabled(int enabled) {

this.enabled = enabled;

}

}



파일명: CompUsers.java


[첨부(Attachments)]

CompUsers.zip




10. CompUsersMapper.java - Mapper(DAO)


이전 글과는 달리 공식 메뉴얼에서 제시하는 방법을 적용하도록 하겠다.



그림 25. Mapper 클래스 가이드



그림 26. Mapper 클래스 가이드 - 현실의 문제에 맞게 적용하기


package com.exmple.web.mapper;


import org.apache.ibatis.annotations.Mapper;

import org.apache.ibatis.annotations.Select;


import com.exmple.web.model.CompUsers;


@Mapper

public interface CompUsersMapper {

  @Select("SELECT * FROM comp_users WHERE username = #{username}")

  public CompUsers findByUsername(String username);

  

}



파일명: CompUsersMapper.java


[첨부(Attachments)]

CompUsersMapper.zip


대응하는 방식에는 XML Mapper를 적용하는 방법도 있다.
(참고로 자바방식으로 구현했다면, XML Mapper는 사용할 수 없다.)

(DB를 설계하면서 View도 만들 수도 있고 각종 조합을 할 수도 있는데, 엔터티를 잘 파악해서 구현하면 될 것 같다.)


[Spring Boot 버전의 MyBatis 핵심 사용법]


@Mapper

public interface StudentMyBatisRepository {


@Select("select * from student")

public List<Student> findAll();


@Select("SELECT * FROM student WHERE id = #{id}")

public Student findById(long id);


@Delete("DELETE FROM student WHERE id = #{id}")

public int deleteById(long id);


@Insert("INSERT INTO student(id, name, passport) VALUES (#{id}, #{name}, #{passport})")

public int insert(Student student);


@Update("Update student set name=#{name}, passport=#{passport} where id=#{id}")

public int update(Student student);


}




11. FrontController.java - Controller


FrontController의 내용을 수정하도록 하겠다.



그림 27. FrontController.java - 수정하기


package com.exmple.web.controller;


import java.io.IOException;

import javax.servlet.ServletException;

import javax.servlet.http.HttpServlet;

import javax.servlet.http.HttpServletRequest;

import javax.servlet.http.HttpServletResponse;


import org.apache.ibatis.session.SqlSession;

import org.apache.ibatis.session.SqlSessionFactory;


import com.exmple.web.db.SqlMapSessionFactory;

import com.exmple.web.mapper.CompUsersMapper;

import com.exmple.web.model.CompUsers;


public class FrontController extends HttpServlet {

private static final long serialVersionUID = 1L;

    SqlSessionFactory factory = SqlMapSessionFactory.getSqlSessionFactory();


/**

* @see HttpServlet#doGet(HttpServletRequest request, HttpServletResponse response)

*/

protected void doGet(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {

doAction(request, response);

}


/**

* @see HttpServlet#doPost(HttpServletRequest request, HttpServletResponse response)

*/

protected void doPost(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {

doAction(request, response);

}


protected void doAction(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {

try (SqlSession session = factory.openSession()) {

  CompUsersMapper mapper = session.getMapper(CompUsersMapper.class);

  CompUsers user = mapper.findByUsername("user");

  

  System.out.println("계정명:" + user.getUsername());

  

}

}


}



파일명: FrontController.java


[첨부(Attachments)]

FrontController.zip




12. 맺음글(Conclusion)


MyBatis 3.5.5와 Maven, JSP/Servlet 그리고 Oracle Databases를 쉽고 빠르게 연동하는 방법에 대해서 소개하였다.


1. [JSP] 20. MyBatis-3.5.5, HikariCP 3.4.2 연동 - Maven(Servlet) Spring 제거버전 (Oracle 19g) - Java 방식, 2020-10-01

https://yyman.tistory.com/1435





* 참고 자료(References)


1. MyBatis - 마이바티스 3 | 시작하기, https://mybatis.org/mybatis-3/ko/getting-started.html, Accessed by 2020-10-01, Last Modified 2020-06-05.

- 비고: 사용 방법이 담겨있음.


2. [JSP] 8. 영속프레임워크 MyBatis를 활용한 CRUD 구현 - JSP와 Oracle (XML 방식), https://yyman.tistory.com/1390, Accessed by 2020-10-01, Last Modified 2020-09-19.

- 비고: MyBatis 셋팅 방법을 참고하였으며, XML 방식을 시도하였으나 안 되었다. (Spring Beans가 안 되는 것을 알게 되었음.)


3. [Spring-Framework] 22(번외). STS 4.4 - Spring Boot Starter - MyBatis(Boot용), HikariCP 3.4 사용하기, https://yyman.tistory.com/1432, Accessed by 2020-10-01, Last Modified 2020-10-01.

- 비고: HikariCP의 properties 방식에 대해서 다시 살펴보았다.

반응형
728x90
300x250

[JSP] 18. JSP/Servlet MVC2 - 페이징네이션과 검색 그리고 오라클 프로젝트 (2)


2부에서는 페이징, DB구현부, 모델, 서비스 영역에 대해서 소개하겠다.


1. [JSP] 18. JSP/Servlet MVC2 - 페이징네이션과 검색 그리고 오라클 프로젝트 (1)

https://yyman.tistory.com/1428




14. 페이징네이션 


실제경로: /target/generated-sources/annotations/com/smile/web/model

패키지명: com.smile.web.logic

파일명: Paging.java


아마 이거 하나면, 현존에 있는 페이지 기법은 충분히 소화되지 않겠냐는 생각이다.

물론 정답은 아니다. 오류가 있을 수도 있다.


이러한 복잡한 문제에 직면했을 때, 수학을 공부 많이 해야 한다고 생각한다.

머리가 좋으신 분들이 많으시니깐 더 좋은 방법이 있다면, 개선해봐도 좋을 듯 싶다.


package com.smile.web.logic;


public class Paging {

    private long pageSize; // 게시 글 수

    private long firstPageNo; // 첫 번째 페이지 번호

    private long prevPageNo; // 이전 페이지 번호

    private long startPageNo; // 시작 페이지 (페이징 네비 기준)

    private long pageNo; // 페이지 번호

    private long endPageNo; // 끝 페이지 (페이징 네비 기준)

    private long nextPageNo; // 다음 페이지 번호

    private long finalPageNo; // 마지막 페이지 번호

    private long totalCount; // 게시 글 전체 수

    private long dbStartNum; // db 조회 (시작번호)

    private long dbEndNum; // db 조회 (종료번호)


    /**

     * @return the pageSize

     */

    public long getPageSize() {

        return pageSize;

    }


    /**

     * @param pageSize the pageSize to set

     */

    public void setPageSize(long pageSize) {

        this.pageSize = pageSize;

    }


    /**

     * @return the firstPageNo

     */

    public long getFirstPageNo() {

        return firstPageNo;

    }


    /**

     * @param firstPageNo the firstPageNo to set

     */

    public void setFirstPageNo(long firstPageNo) {

        this.firstPageNo = firstPageNo;

    }


    /**

     * @return the prevPageNo

     */

    public long getPrevPageNo() {

        return prevPageNo;

    }


    /**

     * @param prevPageNo the prevPageNo to set

     */

    public void setPrevPageNo(long prevPageNo) {

        this.prevPageNo = prevPageNo;

    }


    /**

     * @return the startPageNo

     */

    public long getStartPageNo() {

        return startPageNo;

    }


    /**

     * @param startPageNo the startPageNo to set

     */

    public void setStartPageNo(long startPageNo) {

        this.startPageNo = startPageNo;

    }


    /**

     * @return the pageNo

     */

    public long getPageNo() {

        return pageNo;

    }


    /**

     * @param pageNo the pageNo to set

     */

    public void setPageNo(long pageNo) {

        this.pageNo = pageNo;

    }


    /**

     * @return the endPageNo

     */

    public long getEndPageNo() {

        return endPageNo;

    }


    /**

     * @param endPageNo the endPageNo to set

     */

    public void setEndPageNo(long endPageNo) {

        this.endPageNo = endPageNo;

    }


    /**

     * @return the nextPageNo

     */

    public long getNextPageNo() {

        return nextPageNo;

    }


    /**

     * @param nextPageNo the nextPageNo to set

     */

    public void setNextPageNo(long nextPageNo) {

        this.nextPageNo = nextPageNo;

    }


    /**

     * @return the finalPageNo

     */

    public long getFinalPageNo() {

        return finalPageNo;

    }


    /**

     * @param finalPageNo the finalPageNo to set

     */

    public void setFinalPageNo(long finalPageNo) {

        this.finalPageNo = finalPageNo;

    }


    /**

     * @return the totalCount

     */

    public long getTotalCount() {

        return totalCount;

    }


    /**

     * @param totalCount the totalCount to set

     */

    public void setTotalCount(long totalCount) {

        this.totalCount = totalCount;

        this.makePaging();

        this.makeDbPaging();

    }


    /**

     * 페이징 생성 (setTotalCount에서 호출함)

     */

    private void makePaging() {

   

        if (this.totalCount == 0) 

        return; 

        

        // 게시 글 전체 수가 없는 경우

        

        if (this.pageNo == 0) 

        this.setPageNo(1); // 기본 값 설정

        

        if (this.pageSize == 0) 

        this.setPageSize(10); // 기본 값 설정


        long finalPage = (totalCount + (pageSize - 1)) / pageSize; // 마지막 페이지

        

        if (this.pageNo > finalPage) 

        this.setPageNo(finalPage); // 기본 값 설정


        if (this.pageNo < 0 || this.pageNo > finalPage) 

        this.pageNo = 1; // 현재 페이지 유효성 체크


        boolean isNowFirst = pageNo == 1 ? true : false; // 시작 페이지 (전체)

        boolean isNowFinal = pageNo == finalPage ? true : false; // 마지막 페이지 (전체)


        long startPage = ((pageNo - 1) / 10) * 10 + 1; // 시작 페이지 (페이징 네비 기준)

        long endPage = startPage + 10 - 1; // 끝 페이지 (페이징 네비 기준)


        if (endPage > finalPage) { // [마지막 페이지 (페이징 네비 기준) > 마지막 페이지] 보다 큰 경우

            endPage = finalPage;

        }


        this.setFirstPageNo(1); // 첫 번째 페이지 번호


        if (isNowFirst) {

            this.setPrevPageNo(1);    // 이전 페이지 번호

        } else {

            this.setPrevPageNo(((pageNo - 1) < 1 ? 1 : (pageNo - 1))); // 이전 페이지 번호

        }


        this.setStartPageNo(startPage); // 시작 페이지 (페이징 네비 기준)

        this.setEndPageNo(endPage); // 끝 페이지 (페이징 네비 기준)


        if (isNowFinal) {

            this.setNextPageNo(finalPage); // 다음 페이지 번호

        } else {

            this.setNextPageNo(((pageNo + 1) > finalPage ? finalPage : (pageNo + 1))); // 다음 페이지 번호

        }


        this.setFinalPageNo(finalPage); // 마지막 페이지 번호

        

    }

    

    private void makeDbPaging() {

   

    long current = this.getPageNo();

    long weight = this.getPageSize();

    long endNum = 0;

   

    this.setDbEndNum( current * weight );

   

    endNum = this.getDbEndNum();

    this.setDbStartNum( (endNum - weight) + 1 );

   

    }


    /**

     * @return the dbStartNum

     */

public long getDbStartNum() {

return dbStartNum;

}



    /**

     * @param dbStartNum the dbStartNum to set

     */

public void setDbStartNum(long dbStartNum) {

this.dbStartNum = dbStartNum;

}


    /**

     * @return dbEndNum

     */


public long getDbEndNum() {

return dbEndNum;

}



    /**

     * @param dbEndNum the dbEndNum to set

     */

public void setDbEndNum(long dbEndNum) {

this.dbEndNum = dbEndNum;

}

    

}


파일명: Paging.java


[첨부(Attachments)]

Paging.zip




15. Board.java - 모델(Model)


실제경로: /target/generated-sources/annotations/com/smile/web/model

패키지명: com.smile.web.model

파일명: Board.java


게시판 모델에 대한 설계이다.


package com.smile.web.model;


import java.sql.Timestamp;


public class Board {


private long id;

private String name;

private String subject;

private String memo;

private long count;

private Timestamp regidate;

public long getId() {

return id;

}

public void setId(long id) {

this.id = id;

}

public String getName() {

return name;

}

public void setName(String name) {

this.name = name;

}

public String getSubject() {

return subject;

}

public void setSubject(String subject) {

this.subject = subject;

}

public String getMemo() {

return memo;

}

public void setMemo(String memo) {

this.memo = memo;

}


public long getCount() {

return count;

}


public void setCount(long count) {

this.count = count;

}


public Timestamp getRegidate() {

return regidate;

}


public void setRegidate(Timestamp regidate) {

this.regidate = regidate;

}

}



파일명: Board.java


[첨부(Attachments)]

Board.zip




16. BoardService.java - Service 영역


실제경로: /target/generated-sources/annotations/com/smile/web/service

패키지명: com.smile.web.service

파일명: BoardService.java


게시판 서비스에 필요한 영역을 구현한 것이다.


package com.smile.web.service;


import java.util.List;


import com.smile.web.model.Board;


public class BoardService {

private static BoardService service = null;

private static BoardServiceImpl dao = null;

private BoardService() {}

public static BoardService getInstance() {


        if(service == null){

        service = new BoardService();

    dao = BoardServiceImpl.getInstatnce();

        }


        return service;

}

public List<Board> getBoardList(long startNum, long endNum){

return dao.getBoardList(startNum, endNum);

}

public long getTotalCount() {

return dao.getTotalCount();

}

public List<Board> getBoardKeywordList(String keyword, long startNum, long endNum){

return dao.getBoardKeywordList(keyword, startNum, endNum);

}


public long getTotalKeywordCount(String keyword) {

return dao.getTotalKeywordCount(keyword);

}

}



파일명: BoardService.java


[첨부(Attachments)]

BoardService.zip





16. BoardServiceImpl.java - Service 영역(DAO)


실제경로: /target/generated-sources/annotations/com/smile/web/service

패키지명: com.smile.web.service

파일명: BoardServiceImpl.java


DAO 영역에 관한 것이다. (실제 DB를 처리하는 영역)


package com.smile.web.service;


import java.sql.Connection;

import java.sql.PreparedStatement;

import java.sql.ResultSet;

import java.sql.SQLException;

import java.util.ArrayList;

import java.util.List;


import com.smile.web.db.DBFactory;

import com.smile.web.model.Board;


public class BoardServiceImpl {

private static BoardServiceImpl boardDAO;

private static DBFactory dbPool;

private BoardServiceImpl() {}

public static BoardServiceImpl getInstatnce() {

if ( boardDAO == null ) {

boardDAO = new BoardServiceImpl();

dbPool = new DBFactory();

}

return boardDAO;

}

public List<Board> getBoardList(long startNum, long endNum){

Connection conn = null;

    PreparedStatement pstmt = null;

    ResultSet rs = null;

    Board node = null;

   

    List<Board> boardList = new ArrayList<Board>();


    String sql = "select * from (select /*+INDEX_DESC(tbl_board pk_board) */ rownum rn, A.*" + 

    " from board A order by id desc) " + 

    "where rn >= ? and rn <= ?";


    //System.out.println(sql);

    //System.out.println(startNum + "/" + endNum);

   

    try {

    conn = dbPool.getConnection();

    pstmt = conn.prepareStatement(sql);

    pstmt.setLong(1, startNum);

    pstmt.setLong(2, endNum);

   

    rs = pstmt.executeQuery();


    while ( rs.next() ) {

    // 데이터가 존재할 때, 노드 생성

    node = new Board();

   

    node.setId(rs.getLong(2));

    node.setSubject(rs.getNString(3));

    node.setMemo(rs.getNString(4));

    node.setName(rs.getNString(5));

   

    boardList.add(node);

   

    System.out.println("rs:" + rs.getLong(1));

   

    }


    }catch(Exception ex) {

    System.out.println("오류 발생: " + ex);

    }

    finally {

    dbPool.close(conn, pstmt, rs);

    }

return boardList;

}

public List<Board> getBoardKeywordList(String keyword, long startNum, long endNum){

Connection conn = null;

    PreparedStatement pstmt = null;

    ResultSet rs = null;

    Board node = null;

   

    String allKeyword = "%" + keyword + "%";

   

    List<Board> boardList = new ArrayList<Board>();


    String sql = "select * " + 

    "from (select /*+INDEX_DESC(tbl_board pk_board) */ rownum rn, A.* " + 

    "from board A where subject like ? order by id desc) " + 

    "where rn >= ? and rn <= ?";

   

    //System.out.println(sql);

   

    try {

    conn = dbPool.getConnection();

    pstmt = conn.prepareStatement(sql);

    pstmt.setNString(1, allKeyword);

    pstmt.setLong(2, startNum);

    pstmt.setLong(3, endNum);

   

    rs = pstmt.executeQuery();


    while ( rs.next() ) {

    // 데이터가 존재할 때, 노드 생성

    node = new Board();

   

    node.setId(rs.getLong(2));

    node.setSubject(rs.getNString(3));

    node.setMemo(rs.getNString(4));

    node.setName(rs.getNString(5));

   

    boardList.add(node);

   

    //System.out.println("rs:" + rs.getLong(1));

   

    }


    }catch(Exception ex) {

    System.out.println("오류 발생: " + ex);

    }

    finally {

    dbPool.close(conn, pstmt, rs);

    }

return boardList;

}

public long getTotalCount() {

Connection conn = null;

    PreparedStatement pstmt = null;

    ResultSet rs = null;

    Board node = null;

   

    long cnt = 0;


    String sql = "select count(*) from board";

   

    System.out.println("sql:" + sql);


    try {

    conn = dbPool.getConnection();

    //System.out.println( conn.getSchema() );

    pstmt = conn.prepareStatement(sql);

   

    rs = pstmt.executeQuery();

    //System.out.println( rs.getFetchSize() );


    if ( rs.next() ) {

    cnt = rs.getLong(1);

        System.out.println("전체갯수:" + cnt + "/" + rs.getLong(1));

    }


    }catch(Exception ex) {

    System.out.println("오류 발생: " + ex);

    }

    finally {

    dbPool.close(conn, pstmt, rs);

    }

return cnt;

}

public long getTotalKeywordCount(String keyword) {

Connection conn = null;

    PreparedStatement pstmt = null;

    ResultSet rs = null;

    Board node = null;

   

    String allKeyword = "%" + keyword + "%";

   

    long cnt = 0;


    String sql = "select count(*) from board where subject like ?";

   

    //System.out.println("sql:" + sql + "/키:" + keyword);


    try {

    conn = dbPool.getConnection();

    /// System.out.println( conn.getSchema() );

    pstmt = conn.prepareStatement(sql);

    pstmt.setNString(1, allKeyword);

   

    rs = pstmt.executeQuery();

    /// System.out.println( rs.getFetchSize() );


    if ( rs.next() ) {

    cnt = rs.getLong(1);

        System.out.println("특정 갯수:" + cnt + "/" + rs.getLong(1));

    }


    }catch(Exception ex) {

    System.out.println("오류 발생: " + ex);

    }

    finally {

    dbPool.close(conn, pstmt, rs);

    }

return cnt;

}


}


파일명: BoardServiceImpl.java


[첨부(Attachments)]

BoardServiceImpl.zip



17. 데이터베이스 - 응용 영역


이 부분이 의외로 어려울 수 있다고 본다. 머리를 조금 써야 한다.


[개선 후 SQL 파일 내용]


-- Oracle 11 - 자동번호 생성 테이블 정의
-- Table 생성 (BOARD)
-- NEW.ID (Table의 id를 가리킴)
CREATE TABLE board
(
    id NUMBER PRIMARY KEY,
    name VARCHAR2(30),
    subject VARCHAR2(30),
    memo NCLOB,
    count NUMBER,
    regidate DATE
);

-- Sequence 정의
CREATE SEQUENCE board_sequence
START WITH 1
INCREMENT BY 1;

-- Trigger 생성
-- BEFORE INSERT on '테이블명'
CREATE OR REPLACE TRIGGER board_trigger
BEFORE INSERT
    ON board
REFERENCING NEW AS NEW
FOR EACH ROW
BEGIN
SELECT board_sequence.nextval INTO :NEW.ID FROM dual;
END;


/* 데이터 추가 */
INSERT INTO board (name, subject, memo, count, regidate) VALUES ('홍길동', '안녕하세요.', '메모메모', '0', '2020-09-29 11:11:00');

/* 데이터 등록 후 커밋할 것(대량 정보 처리 후) */
COMMIT;


-- 싱글 쿼리 (페이징)
SELECT * FROM (
SELECT /*+ INDEX_DESC(Z OP_SAMPLE_PK) */ ROWNUM AS RNUM, Z.* FROM (
SELECT * from board order by id desc
) Z WHERE ROWNUM <= 10
) WHERE RNUM >= 1

-- 특정 싱글 쿼리 SQL
SELECT * FROM (
SELECT /*+ INDEX_DESC(Z OP_SAMPLE_PK) */ ROWNUM AS RNUM, Z.* FROM (
SELECT * from board where subject like '%야해해%' order by id desc
) Z WHERE ROWNUM <= 10
) WHERE RNUM >= 1


파일명: board-tbl-oracle_개선후.sql (변경해도 되는 쿼리)  /  board-tbl-oracle_개선전.sql (이 글의 16번 소스코드에 적용된 페이징 쿼리)


(16번 파일의 페이징 쿼리를 수정해서 사용해도 됨.)

-> 개선 전 쿼리라고 해서 파일을 두 개로 두었음.


[첨부(Attachments)]

board-tbl-oracle.zip (업데이트: 2020-10-11)


페이징 로직 설계가 되었다면, 다음 계획해야 할 작업이 실질적으로는 이 작업이다.

의외로 작업을 하다보면, 골치가 아픈 문제들이 많이 생긴다.


[Oracle Databases 코너에 쿼리 관련 작업에 대해서 추가로 보충하여 소개하도록 하겠다.]

- 단일 테이블 게시물 쿼리 방법, 뷰 테이블 쿼리 방법, 테이블 조인(2개 이상) 사용 방법


-> 태스트 결과로는 "Java 페이징 로직"은 크게 문제가 없다. 
    다만, 나머지 부분은 쿼리라고 본다.



[쿼리 관련 - 보충 글]
1. [Oracle Databases] 번외글 - 게시판 페이징 관련 로직 쿼리, 2020-10-11.
- https://yyman.tistory.com/1466




18. 뷰 - css


css 하나 만들어준다. 보안 영역에 만들지 말고 외부 영역에 만들어준다.



그림 23. style.css 프로젝트 위치


생성할 폴더: 

/src/main/webapp/board

/src/main/webapp/board/css


생성할 파일

/src/main/web/app/board/css/style.css


사실 style.css을 먼저 만들고 나서 작업하는 건 절대적으로 아니다.

웹 페이지를 보고 하나 하나 왕복 화면 전환 등 복합적으로 하면서 수많은 태스트과정을 거쳐서 작성하게 된다.

css 작업을 하는데 있어서도 어떻게 하면 깔끔하면서 표준을 지킬지 고민을 많이 해봐야 한다고 본다.


@charset "utf-8";


/* 1. 제목 */

h3{

font-size:20px;

font-family:'Nanum Gothic';

text-align:center;

}


/* 2. 게시판 */

/* 게시판 목록 출력 */

.board_list{

border-top:1px solid #e2e2e2;

border-bottom:1px solid #e2e2e2;

font-size:12px;

font-family:'Nanum Gothic';

width:900px;

margin:auto;

text-align:center;

}


/* 제목 */

.board_list th{


border-right:1px solid #666;

border-bottom:1px solid #666;

font-size:15px;

font-family:'Nanum Gothic';

height:20px;

text-align:center;

background-color:#eeeeee;

}


/* 내용 */

.board_list td{


border-right:1px solid #e2e2e2;

font-size:15px;

font-family:'Nanum Gothic';

height:20px;

text-align:center;

}


/* 3. 페이징 네이션 */

.paginate{


font-size:15px;

font-family:'Nanum Gothic';

font-color:#666;

text-align: center;

}


.paginate .first{


font-size:15px;

font-family:'Nanum Gothic';

font-color:#666;

text-align: center;

margin-right:10px;

}


.paginate .prev{


font-size:15px;

font-family:'Nanum Gothic';

font-color:#666;

text-align: center;

margin-right:10px;

}


.paginate .next{


font-size:15px;

font-family:'Nanum Gothic';

font-color:#666;

text-align: center;

margin-right:10px;

}


.paginate .last{


font-size:15px;

font-family:'Nanum Gothic';

font-color:#666;

text-align: center;

margin-right:10px;

}


.paginate .choice{


font-size:20px;

font-family:'Nanum Gothic';

font-color:#666;

text-align: center;

margin-right:10px;

}


/* 4. 링크 */

a{

text-decoration:none;

color:#666;

}


/* 5. 검색 영역 */

.searchArea{


font-size:15px;

font-family:'Nanum Gothic';

font-color:#666;

text-align: center;

}


파일명: style.css


[첨부(Attachments)]

style.zip




19. 뷰(view) - jsp파일 작성하기


이번에 소개할 것은 include를 실제 적용한 코드이다.



그림 24. board 폴더 내 파일들 (작업할 부분)


생성할 폴더: 

/src/main/webapp/WEB-INF/views/

/src/main/webapp/WEB-INF/views/board


생성할 파일

/src/main/webapp/WEB-INF/views/board/list.jsp

/src/main/webapp/WEB-INF/views/board/search.jsp

/src/main/webapp/WEB-INF/views/board/paging.jsp


list.jsp를 호출하면, search.jsp, paging.jsp도 함께 호출이 된다.

기능을 전문적으로 분리하여 설계한 것이다.




20. 뷰(view) - list.jsp


list.jsp에 관한 것이다.

코드를 보고 한눈에 알 수 없는 사람들을 위해서 실제 동작 결과화면을 소개하겠다.



그림 25. list.jsp 화면 영역 구성도



<%@ page language="java" contentType="text/html; charset=UTF-8" pageEncoding="UTF-8"%>

<%@ taglib uri="http://java.sun.com/jsp/jstl/core" prefix="c" %>

<%@ page import = "java.util.*" %>

<%@ page import = "com.smile.web.model.*" %>

<%@ page session="false" %>

<!DOCTYPE html>

<html>

<head>

<meta charset="UTF-8">

<title>게시물 목록</title>

<style>

@import url('https://fonts.googleapis.com/css?family=Nanum+Gothic:400,700,800');


</style>

<link href="css/style.css" rel="stylesheet" type="text/css" />

</head>

<body>

<%

List<Board> boardList = (List<Board>)request.getAttribute("list");

%>


<h3>게시물 목록</h3>


<!-- 목록 출력 -->

<table class="board_list">

<tr>

<th style="width:15%;">

번호(Num)

</th>

<th>

제목(Subject)

</th>

<th style="width:13%;">

글쓴이(Author)

</th>

<th style="width:13%; border-right:none;">

조회수(Count)

</th>

</tr>

<%

for(Board board:boardList){

%>

<tr>

<td style="width:15%;">

<%= board.getId() %>

</td>

<td>

<%= board.getSubject() %>

</td>

<td style="width:13%;">

<%= board.getName() %>

</td>

<td style="width:13%; border-right:none;">

<%= board.getCount() %>

</td>

</tr>

<%

}

%>

</table>


<!-- 페이징 -->

<jsp:include page="/WEB-INF/views/board/paging.jsp">

<jsp:param name="customURL" value="${pagingUrl}" />

    <jsp:param name="firstPageNo" value="${paging.firstPageNo}" />

    <jsp:param name="prevPageNo" value="${paging.prevPageNo}" />

    <jsp:param name="startPageNo" value="${paging.startPageNo}" />

    <jsp:param name="pageNo" value="${paging.pageNo}" />

    <jsp:param name="endPageNo" value="${paging.endPageNo}" />

    <jsp:param name="nextPageNo" value="${paging.nextPageNo}" />

    <jsp:param name="finalPageNo" value="${paging.finalPageNo}" />

</jsp:include>


<!-- 검색 -->

<jsp:include page="/WEB-INF/views/board/search.jsp" />


</body>

</html>


파일명: list.jsp


[첨부(Attachments)]

list.zip



21. 뷰(view) - paging.jsp


<%@ page language="java" contentType="text/html; charset=UTF-8" pageEncoding="UTF-8"%>

<%@ taglib prefix="c" uri="http://java.sun.com/jsp/jstl/core" %>



<div class="paginate">

    <a href="${param.customURL}page=${param.firstPageNo}" class="first">처음 페이지</a>

    <a href="${param.customURL}page=${param.prevPageNo}" class="prev">이전 페이지</a>

    <span>

        <c:forEach var="i" begin="${param.startPageNo}" end="${param.endPageNo}" step="1">

            <c:choose>

                <c:when test="${i eq param.pageNo}"><a href="${param.customURL}page=${i}" class="choice">${i}</a></c:when>

                <c:otherwise><a href="${param.customURL}page=${i}">${i}</a></c:otherwise>

            </c:choose>

        </c:forEach>

    </span>

    <a href="${param.customURL}page=${param.nextPageNo}" class="next">다음 페이지</a>

    <a href="${param.customURL}page=${param.finalPageNo}" class="last">마지막 페이지</a>

</div>



파일명: paging.jsp


[첨부(Attachments)]

paging.zip




22. 뷰(view) - search.jsp


<%@ page language="java" contentType="text/html; charset=UTF-8" pageEncoding="UTF-8"%>

<%@ taglib prefix="c" uri="http://java.sun.com/jsp/jstl/core" %>


<div class="searchArea">

<form id="searchForm" action="list.do" method='get'>

<select name="type">

<option value="T">제목</option>

<option value="C">내용</option>

<option value="W">작성자</option>

</select>

<input type="text" name="keyword">

<button class="">검색</button>

</form>

</div>


파일명: search.jsp


[첨부(Attachments)]

search.zip




23. 데이터베이스 작업시 성능 - 꼭 파악하면서 작업해보기


오늘 날 게시판 뿐만 아니라 DB의 비중이 매우 중요한 시대에 직면해 있다.

성능 측정 꼭 해보기 바란다.

동작도 물론 중요한데, 결과는 정확한지 등 많이 고민하고 수 십번, 수 백법 이상 찍어봐야 한다.



그림 26. 데이터베이스 작업 모습 - 오라클 SQL Developer




* 맺음글(Conclusion)


페이징네이션 기반의 게시판 프로젝트에 대해서 살펴보았다.




* 참고자료(References)


1. [Oracle] 오라클 데이터타입(DataType) 총정리, https://coding-factory.tistory.com/416, Accessed by 2020-09-29, Last Modified 2019-11-03.


2. [JSP]include 와 forward 의 페이지 이동, https://jerryjerryjerry.tistory.com/31, Accessed by 2020-09-29, Last Modified 2018-04-13.


3. 오라클 페이징 쿼리, 오라클 paging 방법 - 개발자 삽질 일기, https://programmer93.tistory.com/4, Accessed by 2020-09-29, Last Modified 2019.


4. [OracleDB] 페이징(Paging) 처리하는 법, https://m.blog.naver.com/wideeyed/221796538283, Accessed by 2020-09-29, Last Modified 2020-02-04.

반응형
728x90
300x250

[JSP] 17. JSP/Servlet MVC2 - 페이징네이션과 검색 그리고 오라클 프로젝트 (1)


Command 패턴과 Front Controller 패턴을 적용한 페이징네이션과 검색에 대해서 소개하려고 한다.

불필요한 기능은 다 제거하고 순수한 페이징네이션과 검색 그리고 오라클 연동에 대해서 다뤄보았다.


IDE: Spring-Tool Suite 4-4.7.2 RELEASES

Databases: Oracle Databases 19g

 - Maven 3.6.3/1.16.0.20200610-1735

 - javax.servlet-api (4.0.1) - servlet(서블렛)

 - jstl (1.2) - jstl 태그 적용

 - taglibs (1.1.2) - c태그 적용

 - HikariCP (3.4.2) - 커넥션 풀

 - com.oracle.database.jdbc - ojdbc8 (19.7.0.0)



[쿼리 관련해서 조금 더 보충한 글이다.]
1. [Oracle Databases] 번외글 - 게시판 페이징 관련 로직 쿼리, 2020-10-11.
- https://yyman.tistory.com/1466




1. 프로젝트 구조


작성할 프로젝트를 미리 살펴보면 조금 작업해야 할 양이 의외로 상당하다는 것을 알 수 있다.



그림 1, 그림 2. 프로젝트 작성 구조도




2. 게시판 - 설계


데이터베이스 설계는 매우 간단하게 작성하였다.


그림 3. Board - 테이블 설계(1)




그림 4. Board 게시판 설계(2)





3. 사용자 인터페이스 - 결과


이번 주제에서 다뤄볼 프로젝트의 완성 모습이다.

참고로 페이징네이션 로직에 대해서 다 기억할 수 없다.


다만 어떤 흐름인지는 대략적으로 알고는 있으면 하는 바람이다.

워낙 페이징 로직에 대해서 전문적으로 잘 연구하시는 분들이 많이 계시니깐 그건 골고루 참고도 해보고 개선도 해보고 하면 될 것 같다.



그림 5. list.do - 첫 화면



그림 6. list.do - 7번 페이지 / 기초



그림 7. list.do - 10106번 페이지 / 기초


10106번 페이지를 통해서 알 수 있는 것은 약 10106 * 10 = 101,106개의 DB가 존재한다는 것을 알 수 있다.

그림 8부터는 응용부분에 가까워진다.



그림 8. list.do - 키워드 검색 및 페이징 처리(1) / 심화


의외로 응용력이 많이 요구되는 부분이다.



그림 9. list.do - 키워드 검색 및 페이징 처리(2) / 심화


키워드 검색을 했을 때 "친"은 약 13개가 있었다는 사실을 유추할 수 있다.



그림 10. list.do - 키워드 검색 및 페이징 처리(3)


참고로 list.do 반응에 대해서 여러 조건으로 튜닝이 필요하다.



4. 프로젝트 생성하기


프로젝트는 Maven Project로 생성하여 진행하였다.



그림 11. Maven Project 생성하기(1)


File -> New -> Maven Project를 클릭한다.



그림 12. Maven Project 생성하기(2)


org.apache.maven.archetypes | maven-archetype-web을 선택하고 Next를 누른다.



그림 13. Maven Project 생성하기(3)


프로젝트의 Group Id, Artifact Id를 입력한다.

Finish를 누른다.




5. POM.xml - 설정하기


다음은 pom.xml 파일에 대한 설정이다.


<?xml version="1.0" encoding="UTF-8"?>


<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"

  xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">

  <modelVersion>4.0.0</modelVersion>


  <groupId>com.boardMaven</groupId>

  <artifactId>web</artifactId>

  <version>0.0.1-SNAPSHOT</version>

  <packaging>war</packaging>


  <name>board Maven Webapp</name>

  <!-- FIXME change it to the project's website -->

  <url>http://www.example.com</url>


  <properties>

    <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>

    <maven.compiler.source>1.7</maven.compiler.source>

    <maven.compiler.target>1.7</maven.compiler.target>

  </properties>


  <dependencies>

    <dependency>

      <groupId>junit</groupId>

      <artifactId>junit</artifactId>

      <version>4.11</version>

      <scope>test</scope>

    </dependency>

    

<!-- Servlet -->

<!-- https://mvnrepository.com/artifact/javax.servlet/javax.servlet-api -->

<dependency>

    <groupId>javax.servlet</groupId>

    <artifactId>javax.servlet-api</artifactId>

    <version>4.0.1</version>

    <scope>provided</scope>

</dependency>

<!-- JSTL -->

<dependency>

<groupId>javax.servlet</groupId>

<artifactId>jstl</artifactId>

<version>1.2</version>

</dependency>

<!-- taglibs -->

<dependency>

<groupId>taglibs</groupId>

<artifactId>standard</artifactId>

<version>1.1.2</version>

<scope>compile</scope>

</dependency>


<!-- https://mvnrepository.com/artifact/com.zaxxer/HikariCP -->

<dependency>

    <groupId>com.zaxxer</groupId>

    <artifactId>HikariCP</artifactId>

    <version>3.4.2</version>

</dependency>

<dependency>

<groupId>com.oracle.database.jdbc</groupId>

<artifactId>ojdbc8</artifactId>

<version>19.7.0.0</version>

</dependency>

  </dependencies>


  <build>

    <finalName>board</finalName>

    <pluginManagement><!-- lock down plugins versions to avoid using Maven defaults (may be moved to parent pom) -->

      <plugins>

        <plugin>

          <artifactId>maven-clean-plugin</artifactId>

          <version>3.1.0</version>

        </plugin>

        <!-- see http://maven.apache.org/ref/current/maven-core/default-bindings.html#Plugin_bindings_for_war_packaging -->

        <plugin>

          <artifactId>maven-resources-plugin</artifactId>

          <version>3.0.2</version>

        </plugin>

        <plugin>

          <artifactId>maven-compiler-plugin</artifactId>

          <version>3.8.0</version>

        </plugin>

        <plugin>

          <artifactId>maven-surefire-plugin</artifactId>

          <version>2.22.1</version>

        </plugin>

        <plugin>

          <artifactId>maven-war-plugin</artifactId>

          <version>3.2.2</version>

        </plugin>

        <plugin>

          <artifactId>maven-install-plugin</artifactId>

          <version>2.5.2</version>

        </plugin>

        <plugin>

          <artifactId>maven-deploy-plugin</artifactId>

          <version>2.8.2</version>

        </plugin>

      </plugins>

    </pluginManagement>

  </build>

</project>



파일명: pom.xml


[첨부(Attachments)]

pom.zip



6. 프로젝트의 Build-Path, Properties 수정하기


프로젝트의 자바 버전에 대한 환경설정이다.



그림 14. 프로젝트의 마우스 오른쪽 버튼 모습


프로젝트를 클릭한다.

마우스 오른쪽 버튼을 누른다.

Properties를 클릭한다.



그림 15. Build-Path 환경설정


JRE System Library를 JavaSE-14버전으로 변경한다. (14버전으로 변경)

Apply를 누른다.



그림 16. Project Facets 환경 설정


Project Facets의 Java 버전을 14로 변경한다.

Apply를 누른다.

Apply and close를 누른다.



7. Resource 폴더와 db.properties 만들기


Resource 폴더와 db.properties를 생성할 것이다.



그림 17. /src/main 폴더 - 오른쪽 버튼 메뉴 모습


src/main 폴더를 마우스 오른쪽으로 클릭한다.

New-> Folder를 클릭한다.


폴더명: Resource



그림 17. /src/main/resource 폴더 - 오른쪽 버튼 메뉴 모습


src/main/resource 폴더를 마우스 오른쪽으로 클릭한다.

New-> File를 클릭한다.


파일명: db.properties



그림 18. /src/main/resource 폴더와 db.properties 모습


성공적으로 만들어진 것을 확인할 수 있다.



그림 19. db.propeties 모습


jdbcUrl=jdbc:oracle:thin:@localhost:1521:orcl

dataSourceClassName=oracle.jdbc.driver.OracleDriver

dataSource.user=userName

dataSource.password=password

cachePrepStmts=true

prepStmtCacheSize=250

prepStmtCacheSqlLimit=2048


파일명: db.propeties


[첨부(Attachments)]

db.zip




9. Servlet 생성하기


servlet 생성하고 나서 web.xml 수정을 진행하면 타이핑을 적게 해도 되는 장점이 생긴다.



그림 20. 프로젝트 오른쪽 버튼 - 메뉴 모습


프로젝트를 선택한다.

마우스 오른쪽 버튼을 클릭한다.

New->Servlet을 클릭한다.



그림 21. Servlet 생성하기


Java package명을 입력한다. ("예: com.smile.web.controller // 이런 형식으로 작성하면 됨.)

Class name을 입력한다. ("예: FrontController" )

Finish를 누른다.



10. web.xml - 수정하기


charset 초기값을 추가하였다.

Servlet 2.5 문서 양식에서 3.1 양식으로 변경하였다.


<web-app xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" 

xmlns="http://java.sun.com/xml/ns/javaee" 

xsi:schemaLocation="http://java.sun.com/xml/ns/javaee http://java.sun.com/xml/ns/javaee/web-app_3_0.xsd" 

id="WebApp_ID" version="3.0">

 

  <display-name>Archetype Created Web Application</display-name>

  

  <servlet>

  <servlet-name>FrontController</servlet-name>

  <servlet-class>com.smile.web.controller.FrontController</servlet-class>

  <init-param>

<param-name>charset</param-name>

<param-value>UTF-8</param-value>

</init-param>

  </servlet>

  <servlet-mapping>

  <servlet-name>FrontController</servlet-name>

  <url-pattern>*.do</url-pattern>

  </servlet-mapping>

  

</web-app>


파일명: web.xml


[첨부(Attachments)]

web.zip




10. 폴더 2개(views, settings), 파일 하나 만들기(root-servlet.xml)


추가적으로 만들어줄 폴더와 파일이 있다.


폴더1: /src/main/webapp/WEB-INF/settings

폴더2: /src/main/webapp/WEB-INF/views

파일: /src/main/webapp/WEB-INF/settings/root-servlet.xml


Settings 폴더는 큰 의미는 없지만, 활용할 여지를 고려하여 생성하였다.
이 프로젝트에서는 root-servlet.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"
xsi:schemaLocation="http://www.springframework.org/schema/beans https://www.springframework.org/schema/beans/spring-beans.xsd">
<!-- Root Context: defines shared resources visible to all other web components -->
</beans>


파일명: root-servlet.xml


[첨부(Attachments)]

root-servlet.zip



그림 22. 작업된 프로젝트의 모습




11. FrontController.java - 서블렛(Servlet)


실제경로: /target/generated-sources/annotations/com/smile/web/controller

패키지명: com.smile.web.controller

파일명: FrontController.java


FrontController를 list만 정의하였다.

의외로 코드를 살펴보면, 간단하다는 것을 알 수 있다.


package com.smile.web.controller;


import java.io.IOException;

import java.sql.SQLException;


import javax.servlet.ServletConfig;

import javax.servlet.ServletException;

import javax.servlet.http.HttpServlet;

import javax.servlet.http.HttpServletRequest;

import javax.servlet.http.HttpServletResponse;


import com.smile.web.db.DBFactory;


public class FrontController extends HttpServlet {

private static final long serialVersionUID = 1L;

private String charset = null;

/**

* @see HttpServlet#doGet(HttpServletRequest req, HttpServletResponse res)

*/

protected void doGet(HttpServletRequest req, HttpServletResponse res) throws ServletException, IOException {

doAction(req, res);

}


/**

* @see HttpServlet#doPost(HttpServletRequest req, HttpServletResponse res)

*/

protected void doPost(HttpServletRequest req, HttpServletResponse res) throws ServletException, IOException {

doAction(req, res);

}

// FrontController 패턴 & Command 패턴

protected void doAction(HttpServletRequest req, HttpServletResponse res) throws ServletException, IOException {


ServletConfig sc = this.getServletConfig();

charset = sc.getInitParameter("charset");


req.setAttribute("charset", charset);

req.setCharacterEncoding(charset);

res.setContentType("text/html; charset=" + charset);


String uri = req.getRequestURI();

String conPath = req.getContextPath();

String command = uri.substring(conPath.length());


Controller subController = null;


if(command.equals("/board/list.do")){

System.out.println("-----------------------------");

System.out.println("게시판 목록");

System.out.println("-----------------------------");

req.setAttribute("controllerName", "list");

subController = new com.smile.web.controller.board.ListController();

subController.execute(req, res);

}

}


}



파일명: FrontController.java


[첨부(Attachments)]

FrontController.zip




12. Controller.java - 인터페이스


실제경로: /target/generated-sources/annotations/com/smile/web/controller

패키지명: com.smile.web.controller

파일명: Controller.java


인터페이스에 관한 것이다. (forward 정의 관련한 것)


package com.smile.web.controller;


import java.io.IOException;


import javax.servlet.ServletException;

import javax.servlet.http.HttpServletRequest;

import javax.servlet.http.HttpServletResponse;


public interface Controller {

public void execute(HttpServletRequest req, HttpServletResponse res) throws ServletException, IOException;


}


파일명: Controller.java


[첨부(Attachments)]

Controller.zip



12. /Board/ListController.java - 클래스


실제경로: /target/generated-sources/annotations/com/smile/web/controller/board

패키지명: com.smile.web.controller.board

파일명: ListController.java


게시판 목록에 관한 컨트롤러 명세이다.


package com.smile.web.controller.board;


import java.io.IOException;

import java.net.URLEncoder;

import java.util.List;


import javax.servlet.ServletException;

import javax.servlet.http.HttpServletRequest;

import javax.servlet.http.HttpServletResponse;


import com.smile.web.controller.Controller;

import com.smile.web.logic.Paging;

import com.smile.web.model.Board;

import com.smile.web.service.BoardService;

import com.smile.web.util.HttpUtil;


public class ListController implements Controller {


@Override

public void execute(HttpServletRequest req, HttpServletResponse res) throws ServletException, IOException {

String controllerName = (String) req.getAttribute("controllerName");

BoardService service = BoardService.getInstance();

List<Board> boardList = null;

// long totalCount = boardList.size();

long currentPage = 1; // 기본값

long pageSize = 10;

long totalCount = service.getTotalCount();

long startNum, endNum;

String keyword = null;

String pagingUrl = controllerName + ".do?";

// 페이지 번호 존재할 때

if (req.getParameter("page") != null){

currentPage = Integer.valueOf( req.getParameter("page") );

}

// 키워드가 존재할 때

if (req.getParameter("keyword") != null) {

keyword = req.getParameter("keyword");

totalCount = service.getTotalKeywordCount(keyword);

// 키워드 값이 하나라도 존재할 때

if ( keyword.length() > 0) {

pagingUrl = pagingUrl + "keyword=" + URLEncoder.encode(keyword, "UTF-8") + "&";

}

}

        Paging paging = new Paging();

        /*

        paging.setPageNo(1);

        paging.setPageSize(10);

        paging.setTotalCount(totalCount);

        */

        

        paging.setPageNo(currentPage);

        paging.setPageSize(pageSize);

        paging.setTotalCount(totalCount);


        System.out.println("현재페이지번호:" + currentPage);

        System.out.println("페이지크기:" + pageSize);

        System.out.println("키워드:" + keyword);

        System.out.println("페이징URL:" + pagingUrl);

        

        startNum = paging.getDbStartNum();

        endNum = paging.getDbEndNum();


        

// 키워드가 존재할 때

if (keyword != null) {

// 키워드 값이 하나라도 존재할 때

if ( keyword.length() > 0) {

boardList = service.getBoardKeywordList(keyword, startNum, endNum);

}

else {

boardList = service.getBoardList(startNum, endNum);

}

}

else {

        boardList = service.getBoardList(startNum, endNum);

}

        

        req.setAttribute("paging", paging);

        req.setAttribute("list", boardList);

        req.setAttribute("pagingUrl", pagingUrl);

HttpUtil.forward(req, res, "/WEB-INF/views/board/" + controllerName + ".jsp");

}


}



파일명: ListController.java


[첨부(Attachments)]

ListController.zip



13. HttpUtil.java - 클래스


실제경로: /target/generated-sources/annotations/com/smile/web/util

패키지명: com.smile.web.controller.util

파일명: HttpUtil.java


RequestDispatcher와 forward에 대한 정의이다.

추가로 업로드 기능에 대한 명세도 있다.


package com.smile.web.util;


import java.io.File;

import java.io.FileInputStream;

import java.io.FileNotFoundException;

import java.io.IOException;

import java.io.InputStream;

import java.io.OutputStream;

import java.io.PrintWriter;


import javax.servlet.RequestDispatcher;

import javax.servlet.ServletConfig;

import javax.servlet.ServletException;

import javax.servlet.http.HttpServlet;

import javax.servlet.http.HttpServletRequest;

import javax.servlet.http.HttpServletResponse;



public class HttpUtil extends HttpServlet {

private static final long serialVersionUID = 1L;

private static String charset = null;


public static void forward(HttpServletRequest req, HttpServletResponse res,

String path) throws ServletException, IOException {

try {

RequestDispatcher dispatcher = req.getRequestDispatcher(path);

dispatcher.forward(req, res);

}catch(Exception e) {

e.printStackTrace();

}

}

public static void fileUpload(HttpServletRequest req, HttpServletResponse res,

String path) throws ServletException, IOException {


charset = (String) req.getAttribute("charset");

System.out.println(charset);

PrintWriter out = res.getWriter();

// 파일 업로드된 경로

String root = req.getSession().getServletContext().getRealPath("/");

String savePath = root + "upload" + File.separator + "upload";

// 서버에 실제 저장된 파일명

String filename = "1600955663095" ;

System.out.println("파일 실제 폴더경로:" + savePath);

// 실제 내보낼 파일명

String orgfilename = "license한글.txt" ;

req.setCharacterEncoding(charset);

res.setCharacterEncoding(charset);

InputStream in = null;

OutputStream os = null;

File file = null;

boolean skip = false;

String client = "";

try{

    // 파일을 읽어 스트림에 담기

    try{

        file = new File(savePath, filename);

        in = new FileInputStream(file);

    }catch(FileNotFoundException fe){

        skip = true;

    }

    client = req.getHeader("User-Agent");

    // 파일 다운로드 헤더 지정

    res.reset() ;

    res.setContentType("application/octet-stream");

    res.setHeader("Content-Description", "JSP Generated Data");

    if(!skip){

        // IE

        if(client.indexOf("MSIE") != -1){

            res.setHeader ("Content-Disposition", "attachment; filename="+new String(orgfilename.getBytes("KSC5601"),"ISO8859_1"));

        }else{

            // 한글 파일명 처리

            orgfilename = new String(orgfilename.getBytes("KSC5601"),"iso-8859-1");

            res.setHeader("Content-Disposition", "attachment; filename=\"" + orgfilename + "\"");

            res.setHeader("Content-Type", "application/octet-stream; charset=utf-8");

        }  

        res.setHeader ("Content-Length", ""+file.length() );

        os = res.getOutputStream();

        

        byte b[] = new byte[(int)file.length()];

        int leng = 0;

        while( (leng = in.read(b)) > 0 ){

            os.write(b,0,leng);

        }

    }else{

    // 한글 깨짐 - 해결

    res.setContentType("text/html;charset=" + charset);

        out.println("<html><head>");

        out.println("<script language='javascript'>alert('파일을 찾을 수 없습니다.');history.back();</script>");

        out.println("</head><body></body></html>");

    }

    in.close();

    os.close();

}catch(Exception e){

e.printStackTrace();

}

    

}


}



파일명: HttpUtil.java


[첨부(Attachments)]

HttpUtil.zip



13. DBFactory.java - 커넥션 풀 적용


실제경로: /target/generated-sources/annotations/com/smile/web/db

패키지명: com.smile.web.controller.db

파일명: DBFactory.java


HikariCP 3.4.2를 적용한 코드이다.


package com.smile.web.db;



import java.io.IOException;

import java.sql.Connection;

import java.sql.DriverManager;

import java.sql.PreparedStatement;

import java.sql.ResultSet;

import java.sql.SQLException;

import java.sql.Statement;

import java.util.Properties;

import java.io.IOException;


import java.io.InputStream;


import java.io.Reader;

import java.util.Properties;

import java.sql.Connection;

import java.sql.SQLException;

import org.slf4j.Logger;

import org.slf4j.LoggerFactory;

import com.zaxxer.hikari.HikariConfig;

import com.zaxxer.hikari.HikariDataSource;


import oracle.jdbc.pool.OracleDataSource;


public class DBFactory {


private static final Logger logger = LoggerFactory.getLogger(DBFactory.class);


    private static String CLASSNAME;

    private static String JDBC_URL;

    private static String USERNAME;

    private static String PASSWORD;

    private static String CACHE_PREP_STMTS;

    private static HikariDataSource ds;


    private HikariConfig config;


    

    public DBFactory() {


    InputStream inputStream;

    config = new HikariConfig();


        String resource = "db.properties";

        Properties properties = new Properties();


        try {


        inputStream = getClass().getClassLoader().getResourceAsStream(resource);

            properties.load(inputStream);


            System.out.println("jdbcurl:" + properties.getProperty("jdbcUrl"));

            System.out.println("className" + properties.getProperty("dataSourceClassName"));


            CLASSNAME = properties.getProperty("dataSourceClassName");

            JDBC_URL = properties.getProperty("jdbcUrl");

            USERNAME = properties.getProperty("dataSource.user");

            PASSWORD = properties.getProperty("dataSource.password");

            CACHE_PREP_STMTS = properties.getProperty("cachePrepStmts");


            config.setDriverClassName(CLASSNAME);

            config.setJdbcUrl( JDBC_URL );

            config.setUsername( USERNAME );

            config.setPassword( PASSWORD );

            

            config.addDataSourceProperty( "cachePrepStmts" , CACHE_PREP_STMTS );

            config.addDataSourceProperty( "prepStmtCacheSize" , "250" );

            config.addDataSourceProperty( "prepStmtCacheSqlLimit" , "2048" );

            

            ds = new HikariDataSource( config );

            

        } catch (IOException e) {

            e.printStackTrace();

        }


    }


    public Connection getConnection() throws SQLException {

   

    /*

        try(Connection con = ds.getConnection()){

            System.out.println("연결상태확인:" + con);

            

            String sql = "SELECT * from board where id > 4 and id < 10";

            

            PreparedStatement pstmt = con.prepareStatement(sql);

            ResultSet rs = pstmt.executeQuery();

            

            while (rs.next()) {

                System.out.println("진짜 연결되었는가:" + rs.getString(2));

            }

        }

        catch(Exception e) {

        System.out.println("연결실패확인:" + e.getMessage());

        }

    */

   

        return ds.getConnection();

    }

    

    public void close(Connection conn, PreparedStatement ps, ResultSet rs) {


if ( rs != null ) {


try {

rs.close();

}

catch(Exception ex) {

System.out.println("오류 발생: " + ex);

}

close(conn, ps); // Recursive 구조 응용(재귀 함수)

} // end of if


}


public void close(Connection conn, PreparedStatement ps) {


if (ps != null ) {


try {

ps.close();

}

catch(Exception ex) {

System.out.println("오류 발생: " + ex);

}

} // end of if


if (conn != null ) {

try {

conn.close();

}

catch(Exception ex) {

System.out.println("오류 발생: " + ex);

}

} // end of if


}

    

}


파일명: DBFactory.java


[첨부(Attachments)]

DBFactory.zip



* 2부에서는 핵심적인 로직 등에 대해서 소개하겠다.


2부에서 만나요.


1. [JSP] 17. JSP/Servlet MVC2 - 페이징네이션과 검색 그리고 오라클 프로젝트 (2), 2020-09-30

https://yyman.tistory.com/1429


반응형
728x90
300x250

[이야기] JSP/Servlet의 RestEasy 4.8(Jboss)로 Rest 실험 이야기(한글 미지원)


결론부터 이야기하면, REST 구현에는 성공했다. 하지만, 문제는 한글 문제인데, 인코딩 관련해서 많은 실험을 하였으나 동작하지 않았다.

삽질하려고 하는 사람이 있다면, 정리하는 걸 추천한다.


REST가 쉽사리 활성화되지 않은 이유도 있을 거 같다. 

C#이나 PHP, C++은 되는지 아직 확인은 안 해봤으나 C#은 되지 않을까 싶다.



1. 주제, 실험 환경


자바 JSP/Servlet으로 REST를 구축할 수 있는지 실험하였다.


- Apache Tomcat 9 - 2020년 9월 기준 - 최신

- Maven 3.6 - 2020년 9월 기준 - 최신

- 각종 RESTEasy 4.5.3 - 2020년 9월 기준 - 최신 (pom 셋팅)......



2. 내용 - POM.xml 설정 사항


<?xml version="1.0" encoding="UTF-8"?>


<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"

  xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">

  <modelVersion>4.0.0</modelVersion>


  <groupId>com.restmaven</groupId>

  <artifactId>restWeb</artifactId>

  <version>0.0.1-SNAPSHOT</version>

  <packaging>war</packaging>


  <name>restWeb Maven Webapp</name>

  <!-- FIXME change it to the project's website -->

  <url>http://localhost</url>


  <properties>

    <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>

    <maven.compiler.source>1.7</maven.compiler.source>

    <maven.compiler.target>1.7</maven.compiler.target>

  </properties>


  <dependencies>

    <dependency>

      <groupId>junit</groupId>

      <artifactId>junit</artifactId>

      <version>4.11</version>

      <scope>test</scope>

    </dependency>

    

    <!-- https://mvnrepository.com/artifact/javax.servlet/javax.servlet-api -->

<dependency>

    <groupId>javax.servlet</groupId>

    <artifactId>javax.servlet-api</artifactId>

    <version>4.0.1</version>

    <scope>provided</scope>

</dependency>

       

    <!-- https://mvnrepository.com/artifact/com.fasterxml.jackson.core/jackson-databind -->

<dependency>

    <groupId>com.fasterxml.jackson.core</groupId>

    <artifactId>jackson-databind</artifactId>

    <version>2.11.2</version>

</dependency>

<!-- https://mvnrepository.com/artifact/com.fasterxml.jackson.core/jackson-core -->

<dependency>

    <groupId>com.fasterxml.jackson.core</groupId>

    <artifactId>jackson-core</artifactId>

    <version>2.11.2</version>

</dependency>

<!-- https://mvnrepository.com/artifact/com.fasterxml.jackson.core/jackson-annotations -->

<dependency>

    <groupId>com.fasterxml.jackson.core</groupId>

    <artifactId>jackson-annotations</artifactId>

    <version>2.11.2</version>

</dependency>

<!-- https://mvnrepository.com/artifact/org.jboss.resteasy/resteasy-core -->

<dependency>

    <groupId>org.jboss.resteasy</groupId>

    <artifactId>resteasy-core</artifactId>

    <version>4.5.8.Final</version>

</dependency>

<!-- https://mvnrepository.com/artifact/org.jboss.resteasy/resteasy-jaxb-provider -->

<dependency>

    <groupId>org.jboss.resteasy</groupId>

    <artifactId>resteasy-jaxb-provider</artifactId>

    <version>4.5.8.Final</version>

</dependency>


<!-- https://mvnrepository.com/artifact/org.jboss.resteasy/resteasy-jsapi -->

<dependency>

    <groupId>org.jboss.resteasy</groupId>

    <artifactId>resteasy-jsapi</artifactId>

    <version>4.5.8.Final</version>

</dependency>

<!-- https://mvnrepository.com/artifact/org.jboss.resteasy/resteasy-jackson2-provider -->

<dependency>

    <groupId>org.jboss.resteasy</groupId>

    <artifactId>resteasy-jackson2-provider</artifactId>

    <version>4.5.8.Final</version>

</dependency>

<!-- https://mvnrepository.com/artifact/org.jboss.resteasy/resteasy-multipart-provider -->

<dependency>

    <groupId>org.jboss.resteasy</groupId>

    <artifactId>resteasy-multipart-provider</artifactId>

    <version>4.5.8.Final</version>

</dependency>

<!-- https://mvnrepository.com/artifact/commons-io/commons-io -->

<dependency>

    <groupId>commons-io</groupId>

    <artifactId>commons-io</artifactId>

    <version>2.8.0</version>

</dependency>

  

  </dependencies>

 


  <build>

    <finalName>restWeb</finalName>

    <pluginManagement><!-- lock down plugins versions to avoid using Maven defaults (may be moved to parent pom) -->

      <plugins>

        <plugin>

          <artifactId>maven-clean-plugin</artifactId>

          <version>3.1.0</version>

        </plugin>

        <!-- see http://maven.apache.org/ref/current/maven-core/default-bindings.html#Plugin_bindings_for_war_packaging -->

        <plugin>

          <artifactId>maven-resources-plugin</artifactId>

          <version>3.0.2</version>

        </plugin>

        <plugin>

          <artifactId>maven-compiler-plugin</artifactId>

          <version>3.8.0</version>

        </plugin>

        <plugin>

          <artifactId>maven-surefire-plugin</artifactId>

          <version>2.22.1</version>

        </plugin>

        <plugin>

          <artifactId>maven-war-plugin</artifactId>

          <version>3.2.2</version>

        </plugin>

        <plugin>

          <artifactId>maven-install-plugin</artifactId>

          <version>2.5.2</version>

        </plugin>

        <plugin>

          <artifactId>maven-deploy-plugin</artifactId>

          <version>2.8.2</version>

        </plugin>

      </plugins>

    </pluginManagement>

  </build>

</project>



파일명: pom.xml



3. 내용 - web.xml 설정 사항


<?xml version="1.0" encoding="UTF-8"?>

<web-app version="3.0" xmlns="http://java.sun.com/xml/ns/javaee"

xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"

xsi:schemaLocation="http://java.sun.com/xml/ns/javaee http://java.sun.com/xml/ns/javaee/web-app_3_0.xsd">

 

  <display-name>Archetype Created Web Application</display-name>

  

  <context-param>

  <param-name>resteasy.resources</param-name>

  <param-value>com.example.restweb.resources.MyResources</param-value>

  </context-param>

  <context-param>

  <param-name>resteasy.servlet.mapping.prefix</param-name>

  <param-value>/rest</param-value>

  </context-param>


  <listener>

  <listener-class>org.jboss.resteasy.plugins.server.servlet.ResteasyBootstrap</listener-class>

  </listener>


  <servlet>

  <servlet-name>Resteasy JSAPI</servlet-name>

<servlet-class>org.jboss.resteasy.jsapi.JSAPIServlet</servlet-class>

</servlet>

  <servlet-mapping>

  <servlet-name>Resteasy JSAPI</servlet-name>

  <url-pattern>/rest-js</url-pattern>

  </servlet-mapping>


  <servlet>

  <servlet-name>Resteasy</servlet-name>

  <servlet-class>org.jboss.resteasy.plugins.server.servlet.HttpServletDispatcher</servlet-class>

  </servlet>

  <servlet-mapping>

  <servlet-name>Resteasy</servlet-name>

  <url-pattern>/rest/*</url-pattern>

  </servlet-mapping>

 

  <servlet>

  <servlet-name>HomeServlet</servlet-name>

  <servlet-class>com.example.restweb.controller.HomeServlet</servlet-class>

  </servlet>

  <servlet-mapping>

  <servlet-name>HomeServlet</servlet-name>

  <url-pattern>/HomeServlet</url-pattern>

  </servlet-mapping>

  

</web-app>



파일명: web.xml



4. 내용 - 기본 컨트롤러 셋팅하기


RestEasy의 사용자 구현에서는 핵심이라고 보면 된다.

크게 사용방법에서는 어려운 건 아니다.


난해할 수 있는 주제가 있는데, String 등으로 바로 반환이 안 되는지.

고민해볼 수 있는 문제가 있다.


= "안 된다. 미지원이다."


Response로 무조건 return 반환을 해줘야 한다. 

(Spring Framework 5 - REST 공부하다가 이거 하라고 하면 납득이 안 될 수 있는 부분이 많다.)


package com.example.restweb.resources;


import com.example.restweb.model.FileInfo;


import javax.ws.rs.Consumes;

import javax.ws.rs.Encoded;

import javax.ws.rs.FormParam;

import javax.ws.rs.GET;

import javax.ws.rs.POST;

import javax.ws.rs.PUT;

import javax.ws.rs.Path;

import javax.ws.rs.PathParam;

import javax.ws.rs.Produces;

import javax.ws.rs.QueryParam;

import javax.ws.rs.core.Context;

import javax.ws.rs.core.MediaType;

import javax.ws.rs.core.Response;

import javax.ws.rs.core.UriInfo;


import org.jboss.resteasy.annotations.providers.multipart.MultipartForm;


@Path("/api")

public class MyResources

{

// Index

@GET

@Path("")

    @Produces("application/json")

public Response get()

{

StringBuffer buf = new StringBuffer();

buf.append("ok");

return Response.status(200).entity(buf).build();

}


    @GET

    @Path("users/{id}")

    @Produces("application/json")

    public Response getUserById(@PathParam("id") Integer id)

    {

        User user = new User();

        user.setId(id);

        user.setFirstName("Lokesh");

        user.setLastName("Gupta");

        

        return Response.status(200).entity(user).build();

    }

    

    @Path("foo/{param}-{other}")

@PUT

    @Produces("application/json")

public Response putFooParam(@PathParam("param") String param,

  @PathParam("other") String other)

{

StringBuffer buf = new StringBuffer();

buf.append("param").append("=").append(param).append(";");

buf.append("other").append("=").append(other).append(";");

return Response.status(200).entity(buf).build();

}

    

@Path("form")

@POST

    @Produces("application/json")

public String postForm(@FormParam("id") String a,

   @FormParam("passwd") String b){

return a +"/" +b;

}


@Path("lookup")

@GET

    @Produces("application/json")

public Response lookup(@QueryParam("id") String id,

@Context UriInfo uriInfo)

{


StringBuffer buf = new StringBuffer();

buf.append("param").append("=").append(id).append(";");

return Response.status(200).entity(buf).build();

}


    @POST

    @Path("/upload-file")

    @Consumes(MediaType.MULTIPART_FORM_DATA)

    public Response uploadFile(@MultipartForm FileInfo info) throws Exception {

   

    String fileName = info.getFileName();

   

    // RESTEasy는 한글 자체가 안됨.

   

    /*

    form.setFileName(filename);

   

    System.out.println("파일명1:" + form.getFileName());

   

        String fileName = form.getFileName() == null ? "Unknown" : form.getFileName() ;

        String completeFilePath = "c:/temp2/" + fileName;

        try

        {

            //Save the file

            File file = new File(completeFilePath);

              

            if (!file.exists()) 

            {

                file.createNewFile();

            }

      

            FileOutputStream fos = new FileOutputStream(file);

      

            fos.write(form.getFileData());

            fos.flush();

            fos.close();

        } 

        catch (IOException e)

        {

            e.printStackTrace();

        }

        //Build a response to return

        */

   

        return Response.status(200)

            .entity("uploadFile is called, Uploaded file name : " + fileName).build();

        

   

    }

    

    

}


파일명: MyResources.java



5. 결과


실험1)

-> 문자셋 찾기 실험(UTF-8, Euc-kr, us-ascil, windows-1251, ISO-8851-9? 등)

    (반복문으로 해당 문자인지 찾는 작업을 하였음.)

(실패)


실험2)

-> <form 태그 accept- utf-8> 가능하도록 설정

(실패)


실험3)

-> new String (originalText.getBytes("ISO-8851-9), "UTF-8") 등 변환 작업

(실패)

.......

실험4)

-> public response 함수명(@Context ServletRequest req......)에 req.setCharacterEncoding 설정

(실패)



(다수의 방법을 적용하였음.)


결과는 RESTEasy 프로젝트에서 한글 자체를 해결해줘야 한다. 그렇지 않으면 어렵다.

이 프로그램은 영어로 글을 작성한다고 했을 때는 동작한다.


단순한 영어나 숫자 형태로 전송 작업을 시도하고 싶다면, 해봐도 무방하다. 한글 등은 기대 안 하는 게 좋을 듯 싶다.


RESTEasy 이외에 흥미로운 발견을 한 부분도 있다.

Servlet 생성할 때 셋팅화면에 자세히 보면, doPut, doDelete, doGet, doPost 기능이 있다.

문제는 패턴 등을 잡을 때 한계가 생긴다. (힘들고 무척 어렵다는 이야기이다.)


이 실험은 그런 부분은 해소하였다.




[첨부(Attachments)]

restEasy-한글미지원(Unsupported_Korean_Language).zip

(Spring Tool-Suite 4.4에서 작성함.)





* 참고자료(References)


1. RESTEasy JAX-RS, https://docs.jboss.org/resteasy/docs/4.5.8.Final/userguide/html/, Accessed by 2020-09-29, Last Modified 2020-09-23.

   = (RESTEasy 기술 정보가 해외, 국내에 많이 부족해서 공식 메뉴얼을 참고할 수 밖에 없음)


2. Chapter 3. Installation/Configuration, / 3.3. Deploying to other servlet containers, https://docs.jboss.org/resteasy/docs/4.5.8.Final/userguide/html/Installation_Configuration.html


3. RESTEasy JSON Example with Jackson, https://howtodoinjava.com/resteasy/resteasy-jackson-json-example/, Accessed by 2020-09-29, Last Modified 2013.

   = (RESTEasy에서의 다중 업로드 기능에 대해서는 잘 소개하고 있다.)


4. /resteasy/test/smoke/MyResource.java - Github, https://github.com/resteasy/resteasy-examples/blob/master/jsapi-servlet-test/src/main/java/org/jboss/resteasy/test/smoke/MyResource.java, Accessed by 2020-09-29, Last Modified 2016-08-05.

   = 조금 된 소스코드이나 RESTEasy를 쉽게 빠르게 구축하는 방법이 적혀져 있다.

   = 문제는 저 코드대로 전부 따라해보면, 안 된다. 태스트를 수 차례 각종 URL을 넣어봐서 되는 코드는 살리고 참고를 많이 하였다.


5. JSON Example With RESTEasy + Jackson, https://examples.javacodegeeks.com/enterprise-java/rest/resteasy/json-example-with-resteasy-jackson/, Accessed by 2020-09-29, Last Modified 2013-12-09

   = 구축 원리를 차근차근 화면 그림 위주로 소개하고 있다.


6. RESTEasy File Upload – Html Form Example, https://howtodoinjava.com/resteasy/jax-rs-resteasy-file-upload-html-form-example/, Accessed by 2020-09-29, Last Modified 2013.

   = RESTEasy 2.3 기반으로 작성된 것이라서 다소 일부분만 참고하였다. 동작이 안 되는 코드들도 많다.


7. RESTEasy JAX-RS 4.5.8.Final API, https://docs.jboss.org/resteasy/docs/4.5.8.Final/javadocs/, Accessed by 2020-09-29, Last Modified 2020-09.

   = JAVA API 메뉴얼처럼 RestEasy도 API 메뉴얼이 있다. 변화가 다소 있었다. 2.3버전의 메뉴얼과 현재 버전의 기능 변화가 많이 있었다.

반응형
728x90
300x250

[Spring-Framework] 20. Spring MVC - Spring Framework 5 REST, Jackson, Commons-FileUpload - (2)


2부에서는 Controller, Model, View, Util(한글 문제)을 집중적으로 소개하겠다.


1. [Spring-Framework] 19. Spring MVC - Spring Framework 5 REST, Jackson, Commons-FileUpload - (1), 2020-09-28

https://yyman.tistory.com/1425



12. Controller - HomeController.java



초기 프로젝트를 생성하면 자동으로 만들어지는 HomeController.java이다.

REST 프로젝트 작업에 필요한 형태로 추가 작성 및 변형하였다.


- REST Client에 대해서 자세히 소개하였음. (CRUD - GET, POST, PUT, DELETE 클라이언트)


package com.example.restexample2.controller;


import java.io.UnsupportedEncodingException;

import java.net.URI;

import java.net.URLDecoder;

import java.nio.charset.Charset;

import java.text.DateFormat;

import java.util.ArrayList;

import java.util.Arrays;

import java.util.Date;

import java.util.HashMap;

import java.util.List;

import java.util.Locale;

import java.util.Map;

import java.util.concurrent.atomic.AtomicLong;


import org.slf4j.Logger;

import org.slf4j.LoggerFactory;

import org.springframework.http.HttpEntity;

import org.springframework.http.HttpHeaders;

import org.springframework.http.HttpMethod;

import org.springframework.http.MediaType;

import org.springframework.http.ResponseEntity;

import org.springframework.http.client.HttpComponentsClientHttpRequestFactory;

import org.springframework.http.converter.FormHttpMessageConverter;

import org.springframework.http.converter.HttpMessageConverter;

import org.springframework.http.converter.StringHttpMessageConverter;

import org.springframework.stereotype.Controller;

import org.springframework.ui.Model;

import org.springframework.util.LinkedMultiValueMap;

import org.springframework.util.MultiValueMap;

import org.springframework.web.bind.annotation.GetMapping;

import org.springframework.web.bind.annotation.PathVariable;

import org.springframework.web.bind.annotation.RequestMapping;

import org.springframework.web.bind.annotation.RequestMethod;

import org.springframework.web.bind.annotation.RequestParam;

import org.springframework.web.bind.annotation.RestController;

import org.springframework.web.client.HttpClientErrorException;

import org.springframework.web.client.HttpServerErrorException;

import org.springframework.web.client.RestTemplate;

import org.springframework.web.util.UriComponents;

import org.springframework.web.util.UriComponentsBuilder;


import com.example.restexample2.model.Board;

import com.example.restexample2.model.Greeting;

import com.fasterxml.jackson.databind.ObjectMapper;

import com.fasterxml.jackson.databind.deser.impl.CreatorCandidate.Param;


@Controller

public class HomeController {

private static final Logger logger = LoggerFactory.getLogger(HomeController.class);;

@RequestMapping(value = "/", method = RequestMethod.GET)

public String home(Locale locale, Model model) {

logger.info("Welcome home! The client locale is {}.", locale);

Date date = new Date();

DateFormat dateFormat = DateFormat.getDateTimeInstance(DateFormat.LONG, DateFormat.LONG, locale);

String formattedDate = dateFormat.format(date);

model.addAttribute("serverTime", formattedDate );

return "home";

}


    /**

     * 파일 업로드 입력 화면

     */

@RequestMapping(value = "/fileUploadView", method = RequestMethod.GET)

public String fileUploadView(Locale locale, Model model) {

logger.info("Welcome FileUpload! The client locale is {}.", locale);

Date date = new Date();

DateFormat dateFormat = DateFormat.getDateTimeInstance(DateFormat.LONG, DateFormat.LONG, locale);

String formattedDate = dateFormat.format(date);

model.addAttribute("serverTime", formattedDate );

return "file/upload";

}



    /**

     * GET 방식 - 클라이언트

     */

@SuppressWarnings("unchecked")

@RequestMapping(value = "/client/listMapGet", method = RequestMethod.GET)

public String helloClient(String regId, String time) throws UnsupportedEncodingException{

// Get 응답 방법론

String url = "http://localhost:8080/restexample2/testValue2";

        String serviceKey = "서비스키";

        String decodeServiceKey = URLDecoder.decode(serviceKey, "UTF-8");

        

        RestTemplate restTemplate = new RestTemplate();

        HttpHeaders headers = new HttpHeaders();

        headers.setContentType(new MediaType("application", "json", Charset.forName("UTF-8")));    //Response Header to UTF-8  

        

        UriComponents builder = UriComponentsBuilder.fromHttpUrl(url)

                .queryParam("serviceKey", decodeServiceKey)

                .queryParam("regId", regId)

                .queryParam("tmFc", time)

                .queryParam("_type", "json")

                .build(false);    //자동으로 encode해주는 것을 막기 위해 false

        

        //Object response = restTemplate.exchange(builder.toUriString(), HttpMethod.GET, new HttpEntity<String>(headers), String.class);

        

        // Object response = restTemplate.getForEntity(builder.toUriString(), List.class);

        Object response = restTemplate.getForObject(builder.toUriString(), List.class);

        

        if ( response != null) {


            List<Integer> map = (List<Integer>) response;

        System.out.println("map" + map);

        }

        

        //return response;


return "home";

}


    /**

     * POST 방식 - 클라이언트

     */

@SuppressWarnings("unchecked")

@RequestMapping(value = "/client/listMapPost")

public String helloClient2(String regId, String time) throws UnsupportedEncodingException{

// POST 응답 방법

//String url = "http://..............";

String url = "http://localhost:8080/restexample2/testValue2";

        String serviceKey = "서비스키";

        String decodeServiceKey = URLDecoder.decode(serviceKey, "UTF-8");

        

        RestTemplate restTemplate = new RestTemplate();

        HttpHeaders headers = new HttpHeaders();

        headers.setContentType(new MediaType("application", "json", Charset.forName("UTF-8")));    //Response Header to UTF-8  

        

        /*

        UriComponents builder = UriComponentsBuilder.fromHttpUrl(url)

                .queryParam("serviceKey", decodeServiceKey)

                .queryParam("regId", regId)

                .queryParam("tmFc", time)

                .queryParam("_type", "json")

                .build(false);    //자동으로 encode해주는 것을 막기 위해 false

        */

        

        MultiValueMap<String, String> parameters = new LinkedMultiValueMap<String, String>();

        parameters.add("servicekey", decodeServiceKey);

        parameters.add("regId", regId);

        parameters.add("tmFc", time);

        parameters.add("_type", "json");

        

        //Object response = restTemplate.exchange(builder.toUriString(), HttpMethod.GET, new HttpEntity<String>(headers), String.class);

        

        // Object response = restTemplate.getForEntity(builder.toUriString(), List.class);

        Object response = restTemplate.postForObject(url, parameters, List.class);

        // Object response = restTemplate.postForEntity(url, parameters, List.class);

        

        if ( response != null) {


            List<Integer> map = (List<Integer>) response;

        System.out.println("map" + map.get(0));

        }

        

        //return response;


return "home";

}

    /**

     * PUT 방식 - 클라이언트

     */

@SuppressWarnings("unchecked")

@RequestMapping(value = "/client/listMapPut/{boardIdx}")

public String helloClient3(String regId, 

String time,

@PathVariable(name="boardIdx", required=true) int boardIdx)

throws  UnsupportedEncodingException{

// PUT 방법

String url = "http://localhost:8080/restexample2/v1/api/board/2";

        String serviceKey = "서비스키";

        String decodeServiceKey = URLDecoder.decode(serviceKey, "UTF-8");

        

        RestTemplate restTemplate = new RestTemplate();

        HttpHeaders headers = new HttpHeaders();

        headers.setContentType(new MediaType("application", "json", Charset.forName("UTF-8")));    //Response Header to UTF-8  

        

        // 파라메터

        MultiValueMap<String, String> parameters = new LinkedMultiValueMap<String, String>();

        parameters.add("servicekey", decodeServiceKey);

        parameters.add("regId", regId);

        parameters.add("tmFc", time);

        parameters.add("_type", "json");

        

        //Object response = restTemplate.exchange(builder.toUriString(), HttpMethod.GET, new HttpEntity<String>(headers), String.class);

        

        Board updatedBoard = new Board();

        updatedBoard.setId(boardIdx);

        updatedBoard.setSubject("앙하하");

        updatedBoard.setName("수정했다.");

        updatedBoard.setMemo("메모지");

        

        

        // Object response = restTemplate.getForEntity(builder.toUriString(), List.class);

        //Object response = restTemplate.postForObject(url, parameters, List.class);

        restTemplate.put ( url, updatedBoard, parameters );

        

        // 데이터 확인하기

        url = "http://localhost:8080/restexample2/v1/api/board";


        UriComponents builder = UriComponentsBuilder.fromHttpUrl(url)

                .queryParam("serviceKey", decodeServiceKey)

                .build(false);


        Board[] response = restTemplate.getForObject(builder.toUriString(), Board[].class );

        // Object response = restTemplate.postForEntity(url, parameters, List.class);

        

        if ( response != null) {


            List<Board> listData = Arrays.asList(response);

        System.out.println("list(Size):" + listData.size());

       

        try {

        if ( listData.size() != 0 && (boardIdx - 1) >= 0) {

            System.out.println("boardIdx:" + (boardIdx - 1));

        Board boardTmp = listData.get(boardIdx - 1);

        System.out.println("board:" + boardTmp.getId() + "/" + boardTmp.getSubject());

        }

        }catch(Exception e) {

        e.printStackTrace();

        }

       

        }

        

        //return response;

return "home";

}


    /**

     * PUT 방식 - 클라이언트

     */

@SuppressWarnings("unchecked")

@RequestMapping(value = "/client/listMapDelete/{boardIdx}")

public String helloClient4(String regId, 

String time,

@PathVariable(name="boardIdx", required=true) int boardIdx)

throws  UnsupportedEncodingException{

   // PUT 방법

  String url = "http://localhost:8080/restexample2/v1/api/board/" + boardIdx;

        String serviceKey = "서비스키";

        String decodeServiceKey = URLDecoder.decode(serviceKey, "UTF-8");

        

        RestTemplate restTemplate = new RestTemplate();

        HttpHeaders headers = new HttpHeaders();

        headers.setContentType(new MediaType("application", "json", Charset.forName("UTF-8")));    //Response Header to UTF-8  

        

        // 파라메터

        MultiValueMap<String, String> parameters = new LinkedMultiValueMap<String, String>();

        parameters.add("servicekey", decodeServiceKey);

        parameters.add("regId", regId);

        parameters.add("tmFc", time);

        parameters.add("_type", "json");

        

        //Object response = restTemplate.exchange(builder.toUriString(), HttpMethod.GET, new HttpEntity<String>(headers), String.class);

        

        // Object response = restTemplate.getForEntity(builder.toUriString(), List.class);

        //Object response = restTemplate.postForObject(url, parameters, List.class);

        restTemplate.delete ( url, parameters );

        

        // 데이터 확인하기

        url = "http://localhost:8080/restexample2/v1/api/board";


        UriComponents builder = UriComponentsBuilder.fromHttpUrl(url)

                .queryParam("serviceKey", decodeServiceKey)

                .build(false);


        Board[] response = restTemplate.getForObject(builder.toUriString(), Board[].class );

        // Object response = restTemplate.postForEntity(url, parameters, List.class);

        

        if ( response != null) {


            List<Board> listData = Arrays.asList(response);

        System.out.println("list(Size):" + listData.size());

       

        try {

        if ( listData.size() != 0 && (boardIdx - 1) >= 0) {

            System.out.println("boardIdx:" + (boardIdx - 1));

        Board boardTmp = listData.get(boardIdx - 1);

        System.out.println("board:" + boardTmp.getId() + "/" + boardTmp.getSubject());

        }

        }catch(Exception e) {

        e.printStackTrace();

        }

       

        }

        

        //return response;

return "home";

}

}



파일명: HomeController.java


[첨부(Attachments)]

HomeController.zip





13. Controller - JSONController.java



REST를 간단하게 입문하는 용도로 작성하였다.


package com.example.restexample2.controller;


import java.util.ArrayList;

import java.util.HashMap;

import java.util.List;

import java.util.Map;

import java.util.concurrent.atomic.AtomicLong;


import org.slf4j.Logger;

import org.slf4j.LoggerFactory;

import org.springframework.web.bind.annotation.GetMapping;

import org.springframework.web.bind.annotation.PostMapping;

import org.springframework.web.bind.annotation.RequestParam;

import org.springframework.web.bind.annotation.RestController;


import com.example.restexample2.model.Greeting;


/**

 * Handles requests for the application home page.

 */

@RestController

public class JSONController {

private static final Logger logger = LoggerFactory.getLogger(JSONController.class);


private static final String template = "Hello, %s!";

private final AtomicLong counter = new AtomicLong();

@GetMapping("/greeting")

public Greeting greeting(@RequestParam(value = "name", defaultValue = "World") String name) {

return new Greeting(counter.incrementAndGet(), String.format(template, name));

}

@GetMapping(value="/testValue") 

public String getTestValue(){

String TestValue = "레스트컨트롤러 테스트";

return TestValue;

}


@GetMapping(value="/testValue2") 

public List<Integer> getTestValue2(){

List<Integer> mList = new ArrayList<Integer>();

mList.add(1);

mList.add(2);

mList.add(3);

mList.add(4);

return mList;

/* 

* Error: pom.xml의 jackson-databind, jackson-core 추가할 것

* onverter found for return value of type: class java.util.ArrayList]

*/

}

@PostMapping(value="/testValue2") 

public List<Integer> getTestValue3(){

List<Integer> mList = new ArrayList<Integer>();

mList.add(1);

mList.add(2);

mList.add(3);

mList.add(4);

return mList;

/* 

* Error: pom.xml의 jackson-databind, jackson-core 추가할 것

* onverter found for return value of type: class java.util.ArrayList]

*/

}

@GetMapping(value="/getMap")

public Map<String, Greeting> getMap(){

Map<String, Greeting> map = new HashMap<>();

map.put("First", new Greeting(1, "Hello"));

map.put("Second", new Greeting(2, "Rest"));

return map;

}

}



파일명: JSONController.java


[첨부(Attachments)]

JSONController.zip




14. Model - Board.java



매우 간단한 게시판 구조에 대한 모델이다. 방명록도 어찌보면, 게시판의 한 종류가 될 수 있다.


package com.example.restexample2.model;


public class Board {

private long id;

private String subject;

private String name;

private String memo;

public long getId() {

return id;

}

public void setId(long id) {

this.id = id;

}

public String getSubject() {

return subject;

}

public void setSubject(String subject) {

this.subject = subject;

}

public String getName() {

return name;

}

public void setName(String name) {

this.name = name;

}

public String getMemo() {

return memo;

}

public void setMemo(String memo) {

this.memo = memo;

}

}



파일명: Board.java


[첨부(Attachments)]

Board.zip



15. Model - Greeting.java



사실 쉽고 간단한 코드가 더 어려운 코드라고 본다.

꼭 반드시 게시판 형태만 생각할 필요가 없다고 본다.

공부하는 데 있어서는 간단한 것이 사실 더 어려운 부분이라고 본다.


package com.example.restexample2.model;


public class Greeting {


private final long id;

private final String content;


public Greeting(long id, String content) {

this.id = id;

this.content = content;

}


public long getId() {

return id;

}


public String getContent() {

return content;

}

}


파일명: Greeting.java


[첨부(Attachments)]

Greeting.zip




16. Model - FileInfo.java


경로: /src/main/java/com/example/restexample2/model/FileInfo.java


파일에 관한 명세이다.


package com.example.restexample2.model;


import org.springframework.web.multipart.MultipartFile;


public class FileInfo {


private long num;

private String filename;

private long filesize;

private MultipartFile mediaFile;


public long getNum() {

return num;

}

public void setNum(long num) {

this.num = num;

}

public String getFilename() {

return filename;

}

public void setFilename(String filename) {

this.filename = filename;

}

public long getFilesize() {

return filesize;

}

public void setFilesize(long filesize) {

this.filesize = filesize;

}

public MultipartFile getMediaFile() {

return mediaFile;

}


public void setMediaFile(MultipartFile mediaFile) {

this.mediaFile = mediaFile;

}

}



파일명: FileInfo.java


[첨부(Attachments)]

FileInfo.zip





17. Controller - FileController.java


경로: /src/main/java/com/example/restexample2/controller/FileController.java


파일 업로드에 대한 기능이다.


package com.example.restexample2.controller;


import java.io.BufferedOutputStream;

import java.io.File;

import java.io.FileOutputStream;

import java.io.IOException;


import javax.servlet.ServletContext;

import javax.servlet.http.HttpServletRequest;

import javax.servlet.http.HttpServletResponse;


import org.apache.commons.fileupload.disk.DiskFileItemFactory;

import org.apache.commons.fileupload.servlet.ServletFileUpload;

import org.slf4j.Logger;

import org.slf4j.LoggerFactory;

import org.springframework.beans.factory.annotation.Autowired;

import org.springframework.http.HttpHeaders;

import org.springframework.http.HttpStatus;

import org.springframework.http.ResponseEntity;

import org.springframework.ui.Model;

import org.springframework.web.bind.annotation.ModelAttribute;

import org.springframework.web.bind.annotation.RequestMapping;

import org.springframework.web.bind.annotation.RequestMethod;

import org.springframework.web.bind.annotation.RequestParam;

import org.springframework.web.bind.annotation.RestController;

import org.springframework.web.multipart.MultipartFile;


import com.example.restexample2.model.Board;

import com.example.restexample2.model.FileInfo;

import com.example.restexample2.util.HttpUtil;


@RestController

@RequestMapping ("/file")

public class FileController {

private static final Logger logger = LoggerFactory.getLogger(FileController.class);

     

     /**

      * 파일 멀티파트 업로드 Rest

      * {/

      * @param inputFile

      * @return 

      *  (주석 스타일 참고)

      */

     @RequestMapping(value = "/uploadFileModelAttribute/new", 

    method = {RequestMethod.POST },

    produces="text/plain;charset=UTF-8")

     public String multiFileUpload(@ModelAttribute Board boardVO, 

    @RequestParam("mediaFile")MultipartFile[] files, 

    Model model,

    HttpServletRequest req,

    HttpServletResponse res) throws IOException {

     

    boolean filechk = false;

     

         //디스크상의 프로젝트 실제 경로얻기

         //String contextRootPath = "c:" + File.separator + "upload";

    // String charset = "UTF-8";


    // req.setAttribute("charset", charset);

    // req.setCharacterEncoding(charset);

    // res.setContentType("text/html; charset=" + charset);

     

    String dirName = "upload" ; 

String contextRootPath = req.getSession().getServletContext().getRealPath("/") + dirName;

 

         System.out.println("실제경로:" + contextRootPath);


         //1. 메모리나 파일로 업로드 파일 보관하는 FileItem의 Factory 설정

         DiskFileItemFactory diskFactory = new DiskFileItemFactory(); //디스크 파일 아이템 공장

         diskFactory.setSizeThreshold(4096); //업로드시 사용할 임시 메모리

         diskFactory.setRepository(new File(contextRootPath + "/WEB-INF/temp")); //임시저장폴더

         

         //2. 업로드 요청을 처리하는 ServletFileUpload생성

         ServletFileUpload upload = new ServletFileUpload(diskFactory);

         upload.setSizeMax(3 * 1024 * 1024); //3MB : 전체 최대 업로드 파일 크기

         

         

         // 한글 깨짐 해결(버그)

         // String kor_a = new String(boardVO.getSubject().getBytes("8859_1"), "UTF-8");  

         System.out.println("게시물제목:" + HttpUtil.getISO8859toUTF8( boardVO.getSubject()) );

System.out.println("게시물작성자:" + HttpUtil.getISO8859toUTF8( boardVO.getName()) );

System.out.println("게시물내용:" + HttpUtil.getISO8859toUTF8( boardVO.getMemo()) );

System.out.println("파일(길이):" + files.length );

 

 

         for(MultipartFile mFile : files) {


             // 3. 파일 가져오기

    if ( mFile.getOriginalFilename().isEmpty() && 

      filechk == false ) {


            String msg = "Please select at least one mediaFile.<br/>(미디어 파일을 하나 이상 선택하십시오.)";

            model.addAttribute("msg", msg);

           

            return model.getAttribute("msg").toString();

    }

     

             // 4. 파일명 - 현재시간으로 생성

             String uploadedFileName = System.currentTimeMillis() + ""; 

             

        if (!mFile.getOriginalFilename().isEmpty()) {

         

        BufferedOutputStream outputStream = new BufferedOutputStream(

        new FileOutputStream(

        new File( contextRootPath + File.separator + "upload" + File.separator, uploadedFileName )

));

       

       

                  System.out.println("파일명:" + mFile.getOriginalFilename());

                 

                  outputStream.write(mFile.getBytes());

                  outputStream.flush();

                  outputStream.close();

                 

                  filechk = true;                 

              } 

         

         }

         

    return "fileUploadForm";

   

     }

     

}


파일명: FileController.java


[첨부(Attachments)]

FileController.zip


비고: HttpUtil.java의 HttpUtil에 정의된 한글 출력 문제 등이 적용되어 있음.




18. Controller - BoardRestController.java


경로: /src/main/java/com/example/restexample2/controller/BoardRestController.java


게시판 시스템을 적용한 Rest 컨트롤러이다.

자료구조를 적절하게 재배치하여 실습에는 DB없이 가능한 수준으로 구현하였다.


package com.example.restexample2.controller;


import java.util.ArrayList;

import java.util.HashMap;

import java.util.List;

import java.util.Map;


import javax.servlet.http.HttpServletRequest;


import org.slf4j.Logger;

import org.slf4j.LoggerFactory;

import org.springframework.web.bind.annotation.DeleteMapping;

import org.springframework.web.bind.annotation.GetMapping;

import org.springframework.web.bind.annotation.ModelAttribute;

import org.springframework.web.bind.annotation.PatchMapping;

import org.springframework.web.bind.annotation.PathVariable;

import org.springframework.web.bind.annotation.PostMapping;

import org.springframework.web.bind.annotation.PutMapping;

import org.springframework.web.bind.annotation.RequestBody;

import org.springframework.web.bind.annotation.RequestMapping;

import org.springframework.web.bind.annotation.RestController;


import com.example.restexample2.model.Board;


@RestController

@RequestMapping("/v1/api")

public class BoardRestController {

private static final Logger logger = LoggerFactory.getLogger(BoardRestController.class);


private static int num = 1;

private List<Object> tmpBoard = new ArrayList<Object>(); 

//@Autowired

    //private BoardService boardService;


    // 조회 = GET

    // (전체 게시물)

@GetMapping("board")

    public List<Object> listBoard(HttpServletRequest request, @ModelAttribute Board board) throws Exception {

   

    logger.info("게시판 목록");

    logger.info("----------------------------------");

   

    Board createNode = new Board();

    createNode.setId(num);

    createNode.setName("홍길동");

    createNode.setMemo("메모");

    createNode.setSubject("주소지");

   

    // tmpBoard.put(num, createNode);

    // num = num + 1;

   

    return tmpBoard;

    //return this.boardService.selectBoardList(request, board);

    }


    // 조회 = GET

    // (특정 세부 게시물)

    @GetMapping("board/{boardIdx}")

    public Board detailBoard(HttpServletRequest request, 

    @PathVariable(name="boardIdx", required=true) int boardIdx)

    throws Exception {


    logger.info("게시판 조회");

    logger.info("----------------------------------");

    logger.info("게시판 특정 게시물 번호" + boardIdx);

   

    //return this.boardService.selectBoard(request, boardIdx);

    return (Board) tmpBoard.get(boardIdx);

    }

    

    // 등록 = POST

    @PostMapping("board/new")

    public void insertBoard(HttpServletRequest request, @RequestBody Board board) throws Exception {

   

    logger.info("게시판 삽입");

   

    board.setId(num);


    tmpBoard.add(board);

    System.out.println(num);

    num = num + 1;

        //this.boardService.insertBoard(request, board);

    }


    // 수정 = PUT, PATCH (전송 방식)

    // /member/{id} + body (json데이터 등)

    @PutMapping("board/{boardIdx}")

    @PatchMapping("board/{boardidx}")

    public void updateBoard(HttpServletRequest request, 

    @PathVariable(name="boardIdx", required=true) int boardIdx, 

    @RequestBody Board board) throws Exception {

   

    logger.info("게시판 수정");

   

    if ( !tmpBoard.isEmpty() ) {

   

    board.setId(boardIdx); // 고유키 그대로 유지할 것

    tmpBoard.set(boardIdx - 1, board);

    }

    //board.setBoardIdx(boardIdx);

        //this.boardService.updateBoard(request, board);

   

    }

    

    // 삭제 = DELETE(전송 방식)

    @DeleteMapping("board/{boardIdx}")

    public void deleteBoard(HttpServletRequest request,

    @PathVariable(name="boardIdx", 

    required=true) int boardIdx) throws Exception {

   

    logger.info("게시판 삭제");

   

    try {   

    tmpBoard.remove(boardIdx);

    }

    catch(Exception e) {

        logger.info("Null값");

    e.getStackTrace();

    }

   

    //this.boardService.deleteBoard(request, boardIdx);

    }


}



파일명: BoardRestController.java


[첨부(Attachments)]

BoardRestController.zip




19. Util - HttpUtil.java


경로: /src/main/java/com/example/restexample2/util/HttpUtil.java


한글 언어에 대한 문제 해결에 대해서 정의하였다.


package com.example.restexample2.util;


import java.io.UnsupportedEncodingException;


public class HttpUtil {



    // 버그 개선: euc-kr 검증

    public static boolean isEucKr(String s) {

        int len = s.length();

        char c;

        for (int i = 0; i < len; i++) {

            c = s.charAt(i);

            /// System.out.println("" + c + " = " + toHex(c));

            if (((c & 0xFFFF) >= 0xAC00) && ((c & 0xFFFF) <= 0xD7A3))

                return true;

            /// else if (((c & 0xFF00) != 0) && ((c & 0x00) == 0))

            ///     return false;

        }

        return false;

    }

    // 버그 개선: ISO8859-1 검증

    public static boolean isISO8859(String s) {

     

        int len = s.length();

        char c;

        for (int i = 0; i < len; i++) {

            c = s.charAt(i);

            /// System.out.println("" + c + " = " + toHex(c));

            if ((c & 0xFF00) != 0)

                return false;

        }

        

        return true;

    }

    

    public static String getISO8859toUTF8(String s) {

   

    try {

return new String(s.getBytes("8859_1"), "UTF-8");

} catch (UnsupportedEncodingException e) {

e.printStackTrace();

return s;

}

   

    }

}



파일명: HttpUtil.java


[첨부(Attachments)]

HttpUtil.zip



20. View - home.jsp


경로: /src/main/webapp/WEB-INF/views/home.jsp


기본 생성된 jsp파일이다.


<%@ taglib uri="http://java.sun.com/jsp/jstl/core" prefix="c" %>

<%@ page session="false" %>

<html>

<head>

<title>Home</title>

</head>

<body>

<h1>

Hello world! - Rest(REST)

</h1>


<P>

</P>

</body>

</html>



파일명: home.jsp


[첨부(Attachments)]

home.zip



21. View - upload.jsp


경로: /src/main/webapp/WEB-INF/views/file/upload.jsp


업로드 페이지에 관한 정의이다.



그림 33. login.jsp 파일 모습 - 사용자 인터페이스(User Interfaces)


<%@ taglib uri="http://java.sun.com/jsp/jstl/core" prefix="c" %>

<%@ page session="false" %>

<%@ page language="java" contentType="text/html; charset=UTF-8" pageEncoding="UTF-8"%>

<html>

<head>

<title>다중 파일 업로드</title>

<meta charset="UTF-8">

</head>

<body>

<h3>다중 파일 업로드 및 다중 변수</h3>


<form action="file/uploadFileModelAttribute/new" method="POST"

  enctype="multipart/form-data">

    <table>

        <tr>

            <td>

            제목:

            <input type="text" name="subject" >

            </td>

            <td>

            이름:

            <input type="text" name="name" >

            </td>

            <td>

            내용:

            <input type="text" name="memo" >

            </td>

        </tr>

        <tr>

            <td>Select Files(파일을 선택하시오)</td>

            <td>

            <input type="file" name="mediaFile" multiple>

            </td>

            <td>

            <input type="file" name="mediaFile" multiple>

            </td>

        </tr>

        <tr>

        <td colspan="3">

                <button type="submit">Upload(업로드)</button>

        </td>

        </tr>

    </table>

</form>

</body>

</html>



파일명: login.jsp


[첨부(Attachments)]

upload.zip



22. 의외로 어려운 REST 작업 - Client에서 작업하기


REST Client 앱을 설치하여 작업을 시도했을 때 명세 등을 정의하질 못해서 작업이 쉽게 이뤄지지 못한 경우도 있다.

그래서 하나 만들어보게 되었다.


비고: 작업할 때는 조금 많이 찍어봐야 한다. (힘들고 더딘 작업 중 하나이다.)



그림 34. REST Client에서 처리할 때 사용될 수 있는 정보들


이러한 정보는 그냥 나온 것은 아니고, 조금 코드로 셈플이 나오도록 찍어봐야 한다.

안 찍어보면, 어떤 구조인지 모른다. 알 길이 없다.



그림 35. 코드 등으로 객체를 찍어보는 형태로 변형해주기


셈플 코드가 출력될 수 있는 환경으로 코드를 변형해준다.



그림 36. 객체 정보의 체계 - 출력


이런 형태로 정보들이 나오면, 잘 대입해보고 정리해보기도 하고 그래야 한다.

(태스트 작업이 소요됨)



그림 37. YARC REST Client에서 사용하기


자료를 가공해서 입력한 후, Send Request를 누른다.



예를 들면, GET 관련 코드를 통해서 Send Request를 누른 후에 아래에서 결과를 찾아볼 수 있다.

그러면 아래처럼 관련 유추할 수 있는 정보 단위 코드를 출력해볼 수 있다.


"3":{"id":3,"subject":"주소지","name":"홍길동","memo":"메모"}}

(양식)


(응용 -> POST 등록 명령)

-> {"id":3,"subject":"주소지","name":"홍길동","memo":"메모"}



(응용 -> PUT, PATCH 수정 명령)

-> {"id":3,"subject":"주소지","name":"홍길동","memo":"메모"}



-----------------------------------------------------


POST /restexample2/file/uploadFileModelAttribute/new HTTP/1.1

Host: localhost:8080

Connection: keep-alive

Content-Length: 24211

Cache-Control: max-age=0

Upgrade-Insecure-Requests: 1

Origin: http://localhost:8080

Content-Type: multipart/form-data; boundary=----WebKitFormBoundarya1HOiexOytPpWx8U

User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/85.0.4183.121 Safari/537.36 Edg/85.0.564.63

Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.9

Sec-Fetch-Site: same-origin

Sec-Fetch-Mode: navigate

Sec-Fetch-User: ?1

Sec-Fetch-Dest: document

Referer: http://localhost:8080/restexample2/fileUploadView

Accept-Encoding: gzip, deflate, br

Accept-Language: ko,en;q=0.9,en-US;q=0.8,ja;q=0.7,de;q=0.6

Cookie: JSESSIONID=C1BB03C6629263A6B77084AA15CDF947

         }


파일명: sample-keyword.txt


[첨부(Attachments)]

sample-keyword.txt

sample-keyword.zip




* 맺음글(Conclusion)


실전에서 REST를 쉽고 빠르게 적용할 수 있는 방법이 없는지 충분히 고민하였다.

시중 책도 정말 많이 사보고, 인터넷 정보 검색, 공식 메뉴얼 등 삽질을 많이 하였다.


그리하여 이 프로젝트가 작성된 것이다.

REST를 쉽고 빠르게 사용할 수 있었으면 하는 바람이다.



* 참고자료(References)


[RESTEasy - 프로젝트에 대한 것]

1. A Guide to RESTEasy, https://www.baeldung.com/resteasy-tutorial, Accessed by 2020-09-28, Last Modified 2020-02-12.

2. tutorials/resteasy at master · eugenp/tutorials · GitHub, https://github.com/eugenp/tutorials/tree/master/resteasy, Accessed by 2020-09-28, Last Modified .

3. jersey, resteasy(JBoss)

[비고] 

-> resteasy는 현재 xml 출력을 못함. (POST, GET 등은 동작함)

-> jersey는 최신 버전도 인식을 못해버림.


[RestController, RESTful Service - Spring Framework ]

4. Building a RESTful Web Service, https://spring.io/guides/gs/rest-service/, Accessed by 2020-09-28, Last Modified .

[참고할 때 메모]

-> Library없이 가능한지 확인할 것 

[비고]

-> Gradle 기반과 Spring Boot로 작성되었는데, Gradle 생략하고 Spring Framework 작업은 동일해서 많은 도움을 받았다.


5. RestController에 대해 알아보자, https://milkye.tistory.com/283, Accessed by 2020-09-28, Last Modified 2018-12-02.

[비고]

매우 간단하게 RestController를 사용하는 방법에 대해서 소개하고 있음.


6. [Spring Error] No converter found for return value of type: class java.util.ArrayList, https://keichee.tistory.com/274, Accessed by 2020-09-28, Last Modified 2016-11-27.

[비고]: jackson-bind의 적용 방법에 대해서 소개하고 있음. 클래스 변환에 대한 오류 해결은 미약함.


[다중 업로드]

7. Spring MVC - 유연한 다중 파일업로드 핸들러 만들기 (Multipart upload), https://galid1.tistory.com/684, Accessed by 2020-09-28, Last Modified 2020-01-29.


8. 스프링 파일 업로드 처리, https://advenoh.tistory.com/26, Accessed by 2020-09-28, Last Modified 2019-01-01.



[RESTful - 게시판 설계]

9. 4. springboot restful 방식으로 게시판 변경, https://linked2ev.github.io/gitlog/2019/12/28/springboot-restful-4-rest-%EB%B0%A9%EC%8B%9D%EC%9C%BC%EB%A1%9C-%EB%B3%80%EA%B2%BD/ , Accessed by 2020-09-28, Last Modified 2019-12-28.

[비고]: REST 기반의 게시판 설계 아이디어를 많이 얻었음.


[RESTTemplate 관련]

10. Spring REST API 생성, 호출, https://www.leafcats.com/173, Accessed by 2020-09-28, Last Modified 2017.


11. 스프링 (Spring) RestTemplate으로 POST 파라미터 (Parameter) 전송해보기, https://soshanstory.tistory.com/entry/스프링-Spring-RestTemplate으로-POST-파라미터-Parameter-전송해보기, Accessed by 2020-09-28, Last Modified 2014-08-31.

[비고]: Rest Template 사용방법을 매우 간단하게 잘 적어놨음.


[공부하면서 메모]

- XML, Java 설정 방식이 있다. (REST 셋팅 관련 - 범위가 방대해지므로 줄임)


12. RestTemplate (4가지 - 자바 내에서 클라이언트 작업), https://howtodoinjava.com/spring-boot2/resttemplate/spring-restful-client-resttemplate-example/, Accessed by 2020-09-28, Last Modified 2015-03.

[비고]: 추천하고 싶음. 매우 RestTemplate에 대해서 가장 깔끔하게 잘 정리하였음.


(공식적으로는 HTML에서는 POST, GET만 지원함.)


13. RestTemplate list 반환하기, https://luvstudy.tistory.com/52, Accessed by 2020-09-28, Last Modified 2018-11-16.


[REST 한글 깨짐 Response]

14. spring에서 json response 한글 깨짐, https://thswave.github.io/spring/2015/02/22/korean-json-response.html, Accessed by 2020-09-28, Last Modified 2015-02-22.


[POST, GET 이외의 문제]

15. Using PUT method in HTML form, https://stackoverflow.com/questions/8054165/using-put-method-in-html-form, Accessed by 2020-09-28, Last Modified 2012.

[비고]: 8년 전 문제이긴 하지만, 현재에도 해당되는 문제이다.


16. REST - HTML Form에서 GET/POST만 지원하는 이유, http://haah.kr/2017/05/23/rest-http-method-in-html-form/, Accessed by 2020-09-28, Last Modified 2017-05-23.

[비고]: 이론적으로 REST에 대해서 아주 잘 소개하고 있는 사이트이다.


17. HTML 양식에 PUT 및 DELETE 메소드가없는 이유는 무엇입니까?, https://qastack.kr/software/114156/why-are-there-are-no-put-and-delete-methods-on-html-forms, Accessed by 2020-09-28, Last Modified .

[비고]: 토론 형태로 문제에 대해서 의견을 공유하고 있다.


18. [REST] PUT, PATCH, DELETE 미지원 처리, https://velog.io/@ette9844/REST-PUT-PATCH-DELETE-%EB%AF%B8%EC%A7%80%EC%9B%90-%EC%B2%98%EB%A6%AC, Accessed by 2020-09-28, Last Modified 2020-05-19.

[비고]: 이 코드는 동작하지 않음. (이론적으로 동작되는 방법이라고 보는 게 타당함.)


[번외의 주제: Node.js - REST]

19. REST API 예제, https://hyun-am-coding.tistory.com/entry/REST-API-%EC%98%88%EC%A0%9C, Accessed by 2020-09-28, Last Modified 2019-11-17.

[비고]: REST Client와 Server 흐름에 대해서 살펴볼 수 있다. 물론 자바 코드에 직접 도움은 되는 건 아니지만 동일하게 구성될 수 있다는 아이디어를 제공해준다.



반응형
728x90
300x250

[Spring-Framework] 17. Spring MVC, Spring Security 5.4, Oracle - 보안처리(로그인-Java) (2)


이번에는 이전 글에 이어서 Spring-Security를 구현해보려고 한다.

조금 이번 글부터는 난이도가 있어지니깐 개발 전략을 잘 숙지해서 작업하면 좋겠다.


1. [Spring-Framework] 16. Spring MVC, Spring Security 5.4, Oracle - 보안처리(로그인-Java) (1), 2020-09-27

https://yyman.tistory.com/1422



14. 개발 전략


하나 만들면 다 되는 게 아니다. 계속 연속해서 복합적으로 수정작업을 해줘야 한다.

그래서 개발 작업이 힘이 든다. 쉽지만 않다.


그림 24. 개발 전략도


SecurityWebApplicationInitializer.java, SecurityConfig.java는 Spring Security 구현에 있어서 핵심이라고 해도 무방하다.

두 개를 잘 구현한다면, 셈플 로그인 페이지는 볼 수 있다.


문제는 자바 버전으로 구현했을 때 보안 토큰 절차가 xml방식에 비해서 매우 까다롭게 반응한다는 것이다.

그래서 간단한 코드로 태스트를 해보기도 전에 DB 설계를 할 수 밖에 없었다.


이유는 토큰 인증 때문에 그렇다.


SqlMapSessionFactory도 계속 반복해서 다양한 영역에서 재사용될 것이다.



15. config의 SecurityWebApplicationInitializer.java (필수 파일)


이 파일을 보면, 제일 황당한 생각이 들 수 밖에 없는 이유가 코드는 몇 줄 안 되는데, 없으면 동작이 안 된다는 것이다.


package com.springMVC.javaSecurity5.config;


import org.springframework.security.web.context.AbstractSecurityWebApplicationInitializer;


public class SecurityWebApplicationInitializer 

                        extends AbstractSecurityWebApplicationInitializer {


}


파일명: SecurityWebApplicationInitializer.java


[첨부(Attachments)]

SecurityWebApplicationInitializer.zip



16. config의 SecurityConfig.java (필수 파일)


"SecurityConfig.java" 이 파일도 없으면 Spring Security with 자바 버전이 동작되지 않는다.


패키지 경로: com.springMVC.javaSecurity5.config


package com.springMVC.javaSecurity5.config;


import javax.sql.DataSource;


import org.springframework.beans.factory.annotation.Autowired;

import org.springframework.context.annotation.Bean;

import org.springframework.context.annotation.Configuration;

import org.springframework.security.config.annotation.authentication.builders.AuthenticationManagerBuilder;

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.WebSecurityConfigurerAdapter;

import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;

import org.springframework.security.crypto.password.PasswordEncoder;

import org.springframework.security.web.authentication.rememberme.JdbcTokenRepositoryImpl;

import org.springframework.security.web.authentication.rememberme.PersistentTokenRepository;

import org.springframework.security.web.csrf.CsrfFilter;

import org.springframework.web.filter.CharacterEncodingFilter;


import com.springMVC.javaSecurity5.db.SqlMapSessionFactory;

 

@Configuration

@EnableWebSecurity

public class SecurityConfig extends WebSecurityConfigurerAdapter {

 

    @Autowired

    PasswordEncoder passwordEncoder;

    

    @Autowired

    private CustomAuthenticationProvider authProvider;

    

    protected void configure(AuthenticationManagerBuilder auth, HttpSecurity http) throws Exception {

      

    CharacterEncodingFilter filter = new CharacterEncodingFilter();


    /* UTF-8 한글 보완 */

        filter.setEncoding("UTF-8");

        filter.setForceEncoding(true);

        http.addFilterBefore(filter,CsrfFilter.class);

   

        auth

        .authenticationProvider(authProvider);

        

    /* 현재 - 임시

        auth.inMemoryAuthentication()

        .passwordEncoder(passwordEncoder)

        .withUser("user").password(passwordEncoder.encode("1234")).roles("RULE_USER")

        .and()

        .withUser("admin").password(passwordEncoder.encode("1234")).roles("RULE_USER", "RULE_ADMIN");

        

         */

        

        // withUser("admin").password(passwordEncoder.encode("1234")).roles("USER", "ADMIN");

      

        /* 임시

   

    UserBuilder users = User.withDefaultPasswordEncoder();

      auth.inMemoryAuthentication()

        .withUser(users.username("admin").password("1234").roles("USER"))

        .withUser(users.username("user").password("1234").roles("ADMIN"));

    */

    }

 

 

    @Override

    protected void configure(HttpSecurity http) throws Exception {


        http.authorizeRequests()

        

        // index

        .antMatchers("/")

            .permitAll()


         // 접근 오류

     .antMatchers("/member/accessDenied")

         .permitAll()

         

         .antMatchers("/member/accessDeniedView")

         .permitAll()    


         // 회원 로그인 기능

     .antMatchers("/member/loginForm")

         .permitAll()

         

     // 관리자 페이지 기능

        .antMatchers("/admin/**")

        .hasRole("ADMIN")

        // "RULE_ADMIN이라고 DB에 입력되어 있다면, RULE_은 제거하고 입력해야 인식함."

        

        // 폼 로그인 명세

        .and()

            .formLogin()

        .permitAll()

                .loginPage("/member/loginForm")

                .failureForwardUrl("/member/loginForm?error")

                .defaultSuccessUrl("/")

                .usernameParameter("id")

                .passwordParameter("password")

       

        // 로그아웃 처리

.and()

                .logout()

                .logoutUrl("/logout")

                .logoutSuccessUrl("/")

                .invalidateHttpSession(true)

                .deleteCookies("JSESSION_ID")

                .deleteCookies("remember-me")

        // 로그인 

            .and()

            .rememberMe()

            .tokenValiditySeconds(604800)

            .tokenRepository(persistentTokenRepository())

            // 예외처리(

            .and()

        .exceptionHandling()

        .accessDeniedPage("/member/accessDenied")

        // csrf 설정

            .and()

                .csrf().disable();

       

        

            

        

    /*

        http.authorizeRequests()

        .antMatchers("/login")

            .permitAll()

        .antMatchers("/**")

            .hasAnyRole("ADMIN", "USER")

            .hasAnyAuthority("RULE_ADMIN", "RULE_USER")

        .and()

            .formLogin()

            .loginPage("/login")

            .defaultSuccessUrl("/")

            .failureUrl("/login?error=true")

            .permitAll()

            

            .loginPage("/login")

            .usernameParameter("email")

            .passwordParameter("password")

            .successHandler(successHandler())      

            .failureHandler(failureHandler())

            .permitAll();

            

        .and()

            .logout()

            .logoutSuccessUrl("/login?logout=true")

            .invalidateHttpSession(true)

            .permitAll()

        .and()

            .csrf()

            .disable();

    */

        

    }

    

    // 로그아웃 Persistent_Logins에 관한 설정   (주석 해도 무방)...

    @Bean 

public PersistentTokenRepository persistentTokenRepository() {

JdbcTokenRepositoryImpl db = new JdbcTokenRepositoryImpl();

DataSource usrDS = getDataSource();

db.setDataSource(usrDS);

return db;

}

    


    // DataSource 불러오기    (주석 해도 무방)

@Bean

public DataSource getDataSource() {

       // BasicDataSource dataSource = new BasicDataSource(); - Apache DBCP2

SqlMapSessionFactory factory = SqlMapSessionFactory.getInstance();

       return factory.getOracleDataSource(); // 오라클 적용함.

}

    

// 비밀번호 생성 - 암호(BCryptPasswordEncoder)

    @Bean

    public PasswordEncoder passwordEncoder() {

        return new BCryptPasswordEncoder();

        

    }

    

}


파일명: SecurityConfig.java


[첨부(Attachments)]

SecurityConfig.zip



비고: 주석 잘 쳐서 정리해서 빌드해보면, 내장 로그인 페이지를 볼 수 있다.
       - SecurityWebApplicationInitializer.java, SecurityConfig.java 두 개 파일의 힘이 얼마나 큰지 실감 해볼 수 있다.

         Spring-Framework 설정 전체를 제어해버린다고 해도 된다.




17. 로그인 인증 - Spring Security (CustomAuthenticationProvider.java)


이 코드 부분은 찾아보려고 해도 쉽게 나오지 않는다. 어려운 부분 중 하나이다.

공개하는 이유는 삽질을 적게 하라는 의미이다.


패키지 경로: com.springMVC.javaSecurity5.config


package com.springMVC.javaSecurity5.config;


import java.util.List;


import org.springframework.security.authentication.AuthenticationProvider;

import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;

import org.springframework.security.core.Authentication;

import org.springframework.security.core.GrantedAuthority;

import org.springframework.security.core.userdetails.UserDetails;

import org.springframework.security.core.userdetails.UserDetailsService;

import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;

import org.springframework.stereotype.Component;


import com.springMVC.javaSecurity5.service.CustomUserDetailsService;


@Component

public class CustomAuthenticationProvider implements AuthenticationProvider {

    

    private UserDetailsService userDeSer;

 

    @Override

    public Authentication authenticate(Authentication authentication) {

        

        String username = (String) authentication.getPrincipal();

        String password = (String) authentication.getCredentials();

        

        if ( username.equals("fail")) {

        System.out.println("(에러)아이디: 실패");

        return null;

        }

        

        // DB 정보 읽기

        userDeSer = new CustomUserDetailsService();

        UserDetails userDetail = userDeSer.loadUserByUsername(username);

        

        @SuppressWarnings("unchecked")

  List<GrantedAuthority> roles = (List<GrantedAuthority>) userDetail.getAuthorities();

        

        // 권한

        System.out.println("DB불러오기-권한:" + userDetail.getAuthorities());

        System.out.println("DB불러오기-비밀번호:" + userDetail.getPassword());

        System.out.println("roles:" + roles.get(0));

        

        if ( !matchPassword(password, userDetail.getPassword())) {

        System.out.println("(에러)비밀번호: 불일치" + password);

        return null;

        }

        

        UsernamePasswordAuthenticationToken result =

        new UsernamePasswordAuthenticationToken(username, password, roles);

        

        result.setDetails(userDetail);

        

        return result;

    }

 

    @Override

    public boolean supports(Class<?> authentication) {

        return true;

    }

    

    private boolean matchPassword(String loginPwd, String password) {

   

        BCryptPasswordEncoder secure = new BCryptPasswordEncoder();

        return secure.matches(loginPwd, password);

    }

 

}


파일명: CustomAuthenicationProvider.java


[첨부(Attachments)]

CustomAuthenticationProvider.zip


어디에 구체적으로 사용되는 부분인가?


사용하는 영역은 SecurityConfig.java에 http의 auth의 .authenicationProvider()에 사용된다.



그림 25. SecurityConfig.java


왜 이 코드를 사용하는지 소개해본다면,

"No AuthenticationProvider found for org.springframework.security.authentication.UsernamePasswordAuthenticationToken"

이 문제가 디버그 오류창에 뜨는 것을 볼 수 있다.


기본 내장형 계정 생성 등으로 인증을 시도하면 xml방식에서는 처리해줬는데, java방식에서는 인증받지 못한다.


     /* 현재 - 임시


        auth.inMemoryAuthentication()

        .passwordEncoder(passwordEncoder)

        .withUser("user").password(passwordEncoder.encode("1234")).roles("RULE_USER")

        .and()

        .withUser("admin").password(passwordEncoder.encode("1234")).roles("RULE_USER", "RULE_ADMIN");

        

        */


[문제가 발생되는 기본형 - 코드]


이 코드로 작업하면, xml에서는 동작되었던 부분이 동작되질 않는다.


토큰 인증도 해결할 겸 "CustomAuthenicationProvider.java를 설계해서 문제를 해결한 것이다.




18. SQL (Factory) - SqlMapSessionFactory.java


나중에 CP(Connection Pool, 커넥션 풀)이라고 불리는 것으로 구현해봐도 좋을 듯 싶다.

iBatis를 남용해서 프로젝트에 기본마냥 소개하는 책들이 무척 많은데 기본은 순수한 DB를 사용하는 것부터 출발하는 것이다.


iBatis는 SQL 코드 개발 등에서 생산성이 좋아지는 도구 중 하나이지만, 필수 사항은 아니라고 본다.

차라리 필수 사항을 꼽아본다면, 커넥션 풀을 하나 추천해보고 싶다.


아무튼 커넥션 풀 주제가 아니기 때문에 생략한다.


패키지 경로: com.springMVC.javaSecurity5.db


package com.springMVC.javaSecurity5.db;


import java.io.IOException;

import java.sql.Connection;

import java.sql.DriverManager;

import java.sql.PreparedStatement;

import java.sql.ResultSet;

import java.sql.SQLException;

import java.util.Properties;


import javax.sql.DataSource;


import oracle.jdbc.pool.OracleDataSource;


public class SqlMapSessionFactory {

private static SqlMapSessionFactory factory = new SqlMapSessionFactory();


private SqlMapSessionFactory() {}


private final String driverName = "oracle.jdbc.driver.OracleDriver";

private final String dbUrl = "jdbc:oracle:thin:@127.0.0.1:1521:orcl";

private final String userName = "{사용자계정명}";

private final String userPassword = "{비밀번호}";

public static SqlMapSessionFactory getInstance() {

return factory;

}


/*

*     public static DataSource getMySQLDataSource() {

        Properties props = new Properties();

        FileInputStream fis = null;

        MysqlDataSource mysqlDS = null;

        try {

            fis = new FileInputStream("db.properties");

            props.load(fis);

            mysqlDS = new MysqlDataSource();

            mysqlDS.setURL(props.getProperty("MYSQL_DB_URL"));

            mysqlDS.setUser(props.getProperty("MYSQL_DB_USERNAME"));

            mysqlDS.setPassword(props.getProperty("MYSQL_DB_PASSWORD"));

        } catch (IOException e) {

            e.printStackTrace();

        }

        return mysqlDS;

    }

    */

    public DataSource getOracleDataSource(){

        

    OracleDataSource oracleDS = null;

        

    try {

            oracleDS = new OracleDataSource();

            oracleDS.setURL(dbUrl);

            oracleDS.setUser(userName);

            oracleDS.setPassword(userPassword);

        } catch (SQLException e) {

            e.printStackTrace();

        }

        return oracleDS;

        

    }

public Connection connect() {


Connection conn = null;


try {

Class.forName(driverName);

conn = DriverManager.getConnection(dbUrl, userName, userPassword);

}

catch(Exception ex) {

System.out.println("오류 발생: " + ex);

}


return conn;


}


public void close(Connection conn, PreparedStatement ps, ResultSet rs) {


if ( rs != null ) {


try {

rs.close();

}

catch(Exception ex) {

System.out.println("오류 발생: " + ex);

}


close(conn, ps); // Recursive 구조 응용(재귀 함수)


} // end of if


}


public void close(Connection conn, PreparedStatement ps) {


if (ps != null ) {

try {

ps.close();

}

catch(Exception ex) {

System.out.println("오류 발생: " + ex);

}

} // end of if


if (conn != null ) {

try {

conn.close();

}

catch(Exception ex) {

System.out.println("오류 발생: " + ex);

}

} // end of if


}

}



파일명: SqlMapSessionFactory.java


[첨부(Attachments)]

SqlMapSessionFactory.zip



비고: 재사용이 가능한 형태로 설계하였다.



19. Model - CustomUserDetails.java


순수한 Model 형태는 아니고, 부분 개량하였다.

Spring-Security에서 제공하는 UserDetails(인터페이스)를 Model 클래스에 구현해야 한다.

List<role> 기능 문제 등으로 인해서 String authorities를 List<GrantedAuthority>로 변경하였다.


@Override 된 부분들이 UserDetails에 정의된 내용이다.


패키지 경로: com.springMVC.javaSecurity5.model


package com.springMVC.javaSecurity5.model;


import java.util.ArrayList;

import java.util.Collection;

import java.util.List;


import org.springframework.security.core.GrantedAuthority;

import org.springframework.security.core.authority.SimpleGrantedAuthority;

import org.springframework.security.core.userdetails.UserDetails;


public class CustomUserDetails implements UserDetails{


private static final long serialVersionUID = 1L;

private String username;

    private String password;

    // 개량함. (다중 권한 고려)

    private List<GrantedAuthority> authorities;

    private boolean enabled;

        

public String getUsername() {

return username;

}


public void setUsername(String username) {

this.username = username;

}


public String getPassword() {

return password;

}

public void setPassword(String password) {

this.password = password;

}

     public void setAuthority(String authority) {

// 권한 객체 생성

if ( authorities == null ) {

authorities = new ArrayList<GrantedAuthority>();

}

// 권한 추가

SimpleGrantedAuthority grantObj = new SimpleGrantedAuthority(authority);

authorities.add(grantObj);

     }


public boolean getEnabled() {

return enabled;

}

public void setEnabled(boolean enabled) {

this.enabled = enabled;

}


@Override

public Collection<? extends GrantedAuthority> getAuthorities() {


        return authorities;


}


@Override

public boolean isAccountNonExpired() {

// TODO Auto-generated method stub

return false;

}


@Override

public boolean isAccountNonLocked() {

// TODO Auto-generated method stub

return false;

}


@Override

public boolean isCredentialsNonExpired() {

// TODO Auto-generated method stub

return false;

}


@Override

public boolean isEnabled() {

// TODO Auto-generated method stub

return false;

}

    

}


파일명: CustomUserDetails.java


[첨부(Attachments)]

CustomUserDetails.zip





20. Service - CustomUserDetailsService.java


CustomUserDetailsService는 Spring-Security의 "UserDetailsService(인터페이스)"로 정의된 내용을 구현하는 것이다.

인터페이스의 영향도 있지만, DB를 실제로 불러올 때 사용자 관점에서 처리되는 부분이라고 본다.


package com.springMVC.javaSecurity5.service;


import org.springframework.security.core.userdetails.UserDetailsService;

import org.springframework.security.core.userdetails.UsernameNotFoundException;


import com.springMVC.javaSecurity5.dao.SqlSessionTemplate;

import com.springMVC.javaSecurity5.model.CustomUserDetails;


public class CustomUserDetailsService implements UserDetailsService {

    

    private SqlSessionTemplate sqlSession = SqlSessionTemplate.getInstance();

 

    public CustomUserDetails getUserById(String username) {

        return sqlSession.selectOne("user.selectUserById", username);

    }

 

    public CustomUserDetails loadUserByUsername(String username) throws UsernameNotFoundException {


    CustomUserDetails user = sqlSession.selectOne("null", username);

        

        if(user==null) {

            throw new UsernameNotFoundException(username);

        }

        return user;

    }

    

}


파일명: CustomUserDetailsService.java


[첨부(Attachments)]

CustomUserDetailsService.zip




21. DAO - SqlSessionTemplate.java


실제 DB를 구현하는 부분이다.


package com.springMVC.javaSecurity5.dao;


import java.sql.Connection;

import java.sql.PreparedStatement;

import java.sql.ResultSet;


import com.springMVC.javaSecurity5.db.SqlMapSessionFactory;

import com.springMVC.javaSecurity5.model.CustomUserDetails;


public class SqlSessionTemplate {


private SqlSessionTemplate() {}

private static SqlSessionTemplate sqlTemplate;

    private static SqlMapSessionFactory session; 

    

    public static SqlSessionTemplate getInstance(){

   

        if(sqlTemplate == null){

        sqlTemplate = new SqlSessionTemplate();

            session = SqlMapSessionFactory.getInstance();

        }


        return sqlTemplate;

    }

    

// 추후 iBatis 고려

public CustomUserDetails selectOne(String id, String username) {

Connection conn = null;

    PreparedStatement pstmt = null;

    ResultSet rs = null;

   

    CustomUserDetails node = null;


    String sql = "select g1.username, g1.password, g2.authority, " + 

       "g1.enabled from comp_users g1, comp_authorities g2 where g1.username = g2.username " +

       "and g1.username = ?";


    System.out.println(sql);


    try {


    conn = session.connect();


    pstmt = conn.prepareStatement(sql);

    pstmt.setString(1, username);

   

    rs = pstmt.executeQuery();


    while ( rs.next() ) {

   

    // 데이터가 존재할 때, 노드 생성

    node = new CustomUserDetails();

    node.setUsername(rs.getNString(1));

    node.setPassword(rs.getNString(2));

    node.setAuthority(rs.getNString(3));

    System.out.println("rs:" + rs.getNString(3));

    node.setEnabled(rs.getBoolean(4));

    }


    }catch(Exception ex) {

    System.out.println("오류 발생: " + ex);

    }

    finally {

    session.close(conn, pstmt, rs);

    }


    return node;

}

}



파일명: SqlSessionTemplate.java


[첨부(Attachments)]

SqlSessionTemplate.zip



* 3부에서는 View에 대해서 구현하는 방법을 소개하겠다.


3부에서는 "jsp 파일" 등 사용자 인터페이스 화면에 대해서 소개하겠다.


1. [Spring-Framework] 17. Spring MVC, Spring Security 5.4, Oracle - 보안처리(로그인-Java) (3), 2020-09-27

https://yyman.tistory.com/1424


반응형

+ Recent posts