Spring 심화반 - 5주차

귀찮Lee·2022년 4월 24일
0

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

  • 부가기능 모듈화의 필요성

    • 핵심기능 앞 또는 뒤로 반복되는 기능을 하는 부가기능이 존재
      ex) 회원 패턴 분석을 위한 로그 기록, API 수행시간 저장
    • 모든 Controller마다 부가기능을 추가한다면, 새로운 Controller를 만들때마다 내용을 빠짐없이 추가해야 하고, 부가기능을 수정한다면, 모든 Controller마다 다 빠짐없이 수정해주어야 한다.
  • 부가기능을 모듈화

    • AOP (Aspect Oriented Programming) 를 통해 부가기능을 모듈화
    • 부가기능과 핵심기능과는 관점(Aspect)가 다르므로, AOP를 이용해 부가기능을 중심으로 설계, 구현 가능
  • 스프링이 제공하는 AOP

  1. 어드바이스: 부가기능
  2. 포인트컷: 부가기능 적용위치
  • 스프링 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 어노테이션

    • @Aspect : 스프링 빈 (Bean) 클래스에만 적용 가능 (Component 포함)
    • 어드바이스 종류
      • @Around: '핵심기능' 수행 전과 후
      • @Before: '핵심기능' 호출 전 (ex. Client 의 입력값 Validation 수행)
      • @After: '핵심기능' 수행 성공/실패 여부와 상관없이 언제나 동작 (try, catch 의 finally() 처럼 동작)
      • @AfterReturning: '핵심기능' 호출 성공 시 (함수의 Return 값 사용 가능)
      • @AfterThrowing: '핵심기능' 호출 실패 시. 즉, Exception가 발생한 경우만 동작 (ex. 예외가 발생했을 때 개발자에게 email 이나 SMS 보냄)
  • 포인트컷 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

    • public, private, *
  • return-type-pattern

    • void, String, List, *
  • declaring-type-pattern 클래스명 (패키지명 필요)

    • com.sparta.springcore.controller.* - controller 패키지의 모든 클래스에 적용
    • com.sparta.springcore.controller.. - controller 패키지 및 하위 패키지의 모든 클래스에 적용
  • method-name-pattern(param-pattern)

    • 함수명
      • addFolders : addFolders() 함수에만 적용
      • add* : add 로 시작하는 모든 함수에 적용
    • 파라미터 패턴 (param-pattern)
      • (com.sparta.springcore.dto.FolderRequestDto) - FolderRequestDto 인수 (arguments) 만 적용
      • () - 인수 없음
      • (*) - 인수 1개 (타입 상관없음)
      • (..) - 인수 0~N개 (타입 상관없음)
  • @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)

  • 트랜잭션: 데이터베이스에서 데이터에 대한 하나의 논리적 실행단계

    • 더 이상 쪼갤 수 없는 최소단위의 작업
    • 모두 저장되거나, 아무 것도 저장되지 않거나를 보장
  • 트랜잭션의 필요성

    • 특정 주문을 시킨다고 생각해보면, 1.주문자에게 결제 2.생산자에게 생산요청을 보냄
    • 만약 1만 되고 2가 안된다면... 큰일이 난다.
    • 따라서 모두 성공시에는 Transaction Commit , 중간에 하나라도 실패시에는 Transaction Rollback을 실행
  • 트랜잭션의 정체

    • 앞에서 요청(?)을 보내고 이후 정상 실행 여부에 따라 Commit 또는 Rollback을 함
    • 이는 AOP를 이용해서 모듈화 가능 (어노테이션을 통해 적용)
    @Transactional
    public List<Folder> addFolders(List<String> folderNames, User user) {
      // ...
    }
  • Transaction의 Flowchart

◎ 현업에서 DB 운영 방식

  • 현업에서 DB 운영방식

    • 현업에서 DB에 담겨있는 데이터는 웹 서비스를 하는데 매우 중요하다.
    • 그리고 DB는 물리적인 HDD에 담기므로 물리적인 훼손 가능성이 존재한다.
    • 따라서 현업에서는 DB를 2대 이상을 두고 운영함
    • 이때, DB Sync 문제가 생김 (순간적으로 두 DB의 데이터가 연동되지 않았을 때, 요청이 들어올 수 있음)
  • Primary / Replica

    • 쓰기 전용 DB (Primary) 와 읽기 전용 DB (Replica) 를 구분
      • Primary: 쓰기 전용, readOnly 를 코드에 적지 않으면, 기본값은 false
      • Write 된 Data (Create, Update, Delete) 가 Replica 로 Sync 됨 (Replication)
      @Transactional(readOnly = false)
      • Replica (Secondary): 읽기 전용
      @Transactional(readOnly = true)
    • 하지만, 위 개념은 스프링에 Primary DB endpoint, Replica DB endpoint 를 설정해야지만 가능!!
  • Primary 에 문제가 생겼을 때, Replica 중 1개가 Primary 가 됨

◎ 스프링 예외 처리 방법

  • 스프링 기본 에러 처리(Response 메시지 velog 참고)

  • HTTP 상태 코드 종류

    • 2xx Success
    • 4xx Client Error
    • 5xx Server Error
    • Spring org.springframework.http > HttpStatus.js 참고
      ( Ctrl + Shift + N 을 통해 파일 찾기를 할 수 있음)
  • 기본적으로 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
				);
    }
  • 스프링 Global 예외 처리 방법
    • @ControllerAdvice 사용 (@RestControllerAdvice = @ControllerAdvice + @ResponseBody)
// 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
        );
    }
}
  • ErrorCode 선언 (서비스 전체에 사용할 에러코드들 선언후 사용)
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;
    }
}
  1. httpStatus: HTTP 상태코드
  2. errorCode: 에러 종류별로 Unique 한 에러코드를 소유 / 국제화에 사용 가능(클라이언트가 사용하는 언어 (한국어, 영어, 중국어 등) 에 따라 에러메시지를 다르게 보여줌)
  3. errorMessage: 대표 에러 메시지
profile
배운 것은 기록하자! / 오류 지적은 언제나 환영!

0개의 댓글