NormalBoardController.java

페이징을 위해 Pagable을 사용하였으나 swagger에서 default 수치가 적용되지 않아 swagger에서는 보이지 않도록 

@Parameter를 사용

    /*
    * @PageableDefault: 디폴트 페이지 설정(size= 한 페이지 당 보여줄 데이터의 수, sort=등록시간으로 정렬, directin= 내림차순 정렬
    * @Parameter: swagger 문서 작성을 위한 annotation(파라미터 설명 작성),  swagger에 디폴트 파라미터가 적용되지 않아서 추가
    * Pageable: 페이지 설정
    * */
    @PostMapping("/list")
    @Operation(summary = "게시물 목록 조회(")
    public ResponseEntity<ApiResultDto<Page<NormalBoardSearchDto>>> selectNormalBoardTitleList(
            @RequestBody NormalBoardSearchDto board
            , @PageableDefault(size = 10, direction = Sort.Direction.DESC, sort = "firstRegTime") @Parameter(hidden = true) Pageable page
    ) {
        ApiResultDto<Page<NormalBoardSearchDto>> result = new ApiResultDto<>();
        try {


            Page<NormalBoardSearchDto> t = normalBoardService.selectNormalBoardTitles(board, page);
            result.setData(t);
            result.setResultCode("200");
            result.setResultMessage("Success");
        } catch (Exception e) {
            log.error(e.getMessage());
            result.setResultCode("500");
            result.setResultMessage("Fail");
        }

        return new ResponseEntity<>(result, result.getResultCode().equals("200") ? org.springframework.http.HttpStatus.OK
                : org.springframework.http.HttpStatus.INTERNAL_SERVER_ERROR);
    }

 

NormalBaordSearchDto.java

Entity의 NormalBoard나 InsertDto를 사용하려 했으나 swagger에 불필요한 Content, LastUpdTime등 데이터가 표기 되어 별도의 Dto를 만들기로 함.

 

Repository에서 Entity가 아닌 Dto로 return 받을 수 있도록 생성자 추가

package com.portfolio.Dto;

import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Getter;
import lombok.Setter;

import java.time.LocalDateTime;

/**
 * 일반 게시판(목록)
 date : 2024.09.03
 author : 김만기

 ====== Annotation description ======
 Schema: swagger 문서 작성을 위한 annotation

 ====== field description ======
 title: 게시판 제목
 content: 게시판 내용

 ====== method description ======


 ====== etc description ======

 */
@Getter
@Setter
public class NormalBoardSearchDto {

    @Schema(description = "게시판 아이디", example = "게시판 아이디")
    private String boardId;
    @Schema(description = "게시판 제목", example = "게시판 제목")
    private String title;
    @Schema(description = "게시판 작성자", example = "게시판 작성자")
    private String firstRegUser;
    @Schema(description = "작성 시간", example = "작성시간")
    private LocalDateTime firstRegTime;


    public NormalBoardSearchDto(String boardId, String title, String firstRegUser, LocalDateTime firstRegTime) {
        this.boardId = boardId;
        this.title = title;
        this.firstRegUser = firstRegUser;
        this.firstRegTime = firstRegTime;
    }

}

 

 

BoardService.java

BoardServiceImpl.java

Repo의 메소드 호출

package com.portfolio.serviceimpl;

import com.portfolio.Dto.NormalBoardSearchDto;
import com.portfolio.entity.NormalBoard;
import com.portfolio.repo.NormalBoardRepo;
import com.portfolio.service.NormalBoardService;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.data.domain.Page;
import org.springframework.data.domain.Pageable;
import org.springframework.stereotype.Service;

import java.util.List;
/*
    * 일반 게시판 서비스 구현체
    * date: 2024-07-26
    * last_modified: 2024-10-28
    * author: 김만기
    *
    * ====== Annotation description ======
    * Service: 서비스 빈으로 등록
    *
    * ====== method description ======
    * insertNormalBoard: 일반 게시판 등록
    * selectAllBoardList: 모든 게시판 조회
    * selectNormalBoardTitles: 게시판 제목 조회
 */
@Service
@RequiredArgsConstructor
@Slf4j
public class NormalBoardServiceImpl implements NormalBoardService {
    private final NormalBoardRepo boardRepo;

    @Override
    public void insertNormalBoard(NormalBoard board) {
        boardRepo.save(board);
    }

    @Override
    public List<NormalBoard> selectAllBoardList() {
        return boardRepo.findAll();
    }

