주노 님의 블로그

20240910 본캠프 42일차 TIL 본문

TIL

20240910 본캠프 42일차 TIL

juno0432 2024. 9. 11. 09:57

본캠프 42일차 내용 간단요약

  • 09:00 ~ 10:00 : 코드카타
  • 10:00 ~ 12:00 : 강의
    1-7 ~ 1-10
  • 12:00 ~ 13:00 : 점심시간
  • 13:00 ~ 14:00 : 강의
    1-11 ~ 1-12
  • 14:00 ~ 15:00 : AOP 특강 
  • 18:00 ~ 19:00 : 저녁시간

오늘 해야할 일 ✔️ 🔺 ❌

 


AOP 강의

더보기

AOP - 횡단 관심사 분리

복잡한 뭐는 관심사끼리 분리해서

 

시간 측정 메서드를 등록할때

public CourseListResponseDto getCourseList() {

        long start = System.currentTimeMillis();

        try {
            // 조회: 수업 엔티티 목록 조회
            log.info("::: 수업 목록 조회 :::");
            List<Course> foundCourseList = courseRepository.findAll(); // 조회 쿼리 1 발생

            // DTO 변환: 수업 엔티티 -> CourseDto
            log.info("::: DTO 변환 - Course -> CourseDto :::");
            List<CourseDto> courseDtoList = foundCourseList.stream().map(course -> new CourseDto(
                course.getId(),
                course.getName(),
                course.getMembers().size()
            )).toList();

            // 응답 반환
            return new CourseListResponseDto(courseDtoList);
        } finally {
            //측정 완료

            long end = System.currentTimeMillis();

            long executionTime = end - start;
            log.info("::: 측정 결과 ::: 서비스 목록 조회 완료, 측정시간: {} ms", executionTime);
        }
    }

 

이렇게 모든 메서드에다가 시간 측정을 등록해야할까?

- 추후 측정 단위가 변경되면?

- 추후 시간 측정 로직이 변경된다면?

 

  • 핵심기능
    시스템의 주요 목적

    부가 기능
    주요 목적이 아닌 기능

    횡단 관심사
    부가 기능이 여러곳에서 반복적용되는 상황

  • 위 코드에서 횡단 관심사란?
    시간 측정 로직
    왜? 반복적용되니까

  • 그래서 aop란?
    핵심기능과 부가 기능(횡단 관심사)을 분리하는것

  • aop 용어
    어드바이스 : 실제로 실행되는 횡단관심사
    @Around, @Before, @After, @AfterThrowing
    포인트컷 : 어드바이스를 적용할 구체적인 범위를 선택하는 구칙
    @execution, @annotation, @within, @this, @target, @args
    타겟 : 어드바이스가 적용될 객체
    조인 포인트 : 어드바이스가 적용되는 실행지점
    애스팩트 : 어드바이스와 포인트컷을 하나로 묶은 모듈

테스트

더보기
  • Given - when - then
    given : 주어진 전제 조건을 정의, 테스트 실행을 위한 준비.
    >> int a = 1; int b = 3;
    when : 테스트하려는 메서드나 기능을 실행하는 과정.
    >> int sum = a+b;
    then : 메서드나 기능이 실행된 후 예상되는 결과가 나오는지 확인.
    assertEquals(4, sum);

    then에서 예상된 결과가 나오는지 테스트를 해본다.

  • Mock
    가짜 객체를 만들어서 테스트를 도와준다

    mockito프레임워크를 사용하여 구현한다.

    BDD MOCKITO : 특정한 행동을 정의한다(레파지토리로직을 정의하겠다.)
  •  

Unit Test

  • 단위테스트라고도 하며 단위는 프로젝트에서 가장 작은 테스트 가능 요소라고 할 수 있다.
    실제 객체를 사용하지않고 MOCKING을 사용함으로써, 의존성이 적고 빠르게 테스트를 할 수 있다.
    테스트는 개발간에 즉시 작성하는게 좋다.

    클래스명 : 클래스 + Test 형태로 작성하는 평
    DisplayName을 사용하고 함수명을 영문 테스트명을 작성하거나
    DisplayName을 사용하지 않고 한글 테스트명을 작성하거나

..... 이 이상은 튜터님 강의를 보기만했다.. 복습필요


강의

더보기

