[01.04] 내일배움캠프[Spring] TIL-46

박상훈·2023년 1월 4일
0

내일배움캠프[TIL]

목록 보기
46/72

[01.04] 내일배움캠프[Spring] TIL-46

1. Spring 중간 미니 프로젝트

오늘 한 것

  • 현재 권한을 분류하여 회원가입 완료
  • 권한에 따른 게시글 작업 완료
  • 권한에 따른 댓글 작업 완료
  • 권한에 따른 댓글 좋아요 / 게시글 좋아요 완료

오늘 배운 것과 느낀점

Exception 처리

  • 근본적인 의문이 들었다.
    -> 우리 로직은 서비스 딴에서 권한을 판단하고 IllegalArgumentException을 던지는데고 이렇게 만 작업하면 코드상의 500번대 오류가 되기 때문에
    -> ExceptionHandler를 사용하여 잡아줬다.
@ExceptionHandler(value = { IllegalArgumentException.class })
  • 그런데 이렇게 되면 권한이 없는 HttpStatus가 나가야 하는데 BAR_REQUEST가 나간다.
    -> 그럼 요청을 잘못했다? 우리는 권한이 없어서 막은건데?
  • 물론 다른 Exception을 던지고 그걸 잡아주는 @ExceptionHandler를 만들어서 UNAUTHORIZED를 던져줘도 된다.
  • 하지만, 이미 Config딴에서 AccessDeniedExcepion처리가 가능하다.
http.exceptionHandling().accessDeniedHandler(customAccessDeniedHandler);@Secured 권한 오류 잡고 커스텀 한 cutomAccessDeniedHanlder에서 인증실패 던져주면 된다!!!'

CSRF을 왜 Disabled?

  • 만약 인증된 사용자가 잘못된 이메일이나 아이디를 넘긴다면?? -> 모순!!!
  • RestAPI 방식으로 요청 시 헤더에 토큰을 넘겨주는 방식에서는 상관없다.( 계속 검증하니까..)

Build 패턴??

  • 이 패턴에 대한 의문을 ResponseEntity를 사용하면서 발생했다.
1. return new ResponseEntity<>(responseDto,HttpStatus.OK)
2. return ResponseEntity.status(HttpStatus.OK).body(responseDto)
  • 도대체 무슨차이일까?
    -> 1번은 생성자를 사용해서 값을 초기화
    -> 2번은 빌더를 사용해서 값을 초기화
  • 만약 해당 생성자에 넣어줘야 할 값이 자주 바뀌거나 여러가지 쓰인다
    -> 그 만큼 생성사를 만들어줘야함..!!
    -> 그 안에 파라미터가 뭐가 들어가는지 한눈 파악하기 어려움
User.UserBuilder().username(name).password(password)
-> 가독성도 좋고 생성자가 유연하다고 표현할 수 있음!

2. 기존 했던 예제 보고 복습하기

@EnableScheduling

  • 전에 했던 예제 중 네이버 API를 통해 상품을 검색하고 최저가를 갱신하는 예제가 있었다.
  • 해당 상품의 최저가 -> 네이버 측에서 가격 변동 시 데이터 바꿈 -> 내가 원하는 최저가를 등록해놓고 거기 이하 일 때 어떠한 이벤트가 발생!
  • 네이버는 1초에 한번씩 아이템을 조회할 수 있게 제한..!
  • 스프링 프로젝트를 돌릴 때 @Scheduled()를 사용할 수 있게 하자
  • 이해 안될 때 보는 좋은 블로그 : https://kkh0977.tistory.com/1165
  • 네이버 서비스 예제 코드
package com.sparta.myselectshop.scheduler;

import com.sparta.myselectshop.dto.ItemDto;
import com.sparta.myselectshop.entity.Product;
import com.sparta.myselectshop.repository.ProductRepository;
import com.sparta.myselectshop.service.NaverApiService;
import com.sparta.myselectshop.service.ProductService;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.scheduling.annotation.Scheduled;
import org.springframework.stereotype.Component;

import java.util.List;
import java.util.concurrent.TimeUnit;

@Slf4j
@Component 
@RequiredArgsConstructor
public class Scheduler {

    private final NaverApiService naverApiService;
    private final ProductService productService;
    private final ProductRepository productRepository;

    // 초, 분, 시, 일, 월, 주 순서
    // 설정 시간에 해당 메서드를 실행할 수 있게하는 어노테이션
    @Scheduled(cron = "0 0 1 * * *")
    public void updatePrice() throws InterruptedException {
        log.info("가격 업데이트 실행");
        List<Product> productList = productRepository.findAll();
        for (Product product : productList) {
            // 1초에 한 상품 씩 조회합니다 (NAVER 제한)
            TimeUnit.SECONDS.sleep(1);

            String title = product.getTitle();
            List<ItemDto> itemDtoList = naverApiService.searchItems(title);
            ItemDto itemDto = itemDtoList.get(0);

            // i 번째 관심 상품 정보를 업데이트합니다.
            Long id = product.getId();
            productService.updateBySearch(id, itemDto);
        }
    }
}
  • 서비스를 사용할 클레스 빈 등록 = @Component
  • @Scheduled()요소로 ()안에 지정한 시간 혹은 날짜 알고리즘으로 해당 스케줄 알고리즘 실행
  • @Sheduled()예시
  // [2초 후 3초마다 반복 실행 스케줄링]
    @Scheduled(fixedDelay = 3000,  initialDelay = 2000)
    
    // [1초 후 5초마다 반복 실행 스케줄링]
    @Scheduled(fixedDelay = 5000,  initialDelay = 1000)
    
     // [매일 오전 8시 58분에 실행되는 스케줄링]
    // [* 초(0-59)     * 분(0-59)   * 시간(0-23)   * 일(1-31)  * 월(1-12)  * 요일(0-7)]
    @Scheduled(cron = "0 58 8 * * *")

