-
[과제] 일정 관리 앱 JPA내일배움캠프/과제 2025. 4. 4. 05:43
✅ 기본 구현 요구사항 (필수)
1️⃣ 일정 CRUD
- 일정 등록 시 저장 데이터:
- 할일 제목
- 할일 내용
- 작성자명
- 작성일/수정일 (날짜와 시간 포함)
- 일정 고유 ID 자동 생성
- 작성일과 수정일은 JPA Auditing 활용
2️⃣ 유저 CRUD
- 유저 등록 시 저장 데이터:
- 유저명
- 이메일
- 작성일/수정일 (날짜와 시간 포함)
- 유저 고유 ID 자동 생성
- 작성일과 수정일은 JPA Auditing 활용
- 일정과 작성 연관관계 구현
3️⃣ 회원가입
- 유저
비밀번호
필드 추가
4️⃣ 로그인(인증)
Cookie/Session
을 활용해 로그인 기능을 구현이메일
과비밀번호
를 활용해 로그인 기능을 구현- 회원가입, 로그인 요청은 인증 처리에서 제외
- 예외처리
- 로그인 실패 시
401
반환
- 로그인 실패 시
🚀 도전 기능 (선택)
1️⃣ 다양한 예외처리 적용
- Validation 적용 ex) @Valid, @Pattern
2️⃣ 비밀번호 암호화
비밀번호
에 암호화 적용PasswordEncoder
활용
3️⃣ 댓글 CRUD
- 댓글 등록 시 저장 데이터:
- 댓글 내용
- 작성일/수정일 (날짜와 시간 포함)
- 유저 고유 ID
- 일정 고유 ID
- 댓글 고유 ID 자동 생성
- 작성일과 수정일은 JPA Auditing 활용
- 일정, 유저와 작성 연관관계 구현
4️⃣ 일정 페이징 조회
Pageable
,Page
활용한 페이지네이션페이지 번호
,페이지 크기
를 요청할일 제목
,할일 내용
,댓글 개수
,일정 작성일
,일정 수정일
,일정 작성 유저명 필드
를 조회- 수정일 기준 내림차순 조회
🔧 트러블 슈팅
저번 과제는 같은 내용을 JDBC Template 으로 진행했는데 이번에는 JPA 로 진행이 되었다.
그리고 Session을 활용한 로그인을 사용하였다.
1. Login
HTtpServletRequest에서 Session을 get한다음 정보를 저장했다.
public BaseResponse<Boolean> login(@RequestBody @Valid LoginDto dto, HttpServletRequest request) {MemberResDto memberResDto = authService.login(dto);HttpSession session = request.getSession(); // Session 을 가져온다.// Session 에 로그인 회원 정보를 저장한다.session.setAttribute(SystemValues.LOGIN_USER.getValue(), memberResDto);return BaseResponse.from(true);}public MemberResDto login(LoginDto dto) {Member member = memberRepository.findByEmail(dto.getEmail()) // email로 Member 조회.orElseThrow(() -> new CustomException(CommonExceptionResultMessage.LOGIN_FAILED)); // 없을 경우 throwif (!passwordEncoder.matches(dto.getPassword(), member.getPassword())) { // 비밀번호 검증throw new CustomException(CommonExceptionResultMessage.LOGIN_FAILED); // 비밀번호 검증 실패 시 throw}return new MemberResDto(member); // MemberResDto 로 반환}public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain)throws IOException, ServletException {HttpServletRequest httpRequest = (HttpServletRequest) request;HttpServletResponse httpResponse = (HttpServletResponse) response;String requestURI = httpRequest.getRequestURI();log.info("로그인 필터 로직 실행");// 화이트리스트에 해당하지 않는 경우 로그인 체크if (!isWhiteList(requestURI)) {if (httpRequest.getSession(false) == null ||httpRequest.getSession(false).getAttribute(SystemValues.LOGIN_USER.getValue()) == null) {throw new CustomException(CommonExceptionResultMessage.AUTHENTICATION_FAILED);}}// 필터 체인 계속 실행 (다음 필터 또는 서블릿/컨트롤러 호출)chain.doFilter(request, response);}// URL이 화이트 리스트에 포함되어 있는지 확인하는 메서드private boolean isWhiteList(String requestURI) {AntPathMatcher matcher = new AntPathMatcher();for (String pattern : WHITE_LIST) {if (matcher.match(pattern, requestURI)) {return true;}}return false;}그리고 특정 Url 을 기준으로 WhiteList에 속하지 않으면 Filter에서 로그인하라고 걸러주었다.
근데 Custom으로 정의한 Exception이 핸들링이 되지 않았따.
Filter 에서 발생한 예외는 Spring MVC의 예외 처리 메커니즘(예: @ControllerAdvice, @ExceptionHandler)보다 먼저 발생하기 때문에, 해당 핸들러에서 잡히지 않는 것이었다. Filter는 DispatcherServlet보다 먼저 동작하기 때문에 CustomException이 던져지면 스프링의 예외 처리 체인에 도달하지 못하고, 컨테이너의 기본 에러 처리 방식(또는 등록된 에러 페이지)으로 넘어간다.
그래서 따로 Filter 내부에서 try-catch 하여 HttpServletResponse에 직접 에러 응답을 작성하기로 했다.
try {// 화이트리스트에 해당하지 않는 경우 로그인 체크if (!isWhiteList(requestURI)) {if (httpRequest.getSession(false) == null ||httpRequest.getSession(false).getAttribute(SystemValues.LOGIN_USER.getValue()) == null) {throw new CustomException(CommonExceptionResultMessage.AUTHENTICATION_FAILED);}}// 필터 체인 계속 실행 (다음 필터 또는 서블릿/컨트롤러 호출)chain.doFilter(request, response);} catch (CustomException ex) {// 필터 내에서 직접 예외 처리 및 JSON 응답 작성handleCustomException(httpRequest, httpResponse, ex);}...private void handleCustomException(HttpServletRequest request, HttpServletResponse response, CustomException ex)throws IOException {log.error("Exception URI : " + request.getRequestURI());log.error("customException : " + ex.getMessage(), ex);ObjectMapper objectMapper = new ObjectMapper();objectMapper.registerModule(new JavaTimeModule());objectMapper.disable(SerializationFeature.WRITE_DATES_AS_TIMESTAMPS);// BaseResponse 및 JSONResult 객체 구성BaseResponse res = new BaseResponse();res.setJsonResult(JSONResult.failure(CommonExceptionResultMessage.AUTHENTICATION_FAILED, ""));String responseBody = objectMapper.writeValueAsString(res);response.setContentType(MediaType.APPLICATION_JSON_VALUE);response.setStatus(HttpStatus.UNAUTHORIZED.value());response.setCharacterEncoding("UTF-8");response.getWriter().write(responseBody);}응답 Res 내용은 response.getWriter().write에 작성해주고 Status도 설정해서 전송해주었더니 정상 작동하였다
2. Session에서 객체 꺼내기
set을 한 것처럼 get을 해주면된다. session 에서 get을 하면 Object로 Return이 되므로 MemberResDto로 형변환을 해준다.
public BaseResponse<ScheduleResDto> saveSchedule(@RequestBody @Valid ScheduleReqDto dto, HttpServletRequest request) {HttpSession session = request.getSession();MemberResDto sessionMember = (MemberResDto) session.getAttribute(SystemValues.LOGIN_USER.getValue()); // sessionMemberreturn BaseResponse.from(scheduleService.saveSchedule(dto, sessionMember.getId()));}3. 기존 LogginInterceptor -> Filter 로 변경
LoggingInterceptor의 경우 잘 작성해주셨어요. 해당 log의 경우는 servlet 전 후로 처리할 필요는 없어보여요. 오히려 servlet에 도착하기 전에 filter로 logging하는게 더 적절해보이네요.
라는 피드백을 받았었다.
그래서 Interceptor을 Filter로 변환하는 작업을 했다.
기존 LogginInterceptor
@Slf4jpublic class LoggingInterceptor implements HandlerInterceptor {@Overridepublic boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) {log.info("====================Request Info====================");log.info("request.getRequestURI() : " + request.getRequestURI());log.info("request.getRequestURL() : " + request.getRequestURL());log.info("request.getServletPath() : " + request.getServletPath());log.info("request.getContextPath() : " + request.getContextPath());log.info("request.getPathInfo() : " + request.getPathInfo());log.info("request.getMethod() : " + request.getMethod());log.info("request.getRemoteAddr() : " + request.getRemoteAddr());log.info("request is Ajax : " + ServletUtils.isAjaxRequest());log.info("====================================================");log.info("====================Header Info=====================");Enumeration<String> headNames = request.getHeaderNames();while (headNames.hasMoreElements()) {String headName = headNames.nextElement();log.info("header ::: [" + headName + "] " + request.getHeader(headName));}log.info("====================Body Info=======================");Enumeration<String> enums = request.getParameterNames();while (enums.hasMoreElements()) {String paramName = enums.nextElement();String[] parameters = request.getParameterValues(paramName);for (String parameter : parameters) {log.info("parameter ::: [" + paramName + "] " + parameter);}}log.info("====================================================");return true;}@Overridepublic void postHandle(HttpServletRequest request, HttpServletResponse response, Object handler, ModelAndView modelAndView) throws Exception {HandlerInterceptor.super.postHandle(request, response, handler, modelAndView);}@Overridepublic void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) throws Exception {HandlerInterceptor.super.afterCompletion(request, response, handler, ex);}}변경 LoggingFilter
@Slf4jpublic class LoggingFilter implements Filter {@Overridepublic void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException {// Filter에서 수행할 로깅 처리HttpServletRequest httpRequest = (HttpServletRequest) request;String requestURI = httpRequest.getRequestURI();log.info("=============== Request Info ===============");log.info("Request URI: {}", requestURI);log.info("Method: {}", httpRequest.getMethod());log.info("Remote Addr: {}", httpRequest.getRemoteAddr());// Header 로깅Enumeration<String> headerNames = httpRequest.getHeaderNames();while(headerNames.hasMoreElements()){String header = headerNames.nextElement();log.info("Header [{}]: {}", header, httpRequest.getHeader(header));}// Parameter 로깅Enumeration<String> paramNames = httpRequest.getParameterNames();while(paramNames.hasMoreElements()){String paramName = paramNames.nextElement();String[] paramValues = httpRequest.getParameterValues(paramName);for(String value : paramValues){log.info("Parameter [{}]: {}", paramName, value);}}log.info("============================================");// 다음 필터 또는 서블릿 호출chain.doFilter(request, response);}}그리고 WebConfig 에서 Filter 를 등록해줄 때 순서를 설정해주었다.
@Beanpublic FilterRegistrationBean loggingFilter() {FilterRegistrationBean<Filter> filterRegistrationBean = new FilterRegistrationBean<>();filterRegistrationBean.setFilter(new LoggingFilter()); // Filter 등록filterRegistrationBean.setOrder(1); // Filter 순서 1 설정filterRegistrationBean.addUrlPatterns("/*"); // 전체 URL에 Filter 적용return filterRegistrationBean;}@Beanpublic FilterRegistrationBean loginFilter() {FilterRegistrationBean<Filter> filterRegistrationBean = new FilterRegistrationBean<>();filterRegistrationBean.setFilter(new LoginFilter()); // Filter 등록filterRegistrationBean.setOrder(2); // Filter 순서 2 설정filterRegistrationBean.addUrlPatterns("/*"); // 전체 URL에 Filter 적용return filterRegistrationBean;}'내일배움캠프 > 과제' 카테고리의 다른 글
[과제] 일정 관리 앱 만들기 도전 Lv5, Lv6 (0) 2025.03.25 [과제] 일정 관리 앱 만들기 도전 Lv4 (0) 2025.03.25 [과제] 일정 관리 앱 만들기 도전 Lv3 (0) 2025.03.25 [과제] 일정 관리 앱 만들기 필수 Lv2 (0) 2025.03.25 [과제] 일정 관리 앱 만들기 필수 Lv1 (0) 2025.03.25 - 일정 등록 시 저장 데이터: