Sparta/What I Learned

23.2.1

코딩하는 또롱이 2023. 2. 2. 15:04
시큐리티 - Spring AOP

 

 

 

🤔 Top5 회원을 찾아보자 (With. Timstamped)

 

👉 API 사용시간 측정 방법

Scratch File Java 로 생성 하고 코드 붙여넣기

class Scratch {
    public static void main(String[] args) {
        // 측정 시작 시간
        long startTime = System.currentTimeMillis();

        // 함수 수행
        // sumFromOneTo() 함수 : 1 에서 "입력된 숫자" 까지의 합계를 구하는 함수
        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;
    }
}

 

 

👉 일반 회원

  • 회원별 사용시간 누적 저장
    • 일단, 관심상품 저장하는 API (POST /api/products) 에만 적용

 

👉 관리자

  • '관리자' 만 조회 가능
    • API 위에 @Secured(UserRoleEnum.Authority.ADMIN) 추가
  • API 동작 확인
    • 테스트 회원 추가
      1. 일반 회원 2명 이상 추가
      2. 관리자 회원 추가
    • 관심상품 등록시마다 API 사용시간이 잘 저장 되는지 확인
      1. H2 Console 로 DB 확인

오잉 갑자기 html이 하나도 적용되지 않는다,.. ㅁㅓ선 123,,,,,

이렇든 저렇든 일단 API는 잘 된다.

 

그럼 조금 전 ProductController에서만 API를 구현 해놨는데 , 사이트를 이용하는 TOP 5를 구해야하므로 모든 Controller에다가 아래의 코드를 넣어줘야한다.

@PostMapping("/products")
public ProductResponseDto createProduct(@RequestBody ProductRequestDto requestDto, @AuthenticationPrincipal UserDetailsImpl userDetails) {
    // 측정 시작 시간
    long startTime = System.currentTimeMillis();
    try {
        return productService.createProduct(requestDto, userDetails.getUser());
    } finally {
        // 측정 종료 시간
        long endTime = System.currentTimeMillis();
        // 수행시간 = 종료 시간 - 시작 시간
        long runTime = endTime - startTime;

        // 로그인 회원 정보
        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);
    }
}

와 매우 번거롭고 엄두도 안 난다. (물론 몇 개 안되지만!)

 

그래서 부가기능 모듈화의 필요성을 느낀다.

 

 

사진에서 보다시피 핵심 기능마다 같은 부가 기능을 붙여줘야만 할 때, 만약 핵심 기능을 수정해야한다면??

😱 핵심 기능이 뭔지 부터 알아야겠지? 그럼 나는 또 부가 기능까지 알아야 하는 사태가 발생하겠지? 오케이. 알았어. 이해했어. 이제 수정해보자! 하고 핵심 기능 수정하는데 부가 기능도 수정을 해야하네?? 그럼 또 일일이 다른 핵심기능 여기저기로 가서 하나하나 다 수정해줘야겠지?? 와 상상만 해도 아찔해. 

😱 부가 기능 수정 요청이 들어왔다면??? 이것도 아찔하네.. 언제 여기저기 가서 다 고치지???

🤔 이거 어떻게 해결 할 수 있을까??

해서 나온게 AOP이다.

 

✅ AOP (Aspect Oriented Programming) 를 통해 부가 기능을 모듈화

  • '부가 기능' 은 '핵심 기능' 과는 관점(Aspect), 관심이 다름
  • 따라서 '핵심 기능'과 분리해서 '부가 기능'  중심으로 설계, 구현 가능

 

 

✅ 스프링이 제공하는 AOP

 

 

위의 코드로 부가 기능을 분석하면 아래의 사진과 같다.

형광색칠 되어 있는 부분이 부가기능!

 

시퀀스 다이어그램 (Sequence Diagram)

AOP 적용 전

AOP 적용 후

DispatcherServlet 과 ProductController 입장에서는 변화가 전혀 없음

  • 호출되는 함수의 input, output 이 완전 동일
  • "joinPoint.proceed()" 에 의해서 원래 호출하려고 했던 함수, 인수(argument) 가 전달됨
    → createProduct(requestDto)

 

✅ 스프링 AOP 어노테이션

(이 부분은 실제 구현과 관련된 내용이고, 굳이 외우지 말고 필요한 어노테이션을 그때 그때 찾아서 본다고 생각하면 된다)

 

1. @Aspect

  • 스프링 빈 (Bean) 클래스에만 적용 가능
  • @Component 어노테이션이랑 짝꿍이라고 생각하면 된다.

2. 어드바이스 종류

