로그인 체크가 필요한 상황
회원의 정보가 필요하거나, 해당 회원의 접근권한을 확인해야 할 때가 있습니다.
마이페이지, 내 정보 수정 등이 대표적인 예 입니다.
DelFood 화면 프로토타입의 마이페이지는 다음과 같습니다.
해당 화면을 출력하려면 일단 회원 로그인이 먼저 진행되어야 합니다. 비회원의 경우에는 내 정보를 볼 수 없겠죠.
변경 전 프로젝트 코드
AOP를 적용하지 않은 상태에서는 다음과 같이 코드를 작성하여 권한을 확인하였습니다.
1. HttpSession에서 Member id 정보를 가져온다.
2. 가져온 id 정보가 null이면 401 status를 반환한다.
3. id가 null이 아니면 핵심 로직을 수행한 후 반환한다.
Session 체크용 공통 모듈
SessionUtil Code
public class SessionUtil {
private static final String LOGIN_MEMBER_ID = "LOGIN_MEMBER_ID";
private static final String LOGIN_OWNER_ID = "LOGIN_OWNER_ID";
// 인스턴스화 방지
private SessionUtil() {}
/**
* 로그인한 고객의 아이디를 세션에서 꺼낸다.
* @author jun
* @param session 사용자의 세션
* @return 로그인한 고객의 id 또는 null
*/
public static String getLoginMemberId(HttpSession session) {
return (String) session.getAttribute(LOGIN_MEMBER_ID);
}
/**
* 로그인 한 고객의 id를 세션에 저장한다.
* @author jun
* @param session 사용자의 session
* @param id 로그인한 고객의 id
*/
public static void setLoginMemberId(HttpSession session, String id) {
session.setAttribute(LOGIN_MEMBER_ID, id);
}
.... 중략
}
고객 마이페이지 조회 코드
@GetMapping("myInfo")
public ResponseEntity<MemberDTO> memberInfo(HttpSession session) {
ResponseEntity<MemberDTO> responseEntity = null;
String id = SessionUtil.getLoginMemberId(session);
if (id == null) {
responseEntity = new ResponseEntity<MemberDTO>(HttpStatus.UNAUTHORIZED);
} else {
MemberDTO memberInfo = memberService.getMemberInfo(id);
responseEntity = new ResponseEntity<MemberDTO>(memberInfo, HttpStatus.OK);
}
return responseEntity;
}
마이페이지 조회 코드이지만 실제로 꼭 필요한 핵심 로직보다 로그인 검사에 더 많은 코드가 있습니다. 또한 로그인이 필요한 코드를 제작할 때 똑같은 로그인 검사 코드가 반복됩니다.
이럴 경우, Spring에서 제공해주는 강력한 기능인 AOP를 적용할 수 있습니다. 저는 로그인 체크를 어노테이션 하나만 붙여주면 자동으로 진행되도록 만들어 보았습니다.
리펙토링 과정
메서드에 혼재되어있는 로직을 우선 걷어내야 합니다. 로그인 체크 부분을 없에고 프록시 객체를 사용하여 로그인 체크를 진행한 후 로그인이 되어있지 않다면 핵심 로직 진입 전에 예외처리로 throw할 예정입니다.
@MemberLoginCheck
package com.delfood.aop;
public @interface MemberLoginCheck {
}
어노테이션은 표시할 용도이니 따로 내부에 필요한 것은 없습니다. 이제 권한을 체크할 AOP 전용 체크 클래스를 제작해야 합니다. 해당 과정을 위해서는 spring-boot-starter-aop 의존성이 필요합니다.
pom.xml
<!-- AOP -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-aop</artifactId>
</dependency>
의존성을 추가해 준 후 이제 AOP를 사용하겠습니다! 라고 최상위 클래스에 AOP를 스캔하는 @EnableAspectJAutoProxy 를 적용합니다.
@EnableAspectJAutoProxy // 최상위 클래스에 적용해야 AOP를 찾을 수 있도록 만들어준다.
public class FoodDeliveryApplication {
public static void main(String[] args) {
SpringApplication.run(FoodDeliveryApplication.class, args);
}
}
이제 준비는 끝났습니다. 실제로 AOP를 적용하는 로직을 작성합니다
@Aspect
@Component
@Log4j2
@SuppressWarnings("unchecked")
public class AuthCheckAspect {
@Autowired
private ShopService shopService;
....
/**
* 고객의 로그인을 체크한다.
* @author jun
* @param pjp
* @return
* @throws Throwable
*/
@Before("@annotation(com.delfood.aop.MemberLoginCheck)")
public void memberLoginCheck(JoinPoint jp) throws Throwable {
log.debug("AOP - Member Login Check Started");
HttpSession session = ((ServletRequestAttributes)(RequestContextHolder.currentRequestAttributes())).getRequest().getSession();
String memberId = SessionUtil.getLoginMemberId(session);
if (memberId == null) {
throw new HttpStatusCodeException(HttpStatus.UNAUTHORIZED, "NO_LOGIN") {};
}
}
}
HttpSession을 RequestContextHolder에서 꺼낸 후 미리 만들어둔 SessionUtil을 사용하여 검사를 진행합니다. 만약 세션에 값이 없다면 HttpStatusCodeException을 사용하여 HttpStatus값과 에러 바디를 조정하여 예외를 던집니다.
리팩토링 후
MyPage 로직
@GetMapping("myInfo")
@MemberLoginCheck
public ResponseEntity<MemberInfoResponse> memberInfo(HttpSession session) {
String id = SessionUtil.getLoginMemberId(session);
MemberDTO memberInfo = memberService.getMemberInfo(id);
return new ResponseEntity<MemberInfoResponse>(new MemberInfoResponse(memberInfo), HttpStatus.OK);
}
권한 확인에 필요한 모든 로직이 사라졌습니다. 다른 로그인 체크 메서드와 권한 확인 메서드도 위와 같이 변경하여 같은 로직이 반복되는 코드를 다수 제거할 수 있었습니다.
'Project > DelFood' 카테고리의 다른 글
[이슈 #7] 서버 부하를 줄이기 위한 캐싱 적용 (0) | 2019.11.21 |
---|---|
[이슈 #6] 주소 데이터의 빠른 조회를 위해 인덱스 설정하기 (2) | 2019.11.01 |
[이슈 #4] 주소를 외래키로 관리하도록 변경(공공데이터 사용) (1) | 2019.10.27 |
[이슈 #3] 아이디 중복 체크시 Http Status값을 어떻게 설정해야 할까? (2) | 2019.10.06 |
[이슈 #2] 고객의 주문 내역을 조회할 때 테이블 다수 조인 이슈 - 2 (1) | 2019.09.23 |
댓글