[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) {
TimeUnit.SECONDS.sleep(1);
String title = product.getTitle();
List<ItemDto> itemDtoList = naverApiService.searchItems(title);
ItemDto itemDto = itemDtoList.get(0);
Long id = product.getId();
productService.updateBySearch(id, itemDto);
}
}
}
- 서비스를 사용할 클레스 빈 등록 =
@Component
@Scheduled()
요소로 ()안에 지정한 시간 혹은 날짜 알고리즘으로 해당 스케줄 알고리즘 실행
@Scheduled(fixedDelay = 3000, initialDelay = 2000)
@Scheduled(fixedDelay = 5000, initialDelay = 1000)
@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..*(..))")
public synchronized Object execute(ProceedingJoinPoint joinPoint) throws Throwable {
long startTime = System.currentTimeMillis();
try {
Object output = joinPoint.proceed();
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();
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
라고 생각하면 편하다.