AOP

  • 저번에 포스팅 했지만 복습할겸 간단하게 다시!
  • 기본 개념은 주요 기능( 회원가입, 로그인, 포스팅하기, 댓글남기기) 등이 아닌 서브 기능( 시간 측정 등..)이 주요 로직 사이사이에 들어간다면, 그것을 전부 작성 해주지말고, 모듈화해서 작업하자!!
  • Aspect : 위에서 설명한 흩어진 관심사를 모듈화 한 것. 주로 부가기능을 모듈화함.
  • Component : 당연히 빈 등록이 필요하기 때문.
  • Around : 핵심로직 즉, 실행할 로직의 부분 명시
package com.sparta.myselectshop.aop;

import com.sparta.myselectshop.entity.ApiUseTime;
import com.sparta.myselectshop.entity.User;
import com.sparta.myselectshop.repository.ApiUseTimeRepository;
import com.sparta.myselectshop.security.UserDetailsImpl;
import lombok.RequiredArgsConstructor;
import lombok.Synchronized;
import lombok.extern.slf4j.Slf4j;
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.Around;
import org.aspectj.lang.annotation.Aspect;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.stereotype.Component;

@Slf4j
@Aspect
@Component
@RequiredArgsConstructor
public class UseTimeAop {

    private final ApiUseTimeRepository apiUseTimeRepository;

    @Around("execution(public * com.sparta.myselectshop.controller..*(..))") // 모든 Controller부분에 다 동작시키겠다.
    public synchronized Object execute(ProceedingJoinPoint joinPoint) throws Throwable {
        // 측정 시작 시간
        long startTime = System.currentTimeMillis();

        try {
            // 핵심기능 수행
            Object output = joinPoint.proceed(); // ProceedingJoinPoint joinPoint 를 받는데 , Controller 즉, 핵심로직을 실행시키는 부분이다.
            return output;
        } finally {
            // 측정 종료 시간
            long endTime = System.currentTimeMillis();
            // 수행시간 = 종료 시간 - 시작 시간
            long runTime = endTime - startTime;

            // 로그인 회원이 없는 경우, 수행시간 기록하지 않음
            Authentication auth = SecurityContextHolder.getContext().getAuthentication();

            if (auth != null && auth.getPrincipal().getClass() == UserDetailsImpl.class) {
                // 로그인 회원 정보
                UserDetailsImpl userDetails = (UserDetailsImpl) auth.getPrincipal();
                User loginUser = userDetails.getUser();

                // API 사용시간 및 DB 에 기록
                ApiUseTime apiUseTime = apiUseTimeRepository.findByUser(loginUser)
                        .orElse(null);
                if (apiUseTime == null) {
                    // 로그인 회원의 기록이 없으면
                    apiUseTime = new ApiUseTime(loginUser, runTime);
                } else {
                    // 로그인 회원의 기록이 이미 있으면
                    apiUseTime.addUseTime(runTime);
                }

                log.info("[API Use Time] Username: " + loginUser.getUsername() + ", Total Time: " + apiUseTime.getTotalTime() + " ms");
                apiUseTimeRepository.save(apiUseTime);

            }
        }
    }
}

@Exception Handler

  • 핵심로직에서 발생하는 Exception에 대한 반환을 모듈화 하기위한 구조
  • 지금 비즈니스로직에 회원정보가 일치하지 않거나, 정상적 흐름에 반하는 경우 new IllegalArgumentException을 내뱉게 되는데, 그렇게만 처리할 경우 사실상 Client의 오류 때문에 Exception이 발생했으나, 결과를 보면 서버 코드의 오류 때문으로 착각할 수 있다.
  • 따라서 발생하는 Exception을 잡아 잘못된 요청, 400번대 오류를 내 뱉어준다면 클라이언트의 실수라고 파악할 수 있다.
  • 만약 위와 같은 로직이 @ExceptionHanlder을 통해 Response설정이 되어있지 않다면, 비즈니스 로직에서 Exception을 내뱉는 곳에 하나씩 전부 추가해줘야한다.
  • Global Exception 처리 전 컨트롤러 마다 있는 ExceptionHandler의 모습
    @ExceptionHandler(IllegalArgumentException.class)
    public ResponseEntity<Object> handlerApiRequestException(IllegalArgumentException e) {
        RestApiResponse restApiResponse = new RestApiResponse(HttpStatus.BAD_REQUEST, e.getMessage());
        return new ResponseEntity<>(restApiResponse, HttpStatus.BAD_REQUEST);
    }
  • Global Exception 처리 후 모듈화 되어있는 ExceptionHandler의 모습
package com.sparta.myselectshop.exception;

import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.ExceptionHandler;
import org.springframework.web.bind.annotation.RestControllerAdvice;

@RestControllerAdvice
public class RestApiExceptionHandler {

    @ExceptionHandler(value = { IllegalArgumentException.class })
    public ResponseEntity<Object> handleApiRequestException(IllegalArgumentException ex) {
        RestApiException restApiException = new RestApiException();
        restApiException.setHttpStatus(HttpStatus.BAD_REQUEST);
        restApiException.setErrorMessage(ex.getMessage());

        return new ResponseEntity(
                restApiException,
                HttpStatus.BAD_REQUEST
        );
    }
}
  • @RestControllerAdvice vs @ControllerAdvice
    -> 사실 @ControllerAdvice + @ResponseBody라고 생각하면 편하다.
profile
기록하는 습관

0개의 댓글