    /**
     * 게시판 제목 조회
     * date 2024.10.28
     * last_modified 2024.10.28
     */
    @Override
    public Page<NormalBoardSearchDto> selectNormalBoardTitles(NormalBoardSearchDto board, Pageable pageable) {
        return boardRepo.findByTitle(board, pageable);
    }
}

 

 

NormalBoardRepo.java

package com.portfolio.repo;


import com.portfolio.Dto.NormalBoardSearchDto;
import com.portfolio.entity.NormalBoard;
import org.springframework.data.domain.Page;
import org.springframework.data.domain.Pageable;
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.data.jpa.repository.Query;

/*
    * date: 2024-07-26
    * author: 김만기
    * JpaRepository를 상속받아 NormalBoard 엔티티를 관리하는 레포지토리
    *
    *   ====== Annotation description ======
    * Query: JPQL을 사용하여 쿼리문 작성
    *  - select new com.portfolio.Dto.NormalBoardSearchDto: NormalBoardSearchDto객체에 매핑
    *  - where board.title like %:#{#board.title}%: searchBoard.title에 해당하는 값을 포함하는 데이터 조회
    *
 */
public interface NormalBoardRepo extends JpaRepository<NormalBoard, String> {

    /*
     * last_modified: 2024-10-28
     */
    @Query("select new com.portfolio.Dto.NormalBoardSearchDto( board.boardId, board.title, board.firstRegUser, board.firstRegTime)  " +
            "from NormalBoard board " +
            "where board.title like %:#{#searchBoard.title}% ")
    Page<NormalBoardSearchDto> findByTitle(NormalBoardSearchDto searchBoard, Pageable pageable);
}

 


SWAGGER Parameter 적용 전 

page의 데이터가 default로 설정한 값이 들어가지 않음.

 

조회 결과

 

requestBody

Response

{
  "resultCode": "200",
  "resultMessage": "Success",
  "data": {
    "content": [
      {
        "boardId": "8d6341c8-9e24-48a6-a3b6-2c657b7575fa",
        "title": "게시판 제목",
        "firstRegUser": "admin",
        "firstRegTime": "2024-10-28T13:11:48.744752"
      },
      {
        "boardId": "08daea24-0be3-481f-a5f4-2eb56bb75a1d",
        "title": "게시판 제목",
        "firstRegUser": "admin",
        "firstRegTime": "2024-10-28T13:11:48.600051"
      },
      {
        "boardId": "55416544-3a77-4418-ad97-d47b90d43cd8",
        "title": "게시판 제목",
        "firstRegUser": "admin",
        "firstRegTime": "2024-10-28T13:11:48.451605"
      },
      {
        "boardId": "15dfeff3-2883-43b5-8b43-19b32c6d1ad2",
        "title": "게시판 제목",
        "firstRegUser": "admin",
        "firstRegTime": "2024-10-28T13:11:48.311657"
      },
      {
        "boardId": "e49e5819-bded-4584-ab81-83982a34a8c0",
        "title": "게시판 제목",
        "firstRegUser": "admin",
        "firstRegTime": "2024-10-28T13:11:48.156085"
      },
      {
        "boardId": "1c33ff14-d99f-4263-bc02-a3eb8c87a894",
        "title": "게시판 제목",
        "firstRegUser": "admin",
        "firstRegTime": "2024-10-28T13:11:48.017085"
      },
      {
        "boardId": "766a45bb-7831-41c9-ae0c-d8d13a449c9c",
        "title": "게시판 제목",
        "firstRegUser": "admin",
        "firstRegTime": "2024-10-28T13:11:47.88268"
      },
      {
        "boardId": "321ba6bb-70f1-4fa4-ad34-5772870fb8c4",
        "title": "게시판 제목",
        "firstRegUser": "admin",
        "firstRegTime": "2024-10-28T13:11:47.740277"
      },
      {
        "boardId": "75530f37-230a-4703-8a69-b418f7b9dc96",
        "title": "게시판 제목",
        "firstRegUser": "admin",
        "firstRegTime": "2024-10-28T13:11:47.599972"
      },
      {
        "boardId": "0d95876d-99f6-4266-93d4-c6381b699b6d",
        "title": "게시판 제목",
        "firstRegUser": "admin",
        "firstRegTime": "2024-10-28T13:11:47.4611"
      }
    ],
    "pageable": {
      "pageNumber": 0,
      "pageSize": 10,
      "sort": {
        "empty": false,
        "sorted": true,
        "unsorted": false
      },
      "offset": 0,
      "paged": true,
      "unpaged": false
    },
    "last": false,
    "totalElements": 28,
    "totalPages": 3,
    "first": true,
    "size": 10,
    "number": 0,
    "sort": {
      "empty": false,
      "sorted": true,
      "unsorted": false
    },
    "numberOfElements": 10,
    "empty": false
  }
}

