Interceptor란
Interceptor는 낚아채다의 의미를 가지고있다.
Client에서 Server로 들어온 Request 객체를 Controller의 Handler로 도달하기 전 가로채어,
원하는 추가 작업이나 로직을 수행 한 후 Handler로 보낼 수 있도록 해주는 Module이다.
Handler(우린 @Controller)가 실행되기 전에 HandlerInterceptor가 먼저 실행된다.
HandlerInterceptor를 거쳐 Request에 대해 원하는 작업, 로직을 수행한 후
Controller로 Request 객체를 전달한다.
보통 Login, 권한체크, Header나 Login Session 검증, 권한체크, API TOKEN 검증 등에 사용된다.
Login인을 예로 들면 로그인을 하지않고 '개인정보페이지'를 요청하면
개인정보페이지를 보여주는게 아니라 로그인페이지 등으로 이동하도록 하는데 사용된다.
Interceptor위치를 그림으로 보면 다음과 같다.
우리는 Handler(Controller)가 실행되기전에 interceptor가 먼저 실행 되는 것에 주의하자.
또, Filter는 DispatcherServlet밖에 있지만, Interceptor는 DispatcherServlet안에 있다.
즉, 실제 일을 수행하는 Interceptor클래스를 만들었으면
빈 등록을 DispatcherServlet 설정파일에다 해야한다.
Interceptor 구현하기
Interceptor를 만드는 방법은 HandlerInterceptorAdapter를 상속 받으면 된다.
/**
* @author Juergen Hoeller
* @since 05.12.2003
*/
public abstract class HandlerInterceptorAdapter implements AsyncHandlerInterceptor {
@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler)
throws Exception {
return true;
}
@Override
public void postHandle(
HttpServletRequest request, HttpServletResponse response, Object handler, ModelAndView modelAndView)
throws Exception {
}
@Override
public void afterCompletion(
HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex)
throws Exception {
}
@Override
public void afterConcurrentHandlingStarted(
HttpServletRequest request, HttpServletResponse response, Object handler)
throws Exception {
}
}
- preHandle: Controller 진입 전 실행 . return값이 true이면 Controller 정상 실행, false이면 Controller 진입x.
Controller 진입하지 않고 바로 응답객체를 클라이언트에 보낸다. - postHandle : Controller 실행 후 View 가 render 되기 전에 실행
- afterCompletion : View가 정상적으로 render 된 후 실행
- afterConcurrentHandlingStarted : 비동기 요청 시에만 실행. postHandle,afterColmpletion 대신 해당 메소드 실행
실습
이 글에서는 로그인체크, 권한체크에 관해서 실습을 할 거고 실습은 preHandle만 하겠다.
Interceptor 적용방법은 다음과 같다.
- HandlerInterceptorAdaper를 상속받은 Interceptor 클래스를 만든다
이 때 preHandle,postHandle 등 적용 지점에 따라 메소드를 override한다. - DispatcherServlet 설정파일에 Interceptor를 등록한다.
LoginCheckInterceptor
현재 MypageController에 있는 @RequestMapping url은 모두 다음과 같은 형태다.
"/mypage/*"
내 개인페지이는 로그인한 사람만 볼 수 있어야 하기 때문에
이 해당메소드들에는 다음과 같은 코드가 있다.
MypageController.java
@Controller
public class MypageController {
@RequestMapping("/mypage/info.wow")
public String info(Model model, HttpSession session,HttpServletRequest req) {
UserVO user = (UserVO) session.getAttribute("USER_INFO");
if(user==null) {
return "redirect:"+req.getContextPath()+"/login/login.wow";
}
try {
//기타내용
}
}
@RequestMapping("/mypage/edit.wow")
public String edit(Model model, HttpSession session,HttpServletRequest req) {
UserVO user = (UserVO) session.getAttribute("USER_INFO");
if(user==null) {
return "redirect:"+req.getContextPath()+"/login/login.wow";
}
try {
//기타내용
}
}
@RequestMapping("/mypage/modify.wow")
public String modify(Model model, HttpSession session, HttpServletRequest req,
@ModelAttribute("member") @Validated(value = {Modify.class}) MemberVO member
,BindingResult error) {
if(error.hasErrors()) {
return "mypage/edit";
}
UserVO user = (UserVO) session.getAttribute("USER_INFO");
if(user==null) {
return "redirect:"+req.getContextPath()+"/login/login.wow";
}
try {
//기타내용
}
}
}
세 메소드 모두 로그인이 되어있는지 전부 확인하는 코드가 중복되게 있다.
if(user==null) {
return "redirect:"+req.getContextPath()+"/login/login.wow";
}
Interceptor가 없으면 @RequestMapping url이 "/mypage/something" 추가 될 때마다
로그인확인 코드를 다시 작성해야 할 것이다.
하지만 "/mypage/*"로 요청이 올때마다 Interceptor를통과하다록 한다면 로그인확인코드를
@RequestMapping메소드에서는 작성할 필요가 없다.
@RequestMapping메소드에 도착했다는것은 Interceptor를 통과했다는 것이고
이는 곧 로그인이 되었다는걸 의미하기 때문이다.
LoginCheckInterceptor 적용하기
LoginCheckInterceptor.java
package com.study.common.interceptor;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import javax.servlet.http.HttpSession;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.servlet.handler.HandlerInterceptorAdapter;
import com.study.login.vo.UserVO;
public class LoginCheckInterceptor extends HandlerInterceptorAdapter {
@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler)
throws Exception {
HttpSession session=request.getSession(); //getSession()을 통해 session이 언제생기는지에 대해 복습하자.
UserVO user=(UserVO)session.getAttribute("USER_INFO");
if(user==null) {
response.sendRedirect(request.getContextPath()+"/login/login.wow");
return false;
}
return true;
}
}
이 Interceptor는 로그인을 했으면 컨트롤러로 요청이 전달되고,
로그인이 안되어있으면 login페이지로 redirect하도록 하는 Interceptor다.
이렇게 Interceptor를 만들기만 하면 아무런 일도 하지 않는다.
이 Interceptor가 일을 하도록 DispatcherServlet에 등록해보자.
mvc-servlet.xml에 다음과 같은 태그를 추가하자.
mvc-servlet.xml
<interceptors>
<interceptor>
<mapping path="/mypage/*" /> <!-- mapping은 특정 url요청이 올때 무언가 하겠다 -->
<beans:bean
class="com.study.common.interceptor.LoginCheckInterceptor"></beans:bean> <!-- 무언가에 대한 내용 -->
</interceptor>
</interceptors>
/mypage/* 에 해당하는 url요청은 LoginCheckInterceptor를 통과하도록한다는 의미이다.
(freeList처럼 로그인이 필요없는 요청도 LoginCheckInterceptor를 통과하면 안된다).
이렇게 LoginCheckInterceptor를 만들면 MypageController에서
로그인여부를 검사하는 코드는 필요없어진다.
MypageController에 도착했다는건 LoginCheckInterceptor를 통과한거고
이미 로그인이 되어있다는 것이기 때문이다.
MypageController.java
package com.study.mypage.web;
import java.util.List;
import javax.inject.Inject;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpSession;
import org.apache.commons.beanutils.BeanUtils;
import org.springframework.stereotype.Controller;
import org.springframework.ui.Model;
import org.springframework.validation.BindingResult;
import org.springframework.validation.annotation.Validated;
import org.springframework.web.bind.annotation.ModelAttribute;
import org.springframework.web.bind.annotation.RequestMapping;
import com.study.code.service.CommCodeServiceImpl;
import com.study.code.service.ICommCodeService;
import com.study.code.vo.CodeVO;
import com.study.common.valid.Modify;
import com.study.common.vo.ResultMessageVO;
import com.study.exception.BizNotEffectedException;
import com.study.exception.BizNotFoundException;
import com.study.login.vo.UserVO;
import com.study.member.service.IMemberService;
import com.study.member.service.MemberServiceImpl;
import com.study.member.vo.MemberVO;
@Controller
public class MypageController {
@Inject
IMemberService memberService;
@Inject
ICommCodeService codeService;
@ModelAttribute("jobList")
public List<CodeVO> jobList() {
return codeService.getCodeListByParent("JB00");
}
@ModelAttribute("hobbyList")
public List<CodeVO> hobbyList() {
return codeService.getCodeListByParent("HB00");
}
@RequestMapping("/mypage/info.wow")
public String info(Model model, HttpSession session) {
UserVO user = (UserVO) session.getAttribute("USER_INFO"); //interceptor를 통과했기때문에 null 절대 아님
try {
MemberVO member = memberService.getMember(user.getUserId());
model.addAttribute("member", member);
return "mypage/info";
} catch (BizNotFoundException e) {
ResultMessageVO resultMessageVO = new ResultMessageVO();
resultMessageVO.messageSetting(false, "회원없음", "회원이없습니다", "/", "홈으로");
model.addAttribute("resultMessageVO", resultMessageVO);
return "common/message";
}
}
@RequestMapping("/mypage/edit.wow")
public String edit(Model model, HttpSession session) {
UserVO user = (UserVO) session.getAttribute("USER_INFO"); //interceptor를 통과했기때문에 null 절대 아님
try {
MemberVO member = memberService.getMember(user.getUserId());
model.addAttribute("member", member);
return "mypage/edit";
} catch (BizNotFoundException e) {
ResultMessageVO resultMessageVO = new ResultMessageVO();
resultMessageVO.messageSetting(false, "회원없음", "회원이없습니다", "/", "홈으로");
model.addAttribute("resultMessageVO", resultMessageVO);
return "common/message";
}
}
@RequestMapping("/mypage/modify.wow")
public String modify(Model model, HttpSession session, HttpServletRequest req,
@ModelAttribute("member") @Validated(value = {Modify.class}) MemberVO member
,BindingResult error) {
if(error.hasErrors()) {
return "mypage/edit";
}
UserVO user = (UserVO) session.getAttribute("USER_INFO");
// if(user==null) {
// return "redirect:"+req.getContextPath()+"/login/login.wow";
// }
try {
memberService.modifyMember(member);
user.setUserName(member.getMemName());
user.setUserPass(member.getMemPass());
session.setAttribute("USER_INFO", user);
return "redirect:" + "/";
} catch (BizNotFoundException e) {
ResultMessageVO resultMessageVO = new ResultMessageVO();
resultMessageVO.messageSetting(false, "내정보없음", "내정보가 없습니다", "/", "홈으로");
model.addAttribute("resultMessageVO", resultMessageVO);
return "common/message";
} catch (BizNotEffectedException e) {
ResultMessageVO resultMessageVO = new ResultMessageVO();
resultMessageVO.messageSetting(false, "내정보수정실패", "내정보 수정실패했습니다", "/", "홈으로");
model.addAttribute("resultMessageVO", resultMessageVO);
return "common/message";
}
}
}
로그인 안하고 /mypage/info.wow로 요청해보자.
로그인 화면으로 redirect된다.
로그인을 하고 "/mypage/info.wow" 을 요청하면 개인페이지가 보인다.
ManagerCheckInterceptor
지금까지는 게시판 연습을 위해서 member 게시판에 그냥 접근하도록 했지만,
사실 member게시판에는 다른 회원의 정보까지 있다.
그래서 이 member게시판에는 아무나 접근할 수 없고
MANAGER만 접근해야한다. 이를 위해 ManagerCheckInterceptor를 만들어보자.
ManagerCheckInterceptor.java
package com.study.common.interceptor;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import javax.servlet.http.HttpSession;
import org.springframework.web.servlet.handler.HandlerInterceptorAdapter;
import com.study.login.vo.UserVO;
public class ManagerCheckInterceptor extends HandlerInterceptorAdapter{
@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler)
throws Exception {
HttpSession session=request.getSession();
UserVO user=(UserVO)session.getAttribute("USER_INFO");
if(user==null) {
response.sendRedirect(request.getContextPath()+"/login/login.wow");
return false;
}
if(!user.getUserRole().equals("MANAGER")) {
response.sendError(HttpServletResponse.SC_FORBIDDEN, "당신은 매니저가 아닙니다.");
return false;
}
return true;
}
}
로그인을 안했으면 역시 login페이지로,
로그인을 했어도 MANAGER가 아니면 error를 보내도록 했다.
MANAGER이면 정상적으로 member게시판을 볼 수 있다.
mvc-servlet.xml
<interceptors>
<interceptor>
<mapping path="/mypage/*" /> <!-- mapping은 특정 url요청이 올때 무언가 하겠다 -->
<beans:bean
class="com.study.common.interceptor.LoginCheckInterceptor"></beans:bean> <!-- 무언가에 대한 내용 -->
</interceptor>
<interceptor>
<mapping path="/member/********" /> <!-- mapping은 특정 url요청이 올때 무언가 하겠다 -->
<beans:bean
class="com.study.common.interceptor.ManagerCheckInterceptor"></beans:bean> <!-- 무언가에 대한 내용 -->
</interceptor>
</interceptors>
"/member/*"로 요청이 오면 ManagerCheckInterceptor를 통과하게된다.
근데 아직 로그인했을 때 session에 "MANAGER" 가 담기지 않는다.
LoginServiceImpl에서 UserVO 생성해서 session 엗 담을 때 "MEMBER" 만 담긴다.
이를 특정 ID의 경우 "MANAGER"가 세팅되도록해보자.
먼저 DB에 USER_ROLE테이블을 만들자.
CREATE TABLE user_role (
user_id VARCHAR2(30) NOT NULL,
role_nm VARCHAR2(30) NOT NULL,
CONSTRAINT pk_user_role PRIMARY KEY (user_id, role_nm)
);
-- 모든 사용자 에게 "MEMBER"
INSERT INTO user_role ( user_id , role_nm)
select mem_id , 'MEMBER' from member ;
-- 특정 아이디 "MANAGER" 나는 a004를 MANAGER로 할거야
UPDATE user_role SET
role_nm='MANAGER'
WHERE user_id='a004'
IMemberDao.java
@Mapper
public interface IMemberDao {
public int getTotalRowCount(MemberSearchVO searchVO);
public List<MemberVO> getMemberList(MemberSearchVO searchVO);
public MemberVO getMember(String memId);
public int updateMember(MemberVO member);
public int deleteMember(MemberVO member);
public int insertMember(MemberVO member);
public String getUserRoleByMemId(String memId); //userRole을 위해 추가된 메소드
public int insertUserRole(String memId); //userRole을 위해 추가된 메소드
}
member.xml
<select id="getUserRoleByMemId" parameterType="String" resultType="String">
SELECT role_nm
FROM user_role
WHERE user_id = #{memId}
</select>
<insert id="insertUserRole" parameterType="String" >
INSERT INTO user_role(user_id,role_nm)
values (#{memId},'MEMBER')
</insert>
getUserRoleByMemId메소드는 로그인 시 UserVO에 userRole을 세팅하는데 사용되고
insertUserRole은 회원가입시 member테이블 뿐만 아니라
user_role테이블에 id와 role이 추가되도록 하는데 사용된다.
로그인 UserVO를 session에 추가하는 부분은
LoginController의 @PostMapping("/login/login.wow") 메소드
=> LoginServiceImpl의 getUser메소드이다.
LoginServiceImpl.java
package com.study.login.service;
import javax.inject.Inject;
import org.apache.ibatis.session.SqlSession;
import org.apache.ibatis.session.SqlSessionFactory;
import org.springframework.stereotype.Repository;
import org.springframework.stereotype.Service;
import com.study.login.vo.UserVO;
import com.study.member.dao.IMemberDao;
import com.study.member.vo.MemberVO;
@Service
public class LoginServiceImpl implements ILoginService {
@Inject
IMemberDao memberDao;
@Override
public UserVO getUser(String id) {
MemberVO member = memberDao.getMember(id);
if(member==null) {
return null;
}else {
UserVO user=new UserVO();
user.setUserId(member.getMemId());
user.setUserName(member.getMemName());
user.setUserPass(member.getMemPass());
String userRole=memberDao.getUserRoleByMemId(member.getMemId());
//이제 userRole을 단순히 MEMBER로 세팅하는게 아니라
//user_role테이블을 조회해서 세팅한다.
if(userRole.equals("MANAGER")) {
user.setUserRole("MANAGER");
}else {
user.setUserRole("MEMBER");
}
return user;
}
}
}
사실 여기까지만 하면 ManagerCheckInterceptor관련 실습은 무리가 없지만
앞으로 회원가입 할 때 user_role에 "MEMBER"가 insert되도록하자.
MemberServiceImpl.java
@Override
public void registMember(MemberVO member) throws BizNotEffectedException, BizDuplicateKeyException {
MemberVO vo = memberDao.getMember(member.getMemId());
if (vo != null) {
throw new BizDuplicateKeyException();
}
int cnt = memberDao.insertMember(member);
if (cnt == 0)
throw new BizNotEffectedException();
int cnt2= memberDao.insertUserRole(member.getMemId());
if(cnt2==0) throw new BizNotEffectedException();
}
memberDao.insertMember 후 memberDao.insertUserRole을 실행한다.
로그인을 안하고 "member/memberList.wow" 로 요청하자.
로그인 페이지로 redirect 된다.
"MEMBER" 아이디로 로그인하고 "/member/memberList.wow" 요청해보자.
"MANGER" 아이디로 로그인하고 "/member/memberList.wow"로 요청해보자. 회원리스트가 잘 나온다.
※여기서 잠깐. MEMBER테이블에 member_role 컬럼을 추가하면 될 걸
왜 굳이 USER_ROLE 테이블을 만들었을까?
일반적으로 시스템 성능저하 문제의 대부분은 잘못된 DB설계이다.
DB설계를 어떻게 하느냐가 전체 성능에 큰 영향을 끼치는데
이 DB설계 기법중 하나가 데이터베이스 분할(파티셔닝)이다.
이 파티셔닝의 가장 기본적인게 수평분할과 수직분할이다.
수평분할
하나의 테이블의 각 행을 다른 테이블에 분산시키는 것이다.
예를 들어 방대한 고객 데이터 테이블을 성별에 따라 '남녀'로 나누어
CustomerMen과 CustomerWomen 두 개의 테이블로 분할한다.
남자고객일 때는 CustomerMen테이블을 조회하고,
여자고객일 때는 CustomerWomen 테이블을 조회한다.
두 테이블의 칼럼은 같다.
수직분할
테이블의 일부 열을 빼내는 형태로 분할한다.
관계의 정규화는 본질적으로 수직 분할에 관련된 과정이다.
하지만 정규화와 구별되어야하는점은 정규화가 된 이후에도
자주쓰지않는 칼럼이거나, 보안 등의 이유로 따로 컬럼을 나누는 경우가 있을 수 있다.