이번에 학원에서 마이바티스를 활용한 MVC 패턴을 한 번 짜봤는데요. 이전에 스프링 부트로 간이 프로젝트를 할 때는 몰랐는데, 막상 해당 시간을 가지다보니 "이래서 근본을 아는게 중요하구나" 라는 생각이 막 꽂혀서 되게 의미있는 시간이었다고 생각합니다.
물론 해당 시간에 제가 급하게 병원에 입원을 하는 바람에 코드의 나머지를 병원에서 작성해야만 했지만, 그래도 해당 패턴을 하나 하나 뜯어보고나니 "아... 되게 심오하면서 재밌다 ㅋㅋ" 라는 생각을 지울 수는 없더군요.
아무튼 이번 프로젝트에 사용된 프로젝트 구조인 MVC패턴을 잠깐 소개해 드리자면
(자료 출처 : https://developer.mozilla.org/ko/docs/Glossary/MVC)
- 사용자의 요청을 VIEW가 받아서 Controller에 전송
- Controller는 요청을 정제화 해서 Model에 전송
- Model은 전송 받은 요청을 이용해 DB를 업데이트 한 뒤 그 결과를 VIEW에 전달
- VIEW는 최종적으로 전달 받은 정보를 화면에 업데이트
그리고 위 로직을 좀 더 자세히 설명드리자면 아래의 그림, 설명과 같습니다.
(자료 출처 : https://hyuntaekhong.github.io/til/TIL-200824/)
- 클라이언트의 요청이 Controller로 제출됨 (submit)
- Controller는 요청에 해당하는 Service 객체에 요청
- Service 객체는 DAO를 호출하여 DB 데이터를 전달 받도록 요청
- DAO객체는 Mybatis를 이용하는 Mapper.xml을 이용해 작업을 수행한 뒤 결과를 반환
- 데이터는 DAO - Service - Controller로 역순으로 전달
- Controller는 요청에 맞는 JSP로 응답
- JSP 페이지는 응답 정보를 사용자에게 보여줌
여기서 의문이 들었던건 왜 Service를 거쳐 DAO로 가는지? 그냥 Service에서 다 해도 되지 않는지에 대한 의문이었는데요.
도달한 결론만 좀 간추려 정리하자면 DAO는 DB와의 상호작용에 집중하고, Service는 데이터 가공과 같이 특정 비즈니스 로직에 좀 더 집중을 하는 역할을 수행하기 위해 모듈화 했다고 생각이 됩니다. (물론 첫 시간이라 DAO 쪽에서 데이터를 정제하고 있긴 하지만 ㅎ...)
우선 JSP 페이지는 다섯 페이지로 구성이 되어있으며, 라이브러리는 ojdbc와 마이바티스, taglib(아파치 태그 lib을 사용하기 위함. c만 임포트 해도 됨)를 폴더와 빌드 패스에 추가해 줬습니다.
그 다음 자바 파일의 각 패키지에 Controller, VO, Service, DAO 및 마이바티스 환경 설정을 위한 xml 파일과 매퍼 xml 파일, 커넥션 풀 관리 파일을 다음과 같이 만들어 줬습니다.
이번 섹션에서는 마이바티스를 활용했는데요. 여기서 마이바티스란 자바 앱에서 DB와의 상호작용을 효율적으로 처리하게 해주는 프레임워크로, 그냥 편하게 JPA와 같은 역할을 하는 친구라고 생각하시면 되는데요.
마이바티스에 대한 개념글 정리를 굉장히 잘한 블로그가 있어서 아래의 블로그를 먼저 한 번 훑어보시고 난 뒤 해당 프로젝트의 설명을 참고하시면 좀 더 이해가 되시지 않을까 하여 소개를 드리고자 합니다.
오라클 DB의 경우 오라클에서 기본적으로 제공 해주는 HR 계정의 employees 테이블을 사용했는데요. 해당 테이블 사용을 위해서는 다음 명령어와 같이 언락을 해주어야 사용이 가능합니다.
ALTER USER hr IDENTIFIED BY 1234 ACCOUNT UNLOCK;
해당 계정을 언락 해주고 접속 계정을 다음과 같이 만들어 줬습니다.
그리고 해당 계정으로 접속하여 employees 테이블을 조회하면 다음과 같이 구조를 확인할 수 있죠.
select * from employees; desc employees;
그럼 다음 파일들의 로직에 대해서 설명해 드리고자 하는데요. 제가 현재 병원에 있는 상태로 (마취가 덜 풀렸 ㅇ...) 주석으로 설명을 대체하고자 합니다.
그래도 최소한 주석을 달려고 노력을 했으니 가엾게 봐주시면 감사하겠습니다 ㅜㅜ
<%@ page language="java" contentType="text/html; charset=UTF-8" pageEncoding="UTF-8"%> <!DOCTYPE html> <html> <head> <meta charset="UTF-8"> <title>Insert title here</title> <style type="text/css"> div{ width: 800px; text-align: center; margin: auto; } table{ width: 100%; border-collapse: collapse; } th, td{ border: 1px solid gray; } th{ padding: 5px; background-color: darkslateblue; color: white; } </style> <script type="text/javascript"> function search1(){ location.href='/chapter16_search/Controller?cmd=allList'; } function search2(){ location.href='/chapter16_search/Controller?cmd=inputDept'; } function search3(){ location.href='/chapter16_search/Controller?cmd=inputDynamic'; } </script> </head> <body> <div> <h1>원하는 검색 버튼 클릭</h1> <form> <table> <tr> <th>검색 버튼을 클릭하세요.</th> </tr> <tr> <td> <input type="button" value="전체 직원 보기" onclick="search1()"> <input type="button" value="부서별 검색" onclick="search2()"> <input type="button" value="동적 검색" onclick="search3()"> </td> </tr> </table> </form> </div> </body> </html>
<%@ page language="java" contentType="text/html; charset=UTF-8" pageEncoding="UTF-8"%> <!DOCTYPE html> <html> <head> <meta charset="UTF-8"> <title>Insert title here</title> <style type="text/css"> div{ width: 800px; text-align: center; margin: auto; } table{ width: 100%; border-collapse: collapse; } th, td{ border: 1px solid gray; } th{ padding: 5px; background-color: darkslateblue; color: white; } </style> </head> <body> <div> <h1>부서를 선택하고 검색 버튼을 누르세요.</h1> <form> <table> <tr> <th>부서 검색</th> </tr> <tr> <td> <select name="department_id"> <option value="" disabled selected>-부서선택-</option> <option value="10" >10</option> <option value="20">20</option> <option value="30">30</option> <option value="40">40</option> <option value="50">50</option> <option value="60">60</option> <option value="70">70</option> <option value="80">80</option> <option value="90">90</option> <option value="100">100</option> <option value="110">110</option> </select> <input type = "hidden" value = "deptList" name = "cmd"> <input type="button" value="검색" onclick="dynamicSearching(this.form)"> </td> </tr> </table> </form> <button style = "margin-top : 20px;" onclick = "location.href='index.jsp'">홈으로 돌아가기</button> </div> </body> <script type="text/javascript"> function dynamicSearching(f){ f.action="/chapter16_search/Controller" f.submit(); } </script> </html>
<%@ page language="java" contentType="text/html; charset=UTF-8" pageEncoding="UTF-8"%> <!DOCTYPE html> <html> <head> <meta charset="UTF-8"> <title>Insert title here</title> <style type="text/css"> div{ width: 800px; text-align: center; margin: auto; } table{ width: 100%; border-collapse: collapse; } th, td{ border: 1px solid gray; } th{ padding: 5px; background-color: darkslateblue; color: white; } </style> <script type="text/javascript"> function search1(){ location.href='/chapter16_search/Controller?cmd=allList'; } function search2(){ location.href='/chapter16_search/Controller?cmd=inputDept'; } function search3(){ location.href='/chapter16_search/Controller?cmd=inputDynamic'; } </script> </head> <body> <div> <h1>무엇을 검색 하시겠습니까?</h1> <form> <table> <tr> <th>검색 옵션</th> </tr> <tr> <td> 선택 검색 : <select name = "type" onchange = "placeholderChanger(this)"> <option value = "employee_id">직원 ID</option> <option value = "full_name">직원 이름</option> <option value = "job_id">직급</option> <option value = "email">직원 이메일</option> <option value = "phone_number">직원 전화번호</option> <option value = "hire_date">입사 일자</option> </select> </td> </tr> <tr> <td> 입력 검색 : <input type = "text" placeholder="ID를 입력해 주세요." id = "searching_content" name = "searching_content" onkeydown="handleKeydown(event, this.form)"> </td> </tr> <tr> <td> <input type = "hidden" value="dynamicList" name = "cmd"> <input type = "button" value = "검색" onclick = "dynamicSearching (this.form)"> </td> </tr> </table> </form> <button style = "margin-top : 20px;" onclick = "location.href='index.jsp'">홈으로 돌아가기</button> </div> </body> <script type="text/javascript"> // 사용자가 선택한 옵션 값과 입력한 값을 checkInfo 함수로 체킹하고 반환 받은 논리값을 이용해 // 무사히 통과 받은 상태(true인 상태) 라면 컨트롤러로 폼들을 제출함 function dynamicSearching(f){ // select의 value값은 type(type in this case(option)), // 즉 해당 option의 value값으로 정의되어 있다고 함. let option = f.type.value; let searchValue = f.searching_content.value; let state = checkInfo(option, searchValue); if(state){ f.action = "/chapter16_search/Controller" f.submit(); } } // 각 입력값들이 유효한지를 선택된 select의 옵션 값과 사용자 입력값을 이용하여 비교 function checkInfo(option, value){ let checkId, checkName, checkEmail, checkNumber, checkJob, checkHireDate; // 옵션(사용자가 검색하고자 하는 카테고리)에 따라 조건문을 타고 미리 정의해 둔 // 각 변수들에 정규식을 할당한 뒤 exec 메서드를 호출해 정규식에 해당 사용자의 입력값이 유효한지를 검사 // 유효하지 않은 경우 경고 문구 후 함수 종료 if(option === "employee_id"){ // id는 정수 범위에서 최소1 최대4자리까지 (늘어날 사원들의 id를 생각) checkId = /^[0-9]{1,4}$/; if(!checkId.exec(value)){ alert("올바른 사원의 ID를 입력해 주세요. (예시 : 100, 200)"); return false; } }else if(option === "full_name"){ // full name은 first_name과 last_name을 공백을 두고 서버에 전달할것이기 때문에 // 사용자에게 알파벳 범위 내에서 공백을 사이에 두고 한 자 이상씩을 받도록 설정 let checkName = /^[a-zA-Z]+ [a-zA-Z]+$/; if(!checkName.exec(value)){ alert("올바른 사원의 이름을 입력해 주세요. (예시 : Steven King, neena kochhar)") return false; } }else if(option === "email"){ // 이메일의 경우 단순히 연속되는 문자열로만 구성되어 있기 때문에 1자 - 20자의 알파벳 범위 안에서 체킹 let checkEmail = /^[a-zA-Z]{1,20}$/; if(!checkEmail.exec(value)){ alert("올바른 사원의 이메일을 1자 이상 입력해 주세요. (예시 : sking, LDEHAAN)") return false; } }else if(option === "phone_number"){ // phone_number의 경우 점(.)을 기준으로 3-3-4 자리와 3-2-4-6자리, 두 종류로 나뉘어짐 // 그래서 입력되는 값은 점을 사이에 두고 3-3-4 자리의 정수값이거나 3-2-4-6자리의 정수로 // 선택적으로 사용자에게 받음 let checkNumber = /^(?:\d{3}\.\d{3}\.\d{4}|\d{3}\.\d{2}\.\d{4}\.\d{6})$/; if(!checkNumber.exec(value)){ alert("올바른 사원의 전화번호를 입력해 주세요. (예시 : 515.123.7777 또는 011.44.1644.429267)"); return false; } }else if(option === "job_id"){ // 사원의 직급은 앞이 부서고 뒤가 직급을 의미함. 그래서 무난하게 알파벳 열자리로 입력 받도록 함 let checkJob = /^[a-zA-Z]{1,10}$/; if(!checkJob.exec(value)){ alert("올바른 사원의 직급을 입력해 주세요. (예시 : clerk 또는 MGR)"); return false } }else if (option === "hire_date") { // 입사 일자의 경우 RR(세기 구분 연도)/MM(월)/DD(일) 형식으로 저장되어 있는데 // 이에 대한 정규식으로 슬래쉬(/)로 구분지어 각 두자리씩 00년부터 39년 중 하나, // 01월부터 12월 중, 01일부터 31일 중 하나의 값을 가지도록 함 let checkHireDate = /^(0[0-9]|1[0-9]|2[0-9]|3[0-9])\/(0[1-9]|1[0-2])\/(0[1-9]|[1-2][0-9]|3[0-1])$/; if (!checkHireDate.test(value)) { alert("올바른 사원의 입사 일자를 입력해 주세요. (예시 : 07/01/01)"); return false; } } return true; } // 검색 창에 Enter 이벤트 키가 발생할 때 기본 동작 방식을 방지하고 dynamicSearching 함수를 실행 // 검색 버튼을 눌렀을 때와 동일한 과정이 진행 function handleKeydown(event, form) { if (event.key === 'Enter') { event.preventDefault(); dynamicSearching(form); } } // 옵션 변경시 입력 내용 초기화하는 함수 function placeholderChanger(option) { let text; // this(select)의 value(선택된 option 중 하나의 값)에 따라 text에 선택지의 문구를 골라 할당 switch (option.value) { case "employee_id": text = "ID를 입력해 주세요."; break; case "full_name": text = "이름을 입력해 주세요."; break; case "email": text = "이메일을 입력해 주세요."; break; case "phone_number": text = "전화번호를 입력해 주세요."; break; case "job_id": text = "직급을 입력해 주세요."; break; case "hire_date": text = "입사일자를 입력해 주세요."; break; } // 검색 입력 요소 선택 후 placeholder를 type(선택된 옵션의 name)에 따라 할당 된 text값으로 할당 // 이때 value(입력 상태)도 빈 문자열로 같이 초기화 let item = document.querySelector('#searching_content') item.placeholder = text; item.value = ""; } </script> </html>
<%@page import="org.joonzis.vo.EmployeeVO"%> <%@page import="java.util.List"%> <%@ page language="java" contentType="text/html; charset=UTF-8" pageEncoding="UTF-8"%> <%@ taglib prefix="c" uri="http://java.sun.com/jsp/jstl/core" %> <!DOCTYPE html> <html> <head> <meta charset="UTF-8"> <title>Insert title here</title> <style type="text/css"> div{ width: 800px; text-align: center; margin: auto; } table{ width: 100%; border-collapse: collapse; } th, td{ border: 1px solid gray; } th{ padding: 5px; background-color: darkslateblue; color: white; } </style> </head> <body> <div> <h1>전체 직원 목록</h1> <table> <thead> <tr> <th>직원ID</th> <th>직원이름</th> <th>직원연락처</th> <th>직원연봉</th> <th>부서ID</th> <th>고용일</th> </tr> </thead> <!-- 2명 이상의 직원들의 리스트를 받을 때 출력되는 곳으로 request에 세팅 된 list 속성이 비어있지 않은 상태라면 forEach 태그로 하나하나 꺼낸 요소들(vo)의 각 필드값들을 출력하도록함. --> <tbody> <c:choose> <c:when test="${not empty list}"> <c:forEach var="vo" items="${list}"> <tr> <td>${vo.job_id}</td> <td>${vo.first_name} ${vo.last_name}</td> <td>${vo.phone_number}</td> <td>${vo.salary}</td> <td>${vo.department_id}</td> <td>${vo.hire_date}</td> </tr> </c:forEach> </c:when> <c:otherwise> <tr> <td colspan="6">데이터가 존재하지 않습니다. 올바른 값을 입력해 주세요.</td> </tr> </c:otherwise> </c:choose> </tbody> </table> <button style = "margin-top : 20px;" onclick = "location.href='index.jsp'">홈으로 돌아가기</button> </div> </body> </html>
<%@page import="org.joonzis.vo.EmployeeVO"%> <%@page import="java.util.List"%> <%@ page language="java" contentType="text/html; charset=UTF-8" pageEncoding="UTF-8"%> <%@ taglib prefix="c" uri="http://java.sun.com/jsp/jstl/core" %> <!DOCTYPE html> <html> <head> <meta charset="UTF-8"> <title>Employee List</title> <style type="text/css"> div { width: 800px; text-align: center; margin: auto; } table { width: 100%; border-collapse: collapse; } th, td { border: 1px solid gray; } th { padding: 5px; background-color: darkslateblue; color: white; } </style> </head> <body> <div> <!-- 동적으로 전달 받은 한 명의 개인 정보를 출력하는 페이지 --> <h1>${person.first_name} ${person.last_name} 직원 목록</h1> <table> <thead> <tr> <th>직원ID</th> <th>직원이름</th> <th>직원연락처</th> <th>직원연봉</th> <th>부서ID</th> <th>고용일</th> </tr> </thead> <!-- 전달받은 request의 속성 값(person)이 비어있지 않은 경우 각 값들을 순차적으로 꺼내어 표시 --> <!-- 전달받은 request의 속성 값(person)이 비어있는 경우 데이터가 존재하지 않음을 표시 --> <tbody> <c:choose> <c:when test="${person != null}"> <tr> <td>${person.employee_id}</td> <td>${person.first_name} ${person.last_name}</td> <td>${person.phone_number}</td> <td>${person.salary}</td> <td>${person.department_id}</td> <td>${person.hire_date}</td> </tr> </c:when> <c:otherwise> <tr> <td colspan="6">데이터가 존재하지 않습니다. 올바른 값을 입력해 주세요.</td> </tr> </c:otherwise> </c:choose> </tbody> </table> <button style = "margin-top : 20px;" onclick = "location.href='index.jsp'">홈으로 돌아가기</button> </div> </body> </html>
package org.joonzis.controller; import java.io.IOException; import java.util.HashMap; import java.util.List; import java.util.Map; import javax.servlet.ServletException; import javax.servlet.annotation.WebServlet; import javax.servlet.http.HttpServlet; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; import org.joonzis.service.EmployeeService; import org.joonzis.service.EmployeeServiceImpl; import org.joonzis.vo.EmployeeVO; @WebServlet("/Controller") public class Controller extends HttpServlet { private static final long serialVersionUID = 1L; public Controller() { super(); } protected void doGet(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException { request.setCharacterEncoding("utf-8"); response.setContentType("text/html; charset=utf-8"); // 어떤 경로로 이동할건지가 담긴 cmd 형태의 요청 파라미터를 추출하여 cmd 변수에 할당 // 이 변수는 switch문의 분기점으로 활용될 예정 String cmd = request.getParameter("cmd"); //단순 화면 이동인지 데이터를 전달하는 이동인지 구분해야함 // 그래서 forward 방식일 경우 isForward를 true로 지정한 뒤 // 조건값으로 forward문을 태울거고, false일 경우 sendRedirect문을 태울거임 // 일종의 플래그와도 같음 boolean isForward = false; // forward와 sendRedirect의 경로가 할당될 변수 String path = ""; // 서비스 인스턴스 생성 EmployeeService service = new EmployeeServiceImpl(); // request에 세팅할 다중 속성(EmployeeVO 타입의 List)을 받을 list 변수와 // 단일 속성(EmployeeVO person 변수를 미리 선엉ㄴ List<EmployeeVO> list = null; EmployeeVO person = null; // cmd 값에 따라 switch문 실행 switch (cmd) { // allList (모든 사용자 조회) 의 경우 service 인스턴스의 getAll 메서드 실행 후 반환값을 list에 할당 // 할당받은 속성을 request의 속성에 설정 후 isForward(실어나를 값이 있는지)을 true(값이 있음)로 변경 // path(사용자가 이동할 경로)를 allList.jsp로 지정 후 break(switch문 종료) case "allList": list = service.getAll(); request.setAttribute("list", list); isForward = true; path = "allList.jsp"; break; // 따로 실어나를(request에 할당할 속성, 즉 전달할 값이 없는 상태) 값이 없는 경우 // isForward는 놔두고 path(경로)만 지정한 뒤 break case "inputDept": path = "inputDept.jsp"; break; case "inputDynamic": path = "inputDynamic.jsp"; break; // deptList의 설명은 allList와 동일 // 단 전달하는 값은 정수형 부서 번호 (오라클에서 department_id가 Number로 되어있기 때문에 맞춰줌) // 값을 보낼때에도 service의 getOne 메서드에 지정한 매개변수의 타입대로 int로 변환해 전달 case "deptList": list = service.getOne(Integer.parseInt(request.getParameter("department_id"))); request.setAttribute("list", list); isForward = true; path = "allList.jsp"; break; // ★ 동적 생성을 위한 페이지. 여기서 설명이 조금 복잡해지기 때문에 주석을 세세히 나눌 예정 ★ case "dynamicList": //searching은 사용자의 옵션(option에서 선택한 값)과 searching_content(입력한 값) //을 담기 위해 Map을 하나 만들어줌. 그냥 객체 하나 생성했다고 보면 됨 Map<String, String> searching = new HashMap<>(); String type = request.getParameter("type"); String searchingContent = request.getParameter("searching_content"); // 위에서 꺼낸 두 파라미터 값을 Map으로 선언한 searching 변수의 put 메서드를 이용해 // 아래와 같이 문자열 타입의 속성 이름과 문자열 타입의 값(getParameter 한 값들)으로 할당함 // 사용자가 만약 입력창에서 employee_id 옵션을 선택한 뒤 searching_content의 값으로 // 135를 입력했다면 첫번째 put메서드에는 "type" : "employee_id"가 // 두번째 put 메서드에는 "searching_content" : "135"가 입력됨 searching.put("type", type); searching.put("searching_content", searchingContent); // hire_date, job_id, full_name의 경우 반환값이 한 명이 아닐수도 있음. // 입사 일자 이후의 값을 조회하기 때문이고, job_id(직급)이 같은 경우가 있고 // 풀 네임이 같은 사람이 있을 수도 있기 때문임. 그래서 사용자가 입력한 type(옵션)이 이들 값이 아닌 경우, // 즉 단일 쿼리 반환값인 person(EmployeeVO)만 반환하는 상황인 경우 if(!type.equals("hire_date") && !type.equals("job_id") && !type.equals("full_name")) { // EmployeeVO 타입의 Person에 service 인스턴스의 getDynamicResult(단일 VO 반환 함수) // 를 호출하고 매개변수로 put 메서드로 세팅한 searching을 전달함 person = service.getDynamicResult(searching); // 그 뒤 반환 받은 person의 값(VO)을 request에 아래의 명칭과 같이 싣고 request.setAttribute("person", person); // 경로(path)를 단일 VO를 출력하는 페이지로 지정 path = "searchOne.jsp"; // if조건에 걸리지 않은 타입들, 즉 다중 리스트(List<EmployeeVO>)를 반환 받는 상황인 경우 }else { // service 인스턴스의 다중 리스트를 반환해주는 메서드를 호출하여 마찬가지로 searching 전달 후 // 반환 받은 다중 리스트를 미리 선언해 놓은 list(List<EmployeeVO>)에 할당한 뒤 옵션에 세팅 list = service.getDynamicResultList(searching); request.setAttribute("list", list); // allList 페이지를 활용해줌 (어차피 다 보여주는 페이지는 동일하되 반환되는 쿼리 값들만 다르므로) path = "allList.jsp"; } // isForward = true; break; } // 포워드 값이 true(전달할 request 속성이 있는 경우라면)라면 디스패쳐 포워드로 패스와 함께 요청 처리를 날림 if(isForward) { request.getRequestDispatcher(path).forward(request, response); // 전달할 값(request에 세팅이 되어있지 않은 상태)이 없는 경우 그냥 경로(path)로 응답 페이지를 제공 }else { response.sendRedirect(path); } } protected void doPost(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException { doGet(request, response); } }
package org.joonzis.vo; import java.util.Date; public class EmployeeVO { private String first_name, last_name, email, phone_number, job_id; private int employee_id, salary, commision_pct, manager_id, department_id; private Date hire_date; public EmployeeVO() {} public EmployeeVO(String first_name, String last_name, String email, String phone_number, String job_id, int employee_id, int salary, int commision_pct, int manager_id, int department_id, Date hire_date) { super(); this.first_name = first_name; this.last_name = last_name; this.email = email; this.phone_number = phone_number; this.job_id = job_id; this.employee_id = employee_id; this.salary = salary; this.commision_pct = commision_pct; this.manager_id = manager_id; this.department_id = department_id; this.hire_date = hire_date; } public String getFirst_name() { return first_name; } public void setFirst_name(String first_name) { this.first_name = first_name; } public String getLast_name() { return last_name; } public void setLast_name(String last_name) { this.last_name = last_name; } public String getEmail() { return email; } public void setEmail(String email) { this.email = email; } public String getPhone_number() { return phone_number; } public void setPhone_number(String phone_number) { this.phone_number = phone_number; } public String getJob_id() { return job_id; } public void setJob_id(String job_id) { this.job_id = job_id; } public int getEmployee_id() { return employee_id; } public void setEmployee_id(int employee_id) { this.employee_id = employee_id; } public int getSalary() { return salary; } public void setSalary(int salary) { this.salary = salary; } public int getCommision_pct() { return commision_pct; } public void setCommision_pct(int commision_pct) { this.commision_pct = commision_pct; } public int getManager_id() { return manager_id; } public void setManager_id(int manager_id) { this.manager_id = manager_id; } public int getDepartment_id() { return department_id; } public void setDepartment_id(int department_id) { this.department_id = department_id; } public Date getHire_date() { return hire_date; } public void setHire_date(Date hire_date) { this.hire_date = hire_date; } }
package org.joonzis.service; import java.util.List; import java.util.Map; import org.joonzis.vo.EmployeeVO; public interface EmployeeService { public List<EmployeeVO> getAll(); public List<EmployeeVO> getOne(int department_id); public EmployeeVO getDynamicResult(Map<String, String> searching); public List<EmployeeVO> getDynamicResultList(Map<String, String> searching); }
package org.joonzis.service; import java.util.List; import java.util.Map; import org.joonzis.dao.EmployeeDao; import org.joonzis.dao.EmployeeDaoImpl; import org.joonzis.vo.EmployeeVO; public class EmployeeServiceImpl implements EmployeeService{ private EmployeeDao dao = EmployeeDaoImpl.getInstance(); @Override public List<EmployeeVO> getAll() { return dao.getAllEmployees(); } @Override public List<EmployeeVO> getOne(int department_id) { return dao.getOneEmployees(department_id); } @Override public EmployeeVO getDynamicResult(Map<String, String> searching) { return dao.getDynamicSearching(searching); } @Override public List<EmployeeVO> getDynamicResultList(Map<String, String> searching) { return dao.getDynamicSearchingList(searching); } }
package org.joonzis.dao; import java.util.List; import java.util.Map; import org.joonzis.vo.EmployeeVO; public interface EmployeeDao { public List<EmployeeVO> getAllEmployees(); public List<EmployeeVO> getOneEmployees(int department_id); public EmployeeVO getDynamicSearching(Map<String, String> searching); public List<EmployeeVO> getDynamicSearchingList(Map<String, String> searching); }
package org.joonzis.dao; import java.util.HashMap; import java.util.List; import java.util.Map; import org.apache.ibatis.session.SqlSession; import org.apache.ibatis.session.SqlSessionFactory; import org.joonzis.mybatis.config.DBService; import org.joonzis.vo.EmployeeVO; public class EmployeeDaoImpl implements EmployeeDao{ // DAO 객체 생성 private static EmployeeDaoImpl instance = null; public EmployeeDaoImpl() {} // DAO 획득하는 함수 public static EmployeeDaoImpl getInstance() { if(instance == null) { instance = new EmployeeDaoImpl(); } return instance; } // 필드 private static SqlSession sqlsession = null; // 팩토리 얻어오는 함수, 단 동기화를 시켜 여러 접속으로부터의 팩토리 획득을 방지하도록 함 // 왜냐하면 팩토리는 공유 자원(static)인건 맞으나, 팩토리 세션을 생성하는 작업은 1회만 발생(static 구역) // 즉 단 한 번 생성된 팩토리 자원을 안정적으로 다수의 호출자로부터의 호출 충돌을 방지하기 위한 작업이라고 보면 됨 private synchronized static SqlSession getSqlSession() { if(sqlsession == null) { sqlsession = DBService.getFactory().openSession(false); } return sqlsession; } // 테이블의 모든 사원들의 정보를 획득하여 전달하는 역할 @Override public List<EmployeeVO> getAllEmployees() { return getSqlSession().selectList("select_all"); } // 테이블의 특정 부서 사원들의 정보를 획득하여 전달하는 역할 @Override public List<EmployeeVO> getOneEmployees(int department_id) { return getSqlSession().selectList("select_one", department_id); } // 동적 검색을 할 때 테이블의 특정 사원의 정보를 획득하여 전달하는 역할 @Override public EmployeeVO getDynamicSearching(Map<String, String> searching) { String type = searching.get("type"); String searching_content = searching.get("searching_content"); //id 전달 전용 변수 int number_content = 0; // type의 분기점에 따라 마이바티스에 전달할 searching_content의 값을 number_content(사원 id는 Number이므로) // 로 변환하거나, 그대로 냅둬서 selectOne 메서드에 전달함 if(type.equals("employee_id")) { number_content = Integer.parseInt(searching.get("searching_content")); System.out.println(number_content); return getSqlSession().selectOne("dynamic_searching_byid", number_content); }else if(type.equals("email")){ return getSqlSession().selectOne("dynamic_searching_byemail", searching_content); }else{ return getSqlSession().selectOne("dynamic_searching_bynumber", searching_content); } } // 동적 검색을 할 때 테이블의 특정 사원들의 정보를 획득하여 전달하는 역할 public List<EmployeeVO> getDynamicSearchingList(Map<String, String> searching) { String type = searching.get("type"); String searching_content = searching.get("searching_content"); // 이름 전달 전용 변수 Map<String, String> names = new HashMap<>(); // 위 단일 VO를 반환하는 동적 검색 함수와의 차이점은 selectList를 반환, 즉 다수의 VO를 반환한다는 // 차이점이 존재함, (매개변수는 같기 때문에 오버로딩이 안됨, 어차피 반환 타입도 다름) // 그래서 List<EmployeeVO>를 반환하는 메서드를 하나 더 만들어줌 (설명은 단일 동적 검색 함수 주석과 동일) if(type.equals("job_id")) { return getSqlSession().selectList("dynamic_searching_byjob", searching_content); }else if(type.equals("hire_date")) { return getSqlSession().selectList("dynamic_searching_byhiredate", searching_content); }else { // else문에는 type이 full_name인 경우가 되는데, 이때 searching(사용자가 전달한 map)을 // 공백을 기준으로 나눠 nameArr 배열에 할당함 // 이럴 경우 공백을 기준으로 앞의 값은 names 맵에 first_name 이름으로 할당하고, // 뒤의 값은 last_name 이름으로 할당한 뒤 마이바티스에 names를 전달 String[] nameArr = searching.get("searching_content").split(" "); names.put("first_name", nameArr[0]); names.put("last_name", nameArr[1]); return getSqlSession().selectList("dynamic_searching_byname", names); } } }
package org.joonzis.mybatis.config; import java.io.InputStream; import org.apache.ibatis.io.Resources; import org.apache.ibatis.session.SqlSessionFactory; import org.apache.ibatis.session.SqlSessionFactoryBuilder; public class DBService { // factory를 만드는게 목적 // 필드 private static SqlSessionFactory factory = null; // 정적 초기화 변수 (클래스가 로드될때 자동으로 실행되는 코드라고 보면 됨) // 아래의 메서드들은 마이바티스를 사용하기 위한 주요 메서드들이라서 // 마이바티스 사용을 위한 하나의 그림으로 외워둘 필요가 있음. static { try { // 클래스 패스에서 위치한 sqlmap (마이바티스 설정 관련 xml)을 읽어오기 위한 경로를 설정 String resource = "org/joonzis/mybatis/config/sqlmap.xml"; // 설정된 경로의 파일을 읽어들어와 is 스트림에 저장 InputStream is = Resources.getResourceAsStream(resource); // 마이바티스의 세션 팩토리를 생성(빌더의 builder메서드)한 뒤 factory 필드에 할당 factory = new SqlSessionFactoryBuilder().build(is); } catch (Exception e) { e.printStackTrace(); } } // 사용자가 getFactory 메서드를 호출하면 정적으로 public static SqlSessionFactory getFactory() { return factory; } }
<?xml version="1.0" encoding="UTF-8"?> <!-- 해당 xml 문서의 구조가 마이바티스의 환경설정을 구성하는 구조로 나타나 있음을 알려주는 설정--> <!DOCTYPE configuration PUBLIC "-//mybatis.org//DTD Config 3.0//EN" "http://mybatis.org/dtd/mybatis-3-config.dtd" > <!-- 설정값들 --> <configuration> <!-- 마이바티스 설정 중 표준 출력 로깅을 설정하는 세팅, 이 설정을 세팅해주면 마이바티스가 실행될때 해당 IDE의 콘솔창에 마이바티스의 실행 과정과 결과가 콘솔에 출력됨 이 세팅을 이용하면 IDE 환경에서도 쉽게 쿼리문을 디버깅 할 수 있음 --> <settings> <setting name="logImpl" value="STDOUT_LOGGING"/> </settings> <!-- 마이바티스의 실행 환경을 세팅한 구간 특히 이 구간에서는 마이바티스가 JDBC 수준에서 트랜잭션을 관리(transactionManager)하게 되며 데이터 소스의 유형을 POOLED로 설정, 쉽게 말하자면 커넥션 풀의 환경 설정이라고 보면됨 --> <environments default="development"> <environment id="development"> <transactionManager type="JDBC"/> <dataSource type="POOLED"> <property name="driver" value="oracle.jdbc.driver.OracleDriver"/> <property name="url" value="jdbc:oracle:thin:@localhost:1521:xe"/> <property name="username" value="hr"/> <property name="password" value="1234"/> </dataSource> </environment> </environments> <!-- 쿼리를 정의하는 java와 DB 사이에 매핑을 수행하도록 하는 xml이 위치한 경로를 지정하는 곳. 이 설정을 지정해줘야 --> <mappers> <mapper resource="org/joonzis/mybatis/mapper/employees.xml"/> </mappers> </configuration>
<?xml version="1.0" encoding="UTF-8"?> <!-- 해당 xml 문서의 구조가 마이바티스의 매퍼를 구성하는 구조로 나타나 있음을 알려주는 설정 특히 이 설정은 configure 부분의 xml과 다르므로 설정시 주의 필요 --> <!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd" > <mapper namespace="org.joonzis.mybatis.mapper.employees"> <!-- 마이바티스 환경 설정 xml에 사용할 이름 --> <!-- 1. 모든 부서 목록 조회 getAllEmployees --> <select id="select_all" resultType = "org.joonzis.vo.EmployeeVO"> select * from employees order by employee_id asc </select> <!-- 2. 부분 부서 목록 조회 getOneEmployees --> <select id="select_one" parameterType="int" resultType = "org.joonzis.vo.EmployeeVO"> select * from employees where department_id = ${department_id} order by department_id asc </select> <!-- 3-1. 동적 목록 조회 getDynamicSearching (아이디 전용) --> <select id="dynamic_searching_byid" parameterType="int" resultType = "org.joonzis.vo.EmployeeVO"> select * from employees where employee_id = #{number_content} </select> <!-- 3-2. 동적 목록 조회 getDynamicSearching (이메일 전용) --> <select id="dynamic_searching_byemail" parameterType="map" resultType="org.joonzis.vo.EmployeeVO"> select * from employees where email = #{searching_content} </select> <!-- 3-3. 동적 목록 조회 (전화번호 전용) getDynamicSearching 오름차순 --> <select id="dynamic_searching_bynumber" parameterType="map" resultType="org.joonzis.vo.EmployeeVO"> select * from employees where phone_number = #{searching_content} </select> <!-- 3-4. 동적 목록 조회 (이름 전용) getDynamicSearching 오름차순 --> <select id="dynamic_searching_byname" parameterType="map" resultType = "org.joonzis.vo.EmployeeVO"> select * from employees where first_name = #{first_name} and last_name = #{last_name} </select> <!-- 3-4. 동적 목록 조회 (직급 전용) getDynamicSearching 오름차순 (최신 부서 순 대로) 참고로 매퍼에서 마이바티스 EL을 사용하는 이유는 동적으로 데이터를 집어 넣기 위함인데 '%UPPER(#{searching_content})' 이렇게 문자열로 감싸게 되면 문자열로 인식되어 버리기 때문에 동적 생성이 불가능함 그래서 결합 연산자 (||)를 사용하여 따로 도출된 결과값들을 이어붙인거임. 덧붙여 오라클의 문자 검색의 경우 칼럼 조회와 달리 대소문자를 구별하기 때문에 사용자의 입력값을 UPPER 메서드로 대문자화 시켜줘야 칼럼 내의 대문자의 값들 중 일치하는 값을 찾을 수 있음 --> <select id="dynamic_searching_byjob" parameterType="string" resultType="org.joonzis.vo.EmployeeVO"> select * from employees where job_id like '%' || UPPER(#{searching_content}) order by department_id asc </select> <!-- 3-5. 동적 목록 조회 (입사일자 전용) getDynamicSearching 내림차순 (입사 일자가 최신인 순서대로 연-월-일) 날짜 검색의 경우 Extract 함수를 이용해 hire_date 중 연도를 추출해 정렬하고, 연도가 같을시 월을 추출해 정렬하고, 월도 같으면 일자를 추출해 일로 정렬하도록 함 --> <select id="dynamic_searching_byhiredate" parameterType="string" resultType="org.joonzis.vo.EmployeeVO"> select * from employees where hire_date >= TO_DATE(#{searching_content}, 'RR/MM/DD') order by EXTRACT(year from hire_date) desc, EXTRACT(month from hire_date) desc, EXTRACT(day from hire_date) desc </select> </mapper>
예외 처리를 따로 해주진 않았는데 (안해준게 아니라 그것까지 신경 쓸 겨를이...) 그래도 입력 할 때 정규식으로 검사를 하고 있어서 크게 무리는 없을 것 같습니다. 그래도 할 수 있다면 예외 처리를 또 따로 해줘야 하지 않을까 하는 아쉬움이 드네요.
DAO에서 데이터를 정제하는 부분을 Service로 보내줬어야 하는데 병상에서 코드를 짜는지라 그것까지 미처 생각을 하지 못했습니다. 그래도 이정도면 나름 만족을 하지 않을까 싶고, 다음에는 Service 부분에서 데이터를 정제해 DAO에 보내줘야 겠구나 하는 교훈을 얻었지 않았나 싶습니다.
학원에서 수업 내용을 배우면서 빠르게 전개 되느라 따라가기 좀 벅차긴한데, 그래도 복습을 하면서 정리를 하니까 왜 빠르게 전개가 되는지 알겠더라고요. (요즘은 SPA가 대세라서 이렇게 복잡하게 페이징을 하진 않으니까... 그래도 모듈화 개념은 꼭 익혀야 나중에 컴포넌트나 기능 만들 때 잘 익혀둘 수 있기에 빠르게 훑되 중요한 개념만 공부한 것 같았습니다.)
게시글이나 파일 업로드 부분은 따로 처리하지 하지 않았는데, 나중에 하게 된다면 여지껏 배운 내용으로 스프링을 쓰지 않고 간이 사내 사이트를 하나 만들어보고 싶다는 생각이 들었던 시간이기도 했습니다.