컨트롤러 테스트

 

  • 컨트롤러는 어떻게 테스트 해야할까?
    controller를 테스트 하려면 @WebMvcTest어노테이션을 사용해야하며
     security를 사용하고 있으니, 가짜 security와 필터를 만들어야하며
    mockmvc를 사용하여 가짜 html메서드를 날릴 수 있고
    가짜 유저를 만들어서 테스트 해야한다.

    @MockBean
    UserService userService;
    
    @MockBean
    KakaoService kakaoService;
    
    @MockBean
    ProductService productService;
    
    @MockBean
    FolderService folderService;
     
    또한 가짜bean인 mockbean을 주입한다.

    @Test
    @DisplayName("로그인 Page")
    void test1() throws Exception {
        // when - then
        //perform 메서드를 사용함 http 프로토콜 방식을 지정, url을 정함
        mvc.perform(get("/api/user/login-page"))
            //예측 결과값
            .andExpect(status().isOk())
            //반환되는 html파일을 예측.
            .andExpect(view().name("login"))
            .andDo(print());
    }


    @Test
    @DisplayName("회원 가입 요청 처리")
    void test2() throws Exception {
        // given
        MultiValueMap<String, String> signupRequestForm = new LinkedMultiValueMap<>();
        signupRequestForm.add("username", "1234");
        signupRequestForm.add("password", "1234");
        signupRequestForm.add("email", "1234@sparta.com");
        signupRequestForm.add("admin", "false");
    
        // when - then
        mvc.perform(post("/api/user/signup")
                .params(signupRequestForm)
            )
            .andExpect(status().is3xxRedirection())
            .andExpect(view().name("redirect:/api/user/login-page"))
            .andDo(print());
    }


    회원가입 데이터를 전송하기위하여 map을 보내준다
    mvc.perfom을 사용하고, post방식과 url을 전송해준다.
    예측 상태코드, 예측 viewname을 지정한다.

    @Test
    @DisplayName("신규 관심상품 등록")
    void test3() throws Exception {
        // given
        this.mockUserSetup();
        String title = "Apple 아이폰 14 프로 256GB [자급제]";
        String imageUrl = "https://shopping-phinf.pstatic.net/main_3456175/34561756621.20220929142551.jpg";
        String linkUrl = "https://search.shopping.naver.com/gate.nhn?id=34561756621";
        int lPrice = 959000;
        ProductRequestDto requestDto = new ProductRequestDto(
            title,
            imageUrl,
            linkUrl,
            lPrice
        );
    
        String postInfo = objectMapper.writeValueAsString(requestDto);
    
        // when - then
        mvc.perform(post("/api/products")
                .content(postInfo)
                .contentType(MediaType.APPLICATION_JSON)
                .accept(MediaType.APPLICATION_JSON)
                .principal(mockPrincipal)
            )
            .andExpect(status().isOk())
            .andDo(print());
    }

    신규 상품을 등록하기위해, user를 만들어야한다.
    또한, 상품객체를 생성한다음

    objectMapper를 사용하여, 객체를 json타입으로 변경한다.

    perform을 통해
    송신타입(?)
    수신타입(?)을 지정하고
    원하는 값을 설정한다.

요구사항 변경

  • 유저가 얼마나 오래머무나를 측정할것이다.
    페이지의 머문 시간? > 서버 사용시간으로 측정한다.
    서버 사용시간? > controller에 요청이 들어온시간 ~ 응답이나간 시간
    api 사용시간 = controller에 응답이 나간 시간 - 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);
            }
    
            System.out.println("[API Use Time] Username: " + loginUser.getUsername() + ", Total Time: " + apiUseTime.getTotalTime() + " ms");
            apiUseTimeRepository.save(apiUseTime);
        }
    }

    조회를 하고싶은 controller에 시간 측정을 구현한다.
    접속시간 기록 로직을을 회원들이 알 필요가 있나?

    x >> 상품 조회만 하면된다, 접속시간은 유저들이 몰라도되는부분
    이처럼 api에서 사용해야할 비즈니스 로직을 핵심기능 이라고 하고
    핵심을 보조하는 기능을 부가기능 이라고 한다

    모든 핵심기능에 특정한 부가기능(api 사용시간 측정)을 넣어야한다.
    부가기능 로직이 변경된다면 모든 핵심기능의 부가기능을 수정해야하나?

    AOP를 사용해 부가기능을 모듈화를 한다
    부가기능과 핵심기능은 관점(Aspect)가 다르다
    따라서 핵심기능과 분리하여 부가기능 중심으로 설계해야한다.

스프링 AOP

