2022년 4월 16일(토)
[스파르타코딩클럽] Spring 심화반 - 5주차
class Scratch {
public static void main(String[] args) {
// 측정 시작 시간
long startTime = System.currentTimeMillis();
// 함수 수행
long output = sumFromOneTo(1_000_000_000);
// 측정 종료 시간
long endTime = System.currentTimeMillis();
long runTime = endTime - startTime;
System.out.println("소요시간: " + runTime);
}
private static long sumFromOneTo(long input) {
long output = 0;
for (int i = 1; i < input; ++i) {
output = output + i;
}
return output;
}
}
@Component
public class TestDataRunner implements ApplicationRunner {
// 아래 방법으로 Class를 불러오는 것을 Spring에서 추천하지 않음
// 테스트 데이터 생성에 이용하므로 이번에만 사용
@Autowired
UserService userService;
@Autowired
ProductRepository productRepository;
@Autowired
UserRepository userRepository;
@Autowired
PasswordEncoder passwordEncoder;
@Autowired
ItemSearchService itemSearchService;
// 서버 실행시, 해당 함수를 실행
@Override
public void run(ApplicationArguments args) throws Exception {
// 테스트 User 생성
User testUser1 = new User("tester", passwordEncoder.encode("123"), "jg@sparta.com", UserRoleEnum.USER);
User testUser2 = new User("tester2", passwordEncoder.encode("123"), "hope@sparta.com", UserRoleEnum.USER);
User testAdminUser1 = new User("admin", passwordEncoder.encode("123"), "army@sparta.com", UserRoleEnum.ADMIN);
testUser1 = userRepository.save(testUser1);
testUser2 = userRepository.save(testUser2);
testAdminUser1 = userRepository.save(testAdminUser1);
// 테스트 testUser1 의 관심상품 등록
// 검색어 당 관심상품 10개 등록
createTestData(testUser1, "신발");
createTestData(testUser1, "과자");
createTestData(testUser1, "키보드");
createTestData(testUser1, "휴지");
createTestData(testUser1, "휴대폰"); // createTestData 함수 생략
}
}
부가기능 모듈화의 필요성
부가기능을 모듈화
스프링이 제공하는 AOP
@Aspect
@Component
public class UseTimeAop {
private final ApiUseTimeRepository apiUseTimeRepository;
public UseTimeAop(ApiUseTimeRepository apiUseTimeRepository) {
this.apiUseTimeRepository = apiUseTimeRepository;
}
@Around("execution(public * com.sparta.springcore.controller..*(..))")
public Object execute(ProceedingJoinPoint joinPoint) throws Throwable {
// 측정 시작 시간
long startTime = System.currentTimeMillis();
try {
// 핵심기능 수행
Object output = joinPoint.proceed(); // 들어온 요청을 controller 로 보냄
return output; // controller 에서 처리된 요청을 반환
} 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);
}
System.out.println("[API Use Time] Username: " + loginUser.getUsername() + ", Total Time: " + apiUseTime.getTotalTime() + " ms");
apiUseTimeRepository.save(apiUseTime);
}
}
}
}
시퀀스 다이어그램 (Sequence Diagram)
스프링 AOP 어노테이션
포인트컷 Expression Language
// 위치에 해당하는 용어 (물음표 생략 가능)
@kind-of-Advice("execution(modifiers-pattern? return-type-pattern declaring-type-pattern? method-name-pattern(param-pattern) throws-pattern?)")
// 예시
@Around("execution(public * com.sparta.springcore.controller..*(..))")
public Object execute(ProceedingJoinPoint joinPoint) throws Throwable { ... }
modifiers-pattern
return-type-pattern
declaring-type-pattern 클래스명 (패키지명 필요)
method-name-pattern(param-pattern)
@Pointcut 포인트컷 재사용 및 결합 가능
@Component
@Aspect
public class Aspect {
@Pointcut("execution(* com.sparta.springcore.controller.*.*(..))")
private void forAllController() {}
@Pointcut("execution(String com.sparta.springcore.controller.*.*())")
private void forAllViewController() {}
@Around("forAllContorller() && !forAllViewController")
public void saveRestApiLog() {
...
}
@Around("forAllContorller()")
public void saveAllApiLog() {
...
}
}
트랜잭션: 데이터베이스에서 데이터에 대한 하나의 논리적 실행단계
트랜잭션의 필요성
트랜잭션의 정체
@Transactional
public List<Folder> addFolders(List<String> folderNames, User user) {
// ...
}
Transaction의 Flowchart
현업에서 DB 운영방식
Primary / Replica
@Transactional(readOnly = false)
@Transactional(readOnly = true)
Primary 에 문제가 생겼을 때, Replica 중 1개가 Primary 가 됨
스프링 기본 에러 처리(Response 메시지 velog 참고)
HTTP 상태 코드 종류
기본적으로 Spring에서 Exception처리를 할 경우, 500 Server Error가 남
이때, Client에서 값을 잘못 주었을 경우에는 400 Error로 바꾸어 보내줄 필요가 있음
Spring 적용
// exception > RestApiException
...
import org.springframework.http.HttpStatus;
@Getter
@Setter
public class RestApiException {
private String errorMessage;
private HttpStatus httpStatus;
}
// controller > FolderController (컨트롤러 하나에 적용)
// HttpStatus는 enum 값임
...
@ExceptionHandler({ IllegalArgumentException.class })
public ResponseEntity handleException(IllegalArgumentException ex) {
RestApiException restApiException = new RestApiException();
restApiException.setHttpStatus(HttpStatus.BAD_REQUEST);
restApiException.setErrorMessage(ex.getMessage());
return new ResponseEntity(
// HTTP body
restApiException,
// HTTP status code
HttpStatus.BAD_REQUEST
);
}
// exception > RestApiExceptionHandler
// HttpStatus는 enum 값임
@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
);
}
}
import org.springframework.http.HttpStatus;
import static com.sparta.springcore.service.ProductService.MIN_MY_PRICE;
public enum ErrorCode {
// 400 Bad Request
DUPLICATED_FOLDER_NAME(HttpStatus.BAD_REQUEST, "400_1", "중복폴더명이 이미 존재합니다."),
BELOW_MIN_MY_PRICE(HttpStatus.BAD_REQUEST, "400_2", "최저 희망가는 최소 " + MIN_MY_PRICE + " 원 이상으로 설정해 주세요."),
// 404 Not Found
NOT_FOUND_PRODUCT(HttpStatus.NOT_FOUND, "404_1", "해당 관심상품 아이디가 존재하지 않습니다."),
NOT_FOUND_FOLDER(HttpStatus.NOT_FOUND, "404_2", "해당 폴더 아이디가 존재하지 않습니다."),
;
private final HttpStatus httpStatus;
private final String errorCode;
private final String errorMessage;
ErrorCode(HttpStatus httpStatus, String errorCode, String errorMessage) {
this.httpStatus = httpStatus;
this.errorCode = errorCode;
this.errorMessage = errorMessage;
}
}