- main > source > mappers 패키지 생성 > TimeMapper2.xml 파일 생성(파일 이름을 매퍼 인터페이스와 같게)
<!-- TimeMapper2.xml -->
<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE mapper
PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
"http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace = "org.zerock.springex.mapper.TimeMapper2">
<!-- select의 id 속성값을 매터 인터페이스의 메서드 이름과 같게 설정 -->
<!-- select 태그는 반드시 resultType이나 resultMap 속성을 지정해야 함 -->
<!-- resultType은 select문이 결과를 어떤 타입으로 처리할 지에 대한 설정 -->
<select id = "getNow" resultType = "string">
select now()
</select>
</mapper>
- 마지막으로 root-context.xml 파일의 MyBatis 설정에 XML 파일들을 인식하도록 설정을 추가
<!-- root-context.xml -->
...
<bean id = "sqlSessionFactory" class = "org.mybatis.spring.SqlSessionFactoryBean">
<property name = "dataSource" ref = "dataSource" />
<!-- mapperLocations는 XML 매퍼 파일의 위치 -->
<property name = "mapperLocations" value = "classpath:/mappers/**/*.xml"></property>
</bean>
...
- SampleDAO 인터페이스는 실체가 없기 때문에 SampleDAO 인터페이스를 구현한 클래스를 SampleDAOImpl이라는 이름으로 선언
// SampleDAOImpl
package org.zerock.springex.sample;
import org.springframework.stereotype.Repository;
// @Repository를 이용해서 해당 클래스의 객체를 스프링의 Bean으로 처리하도록 구성
@Repository
public class SampleDAOImpl implements SampleDAO{
}
- SampleService 입장에서는 인터페이스만 바라보고 있기 때문에 실제 객체가 SampleDAOImpl의 인스턴스인지 알 수 없지만, 코드 작성에 문제 x
- 느슨한 결합: 객체와 객체의 의존 관계의 실제 객체를 몰라도 가능하게 하는 방식
- 느슨한 결합을 이용하면 나중에 SampleDAO 타입의 객체를 다른 객체로 변경해도 SampleService 타입을 이용하는 코드를 수정할 일이 없어 더 유연한 구조임
- 다른 SampleDAO 객체로 변경해보기(특정 기간에만 SampleDAO를 다른 객체로 변경해야 하는 경우 생각)
- EventSampleDAOImpl 클래스 작성
// EventSampleDAOImpl
package org.zerock.springex.sample;
import org.springframework.stereotype.Repository;
@Repository
public class EventSampleDAOImpl implements SampleDAO{
}
- SampleService에 필요한 SampleDAO 타입의 Bean이 두 개(SampleDAOImpl, EventSampleDAOImpl)이므로, 어떤걸 주입해야 할 지 모르게 됨
- Test 실행 시, 스프링이 SampleDAO 타입의 객체가 하나이길 기대했지만 2개가 발견됐다는 오류 출력
- 해결 방법으로 두 클래스 중 하나를 @Primary라는 어노테이션으로 지정(지금 사용하고 싶은 것에 지정)
// EventSampleDAO
package org.zerock.springex.sample;
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.context.annotation.Primary;
import org.springframework.stereotype.Repository;
@Repository
@Primary
public class EventSampleDAOImpl implements SampleDAO{
}
- Test 실행 시, 정상적으로 EventSampleDAOImpl이 주입된 것을 확인
- Qualifier 이용하기
- @Primary 이용하는 방법 이외에 @Qualifier를 이용하여 이름을 지정해서 특정한 이름의 객체 주입
- Lombok과 @Qualifier를 같이 이용하기 위해 src/main/java 폴더에 lombok.config 파일 생성
// SampleDAOImpl
@Repository
// @Qualifier를 이용해서 SampleDAOImpl에 'normal'이라는 이름 지정
@Qualifier("normal")
public class SampleDAOImpl implements SampleDAO{
}
// EventSampleDAOImpl
@Repository
// @Qualifier를 이용해서 EventSampleDAOImpl에 'event'라는 이름 지정
@Qualifier("event")
public class EventSampleDAOImpl implements SampleDAO{
}
- SampleService에서 @Qualifier를 이용해 이름을 지정하면 해당 이름의 SampleDAO 객체를 사용
// SampleService
package org.zerock.springex.sample;
import lombok.RequiredArgsConstructor;
import lombok.ToString;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.stereotype.Service;
@Service
@ToString
@RequiredArgsConstructor
public class SampleService {
@Qualifier("normal")
private final SampleDAO sampleDAO;
}
SampleService에서 @Qualifier의 이름을 normal로 지정했을 때 SampleDAOImpl을 사용
// SampleService
package org.zerock.springex.sample;
import lombok.RequiredArgsConstructor;
import lombok.ToString;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.stereotype.Service;
@Service
@ToString
@RequiredArgsConstructor
public class SampleService {
@Qualifier("event")
private final SampleDAO sampleDAO;
}
SampleService에서 @Qualifier의 이름을 event로 지정했을 때 EventSampleDAOImpl을 사용
- 스프링의 Bean으로 지정되는 객체들
- 스프링의 모든 클래스의 객체가 Bean으로 처리되는 것은 x
- 스프링의 Bean으로 등록되는 객체들은 '핵심 배역'을 하는 객체(오랜 시간 프로그램에 상주하며 중요한 역할을 하는 '역할 중심' 객체
- DTO나 VO 등 '역할'보다 '데이터' 중심으로 설계된 객체들은 스프링의 Bean으로 등록되지 않음(특히 DTO는 생명주기가 짧고 데이터 보관이 주된 역할이어서 Bean으로 처리 x)
- XML이나 어노테이션으로 처리하는 객체
- Bean으로 처리할 때 XML 설정을 이용할 수도 있고 어노테이션을 처리할 수도 있음
- 판단 기준은 '코드를 수정할 수 있는가'
- jar 파일로 추가되는 클래스의 객체를 Bean으로 처리해야 하면, 해당 코드가 존재하지 ㅇ낳아 어노테이션을 추가할 수 없어, XML에서 <bean>을 사용해 처리
- 직접 작성되는 클래스는 어노테이션을 이용
4. 웹 프로젝트를 위한 스프링 준비
Bean을 담은 ApplicationContext가 웹 애플리케이션에서 동작하려면, 웹 애플리케이션이 실행될 때 스프링을 로딩해서 해당 웹 애플리케이션 내부에 스프링의 ApplicationContext를 생성하는 작업 필요
web.xml을 이용해서 리스너를 설정
web.xml 설정 이전에, 스프링 프레임워크의 웹 관련 작업은 spring-webmvc 라이브러리를 추가해야 설정 가능
ServletContextEvent를 이용하면 현재 애플리케이션이 실행되는 공간인 ServletContext에 접근 가능
ServletContext는 현재 웹 애플레케이션 내 모든 자원들을 같이 사용하는 공간
이 공간에 무언가를 저장하면 모든 Controller나 JSP 등에서 이를 활용 가능
ServletContext에는 setAttribute()를 이용해서 원하는 이름으로 객체를 보관할 수 있음
다음과 같이 'appName'이라는 이름으로 'W2'라는 이름을 지정하면, EL에서 ${appName}으로 이를 이용 가능
// W2AppListener에서 setAttribute()를 통해 객체를 생성
package org.zerock.w2.listener;
import lombok.extern.log4j.Log4j2;
import javax.servlet.ServletContext;
import javax.servlet.ServletContextEvent;
import javax.servlet.ServletContextListener;
import javax.servlet.annotation.WebListener;
@WebListener
@Log4j2
public class W2AppListener implements ServletContextListener {
@Override
public void contextInitialized(ServletContextEvent sce) {
log.info("-----------------init-----------------");
log.info("-----------------init-----------------");
log.info("-----------------init-----------------");
// 시작과 함께 객체 생성
ServletContext servletContext = sce.getServletContext();
servletContext.setAttribute("appName", "W2");
}
...
}
// TodoListController에서 다음과 같이 객체 활용 가능
package org.zerock.w2.controller;
...
@WebServlet(name = "todoListController", value = "/todo/list")
@Log4j2
public class TodoListController extends HttpServlet {
private TodoService todoService = TodoService.INSTANCE;
@Override
protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
log.info("todo list.................");
// getServletContext() 메서드를 이용해 ServletContext를 이용할 수 있음
ServletContext servletContext = req.getServletContext();
log.info("appName: " + servletContext.getAttribute("appName"));
...
}
}
<!-- list.jsp에 다음과 같이 EL 구문을 추가하면 화면에서 나타내는 용도로 사용가능 -->
<body>
<h1>Todo List</h1>
<!-- appName이라는 이름을 가진 객체(W2)를 호출 -->
<h2>${appName}</h2>
<h2>${loginInfo}</h2>
<h3>${loginInfo.mname}</h3>
...
</body>
${appName}을 입력하여, appName을 이름으로 하는 객체 "W2"를 출력
ServletContextListener와 스프링 프레임워크
ServletContextListener와 ServletContext를 이용하면 프로젝트 실행 시 필요한 객체들을 준비하는 작업 처리 가능
커넥션 풀 초기화 또는 TodoService 같은 객체 미리 생성해서 보관 등
특히, 스프링 프레임워크에서 웹 프로젝트를 미리 로딩하는 작업을 처리할 때 ServletContextListener 이용
실습
2) 세션 관리 리스너
- Servlet의 리스너 중 HttpSession 관련 작업을 감시하는 리스너 등록 가능
(HttpSessionListener나 HttpSessionAttributeListener 등)
- 이를 이용해서 HttpSession이 생성되거나 setAttribute() 등의 작업이 이루어질 때를 감지 가능
- listener 패키지에 LoginListener 클래스 추가
// LoginListener
package org.zerock.w2.listener;
import lombok.extern.log4j.Log4j2;
import javax.servlet.annotation.WebListener;
import javax.servlet.http.HttpSessionAttributeListener;
import javax.servlet.http.HttpSessionBindingEvent;
@WebListener
@Log4j2
public class LoginListener implements HttpSessionAttributeListener {
@Override
public void attributeAdded(HttpSessionBindingEvent event) {
// 이름과 객체의 값들을 받아옴
String name = event.getName();
Object obj = event.getValue();
if(name.equals("loginIndo")) {
log.info("A user logined.........");
log.info(obj);
}
}
}
로그인 실행 시 로그에 위의 메세지가 출력됨
- LoginListener는 HttpSessionAttributeListener 인터페이스를 구현
- HttpSessionAttributeListener 인터페이스는 attributeAdded(), attributeRemoved(), attributeReplaced() 를 이용해서, HttpSession에 setAttribute() / removeAttribute() 등의 작업을 감지
// LoginController
package org.zerock.w2.controller;
import lombok.extern.log4j.Log4j2;
import org.zerock.w2.dto.MemberDTO;
import org.zerock.w2.service.MemberService;
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 javax.servlet.http.HttpSession;
import java.io.IOException;
import java.util.UUID;
@WebServlet("/login")
@Log4j2
public class LoginController extends HttpServlet {
...
@Override
protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
log.info("login post.............");
String mid = req.getParameter("mid");
String mpw = req.getParameter("mpw");
String auto = req.getParameter("auto");
boolean rememberMe = auto != null && auto.equals("on");
try {
MemberDTO memberDTO = MemberService.INSTANCE.login(mid, mpw);
// rememberMe라는 변수가 true(auto 값이 있고 그 값이 "on"이라면, 즉 자동 로그인이 설정되어 있다면)
// UUID(java.util 중 임의의 숫자 생성하는 도구)를 이용해서 임의의 번호 생성
if(rememberMe) {
String uuid = UUID.randomUUID().toString();
MemberService.INSTANCE.updateUuid(mid, uuid);
memberDTO.setUuid(uuid);
}
// 정상적으로 로그인 된 경우, HttpSession을 이용해서 'loginInfo'라는 이름으로 객체 저장
HttpSession session = req.getSession();
session.setAttribute("loginInfo", memberDTO);
resp.sendRedirect("/todo/list");
} catch (Exception e) { // 예외 발생 시 '/login'으로 이동, result라는 파라미터를 전달해서 문제가 발생했다는 사실 전달
resp.sendRedirect("/login?result=error");
}
}
}
- 결과
로그인 화면에 자동로그인을 설정할 체크박스 생성됨체크 상태로 로그인하면 uuid가 새로 추가된 모습데이터베이스의 테이블에도 추가된 uuid 값
- 쿠키의 생성 및 전송: 쿠키에 들어가야 하는 문자열이 제대로 처리되었다면 브라우저에 'remember-me' 이름의 쿠키 생성해서 전송
// LoginController에서 rememberMe 변수가 있을 때 수행하는 코드 부분 변경
if(rememberMe) {
String uuid = UUID.randomUUID().toString();
MemberService.INSTANCE.updateUuid(mid, uuid);
memberDTO.setUuid(uuid);
Cookie rememberCookie = new Cookie("remember-me", uuid);
rememberCookie.setMaxAge(60*60*24*7);
rememberCookie.setPath("/");
resp.addCookie(rememberCookie);
}
- 결과
응답 헤더에 Set-Cookie에 remember-me가 전송됨쿠키도 다음과 같이 확인 가능
브라우저에서 최초로 서버 호출 시 해당 서버에서 발행한 쿠키가 없다면 브라우저는 아무것도 전송 x
서버에서 응답 메세지 보낼 때, 브라우저에 쿠키를 보내는데 이때 'Set Cookie'라는 HTTP 헤더 이용
브라우저는 쿠키를 받은 후 이에 대한 정보를 읽고, 파일 형태로 보관할 지 메모리상에서만 처리할 지 결정(쿠키에 있는 유효기간(만료기간)을 보고 결정)
브라우저에 보관하는 쿠키는 다음에 다시 브라우저가 서버에 요청할 때 HTTP 헤더에 'Cookie'라는 헤더 이름과 함께 전달(쿠키에는 경로를 지정할 수 있어서 해당 경로에 맞는 쿠키 전송)
서버에서는 필요에 따라서 브라우저가 보낸 쿠키를 읽고 사용
1) 쿠키를 생성하는 방법
서버에서 자동으로 생성하는 쿠키: 응답 메세지를 작성할 때 정해진 쿠키가 없는 경우 자동으로 발행(WAS에서 발행되며 WAS마다 고유한 이름 사용(톰캣은 JSESSIONID))
서버에서 발행하는 쿠키는 기본적으로 브라우저 메모리 상에 보관, 브라우저 종료 시 쿠키는 삭제됨
서버에서 발행하는 쿠키의 경로는 '/'
개발자가 생성하는 쿠키: 개발자가 생성하는 쿠키는 서버에서 생성되는 쿠키와 다음이 다름
이름 원하는대로 지정 가능
유효기간 지정가능(유효기간 지정 시, 브라우저가 이를 파일 형태로 보관)
반드시 직접 응답에 추가해 주어야 함
경로나 도메인 등을 지정 가능(특정 서버의 경로를 호출하는 경우에만 쿠키 사용)
2. Servlet 컨텍스트와 세션 저장소
하나의 톰캣은 여러 웹 애플리케이션을 실행할 수 있음
실제 운영 시 웹 애플리케이션 마다 별도의 도메인으로 분리해서 운영됨
프로젝트 실행 경로를 '/'외에 다른 이름으로 각각 지정해서 실행하면 하나의 톰캣에서 여러 애플리케이션 실행 가능
각 애플리케이션은 자신만 사용하는 고유 메모리 영역을 생성하여 이 공간에 Servlet이나 JSP 등을 인스턴스로 만들어 서비스 제공, 이 영역을 Servlet API에서는 Servlet 컨텍스트라고 함
애플리케이션 생성 시 톰캣이 발행하는 쿠키를 관리하기 위한 메모리 영역이 하나 더 생성됨(세션 저장소)
세션 저장소는 기본적으로 키와 값을 보관하는 구조
톰캣에서 JSESSIONID가 키가 됨
새로운 JSESSIONID 쿠키가 만들어질 때마다 메모리 공간을 차지하므로 톰캣은 주기적으로 세션 저장소를 조사하며 사용하지 않는 값 정리(session-timeout 설정 이용, 기본 30분 주기마다 지정된 시간보다 오래된 값 삭제)
1) 세션을 통한 상태 유지 메커니즘
HttpServletRequest의 getSession()이라는 메서드 실행 → 톰캣에서는 JSESSIONID 이름의 쿠키가 요철할 때 있었는지 확인 → 없다면 새로운 값을 만들어 세션 저장소에 보관
A1234와 B111은 해당 공간에 login 정보가 존재하며, 서버에서 프로그램 작성 시 이를 이용해서 해당 사용자가 로그인했다는 것을 인정하는 방식
2) HttpServletRequest의 getSession()
HttpServletRequest의 getSession()은 브라우저가 보내는 정보를 이용해 다음의 작업 수행
JSESSIONID가 없는 경우: 세션 저장소에 새로운 번호로 공간을 만들고 해당 공간에 접근할 수 있는 객체 반환(새로운 번호는 브라우저에 JSESSIONID의 값으로 전송(세션 쿠키))
JSESSIONID가 있는 경우: 세션 저장소에 JSESSIONID 값을 이용해서 할당된 공간을 찾고 이 공간에 접근할 수 있는 객체 반환
getSession()의 결과물은 세션 저장소 내의 공간, 이 공간을 의미하는 타입은 HttpSession타입이며 해당 공간은 세션 컨텍스트 또는 세션이라고 함
HttpSession 타입의 객체를 이용하면 현재 사용자만의 공간에 원하는 객체를 저장 / 수정 / 삭제할 수 있음
isNew() 메서드 등으로 새롭게 공간을 만들 것인지 기존 공간 재사용할 것인지 구분 가능
3. 세션을 이용한 로그인 체크
세션을 이용한 로그인 체크 시나리오
사용자가 로그인에 성공하면 HttpSession을 이용해 해당 사용자의 공간(세션 컨텍스트)에 특정 객체를 이름과 함께 저장
로그인 체크가 필요한 컨트롤러에서 현재 사용자의 공간에 지정된 이름으로 객체가 저장되었는지 확인 → 객체가 존재한다면 해당 사용자는 로그인된 사용자로 간주 → 아니면 로그인 페이지로 이동
실습
1) 등록할 때, 로그인 체크 하기
- 로그인한 사용자만 Todo를 등록할 수 있다고 가정
- TodoRegisterController에서 doGet() 수정
// TodoRegisterController
...
@Override
protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
log.info("/todo/register GET .......");
HttpSession session = req.getSession();
// 기존에 JSESSIONID가 없는 새로운 사용자
if (session.isNew()) {
log.info("User with new JSESSIONID cookie");
resp.sendRedirect("/login");
return;
}
// JSESSIONID는 있지만 해당 세션 켄텍스트에 loginInfo라는 이름으로 저장된 객체가 없는 경우
if (session.getAttribute("loginInfo") == null) {
log.info("User without login information");
resp.sendRedirect("/login");
return;
}
// 정상적인 경우
req.getRequestDispatcher("/WEB-INF/todo/register.jsp").forward(req, resp);
}
...
- 브라우저는 '/login'으로 redirect 됨
- HttpServletRequest의 getSession()을 호출하여 새로운 값이 생성되어 브라우저로 전송되어 저장
(JSESSIONID가 없는 채로 'todo/register' 경로에 접속했을 때, '/login'으로 redirect되고 '/login'에서 개발자 도구를 통해 register의 응답 헤더를 보면 'Set-Cookie' 헤더가 전송된 것을 확인할 수 있음 ↓)
- 로그에는 '기존에 JSESSIONID가 없는 사용자'에 대한 처리를 했을 때 나오도록 설정한 메세지가 출력됨
2) 로그인 처리 Controller 작성
- 로그인은 '/login' 경로에서 GET방식으로 로그인 화면 보여주고, POST 방식으로 실제 로그인 처리하도록 구성
- controller 패키지 내에 LoginController 클래스와 WEB-INF 폴더 내에 로그인 화면을 나타낼 login.jsp 파일 생성
<!-- login.jsp -->
<%@ page contentType="text/html;charset=UTF-8" language="java" %>
<html>
<head>
<title>Title</title>
</head>
<body>
<form action = "/login" method = "post">
<input type = "text" name = "mid">
<input type = "text" name = "mpw">
<button type = "submit">LOGIN</button>
</form>
</body>
</html>
- 로그인 처리와 HttpSession의 setAttribute()
- LoginController에서 POST 방식으로 파라미터 수집, HttpSession에 'loginInfo' 이름을 이용해서 간단한 문자열을 저장하도록 구성
// LoginController
@Override
protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
log.info("login post.............");
String mid = req.getParameter("mid");
String mpw = req.getParameter("mpw");
String str = mid + mpw;
HttpSession session = req.getSession();
// HttpSession을 이용하여 setAttribute() 메서드를 사용자 공간에 loginInfo 라는 이름으로 문자열을 보관
session.setAttribute("loginInfo", str);
resp.sendRedirect("/todo/list");
}
- 결과
login 화면에서 아무 id와 pw를 입력하고 login 버튼을 누르면 정상적으로 tood/list로 redirect 됨로그인 한 사용자만 들어갈 수 있는 todo/register 경로에서 정상적으로 접근 가능
4. 필터를 이용한 로그인 체크
로그인 여부를 체크해야 하는 Controller마다 동일하게 체크하는 로직을 작성하면 같은 코드를 계속 작성해야 하므로 대부분 필터를 이용해 처리
필터: 특정 Servlet이나 JSP 등에 도달하는 과정에서 필터링하는 역할을 위해 존재하는 Servlet API
@WebFilter 어노테이션을 이용해 특정 경로에 접근 시 필터가 동작하도록 설계하면 동일 로직을 필터로 분리
필터는 한 개 이상, 여러 개 적용 가능
프로젝트에 filter 패키지 구성하고 LoginCheckFilter 클래스 추가
// LoginCheckFilter
package org.zerock.w2.filter;
import lombok.extern.log4j.Log4j2;
import javax.servlet.*;
import javax.servlet.annotation.WebFilter;
import java.io.IOException;
// '/todo..'로 시작하는 모든 경로에 필터링 시도
@WebFilter(urlPatterns = {"/todo/*"})
@Log4j2
public class LoginCheckFilter implements Filter {
// doFilter()는 필터가 필터링이 필요한 로직을 구현하는 부분
@Override
public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException {
log.info("Login check filter....");
// 다음 필터나 목적지(Servlet, JSP)로 갈 수 있도록 FilterChain의 doFilter()를 실행
// 문제가 생겨서 더 이상 진행할 수 없다면 다음 단계로 진행하지 않고 다른 방식으로 redirect 처리
chain.doFilter(request, response);
}
}
실습
3) 로그인 체크 구현
- LoginCheckFilter에서 '/todo/...'로 시작하는 모든 경로에 접근 시 동작하도록 설정(위의 코드 구체화)
// LoginCheckFilter
package org.zerock.w2.filter;
import lombok.extern.log4j.Log4j2;
import javax.servlet.*;
import javax.servlet.annotation.WebFilter;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import javax.servlet.http.HttpSession;
import java.io.IOException;
// '/todo..'로 시작하는 모든 경로에 필터링 시도
@WebFilter(urlPatterns = {"/todo/*"})
@Log4j2
public class LoginCheckFilter implements Filter {
@Override
public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException {
log.info("Login check filter....");
// doFilter에서 HttpServletRequest와 HttpServletResponse보다 상위 타입의 파라미터 사용
// 따라서 HTTP 관련 작업을 위해 (HttpServlerRequest)request처럼 다운캐스팅 필요
HttpServletRequest req = (HttpServletRequest)request;
HttpServletResponse resp = (HttpServletResponse)response;
HttpSession session = req.getSession();
// LoginController에서 로그인했을 때 세션에 loginInfo라는 이름으로 저장하도록 하였음
// 이 loginInfo가 없다면 로그인하지 않은 것으로 판단되므로 LoginCheckFilter에 의해 '/login' 경로로 이동
if (session.getAttribute("loginInfo") == null) {
resp.sendRedirect("/login");
return;
}
// 어떤 값이든 '/login'에서 로긍니 정보를 전달해서 로그인이 처리되면 그 이후에는 '/todo/...' 경로 이용가능
chain.doFilter(request, response);
}
}
4) UTF-8 처리 필터
- POST 방식으로 '/todo/register'를 통해 전달되는 문자열은 한글이 깨지므로 HttpServletRequest의 데이터를 SetCharacterEncoding("UTF-8")을 적용해 해결
프로젝트에 util 패키지 추가 > ModelMapper의 설정을 변경하고 쉽게 사용할 수 있는 MapperUtil을 enum으로 생성
package org.zerock.jdbcex.util;
import org.modelmapper.ModelMapper;
import org.modelmapper.convention.MatchingStrategies;
public enum MapperUtil {
INSTANCE;
private ModelMapper modelMapper;
MapperUtil() {
this.modelMapper = new ModelMapper();
// ModelMapper의 설정을 변경하려면 getConfiguration()을 이용해서 private로 선언된 필드도 접근할 수 있도록 설정을 변경
this.modelMapper.getConfiguration()
.setFieldMatchingEnabled(true)
.setFieldAccessLevel(org.modelmapper.config.Configuration.AccessLevel.PRIVATE)
.setMatchingStrategy(MatchingStrategies.STRICT);
}
// get()으로 ModelMapper를 사용할 수 있도록 구성
public ModelMapper get() {
return modelMapper;
}
}
- 앞서 데이터베이스에서 만든 tbl_todo 테이블의 데이터를 자바 객체로 처리하기 위해 테이블과 유사한 구조의 TodoVO 클래스와 객체 이용
- Lombok을 이용하면 반복적으로 생성하는 코드를 줄여 DTO나 VO 작성 시 편리
package org.zerock.jdbcex.domain;
import lombok.Builder;
import lombok.Getter;
import lombok.ToString;
import java.time.LocalDate;
// VO는 주로 읽기 전용으로 사용하는 경우가 많으므로 @Getter 추가(getTno(), getTitle() 등을 호출 가능)
@Getter
// 객체 생성 시 빌더 패턴을 이용하기 위해 @Builder 추가(TodoVO.builder().build() 형태로 객체 생성 가능)
@Builder
@ToString
public class TodoVO {
// tbl_todo 테이블의 칼럼들을 기준으로 작성
private Long tno;
private String title;
private LocalDate dueDate;
private boolean finished;
}
2. HikariCP 설정
프로젝트에서 Connection의 생성은 Connection Pool인 HikaariCP 이용
데이터베이스에서 필요한 작업을 명령하는 SQL 에디터인 HeidiSQL 프로그램이 같이 설치됨
'신규 ' 버튼을 눌러서 root 계정으로 연결
설치할 때 지정했던 암호를 입력하고 열기
데이터베이스 생성
사용자 계정 생성과 권한 추가
'사용자 관리자'에서 사용자 계정 '추가'
사용자 이름 및 암호 입력 후, 호스트설정을 모든 곳에서 접근 가능하도록 설정, 데이터베이스는 앞에서 생성한 webdb로 설정접근 허용 권한을 데이터베이스 모두 선택
생성된 계정 확인
세션 관리자 메뉴에서 데이터베이스를 사용할 수 있는 지 점검, 암호와 데이터베이스 이름 입력최종적으로 webdb로 접속된 환경 확인
2. 프로젝트 생성과 MariaDB 준비
새로운 jdbcex 프로젝트 생성
1) 인텔리제이의 MariaDB 설정
새로 생성한 프로젝트 창에서 사이드바에 Database 버튼을 눌러 데이터베이스 설정MariaDB에서 생성한 user 정보를 입력하여 connect
데이터베이스가 연동되면 생기는 SQL console 창에 현재 시간 테스트
2) 프로젝트 내 MariaDB 설정
자바와 데이터베이스를 연동하기 위해 JDBC 드라이버라고 부르는 라이브러리 필요
build.gradle에 설정 추가
구글에 'mariadb maven'을 검색하여 MariaDB Java Client에서 Gradle 또는 Gradle(Short의 내용을 복사하여 사용
JDBC 프로그램의 구조
JDBC는 Java Database Connectivity이 약자
자바 프로그램과 데이터베이스를 네트워크 상에서 연결해 데이터를 교환하는 프로그램
관련 API로 java.sql과 javax.sql 패키지 사용
3) JDBC 프로그램 작성 순서
JDBC 프로그램은 네트워크를 통해 데이터베이스와 연결을 맺고, SQL을 전달해서 데이터베이스가 이를 실행하는 흐름
네트워크를 통해 데이터베이스와 연결을 맺는 단계
데이터베이스에서 보낼 SQL을 작성하고 전송하는 단계
필요하다면 데이터베이스가 보낸 결과를 받아서 처리하는 단계
데이터베이스와 연결을 종료하는 단계
실습_1. 테스트 프로그램 작성하기
// src > test > java > org.zerock.dao > ConnectTests
package org.zerock.dao;
import org.junit.jupiter.api.Assertions;
import org.junit.jupiter.api.Test;
public class ConnectTests {
// Test를 적용하는 메서드는 반드시 public으로 선언, 파라미터나 return 없이 사용
@Test
public void test1() {
int v1 = 10;
int v2 = 10;
// 인자로 받은 두 변수의 값이 동일해야 test에 성공
Assertions.assertEquals(v1, v2);
}
}
v1과 v2가 같을 때 test를 성공하여 출력된 결과v2를 20으로 바꿔 v1과 v2를 다르게 했을 때 test에 실패 후 출력되는 결과
이를 이용하여 MariaDB와의 연결을 확인하는 용도의 코드 작성
package org.zerock.dao;
import org.junit.jupiter.api.Assertions;
import org.junit.jupiter.api.Test;
import java.sql.Connection;
import java.sql.DriverManager;
public class ConnectTests {
@Test
public void testConnection() throws Exception {
// JDBC 드라이버 클래스를 메모리상으로 로딩하는 역할
// 문자열은 패키지명과 클래스명의 대소문자까지 정확히 일치
Class.forName("org.mariadb.jdbc.Driver");
// java.sql 패키지의 Connection 인터페이스 타입의 변수
// 데이터베이스와의 네트워크 연결을 의미
Connection connection = DriverManager.getConnection(
// jdbc 프로토콜을 이용한다는 의미
// localhost:3306은 네트워크 연결정보,
// webdb는 연결하려는 데이터베이스 정보 의미
"jdbc:mariadb://localhost:3306/webdb",
// 연결을 위해 필요한 사용자 계정과 패스워드
"webuser",
"비밀번호");
// 데이터베이스와 정상적으로 연결이 된가면 Connection 타입의 객체는 null이 아니라는 것을 확신
Assertions.assertNotNull(connection);
// 작업이 완료되면 반드시 데이터베이스와의 연결을 종료
connection.close();
}
}
데이터베이스와 Connection에 성공하여 출력된 결과
실습_2. 데이터베이스 테이블 생성
관계형 데이터베이스에서는 데이터 저장을 위해 테이블 생성
테이블은 여러 칼럼과 로우로 구성
각 칼럼에는 이름과 타입, 제약 조건 등이 결합
MariaDB에서 사용하는 데이터 타입
타입
용도
크기
설명
숫자형 데이터 타입
TINYINT
매우 작은 정수
1 byte
-128 ~ 127 (부호 없이 0 ~ 255)
SMALLINT
작은 정수
2 byte
-32768 ~ 32767
MEDIUMINT
중간 크기의 정수
3 byte
-(-8388608) ~ -1(8388607)
INT
표준 정수
4 byte
-2147483648 ~ 2147483647 (부호 없이 0 ~ 4294967295)
BIGINT
큰 정수
8 byte
-2147483648 ~ 2147483647 (부호 없이 0 ~ 4294967295)
FLOAT
단정도 부동 소수
4 byte
-9223372036854775808 ~ 9223372036854775807 (부호 없이 0 ~ 18446744073709551615)
DOUBLE
배정도 부동 소수
8 byte
-1.7976E+320 ~ -1.7976E+320 (부호 없이 쓸 수 없음)
DECIMAL(m, n)
고정 소수
m과 n에 따라 다름
숫자 데이터이지만 내부적으로 String 형태로 저장됨, 최개 65자
BIT(n)
비트 필드
n에 따라 다름
1 ~ 64 bit 표현
날짜형 데이터 타입
DATE
(형태)YYYY-MM-DD
3 byte
1000-01-01 ~ 9999-12-31
DATETIME
(형태)YYYY-MM-DD hh:mm:ss
8 byte
1000-01-01 00:00:00 ~ 9999-12-31 23:59:59
TIMESTAMP
(형태)YYYY-MM-DD hh:mm:ss
4 byte
1970-01-01 00:00:00 ~ 2037
TIME
(형태)hh:mm:ss
3 byte
-839:59:59 ~ 839:59:59
YEAR
(형태)YYYY 또는 YY
1 byte
1901 ~ 2155
문자형 데이터 타입
CHAR(n)
고정 길이 비이진 문자열
n byte
VARCHAR(n)
가변 길이 비이진 문자열
Length + 1 byte
BINARY(n)
고정 길이 이진 문자열
n byte
VARBINARY(n)
가변 길이 이진 문자열
Length + 1 byte or 2 byte
TINYBLOB
매우 작은 Binary Large Object
Length + 1 byte
BLOB
작은 Binary Large Object
Length + 2 byte
최대 크기 64KB
MEDIUMBLOB
중간 크기 Binary Large Object
Length + 3 byte
최대 크기 16MB
LONGBLOB
큰 Binary Large Object
Length + 4 byte
최대 크기 4GB
TINYTEXT
매우 작은 비이진 문자열
Length + 1 byte
TEXT
작은 비이진 문자열
Length + 1 byte
최대 크기 64KB
MEDIUMTEXT
중간 크기 비이진 문자열
Length + 3 byte
최대 크기 16MB
LONGTEXT
큰 비이진 문자열
Length + 4 byte
최대 크기 4GB
Todo 리스트를 저장하기 위한 테이블 생성
create table tbl_todo (
tno int auto_increment primary key,
title varchar(100) not null,
dueDate date not null,
finished tinyint default 0
);
tbl_todo라는 이름으로 생성
tno는 primary key로 사용하며, auto_increment는 새로운 데이터 추가 시 자동으로 새로운 번호가 생성되도록 함
dueDate는 '년-월-일'로 기록할 수 있는 date타입 이용
MariaDB에서 boolean 값은 true / false 값 대신 0 / 1로 사용하는 경우가 많으므로 tinyint타입으로 처리
정상적으로 생성된 tbl_todo
실습_3. 데이터 insert
데이터 추가
insert into tbl_todo (title, dueDate, finished)
values ('Test...', '2022-12-31', 1);
위의 코드 여러 번 실행실행 결과 tbl_todo에 데이터 여러 개가 추가 된 모습, tno는 자동으로 1부터 5까지 추가되어 있음
실습_4. 데이터 select
데이터 조회
'from'으로 데이터를 가져올 테이블 지정
'where로 조회할 데이터의 조건 지정
select * from tbl_todo where tno=1;
실습_5. 데이터 update
기존 데이터 수정
'set'으로 특정 칼럼 내용 수정
'where'로 수정할 데이터의 조건 지정
3번 데이터의 finished와 title 값을 변경하고 싶다면 아래의 코드 작성
update tbl_todo set finished = 0, title = 'Not Yet...' where tno = 3;
실습_6. 데이터 delete
데이터 삭제
'where ' 조건에 해당하는 데이터 삭제
'where' 조건이 없다면 모든 데이터 삭제할 수 있으므로 경고 메세지와 함께 실행되지 않음
tno가 5보다 큰 데이터를 삭제하고 싶다면 아래의 코드 작성
delete from tbl_todo where tno > 5;
4) DML과 쿼리(select)의 차이
DML(insert, update, delete)와 select의 차이
DML은 몇 개의 데이터가 처리되었는지 숫자로 결과 반환
select문은 데이터를 반환
update문 실행 예시
한 개의 row가 영향을 받았다고 숫자로 결과가 출력됨
select문 실행 예시
select * from tbl_todo;
실제 데이터인 tbl_todo 표 자체가 출력됨
5) JDBC 프로그래밍을 위한 API와 용어들
java.sql.Connection
Connection 인터페이스는 데이터베이스와 네트워크 상의 연결을 의미
데이터베이스에 SQL을 실행하기 위해 반드시 정상적인 Connection 타입의 객체 생성해야 함
가장 중요한 사실은 "Connection은 반드시 close()해야 한다", 연결이 종료되지 않으면 새로운 연결을 받을 수 없는 상황이 발생함
Connection 종료를 위해 try ~ catch ~ finally 또는 try-with-resource를 사용(후자 사용 시 자동으로 close()가 호출됨)
가장 중요한 기능은 Statement 혹은 Prepared-Statement 등 SQL을 실행할 수 있는 객체를 생성하는 기능
// Prepared-Statement 객체 생성 코드
Connection connection = ...
PreparedStatement preparedStatement = connection.preparedStatement("select * from tbl_todo");
java.sql.Statement / PreparedStatement
JDBC에서 SQL을 데이터베이스로 보내기 위해 Statement / PreparedStatement 타입 이용
PreparedStatemetn는 SQL문을 미리 전달하고 나중에 데이터를 보내는 방식
Statement는 SQL문 내부에서 모든 데이터를 같이 전송하는 방식
실제 개발에서는 SQL 내부에 고의적으로 다른 SQL 문을 심는 SQL injection을 막기 위해 PreparedStatement만 사용
Statement / PreparedStatement의 주요 기능
setXXX(): setInt(), setString(), setDate()와 같이 다양한 타입에 맞게 데이터 세팅
executeUpdate(): DML을 실행하고 결과를 int 타입으로 반환(몇 행이 영향을 받았는지)
executeQuery(): 쿼리(select)를 실행할 때 사용, ResultSet이라는 return 타입 이용
Statement도 Connection처럼 마지막에 close() 해주어야, 데이터베이스 내부에서도 메모리와 같이 사용했던 자원들이 즉각 정리됨
java.sql.ResultSet
쿼리(select)를 실행했을 때 반환하는 데이터를 읽어들이기 위한 인터페이스
자바 코드에서 데이터를 읽어 들이기 때문에 getInt(), getString() 등의 메서드를 이용해서 필요한 타입으로 데이터를 읽어 들임
ResultSet의 메서드 next(): ResultSet은 데이터를 순차적으로 읽는 방식으로 구성되기 때문에 next()를 이용해 다음 행의 데이터를 읽을 수 있도록 이동하는 작업이 필요
ResultSet 역시 마지막에 close() 해주어야 데이터베이스에서 자원을 즉각 회수
Connection Pool과 DataSource
JDBC 프로그램은 기본적으로 필요한 순간 잠깐 데이터베이스과 네트워크로 연결하고 데이터를 주고 받는 방식
이 과정에서 데이터베이스와 연결을 맺는 작업은 많은 시간과 자원을 쓰므로 SQL을 여러 번 실행하면 성능 저하
이 때, Connection Pool을 이용하여 문제 해결
Connection Pool: 미리 Connection들을 생성해 보관, 필요할 때 꺼내 쓰는 방식
javax.sql.DataSource 인터페이스는 Connection Pool을 자바에서 API 형태로 지원
Connection Pool은 이미 작성된 라이브러리 이용(DBCP, C3PO, HikariCP 등)
DAO(Data Access Object)
데이터를 전문적으로 처리하는 객체
데이터베이스의 접근과 처리를 전담하는 객체이고, 주로 VO(Value Object, 읽을 수 있는 값) 단위로 처리
DAO를 호출하는 객체는 DAO 내부에서 어떤 식으로 데이터를 처리하는지 알 수 없도록 구성
VO(Value Object) 혹은 엔티티(Entity)
객체지향 프로그램은 데이터를 객체 단위로 처리(ex)테이블 한 행이 자바 프로그램에서 하나의 객체)
데이터베이스에서는 하나의 데이터를 하나의 엔티티라고 하며 자바 프로그램은 이를 처리하기 위해 테이블과 유사한 구조의 클래스를 만들어 객체로 처리
이때 만든 객체는 값을 보관하는 용도라는 의미에서 VO라고 함
DTO는 각 계층을 오고 가는데 사용되는 택배 상자와 비슷 / VO는 데이터베이스의 엔티티를 자바 객체로 표현
DTO는 getter / setter를 이용해 자유롭게 데이터 가공 / VO는 주로 데이터 자체를 의미하므로 getter만 사용