일 | 월 | 화 | 수 | 목 | 금 | 토 |
---|---|---|---|---|---|---|
1 | 2 | |||||
3 | 4 | 5 | 6 | 7 | 8 | 9 |
10 | 11 | 12 | 13 | 14 | 15 | 16 |
17 | 18 | 19 | 20 | 21 | 22 | 23 |
24 | 25 | 26 | 27 | 28 | 29 | 30 |
- 부하테스트
- ci/cd
- nonclustered index
- B+TREE
- method area
- Spring Data Redis
- JPA
- Redis
- Ehcache
- JAVA8
- 공변
- CaffeineCache
- 지연로딩
- backend
- 제네릭
- 동시성
- 재고 시스템
- 리팩터링
- 상태패턴
- Jenkins
- 주문
- JDK14
- lazyloading
- GithubActions
- 카카오 화재
- 웹캐시
- 트랜잭션
- Metaspace
- springboot
- java
- Today
- Total
NDM
확장성을 고려한 로그인 세션 관리하기 본문
넘블 타임딜 챌린지에는 다음과 같은 요구사항이 명시되어 있습니다.
- 로그인 관리는 세션으로만 하며, Spring Security나 JWT의 사용은 제한한다
- WAS 서버는 1대로만 제한한다.
아마 이번 챌린지의 주된 목표는 재고의 동시성 처리와 상품조회에 있다고 생각해 다음과 같은 제약사항을 걸어두신 것으로 예상합니다. 구현하는데 시간이 더 걸리고, 인프라 구성 비용도 늘어나기 때문입니다.
HttpServletRequest와 HttpSession을 이용해 손쉽게 구현할 수 있었지만, 다음과 같은 상황을 고려해보기로 했습니다.
이왕 참가한 만큼 무언가를 더 얻어가고 싶었기 때문입니다.
- WAS가 1대로 제한되어있으나, 이후 확장 가능성까지 고려하지 말라는 말은 없었다. 이후 확장 가능성을 고려한다면 어떻게 세션을 관리하는 것이 좋을까?
# 문제점
HttpSession을 이용해 세션을 로컬에서 관리하게 된다면 다음과 같은 문제점이 있습니다.
WAS서버가 Scale-out 될 때, 사용자의 지속 로그인을 보장할 수 없게 됩니다.
- 첫번째 요청에서 1번서버로 로드밸런싱 된 사용자의 정보는 1번 서버에서 관리하게 됩니다.
- 다음 요청에서 로드밸런서가 1번이 아닌 다른 서버로 로드밸런싱 하게 되면, 사용자의 세션 정보는 1번 서버에서 관리하고 있기에 로그인이 풀리는 현상이 일어나게 되고, 이는 서비스 품질 저하로 이어지게 됩니다.
# 해결방법 ?
Sticky Session
첫번째 요청에서 세션을 저장한 서버로 이후 요청을 고정하는 방식입니다.
다만 특정 서버에 과부하가 될 수 있어 로드밸런싱의 성능을 떨어뜨린다는 단점이 있고,
특정 서버가 Fail될 경우 모든 세션정보는 소실됩니다.
Session Clustering
하나의 서버가 갖고있는 세션 정보를 모든 서버들이 공유하는 방식입니다.
Session 분리
# 나는 어떻게 해결했나
이번 프로젝트에서 세션은 Redis라는 외부 저장소를 사용해 별도로 관리하는 방법을 택했습니다.
사용자 정보라는 특성상 read가 많고, 캐시 특성상 일정 시간이 되면 풀려버리는 것 까지 로그인 관련 기능에 알맞은 데이터 저장소라고 판단하였습니다.
SpringBoot에서는 이러한 Redis로의 세션 관리를 위한 여러 도구를 지원합니다.
@EnableCaching
@EnableRedisHttpSession
@SpringBootApplication
public class TimedealApplication {
public static void main(String[] args) {
SpringApplication.run(TimedealApplication.class, args);
}
}
먼저 @EnableRedisHttpSession 어노테이션을 사용하겠습니다.
HttpSession을 담아둘 저장소로 Redis를 사용하겠다. 이런 뜻입니다.
다음은 로그인 기능을 간단하게 작성하겠습니다. 제 경우는 Spring Security를 사용하지 않고
정말 간단하게 아이디랑 비밀번호로만 로그인한다.. 까지만 구현했습니다.
@Service
@RequiredArgsConstructor
public class SessionLoginService implements LoginService {
public static final String USER_SESSION_KEY = "USER_ID";
private final UserRepository userRepository;
private final HttpSession httpSession;
@Override
public UserSelectResponse logIn(UserLoginRequest request) {
User user = userRepository.findByUserNameAndPassword(request.getUsername(), request.getPassword())
.orElseThrow(() -> new BusinessException(ErrorCode.LOG_IN_FAILURE));
httpSession.setAttribute(USER_SESSION_KEY, AuthUser.of(user));
return UserSelectResponse.of(getCurrentUser());
}
@Override
public void logOut() {
httpSession.invalidate();
}
@Override
public AuthUser getCurrentUser() {
return (AuthUser) httpSession.getAttribute(USER_SESSION_KEY);
}
}
LoginCheck 기능은 AOP로 해결하겠습니다. 관리자 / 사용자 정도만 구현하겠습니다.
@Before("@annotation(com.example.timedeal.common.annotation.LoginCheck) && @annotation(target)")
public void LoginCheck(LoginCheck target) {
AuthUser currentUser = loginService.getCurrentUser();
if(StringUtils.equals(target.role().name(), "GENERAL")) {
checkLogin(currentUser);
}
else if(StringUtils.equals(target.role().name(), "ADMINISTRATOR")) {
checkAdmin(currentUser);
}
}
여기까지 왔으면
처음 : 로그인 -> 세션 정보 저장
이후 : api 요청 -> 로그인 정보 확인
까지는 완료되었습니다. 마지막으로 로그인 되어있을 때, 해당 유저 정보를 가져오겠습니다.
저는 @CurrentUser 어노테이션을 하나 생성하고, 컨트롤러 메서드 파라미터로 사용해 정보를 가져올 생각입니다.
저는 HandlerMethodArgumentResolver를 확장해 해결하였습니다.
@Component
@RequiredArgsConstructor
public class CurrentUserResolver implements HandlerMethodArgumentResolver {
private final LoginService loginService;
private final UserService userService;
@Override
public boolean supportsParameter(MethodParameter parameter) {
return parameter.hasParameterAnnotation(CurrentUser.class);
}
@Override
public Object resolveArgument(MethodParameter parameter, ModelAndViewContainer mavContainer, NativeWebRequest webRequest, WebDataBinderFactory binderFactory) throws Exception {
AuthUser currentUser = loginService.getCurrentUser();
if(currentUser == null) {
throw new BusinessException(ErrorCode.LOG_IN_ESSENTIAL);
}
return userService.findUser(currentUser.getId());
}
}
CurrentUser 어노테이션을 설정하고, 해당 어노테이션에 알맞는 동작을 작성해주었습니다.
물론 ArgumentResolver 이기 떄문에, WebMvcConfigurer 에서 설정해주어야 합니다.
이렇게 하면
- Redis에 로그인 정보가 저장되고,
- LoginCheck를 통해 로그인 여부를 알 수 있으며, 조건에 부합하지 못할 시 예외처리를 해줄 수 있습니다.
- 로그인 여부가 통과되었고 해당 엔드포인트 비즈니스 로직에서 유저 세션 정보가 필요할 때, @CurrentUser를 통해 간단히 가져올 수 있습니다.
# 출처
https://kchanguk.tistory.com/146
https://tlatmsrud.tistory.com/48
'[프로젝트] 타임딜 API 서버' 카테고리의 다른 글
재고 처리 로직 동시성 이슈 해결 일지 2 (0) | 2023.06.05 |
---|---|
시스템 디자인 재설계로 상품 조회 api를 개선해보자 (0) | 2023.06.04 |
상품 조회 api와 주문 api 설계하기 (0) | 2023.06.03 |
Jmeter를 이용한 이벤트 상품 조회 api 부하 테스트와 개선일지 (0) | 2023.06.01 |
6. 프로젝트의 클라우드 아키텍처를 설계해보자 ( feat. Naver Cloud ) (0) | 2023.02.28 |