🙂 aop로 분류해서 모아둔 코드가 실행되는 시점이라고 생각하시면 편하다!
정확한 비유는 아닐 수 있지만, 스케줄러를 만들 때 해당 소스의 실행 시점을 정해주는데 그것처럼 aop 코드가 실행되는 시점을 지정해준다고 이해해봐도 좋다.
  • @Around : '핵심기능' 수행 전과 후 (@Before + @After)
  • @Before : '핵심기능' 호출 전 (ex. Client 의 입력값 Validation 수행)
  • @After : '핵심기능' 수행 성공/실패 여부와 상관없이 언제나 동작 (try, catch 의 finally() 처럼 동작)
  • @AfterReturning : '핵심기능' 호출 성공 시 (함수의 Return 값 사용 가능)
  • @AfterThrowing : '핵심기능' 호출 실패 시. 즉, 예외 (Exception) 가 발생한 경우만 동작 (ex. 예외가 발생했을 때 개발자에게 email 이나 SMS 보냄)

3. 포인트컷

🙂 어드바이스가 “실행될 시점” 을 정해줬다면, 포인트컷은 “실행될 장소” 를 정해준다고 생각하면 된다.
  • 포인트컷 Expression Language
    • 포인트컷 Expression 형태
      • ? 는 생략 가능
execution(modifiers-pattern? return-type-pattern declaring-type-pattern? method-name-pattern(param-pattern) throws-pattern?)
      • 포인트컷 Expression 예제
@Around("execution(public * com.sparta.springcore.controller..*(..))")
public Object execute(ProceedingJoinPoint joinPoint) throws Throwable { ... }

 

🙂 com.sparta.springcore.controller..
프로젝트 패키지 주소이다. 해당 주소 하위에 있는 소스들, 즉 컨트롤러들이 실행되는 장소라고 생각해보면 비유가 얼추 맞을 것 같다.
      • modifiers-pattern
        • public, private, *
      • return-type-pattern
        • void, String, List<String>, *
      • 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
        • 포인트컷 재사용 가능
        • 포인트컷 결합 (combine) 가능
@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() {
      ...
    }
}

 

 

 

 


 

 

시큐리티 - Exception

 

 

🤔 예외 처리를 따로 다루는 이유?

1) 개발자인만큼 웹 어플리게이션에서의 “예외”에 대하여 다시 한 번 인지할 필요가 있다.
웹 어플리케이션에서의 에러를 프론트엔드와 백엔드 모두가 잘 알지 못하면, 서비스하는 환경에서 발생하는 에러에 대해서 제대로 대응 할 수 없다.
2) aop를 배웠던 만큼, 에러를 처리하는 것 역시 관심사를 분리해서 더 효율적으로 처리 할 수 있지 않을까 고민해보는 시간이 필요하다.

 

🤔  웹 어플리케이션의 에러

👉 HTTP 에러 메시지 전달 방법 이해

사실 서버가 응답을 보낼 때 한가지 더 보내주는것이 있는데, 그게 바로 start-line에 있는 응답 코드이다. 즉 응답 헤더에는 상태 코드가 있고, 세자리로 되어있는 해당 번호에는 각각의 의미가 있다.

 

 ✅ Response Message

1. 상태줄: API 요청 결과 (상태 코드, 상태 텍스트)

HTTP/1.1 404 Not Found
티켓팅 할 때 마다 보는 404 혹은 503,,,, 지긋지긋하다,,,,!

👉HTTP 상태 코드 종류

  1. 2xx Success → 200번대의 상태코드는 성공을 의미
  2. 4xx Client Error → 400번대의 상태코드는 클라이언트 에러, 즉 잘못된 요청을 의미
  3. 5xx Server Error → 500번대의 상태코드는 서버 에러, 즉 정확한 요청에 서버쪽 사유로 에러가 난 상황을 의미

👉 org.springframework.http > HttpStatus

더보기
public enum HttpStatus {
    // 1xx Informational
    CONTINUE(100, Series.INFORMATIONAL, "Continue"),
    // ...

    // 2xx Success
    OK(200, Series.SUCCESSFUL, "OK"),
    CREATED(201, Series.SUCCESSFUL, "Created"),
    // ...

    // 3xx Redirection
    MULTIPLE_CHOICES(300, Series.REDIRECTION, "Multiple Choices"),
    MOVED_PERMANENTLY(301, Series.REDIRECTION, "Moved Permanently"),
    FOUND(302, Series.REDIRECTION, "Found"),
    // ...