https://github.com/Kmmanki/portfolio_was

 

GitHub - Kmmanki/portfolio_was: portfolio

portfolio. Contribute to Kmmanki/portfolio_was development by creating an account on GitHub.

github.com

 

프로젝트에서 부하 테스트를 진행 하던 중 일부 구간에서 오동작하는 이슈가 발생하였다.

특이한 것은 적은양의 트래픽으로는 별 다른 이슈는 없었으나 예상 최대 부하를 진행하였을 때 일부의 로직이 잘못 동작하는 것으로 보였다.(로직들이 비동기가 많기 때문에 더욱 문제를 찾기 어려웠음)

결국 최적화 팀을 불러 어느 구간이 문제였는지 찾을 수 있었는데 최적화 팀에서 사용하는 툴을 찾아보니 오픈소스 툴 이었다.

최적화 팀에서 사용한 방법을 해보고자 한다.


Arthas 사이트

https://arthas.aliyun.com/en/doc/

 

Arthas

Alibaba Java Diagnostic Tool Arthas/Alibaba Java诊断利器Arthas - alibaba/arthas: Alibaba Java Diagnostic Tool Arthas/Alibaba Java诊断利器Arthas

arthas.aliyun.com

 

Arthas는 알리바바 미들웨어 팀에서 만든 오픈소스의 JAVA 진단 툴이라고한다.

 

특징

  • Class가 load 되어 있는지, 클레스가 어디로 부터 load 되었는지(jar 파일 충돌에 유용하다고 한다)
  • 예상되로 동작하는지 Decompile
  • 클레스로더의 통계(클래스 로더 수, 클래스 로더당 로드된 클래스 수, 클래스 로더 계층 구조, 가능한 클래스 로더 누수 등)
  • 메소드 호출 세부정보(메소드의 변수, 반환값 등)
  • 지정된 메소드의 stack 추적

등등

 


 

Arthas 테스트를 위한 설치

 

테스트 타겟이 될 JAVA 프로그램(math-game) arthas 다운로드

curl -O https://arthas.aliyun.com/math-game.jar
curl -O https://arthas.aliyun.com/arthas-boot.jar

 

math-game 실행해보기

(1초마다 난수를 생성하고 해당 숫자의 모든 소인수를 구하는 프로그램이라고 한다.)

java -jar math-game.jar

 

 

하나의 터미널을 더 띄워서 Arthas 실행

// math-game의 pid 조회
ps -ef| grep "math"

java -jar arthas-boot.jar ${pid}

 

 

help

여러 기능들을 확인 할수 있다. 

 


대시보드

대시보드를 몇 초주기로 snapshot할 것인가(i 옵션), 총 몇회 snapshot할 것인가(n옵션) 옵션을 사용할수 있으며 

타겟 jvm의 thread, memory, gc 등을 알 수 있다고한다.

 

 


thread

thread의 정보와 stack 정보(어디서부터 호출이 되었는지)

 

 

현재 메인스레드는 "대기상태"이며 Thread.sleep이 동작하고 있다.

 