출처 : 내일배움캠프 스프링 심화주차 8강
  •  핵심기능을 모듈화하고, 부가기능을 모듈화 하여 합친다
    advice : 부가기능을 핵심기능에 언제 수행할지를 정하는것이다 (전, 후, 전후 전부)
    point cut : 적용 위치 어디에서 수행할지

  • 어드바이스 종류
    @Around: '핵심기능' 수행 전과 후 (@Before + @After)
    @Before: '핵심기능' 호출 전 (ex. Client 의 입력값 Validation 수행)
    @After:  '핵심기능' 수행 성공/실패 여부와 상관없이 언제나 동작 (try, catch 의 finally() 처럼 동작)
    @AfterReturning: '핵심기능' 호출 성공 시 (함수의 Return 값 사용 가능)
    @AfterThrowing: '핵심기능' 호출 실패 시. 즉, 예외 (Exception) 가 발생한 경우만 동작 (ex. 예외가 발생했을 때 개발자에게 email 이나 SMS 보냄)

  • 포인트컷 사용방법
    @Around("execution(public * com.sparta.myselectshop.controller..*(..))")
    public Object execute(ProceedingJoinPoint joinPoint) throws Throwable { ... }

    @Around : 어드바이스 지정 (핵심기능 수행 전과 후)
    "execution(public * com.sparta.myselectshop.controller..*(..))" : 위치 지정

    execution(modifiers-pattern? return-type-pattern declaring-type-pattern? method-name-pattern(param-pattern) throws-pattern?)

    modifiers-pattern : 접근제어자 지정
    return-type-pattern : 반환타입
    declearing-type-pattern : 클래스명 지정 (적용할 부분) 
    method-name-pattern : 메서드 명을지정 (addFolders : addforders()메서드에만 적용 / add* : add로 시작하는 모든 메서드)

    execution(public void com.example.MyClass.addFolders(..))
    접근제어자 : public
    반환타입 : void
    클래스명 : com > example > myclass
    메서드명 : addFolders

    execution(* com.example..*(..))
    접근제어자 : all
    반환타입 : all
    클래스명 com > example의 모든 하위 패키지의 클래스(..)
    메서드명 모든 메서드

  • 구현
    @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);
            }
    
            System.out.println("[API Use Time] Username: " + loginUser.getUsername() + ", Total Time: " + apiUseTime.getTotalTime() + " ms");
            apiUseTimeRepository.save(apiUseTime);
        }
    }

    위 코드를 수정하자.

    그리고 aop > useTimeAop 만들자
    @Aspect
    @Component
    public class UseTimeAop {
    
        private final ApiUseTimeRepository apiUseTimeRepository;
    
        public UseTimeAop(ApiUseTimeRepository apiUseTimeRepository) {
            this.apiUseTimeRepository = apiUseTimeRepository;
        }

    @Aspect  : 어노테이션을 부착해 aspect라는것을 알린다
    @Component : Aspect 어노테이션은 bean클래스만 사용가능하다 

    @Pointcut("execution(* com.sparta.myselectshop.controller.ProductController.*(..))")
    private void product() {}
    @Pointcut("execution(* com.sparta.myselectshop.controller.FolderController.*(..))")
    private void folder() {}
    @Pointcut("execution(* com.sparta.myselectshop.naver.controller.NaverApiController.*(..))")
    private void naver() {}

    @pointcut 어노테이션으로 aspect가 적용될수 있는 지점을 선언해준다. 각각 패키지의 모든 메서드들에 적용한다.

    @Around("product() || folder() || naver()")
    public 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();
    
                // 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);
            }
        }
    }


    @around 어노테이션을 적용해 aspect가 실행될수 있는 시점을 적용해준다.
    around 는 메서드 실행 전 후 모든 시간을 측정한다

    // 관심 상품 등록하기
    @PostMapping("/products")
    public ProductResponseDto createProduct(@RequestBody ProductRequestDto requestDto, @AuthenticationPrincipal UserDetailsImpl userDetails) {
        return productService.createProduct(requestDto, userDetails.getUser());
    }
    

    이제 컨트롤러에는 핵심기능만 남아있고
    부가기능은 따로 빠져있음을 확인 할 수 있다.

  • 어떻게 작동하는걸까?
    출처 : 내일배움캠프 스프링 심화주차 9강
    AOP 적용 전에는 클라이언트 요청이 디스패처 서블릿을 통해
    바로 컨트롤러에서 측정을해 결과값을 반환했다면

    출처 : 내일배움캠프 스프링 심화주차 9강
    AOP 적용 후에는 디스패처 서블릿과 컨트롤러사이에 AOP 프록시가 들어가게된다.
    디스패처 서블릿이 보낸 메서드 호출을 가로채서 정의된 로직을 제어한다
    포인트컷에 맞는지 확인한후, 어드바이스를 실행한다, 그후 CONTROLLER에 요청을 하고, AOP 프록시를 거쳐서 반환한다.




오늘의 회고 & 12시간 몰입했는가? 

어흑흑.. 특강의 무덤.. 필히 복습해야할것.. 같다..

 

 

'TIL' 카테고리의 다른 글

20240912 본캠프 44일차 TIL  (0) 2024.09.12
20240911 본캠프 43일차 TIL  (0) 2024.09.11
20240909 본캠프 41일차 TIL  (0) 2024.09.09
20240906 본캠프 40일차 TIL  (0) 2024.09.06
20240905 본캠프 39일차 TIL  (0) 2024.09.06