    // --- 4xx Client Error ---
    BAD_REQUEST(400, Series.CLIENT_ERROR, "Bad Request"),
    UNAUTHORIZED(401, Series.CLIENT_ERROR, "Unauthorized"),
    PAYMENT_REQUIRED(402, Series.CLIENT_ERROR, "Payment Required"),
    FORBIDDEN(403, Series.CLIENT_ERROR, "Forbidden"),
    // ...


    // --- 5xx Server Error ---
    INTERNAL_SERVER_ERROR(500, Series.SERVER_ERROR, "Internal Server Error"),
    NOT_IMPLEMENTED(501, Series.SERVER_ERROR, "Not Implemented"),
    BAD_GATEWAY(502, Series.SERVER_ERROR, "Bad Gateway"),
// ...

 

2. 헤더

 

👉 "Content type"

  • 없음
  • Response 본문 내용이 HTML 인 경우
Content type: text/html
  • Response 본문 내용이 JSON 인 경우
Content type: application/json

 

3. 본문

👉 HTML

<!DOCTYPE html>
<html>
<head><title>By @ResponseBody</title></head>
<body>Hello, Spring 정적 웹 페이지!!</body>
</html>

👉 JSON

{
    "name":"홍길동",
    "age": 20
}

 

이렇게 다양한 상태 코드들이 있고, 백엔드 개발자가 제대로 예외 처리를 하지 않게 되면 500 에러가 떠서 프론트에서 헛고생을 할 수도 있다. 그러니까 잘 알아두고 꼭 예외 처리를 제대로 해서 다른 사람이 헛고생하지 않도록 하자!!

 

 

🤔 스프링의 예외처리

🚨 이제부터는 응답도 조금 더 신경써서 return 해주기 위해서, 스프링이 제공하는 ResponseEntity 클래스를
 사용해 볼 것이다.
  1. Controller 코드 수정
    • ResponseEntity는 HTTP response object 를 위한 Wrapper이다. 아래와 같은 것들을 담아서 response로 내려주면 아주 간편하게 처리가 가능하다.
      • HTTP status code
      • HTTP headers
      • HTTP body
  2. @ExceptionHandler 사용
    • FolderController 의 모든 함수에 예외처리 적용 (AOP)
      (이것 또한 매 컨트롤러마다 추가해주는건 너무 번거로운 일 ….🥲)
  3. 예외처리 적용 결과 확인
    • HTTP 응답 (상태코드) HTTP 400 Bad Request
    • HTTP 응답 본문 (Body)

HTTP 응답 (상태코드)
HTTP 응답 본문 (Body)

 

🤔 스프링 Global 예외 처리 방법

이 앞 번에 단위 테스트를 하면서 코드를 이렇게 짰었다.

형광펜 칠한 것 처럼 해주는 것이 보통이다. (앞으로 주구장창 쓸 것,,,,,🥹)

 

 

 

예외 처리 로직

  • @ControllerAdvice 사용
  • @RestControllerAdvice 사용
    • @ControllerAdvice + @ResponseBody

 

 

 


 

 

 

시큐리티 - Transaction

 

👉 뭣도 모르고 썼던 트랜잭션

@Transactional <<< ???
public List<Folder> addFolders(List<String> folderNames, String name) {

        User user = userRepository.findByUsername(name).orElseThrow(
        () -> new IllegalArgumentException("사용자가 존재하지 않습니다.")
        );

        // 입력으로 들어온 폴더 이름을 기준으로, 회원이 이미 생성한 폴더들을 조회합니다.
        List<Folder> existFolderList = folderRepository.findAllByUserAndNameIn(user, folderNames);

        List<Folder> folderList = new ArrayList<>();

        for (String folderName : folderNames) {
        // 이미 생성한 폴더가 아닌 경우만 폴더 생성
        if (isExistFolderName(folderName, existFolderList).equals("false")) {
        Folder folder = new Folder(folderName, user);
        folderList.add(folder);
        } else {
        throw new IllegalArgumentException("중복된 폴더명 ('" + isExistFolderName(folderName, existFolderList) + "')을 삭제하고 재시도해 주세요");
        }
        }

        return folderRepository.saveAll(folderList);
        }

 

👉 로직