스레드의 상태 (출처 :https://m.blog.naver.com/PostView.nhn?blogId=qbxlvnf11&logNo=220921178603&proxyReferer=https:%2F%2Fwww.google.com%2F)

 


jad

클래스를 디컴파일한다.

thrad에서 찾은 demo.MathGame 클래스를 디컴파일

 

 

       package demo;

       import java.util.ArrayList;
       import java.util.List;
       import java.util.Random;
       import java.util.concurrent.TimeUnit;

       public class MathGame {
           private static Random random = new Random();
           private int illegalArgumentCount = 0;

           public static void main(String[] args) throws InterruptedException {
               MathGame game = new MathGame();
               while (true) {
/*16*/             game.run();
/*17*/             TimeUnit.SECONDS.sleep(1L);
               }
           }

           public void run() throws InterruptedException {
               try {
/*23*/             int number = random.nextInt() / 10000;
/*24*/             List<Integer> primeFactors = this.primeFactors(number);
/*25*/             MathGame.print(number, primeFactors);
               }
               catch (Exception e) {
/*28*/             System.out.println(String.format("illegalArgumentCount:%3d, ", this.illegalArgumentCount) + e.getMessage());
               }
           }

           public static void print(int number, List<Integer> primeFactors) {
               StringBuffer sb = new StringBuffer(number + "=");
/*34*/         for (int factor : primeFactors) {
/*35*/             sb.append(factor).append('*');
               }
/*37*/         if (sb.charAt(sb.length() - 1) == '*') {
/*38*/             sb.deleteCharAt(sb.length() - 1);
               }
/*40*/         System.out.println(sb);
           }

           public List<Integer> primeFactors(int number) {
/*44*/         if (number < 2) {
/*45*/             ++this.illegalArgumentCount;
                   throw new IllegalArgumentException("number is: " + number + ", need >= 2");
               }
               ArrayList<Integer> result = new ArrayList<Integer>();
/*50*/         int i = 2;
/*51*/         while (i <= number) {
/*52*/             if (number % i == 0) {
/*53*/                 result.add(i);
/*54*/                 number /= i;
/*55*/                 i = 2;
                       continue;
                   }
/*57*/             ++i;
               }
/*61*/         return result;
           }
       }

watch

메소드의 반환 오브젝트 확인

// demo.MathGame 클레스의 primeFactors method를 감시하며 감시결과의 Object는 2depth까지 확인
// (최대 4depth), defualt 감시항목은 {params, target, returnObj}
watch demo.MathGame primeFactors -x 2

 

 

첫 번째 결과는 parameter로 -122486이 들어왔으며 result는 null 이다.

두 번째 결과는 parameter로 1이 들어왔으며 resultsms [3,5,7,17,113] 이다

 

당시 Math-Game 로그

 

 

여기까지가 퀵스타트의 기술된 내용들이다. 

위의 내용 외에도 memory, monitor, trace 등 많은 옵션들이 있으니 시간 날 때 하나씩 확인해보도록 하자.


 

profile

최적화 팀에서 봤던 내용으로 jvm stack을 확인하며 동시에 어느 메소드에서 시간이 가장 오래 걸렸는지 확인해볼 수 있다.

 

다양한 옵션들이 있다.

 

어떠한 것을 감시할 것인가 list를 조회해볼수 있다.

최적화 팀에서는 wall 옵션을사용하였다.

 

https://github.com/async-profiler/async-profiler

 

GitHub - async-profiler/async-profiler: Sampling CPU and HEAP profiler for Java featuring AsyncGetCallTrace + perf_events

Sampling CPU and HEAP profiler for Java featuring AsyncGetCallTrace + perf_events - async-profiler/async-profiler

github.com

 

설명: 스레드 상태(실행 중, 휴면 중 또는 차단됨)에 관계없이 지정된 기간마다 모든 스레드를 동등하게 샘플링하도록 지시합니다.  ( 즉 어느 구간이 가장 오래 지연되는지 알 수 있는 것)

 

프로파일링 시작 event는 wall로

 

프로파일링 정지 결과 파일의 포멧은 html로

 

 

html을 열어보면

 

 

 

x축은 소요시간 y축은 하단부터 상단까지 JVM 호출 스택이다 즉 X축이 긴것이 소요시간이 오래 걸린것이다.

main함수를 찾아 클릭하면  Timeunit의 sleep을 사용한 것을 확인할수 있다.

 

그런데 MathGame Class의 run 메소드가 보이지 않는 이유를 모르겠다. 

기간에 대한 샘플링이기 때문에 sleep에 비해서 run의 동작시간이 너무 짧아서 보이지 않는 것일까.?

 

 

build.gradle들이 정상적으로 동작하는지 확인하기 위해 boot jar를 만들어서 실행하기위함

 

intelij의 gradle에서 module-main > tasks > build > boot jar 더블클릭 

 

module-main > build > libs 위치에 jar파일 생성 확인 

 

파일 위치에서 탐색기에 cmd 입력 후 엔터

 

 

java -jar {파일명}

 

 

기동 완료

 

 

 


https://github.com/Kmmanki/portfolio_was

 

GitHub - Kmmanki/portfolio_was: portfolio

portfolio. Contribute to Kmmanki/portfolio_was development by creating an account on GitHub.

github.com

 

잘못 기입된 값을 받으면 400(Bad Request)를 반환하도록 ValidationCheck 로직 추가

 

CreateNromalDto

@NotBlank, @Size 등 Validation을 확인하는 어노테이션 추가

ValidationCheck에서 실패 시 MehthodArgumentNotValidException 발생

package com.portfolio.Dto;

import com.portfolio.entity.NormalBoard;
import io.swagger.v3.oas.annotations.media.Schema;
import jakarta.validation.constraints.NotBlank;
import jakarta.validation.constraints.Size;
import lombok.Getter;
import lombok.Setter;


/**
 * 일반 게시판 생성 DTO
 date : 2024.07.26
 author : 김만기

 ====== Annotation description ======
 Schema: swagger 문서 작성을 위한 annotation

 Validation 관련 Annotation(조건에 부합하지않느다면 MethodArgumentNotValidException 발생)
    NotBlank: null, "" 체크
    Size: 길이 체크

 ====== field description ======
 title: 게시판 제목
 content: 게시판 내용

 ====== method description ======
 toEntity: DTO를 DB에 적재하기 위한 Entity로 변환

 ====== etc description ======
 entity를 사용하고 싶었으나 entity를 사용하면 Swagger의 example Value가 모든 필드를 표기하는 문제가 있어서 DTO를 사용

 */
@Getter
@Setter
public class CreateNormalBoardDto {
    @Schema(description = "게시판 제목", example = "게시판 제목")
    @NotBlank(message = "제목은 필수 입니다.")
    @Size(max = 30, message = "제목은 최대 30자 입니다.")
    private String title;

    @Schema(description = "게시판 내용", example = "게시판 내용")
    @NotBlank(message = "내용은 필수 입니다.")
    private String content;

    public NormalBoard toEntity() {
        return NormalBoard.builder()
                .title(title)
                .content(content)
                .build();
    }
}

 

 


ControllerExceptionHandler

Controller의 Exceltion을 처리하는 Handler

MehthodArgumentNotValidException만 처리하는 메소드 handleValidationException 

package com.portfolio.exception.handler;

import com.portfolio.dto.ApiResultDto;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.MethodArgumentNotValidException;
import org.springframework.web.bind.annotation.ExceptionHandler;
import org.springframework.web.bind.annotation.RestControllerAdvice;

/*
 * 컨트롤러 예외처리 핸들러
 * date: 2024-07-29
 * author: 김만기
 *
 * ====== Annotation description ======
 * RestControllerAdvice: RestController에서 발생하는 예외를 처리하기 위한 annotation
 * ExceptionHandler: 특정 예외를 처리하기 위한 annotation
 * 
 * ====== method description ======
 * handleValidationExceptions: @size, @notnull 등에서 발생하는 예외를 처리
 *  400 Bad Request로 응답하고, data에는 @size 등에서 지정한 message=''를 담아서 전달
 */

@RestControllerAdvice
public class ControllerExceptionHandler {

    @ExceptionHandler(MethodArgumentNotValidException.class)
    public ResponseEntity<ApiResultDto<String>> handleValidationExceptions(MethodArgumentNotValidException ex) {
        ApiResultDto<String> result = new ApiResultDto<>();
        result.setResultCode("400");
        result.setResultMessage("Validation Error");
        result.setData(ex.getBindingResult().getAllErrors().get(0).getDefaultMessage());
        return ResponseEntity.badRequest().body(result);
    }
}

 


프로젝트의 build.gradle

module-normal-board의 build.gralde

// 빌드 시 bootJar로 생성하지않음
bootJar { enabled = false }
// 다른 모듈의 라이브러리 형태가 될 것이기 때문에 jar 형태로 진행
jar { enabled = true }

dependencies {
    // common 모듈에 의존
    implementation project(":module-common")
    implementation group: 'org.mariadb.jdbc', name: 'mariadb-java-client', version: '3.4.0'
    implementation group: 'org.springframework.boot', name: 'spring-boot-starter-data-jpa', version: '3.3.0'
}

 

 

module-common의 build.gradle

bootJar { enabled = false }
jar { enabled = true }

dependencies {
   /*
   * jpa: DB ORM
   * validation: Controller 등, validation Check
   * */
    implementation group: 'org.mariadb.jdbc', name: 'mariadb-java-client', version: '3.4.0'
    implementation group: 'org.springframework.boot', name: 'spring-boot-starter-data-jpa', version: '3.3.0'
    implementation group: 'org.springframework.boot', name: 'spring-boot-starter-validation', version: '2.5.2'
}

 

 


Sweager 조회 및 validation 테스트

request Body에 미리 적어둔 example 적용 완료 

 

 

shcema에는 validation 내용 및 description 확인

 

 

validation에 맞지 않는 데이터 테스트 및 결과

400 badRequest 및 @size에 함께 작성한 message 출력 확인

 

 

 


https://github.com/Kmmanki/portfolio_was

 

GitHub - Kmmanki/portfolio_was: portfolio

portfolio. Contribute to Kmmanki/portfolio_was development by creating an account on GitHub.

github.com

 

+ Recent posts