  1. 폴더들과 이름을 인자로 넘겨받는다.
  2. 입력으로 온 폴더 이름을 기준으로 회원이 이미 생성한 폴더를 조회한다.
  3. 입력으로 온 폴더를 조회한 폴더들과 비교한다.
  4. 있는 폴더가 아니면 폴더를 생성한다.
  5. 이미 생성된 폴더를 만들지 않고 에러를 리턴합니다.
  6. 그런데 만약 5개의 폴더 생성 요청을 받아 처리하던 중 세 번째 폴더가 중복이면?
  7. 요청이 실패했으니, 해당 메서드의 실행 전과 데이터가 같아야 하긴 하는데… 이미 메서드가 있다.
  8. 다시 지워줘야 할까?(벌써 비효율적인 냄시가..)
  9. 이러한 것들을 위해서 트랜잭션을 활용한다!

 

✅트랜잭션이란?

데이터베이스에서 데이터에 대한 하나의 논리적 실행단계

ACID (원자성, 일관성, 고립성, 지속성)는 데이터베이스 트랜잭션이 안전하게 수행된다는 것을 보장하기 위한 성질을 가리키는 약어

특징 : 
1. 더 이상 쪼갤 수 없는 최소 단위의 작업
2. 하나의 최소 단위의 작업에 여러가지 데이터 변경을 넣으면 모두 저장되거나, 아무 것도 저장되지 않거나 둘 중 하나이다.

 

 

  • 비즈니스 로직에 트랜잭션 코드가 포함됨

 

 

  • @Transactional 사용 시 폴더 생성 Flowchart

 

 

 

🤔 데이터베이스를 더 안전하게 관리하려면? (Primary, Replica)

✅ DB 운영방식

  • 웹 서비스에서 DB 에 담겨있는 Data 는 가장 중요한 리소스 입니다.
    • 회원 정보
    • 서비스 이용 정보
  • DB 훼손 가능성
    • DB 도 결국 물리적인 HDD (하드 디스크) 에 존재
    • DB 가 저장된 하드디스크 고장
    • DB 가 저장된 컴퓨터 고장
  • 일반적으로는 DB 를 2대 이상 운영
    • 문제점: DB1 과 DB2 를 어떻게 데이터 Sync 를 할까요??
    • 예제를 통한 이해
      • 회원 A 계좌 잔고: 100만원
        1. DB1: 100만원
        2. DB2: 100만원
          1. 70만원 인출 시도
            1. DB1 에서 70만원 인출하여 잔고 30만원
            2. DB2 에서도 동일하게 잔고 30만원으로 데이터 Sync가 필요합니다.
          2. 50만원 인출 시도
            1. DB1 를 통해 잔고 확인 시
              • 회원 A 의 "잔고 30만원" 이기 때문에, 인출불가 에러 발생
            2. DB2 를 통해 잔고 확인 시
              • DB2 에 "첫번째 인출 시도한 데이터(70만원)" 가 Sync 적용되기 전이라고 가정
                • 혹은 DB2 에 데이터 Sync 중 에러가 발생했다고 가정
              • DB2 에서는 회원 A의 "잔고 100만원"  이 남아 있다고 판단
              • 50만원이 정상 인출됨
          3. DB1 과 DB2 의 데이터 불일치 ⇒ 어느 정보를 믿어야하지?
            1. DB1 에서 회원 A 의 잔고: 30만원
            2. DB2 에서 회원 B 의 잔고: 50만원
🤔 보통은 읽기 전용 DB와 쓰기 전용 DB를 분리하고, 쓰기 전용 DB를 카피(사실 데이터 변경을 추적)하는 읽기 전용 DB들을 다수 두는 방식으로 해결한다. 이렇게하면 부하도 분산되고 안전하게 운영이 가능하다.

 

✅ Primary / Replica 운영방식

쓰기 전용 DB (Primary) 와 읽기 전용 DB (Replica) 를 구분

 

👉 Primary: 쓰기 전용

  • @Transactional 의 readOnly 속성
@Transactional(readOnly = false)
  • readOnly 를 코드에 적지 않으면, 기본값은 false
import org.springframework.transaction.annotation.Transactional;

@Transactional
public List<Folder> createFolders(List<String> folderNameList, User user) {
  • Write 된 Data (Create, Update, Delete) 가 Replica 로 Sync 됨 (Replication)

 

👉 Replica (Secondary): 읽기 전용

하지만 이 개념은 스프링에 Primary DB endpoint, Replica DB endpoint 를 설정해야지만 가능하다.

@Transactional(readOnly = true)

 

👉 Primary 에 문제가 생겼을 때 Replica 중 1개가 Primary 가 됨

'Sparta > What I Learned' 카테고리의 다른 글

23.2.5  (0) 2023.02.06
23.2.2  (0) 2023.02.03
23.1.31  (0) 2023.02.02
23.1.30  (0) 2023.01.30
23.1.29  (0) 2023.01.30