<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0">
  <channel>
    <title>슬기로운 개발생활</title>
    <link>https://dev-coco.tistory.com/</link>
    <description>Hello World</description>
    <language>ko</language>
    <pubDate>Tue, 26 May 2026 07:00:39 +0900</pubDate>
    <generator>TISTORY</generator>
    <ttl>100</ttl>
    <managingEditor>coco3o</managingEditor>
    <image>
      <title>슬기로운 개발생활</title>
      <url>https://tistory1.daumcdn.net/tistory/4381101/attach/1af703d56b9140ee9e949e3618442205</url>
      <link>https://dev-coco.tistory.com</link>
    </image>
    <item>
      <title>[Java] Apache POI 엑셀 다운로드 간단한 모듈화로 쉽게 사용하기</title>
      <link>https://dev-coco.tistory.com/192</link>
      <description>&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;a href=&quot;https://dev-coco.tistory.com/191&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;&lt;span&gt;지난 글&lt;/span&gt;&lt;/a&gt;에 이어서 이번에는 엑셀 다운로드 기존 구조의 문제점과 이를 해결하기 위해 간단한 모듈화로 변경한 내용을 정리하고자 한다.&lt;br /&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;기본적인 형태의 엑셀 다운로드&amp;nbsp;&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Java를 사용하여 엑셀 다운로드 기능을 개발하면 일반적으로 &lt;a href=&quot;https://poi.apache.org/components/spreadsheet/how-to.html#sxssf&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;&lt;span&gt;Apache POI 라이브러리&lt;/span&gt;&lt;/a&gt;를 통해 구현한다.&lt;br /&gt;우선, 엑셀 다운로드 기능의 전체적인 구현 흐름을 먼저 보고 시작하자.&lt;/p&gt;
&lt;pre class=&quot;java&quot; data-ke-type=&quot;codeblock&quot; data-ke-language=&quot;java&quot;&gt;&lt;code&gt;@PostMapping(&quot;api/v1/export/download/excel&quot;)
public void download(HttpServletResponse response) throws Exception {
	// 엑셀 파일 하나를 만든다.
	Workbook workbook = new SXSSFWorkbook();
	// 엑셀 파일 내부에 Sheet 를 하나 생성한다. (엑셀 파일 하나에는 여러 Sheet가 있을 수 있다.)
	Sheet sheet = workbook.createSheet();

	// 엑셀 렌더링에 필요한 DTO 를 가져온다.
	List&amp;lt;UserExcelDto&amp;gt; userExcelDtos = userService.getUserInfo();

	// 헤더를 생성한다.
	int rowIndex = 0;
	Row headerRow = sheet.createRow(rowIndex++);
	Cell headerCell1 = headerRow.createCell(0);
	headerCell1.setCellValue(&quot;이름&quot;);

	Cell headerCell2 = headerRow.createCell(1);
	headerCell2.setCellValue(&quot;이메일&quot;);

	Cell headerCell3 = headerRow.createCell(2);
	headerCell3.setCellValue(&quot;생년월일&quot;);

	Cell headerCell4 = headerRow.createCell(3);
	headerCell4.setCellValue(&quot;가입일시&quot;);

	// 바디에 데이터를 넣어준다.
	for (UserExcelDto dto : userExcelDtos) {
		Row bodyRow = sheet.createRow(rowIndex++);

		Cell bodyCell1 = bodyRow.createCell(0);
		bodyCell1.setCellValue(dto.getName());

		Cell bodyCell2 = bodyRow.createCell(1);
		bodyCell2.setCellValue(dto.getEmail());

		Cell bodyCell3 = bodyRow.createCell(2);
		bodyCell3.setCellValue(dto.getBirthday());

		Cell bodyCell4 = bodyRow.createCell(3);
		bodyCell4.setCellValue(dto.getRegistrationDate());
	}
    
    // 응답 설정
	response.setContentType(&quot;application/vnd.openxmlformats-officedocument.spreadsheetml.sheet&quot;);
	response.setHeader(&quot;Content-Disposition&quot;,
			String.format(&quot;attachment;filename=%s&quot;, URLEncoder.encode(&quot;테스트_유저_정보.xlsx&quot;, StandardCharsets.UTF_8)));
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;
	// 엑셀 파일 작성
	workbook.write(response.getOutputStream());
	workbook.close();
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;기존 엑셀 다운로드 구조&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;기존 코드는 &lt;a href=&quot;https://refactoring.guru/ko/design-patterns/template-method&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;&lt;span&gt;템플릿 메서드 패턴&lt;/span&gt;&lt;/a&gt; 기반으로 적절히 추상화된 abstract class를 상속받아 공통 기능은 사용하면서 엑셀의 헤더와 내용 등 렌더링 부분은 직접 구현하는 구조였다.&lt;/p&gt;
&lt;pre class=&quot;java&quot; data-ke-type=&quot;codeblock&quot; data-ke-language=&quot;java&quot;&gt;&lt;code&gt;public abstract class ExcelFile { // (1)

&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;protected &amp;lt;T&amp;gt; void createCell(Row row, int columnCount, T value, CellStyle style) {

&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;Cell cell = row.createCell(columnCount);
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;if (value instanceof Integer) {
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;cell.setCellValue((Integer) value);
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;}
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;else if(value instanceof Long) {
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;cell.setCellValue((Long) value);
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;}
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;else if (value instanceof Boolean) {
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;cell.setCellValue((Boolean) value);
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;}
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;else {
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;cell.setCellValue((String) value);
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;}
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;cell.setCellStyle(style);
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;}

&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;protected CellStyle createCellStyle(Workbook wb, boolean isBold) {
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;CellStyle style = wb.createCellStyle();
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;Font font = wb.createFont();
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;font.setBold(isBold);
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;style.setFont(font);
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;return style;
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;}
}&lt;/code&gt;&lt;/pre&gt;
&lt;pre class=&quot;java&quot; data-ke-type=&quot;codeblock&quot; data-ke-language=&quot;java&quot;&gt;&lt;code&gt;public abstract class BaseExportExcelService extends ExcelFile { // (2)

&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;protected static final int IN_MEMORY_MAX_ROW_SIZE = 1000;

&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;protected SXSSFSheet sheet;

&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;public abstract void writeHeaderLine(SXSSFWorkbook wb);
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;public abstract int writeDataLines(SXSSFWorkbook wb);

&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;public void writeExcel(HttpServletResponse response,
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp; @Nullable String password) throws IOException {
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;SXSSFWorkbook wb = new SXSSFWorkbook(IN_MEMORY_MAX_ROW_SIZE);

&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;writeHeaderLine(wb);
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;writeDataLines(wb);
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;exportWithEncryption(wb,
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;response.getOutputStream(),
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;baseExportDao.getPassword());
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;}

&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;protected void exportWithEncryption(SXSSFWorkbook wb, 
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;ServletOutputStream stream,
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;String password) throws IOException {
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;if (password == null || password.isBlank()) {
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;wb.write(stream);
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;} else {
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;POIFSFileSystem fileSystem = new POIFSFileSystem();
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;OutputStream encryptorStream = getEncryptorStream(fileSystem, password);
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;workbook.write(encryptorStream);
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;encryptorStream.close(); // this is necessary before writing out the FileSystem

&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;fileSystem.writeFilesystem(stream); // write the encrypted file to the response stream
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;fileSystem.close();
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;}
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;wb.close();
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;wb.dispose();
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;stream.close();
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;}
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;private OutputStream getEncryptorStream(POIFSFileSystem fileSystem, String password) {
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;try {
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;Encryptor encryptor = new EncryptionInfo(EncryptionMode.agile).getEncryptor();
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;encryptor.confirmPassword(password);
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;return encryptor.getDataStream(fileSystem);
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;} catch (IOException | GeneralSecurityException e) {
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;throw new RuntimeException(&quot;Failed to obtain encrypted data stream from POIFSFileSystem.&quot;);
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;}
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;}
}&lt;/code&gt;&lt;/pre&gt;
&lt;pre class=&quot;java&quot; data-ke-type=&quot;codeblock&quot; data-ke-language=&quot;java&quot;&gt;&lt;code&gt;public abstract class BaseExportExcelSheetListService extends SXSSFExcelFile { // (3)

&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;public abstract Map&amp;lt;String, SheetInfo&amp;gt; writeDataLineMap(SXSSFWorkbook workbook);

&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;@Override
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;public void writeExcel(HttpServletResponse response,
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp; @Nullable String password) throws IOException {
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;SXSSFWorkbook wb = new SXSSFWorkbook(IN_MEMORY_MAX_ROW_SIZE);
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;writeHeaderLine(wb);
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;writeDataLineMap(wb);
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;exportWithEncryption(wb,
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;response.getOutputStream(),
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;password());
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;}

&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;@Getter
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;@Setter
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;@Builder
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;public static class SheetInfo {
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;private String sheetName;
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;private SXSSFSheet sheet;
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;private LocalDateTime startDateTime;
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;private LocalDateTime endDateTime;
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;private Integer dataSize;
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;}
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;1. 엑셀 파일에 꼭 필요한 기능을 구현해 놓은 최상위 클래스이다.&lt;br /&gt;2. 단일 시트에 대한 엑셀을 구현할 수 있는 추상 클래스이며, 이를 상속받아 구현한다.&lt;br /&gt;3. 멀티 시트에 대한 엑셀을 구현할 수 있는 추상 클래스이며, 이를 상속받아 구현한다.&lt;br /&gt;&amp;nbsp;&lt;br /&gt;위 추상 클래스를 상속받아 다음 예시와 같이 엑셀 다운로드를 구현할 수 있다.&lt;/p&gt;
&lt;pre class=&quot;java&quot; data-ke-type=&quot;codeblock&quot; data-ke-language=&quot;java&quot;&gt;&lt;code&gt;@PostMapping(&quot;api/v1/export/download/excel&quot;)
public void download(HttpServletResponse response) throws Exception {
	exportExampleService.writeExcel(response, &quot;password&quot;);
}

@Service
@RequiredArgsConstructor
public class ExportExampleService extends BaseExportExcelService {

&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;private final UserInfoRepository userInfoRepository;
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;@Override
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;public void writeHeaderLine(SXSSFWorkbook wb) {
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;super.sheet = workbook.createSheet(&quot;Users&quot;);
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;Row row = super.sheet.createRow(0);
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;CellStyle style = createCellStyle(workbook, true);

&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;super.createCell(row, 0, &quot;이름&quot;, style);
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;super.createCell(row, 1, &quot;이메일&quot;, style);
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;super.createCell(row, 2, &quot;생년월일&quot;, style);
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;super.createCell(row, 3, &quot;가입일시&quot;, style);
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;}

&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;@Override
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;public int writeDataLines(SXSSFWorkbook wb) {
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;int rowCount = 1;

&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;CellStyle style = createCellStyle(workbook, false);

&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;List&amp;lt;UserExcelDto&amp;gt; data = userInfoRepository.getUserInfo();
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;for (UserExcelDto dto : data) {
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;Row row = super.sheet.createRow(rowCount++);
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;int columnCount = 0;

&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;super.createCell(row, columnCount++, dto.getName(),style);
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;super.createCell(row, columnCount++, dto.getEmail(),style);
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;super.createCell(row, columnCount++, dto.getBirthday(),style);
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;super.createCell(row, columnCount, dto.getRegistrationDate(),style);
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;}
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;return data.size();
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;}
}&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;458&quot; data-origin-height=&quot;124&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bAmRqE/btsJFGK5SIc/aHICw38914kHDqQKCkwtf1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bAmRqE/btsJFGK5SIc/aHICw38914kHDqQKCkwtf1/img.png&quot; data-alt=&quot;엑셀 다운로드 결과 예시&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bAmRqE/btsJFGK5SIc/aHICw38914kHDqQKCkwtf1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbAmRqE%2FbtsJFGK5SIc%2FaHICw38914kHDqQKCkwtf1%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;458&quot; height=&quot;124&quot; data-origin-width=&quot;458&quot; data-origin-height=&quot;124&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;엑셀 다운로드 결과 예시&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;위의 코드를 관계도로 보면 아래와 같다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;937&quot; data-origin-height=&quot;578&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/xO2oz/btsKcNaL26J/6K0pl1opiqe0l8WraHHXh0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/xO2oz/btsKcNaL26J/6K0pl1opiqe0l8WraHHXh0/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/xO2oz/btsKcNaL26J/6K0pl1opiqe0l8WraHHXh0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FxO2oz%2FbtsKcNaL26J%2F6K0pl1opiqe0l8WraHHXh0%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;604&quot; height=&quot;373&quot; data-origin-width=&quot;937&quot; data-origin-height=&quot;578&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #333333;&quot;&gt;이 방식은 추상 클래스를 활용하여 엑셀 다운로드 기능을 쉽게 개발할 수 있지만, &lt;/span&gt;&lt;span style=&quot;color: #333333;&quot;&gt;다음과 같은 단점들이 있었다.&lt;/span&gt;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;span style=&quot;color: #ee2323;&quot;&gt;구현 클래스의 증가&lt;/span&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;엑셀 다운로드를 구현할 때마다 구현 클래스가 계속해서 늘어난다.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;color: #ee2323;&quot;&gt;반복적인 코드 작성&lt;/span&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;헤더와 바디에 대해서 Cell 하나하나마다 코드를 매번 직접 작성해야 한다.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;color: #ee2323;&quot;&gt;휴먼 에러 가능성&lt;/span&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;Column의 수가 많아질수록 오타나 실수 등의 휴먼 에러가 발생할 가능성이 높아진다.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;color: #ee2323;&quot;&gt;여러 책임을 가진 구현 클래스&lt;/span&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;데이터 조작 및 엑셀 데이터 렌더링 등의 여러 책임을 갖고 있다.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;실제로 내부 어드민에는 현재 약 30개 이상의 엑셀 다운로드 클래스가 있으며, 엑셀 다운로드 기능이 필요할 때마다 추가 작업을 해야 한다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;372&quot; data-origin-height=&quot;565&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bztFV4/btsJETWKA9N/pOcRZDKbzWKn2OrlKIq6m1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bztFV4/btsJETWKA9N/pOcRZDKbzWKn2OrlKIq6m1/img.png&quot; data-alt=&quot;(실제로는 더 많다..)&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bztFV4/btsJETWKA9N/pOcRZDKbzWKn2OrlKIq6m1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbztFV4%2FbtsJETWKA9N%2FpOcRZDKbzWKn2OrlKIq6m1%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;318&quot; height=&quot;483&quot; data-origin-width=&quot;372&quot; data-origin-height=&quot;565&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;(실제로는 더 많다..)&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;따라서, 현재와 같은 방법은 생산성이 다소 떨어진다 판단했고, 개선 작업의 필요성을 느꼈다.&lt;br /&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;기존 구조를 간단한 모듈로 개선&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;개선 목표는 크게 다음과 같다.&lt;br /&gt;&lt;span style=&quot;color: #ee2323;&quot;&gt;1. 엑셀 다운로드를 구현 할 때마다 늘어나는 보일러플레이트 개선&lt;/span&gt;&lt;br /&gt;&lt;span style=&quot;color: #ee2323;&quot;&gt;2. Cell 작성 과정을 자동화하여 반복적인 작업을 제거&lt;/span&gt;&lt;br /&gt;&lt;span style=&quot;color: #ee2323;&quot;&gt;3. 기능별 책임을 분리&lt;/span&gt;&lt;br /&gt;&amp;nbsp;&lt;br /&gt;엑셀 다운로드 기능을 크게 데이터 헤더라인 작성&lt;span style=&quot;color: #333333;&quot;&gt;,&lt;/span&gt;&lt;span style=&quot;color: #333333;&quot;&gt; &lt;/span&gt;데이터 바디라인 작성 그리고 &lt;span style=&quot;color: #333333;&quot;&gt;엑셀 파일 생성&lt;/span&gt;으로 구분하고,&lt;br /&gt;&lt;span style=&quot;color: #333333;&quot;&gt; 각 역할을 담당하는 클래스를 만들어 책임을&lt;/span&gt;&lt;span style=&quot;color: #333333;&quot;&gt; &lt;/span&gt;&lt;span style=&quot;color: #333333;&quot;&gt;분리했다.&lt;/span&gt;&lt;br /&gt;&lt;span style=&quot;color: #333333;&quot;&gt;이를 간단히 요약하면 &lt;/span&gt;&lt;span style=&quot;color: #333333;&quot;&gt;다음과 같다.&lt;/span&gt;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;1. ExcelMetaData
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;엑셀 렌더링에 필요한 일종의 메타데이터(헤더 및 이름 정보)를 보관하는 객체&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;2. ExcelSheetData
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;엑셀 렌더링에 필요한 바디 데이터 정보를 담은 객체&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;3. SXSSFExcelFile
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;엑셀 파일 생성을 담당하는 객체&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;1. DTO 생성과 함께 데이터 헤더 값 설정하기&lt;/h4&gt;
&lt;pre class=&quot;java&quot; data-ke-type=&quot;codeblock&quot; data-ke-language=&quot;java&quot;&gt;&lt;code&gt;@Target(ElementType.FIELD)
@Retention(RetentionPolicy.RUNTIME)
public @interface ExcelColumn {

&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;String headerName() default &quot;&quot;;

}

@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
public @interface ExcelSheet {

&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;String name() default &quot;&quot;;
}&lt;/code&gt;&lt;/pre&gt;
&lt;pre class=&quot;java&quot; data-ke-type=&quot;codeblock&quot; data-ke-language=&quot;java&quot;&gt;&lt;code&gt;@ExcelSheet(name = &quot;Users&quot;)
public class UserExcelDto {
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;@ExcelColumn(headerName = &quot;이름&quot;)
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;private final String name;
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;@ExcelColumn(headerName = &quot;이메일&quot;)
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;private final String email;
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;@ExcelColumn(headerName = &quot;생년월일&quot;)
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;private final String birthday;
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;@ExcelColumn(headerName = &quot;가입일시&quot;)
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;private final String registrationDate;
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;기존엔 Column에 Cell을 일일이 작성해줘야 했다.&lt;br /&gt;이를 커스텀 어노테이션을 통해 DTO 클래스에서 엑셀에 표시하고 싶은 필드를 @ExcelColumn으로,&lt;br /&gt;시트 명을 @ExcelSheet으로 설정하도록 변경 했다.&lt;br /&gt;&amp;nbsp;&lt;br /&gt;이제 이 어노테이션이 달린 DTO를 받아서 엑셀에 그릴 수 있도록 도와주는 객체를 생성한다.&lt;/p&gt;
&lt;pre class=&quot;java&quot; data-ke-type=&quot;codeblock&quot; data-ke-language=&quot;java&quot;&gt;&lt;code&gt;@Getter
public class ExcelMetadata { // (1)

&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;private final Map&amp;lt;String, String&amp;gt; excelHeaderNames;
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;private final List&amp;lt;String&amp;gt; dataFieldNames;
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;private final String sheetName;

&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;public ExcelMetadata(Map&amp;lt;String, String&amp;gt; excelHeaderNames,
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp; List&amp;lt;String&amp;gt; dataFieldNames,
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp; String sheetName) {
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;this.excelHeaderNames = excelHeaderNames;
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;this.dataFieldNames = dataFieldNames;
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;this.sheetName = sheetName;
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;}


&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;public String getHeaderName(String fieldName) {
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;return excelHeaderNames.getOrDefault(fieldName,&quot;&quot;);
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;}
}&lt;/code&gt;&lt;/pre&gt;
&lt;pre class=&quot;java&quot; data-ke-type=&quot;codeblock&quot; data-ke-language=&quot;java&quot;&gt;&lt;code&gt;public class ExcelMetadataFactory { // (2)
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;private ExcelMetadataFactory() {}

&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;private static class SingletonHolder {
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;private static final ExcelMetadataFactory INSTANCE = new ExcelMetadataFactory();
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;}

&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;public static ExcelMetadataFactory getInstance() {
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;return SingletonHolder.INSTANCE;
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;}

&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;public ExcelMetadata createMetadata(Class&amp;lt;?&amp;gt; clazz) {
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;Map&amp;lt;String, String&amp;gt; headerNamesMap = new LinkedHashMap&amp;lt;&amp;gt;();
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;List&amp;lt;String&amp;gt; dataFieldNamesList = new ArrayList&amp;lt;&amp;gt;();

&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;for (Field field : getAllFields(clazz)) {
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;if (field.isAnnotationPresent(ExcelColumn.class)) {
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;ExcelColumn columnAnnotation = field.getAnnotation(ExcelColumn.class);
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;headerNamesMap.put(field.getName(), columnAnnotation.headerName());
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;dataFieldNamesList.add(field.getName());
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;}
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;}

&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;if (headerNamesMap.isEmpty()) {
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;throw new RuntimeException(String.format(&quot;Class %s has not @ExcelColumn at all&quot;, clazz));
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;}

&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;return new ExcelMetadata(headerNamesMap, dataFieldNamesList, getSheetName(clazz));
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;}

&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;private String getSheetName(Class&amp;lt;?&amp;gt; clazz) {
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;ExcelSheet annotation = (ExcelSheet) getAnnotation(clazz, ExcelSheet.class);
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;if(annotation != null) {
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;return annotation.name();
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;}
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;return &quot;Users&quot;;
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;}
}&lt;/code&gt;&lt;/pre&gt;
&lt;pre class=&quot;java&quot; data-ke-type=&quot;codeblock&quot; data-ke-language=&quot;java&quot;&gt;&lt;code&gt;public final class SuperClassReflectionUtils { // (3)

&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;private SuperClassReflectionUtils() {}

&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;public static List&amp;lt;Field&amp;gt; getAllFields(Class&amp;lt;?&amp;gt; clazz) {
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;List&amp;lt;Field&amp;gt; fields = new ArrayList&amp;lt;&amp;gt;();
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;for (Class&amp;lt;?&amp;gt; clazzInClasses : getAllClassesIncludingSuperClasses(clazz, true)) {
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;fields.addAll(Arrays.asList(clazzInClasses.getDeclaredFields()));
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;}
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;return fields;
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;}

&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;public static Annotation getAnnotation(Class&amp;lt;?&amp;gt; clazz,
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp; Class&amp;lt;? extends Annotation&amp;gt; targetAnnotation) {
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;for (Class&amp;lt;?&amp;gt; clazzInClasses : getAllClassesIncludingSuperClasses(clazz, false)) {
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;if (clazzInClasses.isAnnotationPresent(targetAnnotation)) {
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;return clazzInClasses.getAnnotation(targetAnnotation);
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;}
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;}
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;return null;
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;}

&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;public static Field getField(Class&amp;lt;?&amp;gt; clazz, String name) throws Exception {
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;for (Class&amp;lt;?&amp;gt; clazzInClasses : getAllClassesIncludingSuperClasses(clazz, false)) {
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;for (Field field : clazzInClasses.getDeclaredFields()) {
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;if (field.getName().equals(name)) {
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;return clazzInClasses.getDeclaredField(name);
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;}
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;}
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;}
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;throw new NoSuchFieldException();
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;}


&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;private static List&amp;lt;Class&amp;lt;?&amp;gt;&amp;gt; getAllClassesIncludingSuperClasses(Class&amp;lt;?&amp;gt; clazz, boolean fromSuper) {
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;List&amp;lt;Class&amp;lt;?&amp;gt;&amp;gt; classes = new ArrayList&amp;lt;&amp;gt;();
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;while (clazz != null) {
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;classes.add(clazz);
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;clazz = clazz.getSuperclass();
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;}
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;if (fromSuper) {
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;Collections.reverse(classes);
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;}
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;return classes;
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;}
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;1. 엑셀을 그릴 때 필요한 일종의 메타데이터를 보관하는 객체다.&lt;br /&gt;2. Factory 객체가 DTO의 어노테이션을 파악하여 메타데이터를 정리하는 책임을 가진다.&lt;br /&gt;3. 우아한 형제들 기술 블로그에 올라온 유틸리티 코드를 사용하였다.&lt;br /&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;2. 바디 데이터 정보를 담을 객체 만들기&lt;/h4&gt;
&lt;pre class=&quot;java&quot; data-ke-type=&quot;codeblock&quot; data-ke-language=&quot;java&quot;&gt;&lt;code&gt;@Getter
@AllArgsConstructor
public class ExcelSheetData { // (1)
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;private final List&amp;lt;?&amp;gt; dataList;
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;private final Class&amp;lt;?&amp;gt; type;

&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;public static ExcelSheetData of(List&amp;lt;?&amp;gt; dataList, Class&amp;lt;?&amp;gt; type) {
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;return new ExcelSheetData(dataList, type);
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;}
}&lt;/code&gt;&lt;/pre&gt;
&lt;pre class=&quot;java&quot; data-ke-type=&quot;codeblock&quot; data-ke-language=&quot;java&quot;&gt;&lt;code&gt;public class ExcelSheetDataGroup { // (2)
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;private final List&amp;lt;ExcelSheetData&amp;gt; dataList;

&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;private ExcelSheetDataGroup(List&amp;lt;ExcelSheetData&amp;gt; data) {
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;validateEmpty(data);
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;this.dataList = new ArrayList&amp;lt;&amp;gt;(data);
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;}

&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;public List&amp;lt;ExcelSheetData&amp;gt; getExcelSheetData() {
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;return Collections.unmodifiableList(dataList);
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;}

&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;public static ExcelSheetDataGroup of(ExcelSheetData... data) {
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;List&amp;lt;ExcelSheetData&amp;gt; list = (data == null) ? List.of() : List.of(data);
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;return new ExcelSheetDataGroup(list);
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;}

&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;private void validateEmpty(List&amp;lt;ExcelSheetData&amp;gt; data) {
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;if(data.isEmpty()) {
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;throw new IllegalArgumentException(&quot;lists must not be empty&quot;);
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;}
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;}
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;1. &lt;span style=&quot;color: #333333;&quot;&gt;엑셀&lt;/span&gt;&lt;span style=&quot;color: #333333;&quot;&gt; &lt;/span&gt;단일 시트의 바디 데이터 정보를 담는 객체다.&lt;br /&gt;2. 엑셀 멀티 시트의 각 시트별로 그려질 바디 데이터 정보를 담는 객체다.&lt;br /&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;3. 엑셀 생성을 위한 베이스 구조 만들기&lt;/h4&gt;
&lt;pre class=&quot;java&quot; data-ke-type=&quot;codeblock&quot; data-ke-language=&quot;java&quot;&gt;&lt;code&gt;public interface ExcelFile { // (1)

&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;void write(OutputStream stream) throws IOException;
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;void writeWithEncryption(OutputStream stream, String password) throws IOException;

&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;default &amp;lt;T&amp;gt; void createCell(Row row, int column, T value, CellStyle style) {
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;if(value == null) return; // avoid NPE
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;Cell cell = row.createCell(column);
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;if (value instanceof Integer) {
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;cell.setCellValue((Integer) value);
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;}
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;else if(value instanceof Long) {
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;cell.setCellValue((Long) value);
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;}
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;else if (value instanceof Boolean) {
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;cell.setCellValue((Boolean) value);
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;}
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;else {
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;cell.setCellValue((String) value);
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;}
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;cell.setCellStyle(style);
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;}

&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;default CellStyle createCellStyle(Workbook wb, boolean isBold) {
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;CellStyle style = wb.createCellStyle();
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;Font font = wb.createFont();
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;font.setBold(isBold);
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;style.setFont(font);
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;return style;
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;}
}&lt;/code&gt;&lt;/pre&gt;
&lt;pre class=&quot;java&quot; data-ke-type=&quot;codeblock&quot; data-ke-language=&quot;java&quot;&gt;&lt;code&gt;public abstract class BaseSXSSFExcelFile implements ExcelFile { // (2)

&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;protected static final int ROW_ACCESS_WINDOW_SIZE = 1000;
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;protected static final int ROW_START_INDEX = 0;
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;protected static final int COLUMN_START_INDEX = 0;

&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;protected SXSSFWorkbook workbook;
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;protected Sheet sheet;

&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;public BaseSXSSFExcelFile() {
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;this.workbook = new SXSSFWorkbook(ROW_ACCESS_WINDOW_SIZE);
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;}

&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;protected void renderHeaders(ExcelMetadata excelMetadata) {
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;sheet = workbook.createSheet(excelMetadata.getSheetName());
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;Row row = sheet.createRow(ROW_START_INDEX);
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;int columnIndex = COLUMN_START_INDEX;
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;CellStyle style = createCellStyle(workbook, true);

&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;for (String fieldName : excelMetadata.getDataFieldNames()) {
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;createCell(row, columnIndex++, excelMetadata.getHeaderName(fieldName), style);
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;}
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;}

&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;protected void renderDataLines(ExcelSheetData data) {
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;CellStyle style = createCellStyle(workbook, false);
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;int rowIndex = ROW_START_INDEX + 1;
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;List&amp;lt;Field&amp;gt; fields = getAllFields(data.getType());

&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;for (Object record : data.getDataList()) {
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;Row row = sheet.createRow(rowIndex++);
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;int columnIndex = COLUMN_START_INDEX;
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;try {
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;for (Field field : fields) {
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;field.setAccessible(true);
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;createCell(row, columnIndex++, field.get(record), style);
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;}
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;} catch (IllegalAccessException e) {
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;throw new RuntimeException(&quot;Error accessing data field rendering data lines.&quot;, e);
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;}
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;}
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;}

&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;@Override
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;public void write(OutputStream stream) throws IOException {
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;workbook.write(stream);
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;}

&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;@Override
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;public void writeWithEncryption(OutputStream stream, String password) throws IOException {
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;if (password == null) {
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;write(stream);
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;} else {
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;POIFSFileSystem fileSystem = new POIFSFileSystem();
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;OutputStream encryptorStream = getEncryptorStream(fileSystem, password);
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;workbook.write(encryptorStream);
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;encryptorStream.close(); // this is necessary before writing out the FileSystem

&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;fileSystem.writeFilesystem(stream); // write the encrypted file to the response stream
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;fileSystem.close();
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;}

&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;workbook.close();
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;workbook.dispose();
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;stream.close();
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;}

&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;private OutputStream getEncryptorStream(POIFSFileSystem fileSystem, String password) {
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;try {
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;Encryptor encryptor = new EncryptionInfo(EncryptionMode.agile).getEncryptor();
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;encryptor.confirmPassword(password);
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;return encryptor.getDataStream(fileSystem);
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;} catch (IOException | GeneralSecurityException e) {
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;throw new RuntimeException(&quot;Failed to obtain encrypted data stream from POIFSFileSystem.&quot;);
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;}
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;}
}&lt;/code&gt;&lt;/pre&gt;
&lt;pre class=&quot;java&quot; data-ke-type=&quot;codeblock&quot; data-ke-language=&quot;java&quot;&gt;&lt;code&gt;public class SXSSFExcelFile extends BaseSXSSFExcelFile { // (3)

&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;public SXSSFExcelFile(ExcelSheetData data, 
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;HttpServletResponse response) throws IOException {
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;this(data, response, null);
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;}
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;public SXSSFExcelFile(ExcelSheetData data, 
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;HttpServletResponse response,
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;@Nullable String password) throws IOException {
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;ExcelMetadata metadata = ExcelMetadataFactory.getInstance().createMetadata(data.getType());
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;exportExcelFile(data, metadata, response.getOutputStream(), password);
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;}

&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;private void exportExcelFile(ExcelSheetData data,
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp; ExcelMetadata metadata,
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp; ServletOutputStream stream,
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp; String password) throws IOException {
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;renderHeaders(metadata);
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;renderDataLines(data);
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;writeWithEncryption(stream, password); // if password is null, encryption will not be applied.
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;}
}&lt;/code&gt;&lt;/pre&gt;
&lt;pre class=&quot;java&quot; data-ke-type=&quot;codeblock&quot; data-ke-language=&quot;java&quot;&gt;&lt;code&gt;public class SXSSFMultiSheetExcelFile extends BaseSXSSFExcelFile { // (4)

&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;public SXSSFMultiSheetExcelFile(ExcelSheetDataGroup dataGroup,
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;HttpServletResponse response) throws IOException {
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;this(dataGroup, response, null);
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;}
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;public SXSSFMultiSheetExcelFile(ExcelSheetDataGroup dataGroup,
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;HttpServletResponse response,
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;@Nullable String password) throws IOException {
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;exportExcelFile(dataGroup, response.getOutputStream(), password);
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;}


&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;private void exportExcelFile(ExcelSheetDataGroup dataGroup,
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp; ServletOutputStream stream,
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp; String password) throws IOException {
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;for (ExcelSheetData data : dataGroup.getExcelSheetData()) {
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;ExcelMetadata metadata = ExcelMetadataFactory.getInstance().createMetadata(data.getType());
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;renderHeaders(metadata);
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;renderDataLines(data);
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;}
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;writeWithEncryption(stream, password); // if password is null, encryption will not be applied.
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;}
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;1. 엑셀 파일이 꼭 가져야 할 인터페이스를 정의했다.&lt;br /&gt;2. 인터페이스를 구현하며, 엑셀 데이터를 그려주는 클래스다.&lt;br /&gt;3. 단일 시트 엑셀 파일을 생성시 사용하는 클래스다.&lt;br /&gt;4. 멀티 시트 엑셀 파일을 생성시 사용하는 클래스다.&lt;br /&gt;&amp;nbsp;&lt;br /&gt;위 구조를 통해 다음과 같이 엑셀 파일을 생성할 수 있다.&lt;/p&gt;
&lt;pre class=&quot;java&quot; data-ke-type=&quot;codeblock&quot; data-ke-language=&quot;java&quot;&gt;&lt;code&gt;@PostMapping(&quot;api/v1/export/download/excel&quot;)
public void download(HttpServletResponse response) throws Exception {
	List&amp;lt;UserExcelDto&amp;gt; userInfoList = userService.getUserInfo();
	new SXSSFExcelFile(ExcelSheetData.of(userInfoList, UserExcelDto.class), response);
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;br /&gt;개선된 구조의 객체간 관계도는 다음과 같다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;838&quot; data-origin-height=&quot;413&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/cRYYEU/btsJI0vtsaY/LPvT6KY8waCvzuF0nUfKEk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/cRYYEU/btsJI0vtsaY/LPvT6KY8waCvzuF0nUfKEk/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/cRYYEU/btsJI0vtsaY/LPvT6KY8waCvzuF0nUfKEk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FcRYYEU%2FbtsJI0vtsaY%2FLPvT6KY8waCvzuF0nUfKEk%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;757&quot; height=&quot;373&quot; data-origin-width=&quot;838&quot; data-origin-height=&quot;413&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;br /&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;정리하며&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;새로 만든 구조는 기존보다 훨씬 간결해졌고, 코드의 복잡성을 크게 줄였다..&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이제 엑셀 파일을 생성할 때마다 새로운 구현 클래스를 추가할 필요가 없으며, Cell마다 일일이 작성하는 번거로움도 사라졌다. 비록 best practice는 아닐 수 있지만, 기존 구조의 문제를 해결하고 효율성을 높였다는 점에서 긍정적인 결과를 얻었다고 생각한다.&lt;br /&gt;&amp;nbsp;&lt;br /&gt;&amp;nbsp;&lt;br /&gt;참고 자료 :&amp;nbsp;&lt;br /&gt;&lt;a href=&quot;https://techblog.woowahan.com/2698/&quot; target=&quot;_self&quot;&gt;&lt;span&gt;&lt;span style=&quot;color: #0070d1;&quot;&gt;우아한 형제들 기술 &lt;/span&gt;&lt;/span&gt;&lt;/a&gt;&lt;a href=&quot;https://techblog.woowahan.com/2698/&quot; target=&quot;_self&quot;&gt;&lt;span&gt;&lt;span style=&quot;color: #0070d1;&quot;&gt;블로그&lt;/span&gt;&lt;/span&gt;&lt;/a&gt;&lt;/p&gt;</description>
      <category> ETC/Development Log</category>
      <category>Apache POI</category>
      <category>excel download</category>
      <category>Java</category>
      <category>SXSSFWorkbook</category>
      <category>엑셀 다운로드</category>
      <author>coco3o</author>
      <guid isPermaLink="true">https://dev-coco.tistory.com/192</guid>
      <comments>https://dev-coco.tistory.com/192#entry192comment</comments>
      <pubDate>Tue, 24 Sep 2024 11:41:12 +0900</pubDate>
    </item>
    <item>
      <title>대용량 엑셀 다운로드 OOM(Out Of Memory) 해결 과정</title>
      <link>https://dev-coco.tistory.com/191</link>
      <description>&lt;p data-ke-size=&quot;size16&quot;&gt;서버가 갑자기 먹통이 된 경험을 했다.&lt;br /&gt;운영 중인 서버의 CPU 사용률이 급격히 상승하며 서버가 죽어버렸다.&lt;br /&gt;서버를 즉시 재가동하고 원인을 확인한 결과, OOM(Out Of Memory)으로 인해 서버가 중단된 것이었다.&lt;br /&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;왜 OOM(Out Of Memory)이 발생했는가?&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #ee2323;&quot;&gt;관리자 페이지에서 엑셀 다운로드를 하는데 약 N만 건 이상의 Row를 다운로드하는 과정에 OOM이 발생했다.&lt;/span&gt;&lt;br /&gt;현재 운영 중인 서버는 1개의 WAS 안에 서비스, 관리자 등 여러 프로젝트가 함께 돌아가고 있는 상황이다.&lt;br /&gt;여러 요청에 대한 처리와 N만 건 이상의 Row를 다운받는 요청이 겹치면서 과부하가 발생한 것으로 보인다.&lt;br /&gt;하지만, N만 건 정도의 데이터를 처리하면서 OOM이 발생하는 것은 이상하다고 생각하여 원인을 찾아보았다.&lt;br /&gt;&lt;br /&gt;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;원인 파악&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;현재 엑셀 다운로드를 위해 Apache POI 라이브러리의 XSSFWorkbook을 사용하고 있는데,&lt;br /&gt;문제의 원인은 XSSFWorkbook에서 확인되었다.&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;XSSFWorkbook 메모리 누수&lt;/h4&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;b&gt;메모리 효율성의 부족&lt;/b&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;XSSFWorkbook 는 엑셀 파일 전체가 만들어질때까지 데이터를 메모리에 상주시키는 방식이다.&lt;/li&gt;
&lt;li&gt;큰 데이터셋을 다룰 경우 상당한 양의 메모리를 소비하며, 서버의 메모리 용량에 따라 OOM(Out Of Memory) 에러가 발생할 수 있다.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;b&gt;대용량 데이터 처리시 성능 저하&lt;/b&gt;&amp;nbsp;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;작은 데이터셋에서는 문제가 되지 않지만, 수만 또는 수백만 데이터의 대용량 파일을 처리할 때 처리 속도가 현저히 느려지며 성능이 크게 떨어진다.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;b&gt;가비지 컬렉션 부담&lt;/b&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;대용량 파일을 처리하는 동안, XSSFWorkbook은 많은 수의 임시 객체를 생성할 수 있다.&lt;/li&gt;
&lt;li&gt;이는 가비지 컬렉터에 부담을 주며, 시스템의 전체 성능에 영향을 줄 수 있다.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;문제 해결 방법&lt;/h3&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;1. 서버 메모리 증설하기 (돈으로 찍어 누르기 )&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;사실 이 방법이 제일 쉬운 방법이다. &lt;br /&gt;OOM이 발생한 가장 직관적인 이유는 메모리가 부족한 것이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size14&quot;&gt;현재 운영 중인 서버의 메모리는 8GB 밖에 되지 않는다... &lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;따라서 서버의 메모리를 증설하여 스케일업 하면 해결될 일이다.&lt;br /&gt;하지만 이는 데이터가 계속 증가할 것을 고려하면 메모리 증설만으로는 근본적인 해결책이 될 수 없다.&lt;br /&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;2. 서버 내 여러 프로젝트 분리하기&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;현재 운영 중인 서버는 하나의 톰캣 서버에 여러 프로젝트가 돌아가고 있는 상황이다.&lt;br /&gt;이 프로젝트들을 각각 독립된 서버로 분리하는 방법도 고려해 봤지만,&lt;br /&gt;서버 비용과 들어갈 작업의 양이 많아 당장 진행하기엔 어려움이 있었다..&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;한정된 내부 자원 안에서 해결할 수 있는 방법이 없을까 고민하였고,&lt;br /&gt;&lt;span style=&quot;color: #333333; text-align: start;&quot;&gt;엑셀 다운로드를 지원하는 Apache POI 라이브러리 중&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;/span&gt;&lt;a style=&quot;color: #0070d1; text-align: start;&quot; href=&quot;https://poi.apache.org/apidocs/dev/org/apache/poi/xssf/streaming/SXSSFWorkbook.html&quot;&gt;&lt;span&gt;SXSSFWorkbook&lt;/span&gt;&lt;/a&gt;를 찾을 수 있었다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;3. SXSSFWorkbook 사용&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;SXSSFWorkbook은 지정한 Row만큼만 메모리에 로드하여 처리하다가 지정한 Row에 도달하면&lt;br /&gt;임시 XML 파일로 디스크에 기록하고 메모리를 비워주는 스트리밍 방식이다.&lt;br /&gt;&lt;span style=&quot;color: #ee2323;&quot;&gt;즉, SXSSFWorkbook은 메모리에 있는 데이터를 디스크로 옮기면서 처리하기 때문에 메모리를 적게 먹어 대용량 데이터 처리에 매우 적합하다.&lt;/span&gt;&lt;br /&gt;&lt;br /&gt;비록 디스크에 플러시한 Row는 다시 접근 할 수 없는 단점도 있지만, 현재 상황에서는 큰 문제가 되지 않았다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;640&quot; data-origin-height=&quot;480&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bifDfs/btsI7VtnfF2/UKKhJsIF1pXZFoS11KD5Z0/img.jpg&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bifDfs/btsI7VtnfF2/UKKhJsIF1pXZFoS11KD5Z0/img.jpg&quot; data-alt=&quot;너로 정했다!&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bifDfs/btsI7VtnfF2/UKKhJsIF1pXZFoS11KD5Z0/img.jpg&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbifDfs%2FbtsI7VtnfF2%2FUKKhJsIF1pXZFoS11KD5Z0%2Fimg.jpg&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;640&quot; height=&quot;480&quot; data-origin-width=&quot;640&quot; data-origin-height=&quot;480&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;너로 정했다!&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;br /&gt;아래와 같이 인자 값을 지정해주면 설정한 row 수만큼 작성 후 알아서 flush 해준다. &lt;/p&gt;
&lt;pre class=&quot;java&quot; data-ke-type=&quot;codeblock&quot; data-ke-language=&quot;java&quot;&gt;&lt;code&gt;// auto disk flush
// 메모리에 1000개 rows를 유지하고, 초과 rows는 disk에 auto-flush
SXSSFWorkbook wb = new SXSSFWorkbook(1000);

// 인자 값 없이 생성시 default 100 
SXSSFWorkbook wb = new SXSSFWorkbook();

// auto-flushing 끄기 (모든 rows memory 유지)
SXSSFWorkbook wb = new SXSSFWorkbook(-1);&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;아래와 같이 flushRows 메서드로 데이터를 수동으로 디스크로 옮길 수도 있다.&lt;/p&gt;
&lt;pre class=&quot;java&quot; data-ke-type=&quot;codeblock&quot; data-ke-language=&quot;java&quot;&gt;&lt;code&gt;// manually control
if(rownum % 100 == 0) {
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;// 마지막 100개 rows를 제외한 all rows flush
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;((SXSSFSheet)sh).flushRows(100); 
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;// 모든 rows disk flush
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;((SXSSFSheet)sh).flushRows(); // is a shortcut for ((SXSSFSheet)sh).flushRows(0)
}&lt;/code&gt;&lt;/pre&gt;
&lt;pre class=&quot;java&quot; data-ke-type=&quot;codeblock&quot; data-ke-language=&quot;java&quot;&gt;&lt;code&gt;wb.write(stream);
wb.close();
wb.dispose(); // 디스크에 저장한 임시파일 삭제&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;작업 완료 후엔 디스크에 저장된 임시파일을 삭제해줘야 한다.&lt;br /&gt;&lt;br /&gt;&amp;nbsp;XSSFWorkbook을 SXSSFWorkbook 라이브러리로 사용하도록 변경했다.&lt;br /&gt;&lt;br /&gt;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;개선 전후 비교하기&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;로컬에서 약 10만 건의 데이터(약 6MB 용량)를 추출하는 API를 10회 연속으로 호출하는 테스트를 진행했으며,&lt;br /&gt;VisualVM을 사용해 메모리 변화를 모니터링했다.&lt;br /&gt;&lt;br /&gt;&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;AS-IS&lt;/h4&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;944&quot; data-origin-height=&quot;285&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/nPVIf/btsI6iiIZhp/klGjjYrbUAh5rA7RDM5Vfk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/nPVIf/btsI6iiIZhp/klGjjYrbUAh5rA7RDM5Vfk/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/nPVIf/btsI6iiIZhp/klGjjYrbUAh5rA7RDM5Vfk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FnPVIf%2FbtsI6iiIZhp%2FklGjjYrbUAh5rA7RDM5Vfk%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;652&quot; height=&quot;197&quot; data-origin-width=&quot;944&quot; data-origin-height=&quot;285&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1259&quot; data-origin-height=&quot;464&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/AAkt6/btsI5eBiDOa/9oKY5yCu7KyiX0ObVkzCOK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/AAkt6/btsI5eBiDOa/9oKY5yCu7KyiX0ObVkzCOK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/AAkt6/btsI5eBiDOa/9oKY5yCu7KyiX0ObVkzCOK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FAAkt6%2FbtsI5eBiDOa%2F9oKY5yCu7KyiX0ObVkzCOK%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;564&quot; height=&quot;208&quot; data-origin-width=&quot;1259&quot; data-origin-height=&quot;464&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;개선 전에는 요청을 처리하다가 서버가 죽었고,&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;TO-BE&lt;/h4&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;935&quot; data-origin-height=&quot;313&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/kwKk0/btsI61mXvNA/k8MMvS619K33LfCT3I5kaK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/kwKk0/btsI61mXvNA/k8MMvS619K33LfCT3I5kaK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/kwKk0/btsI61mXvNA/k8MMvS619K33LfCT3I5kaK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FkwKk0%2FbtsI61mXvNA%2Fk8MMvS619K33LfCT3I5kaK%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;708&quot; height=&quot;237&quot; data-origin-width=&quot;935&quot; data-origin-height=&quot;313&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1125&quot; data-origin-height=&quot;463&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bCXrIv/btsI5hZc8Xe/y7iEq5bHKGSO31wNvbKkA1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bCXrIv/btsI5hZc8Xe/y7iEq5bHKGSO31wNvbKkA1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bCXrIv/btsI5hZc8Xe/y7iEq5bHKGSO31wNvbKkA1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbCXrIv%2FbtsI5hZc8Xe%2Fy7iEq5bHKGSO31wNvbKkA1%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;590&quot; height=&quot;243&quot; data-origin-width=&quot;1125&quot; data-origin-height=&quot;463&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;개선 후에는 서버가 안정적으로 작업을 처리했다.&lt;br /&gt;메모리 사용량 그래프도 전과 비교하면 상당히 안정적이며 기존보다 약 80%정도 절감되었다.  &lt;br /&gt;메모리 효율성이 향상되면서 다운로드 속도도 크게 개선됐으며,&lt;br /&gt;10만 건의 데이터를 다운로드하는 데 77초에서 5초로 단축되어, 약 1400% 이상 성능이 개선되었다. &lt;br /&gt;&amp;nbsp;&lt;br /&gt;&lt;br /&gt;라이브러리를 변경하면서 엑셀 다운로드 코드 베이스 구조의 문제점도 몇 가지 보여 나중에 전반적으로 개선 후 다시 블로그에 정리해야겠다.&lt;/p&gt;</description>
      <category> ETC/Development Log</category>
      <category>Apache POI</category>
      <category>Excel</category>
      <category>excel download</category>
      <category>SXSSFWorkbook</category>
      <category>visualvm</category>
      <category>XSSFWorkbook</category>
      <category>엑셀</category>
      <category>엑셀 다운로드</category>
      <author>coco3o</author>
      <guid isPermaLink="true">https://dev-coco.tistory.com/191</guid>
      <comments>https://dev-coco.tistory.com/191#entry191comment</comments>
      <pubDate>Fri, 16 Aug 2024 16:54:16 +0900</pubDate>
    </item>
    <item>
      <title>Service와 Repository를 완전히 분리하기 (with. DDD)</title>
      <link>https://dev-coco.tistory.com/190</link>
      <description>&lt;h3 data-ke-size=&quot;size23&quot;&gt;Intro&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;개발 오픈 톡방에서 &quot;Service와 Repository는 완벽히 분리되어야 한다.&quot;의 내용이 화두 되었다.&lt;br /&gt;즉, &quot;도메인은 특정 기술(인프라)에 의존하지 않고 순수하게 유지되어야 한다.&quot;는 말인데,&lt;br /&gt;&lt;span style=&quot;color: #333333;&quot;&gt;어떻게&lt;/span&gt;&lt;span style=&quot;color: #333333;&quot;&gt; 하면&amp;nbsp;&lt;/span&gt;도메인과 인프라를 완벽히 분리할 수 있는지 알아보도록 하자.&lt;br /&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;Layered Architecture와 DDD(Domain Driven Design)&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;레이어드 아키텍처는 가장 흔하게 사용되는 아키텍처이다.&lt;br /&gt;이름 그대로 프로그램 내에서 &lt;span style=&quot;color: #ee2323;&quot;&gt;계층을 나누는 설계 방식&lt;/span&gt;이며, 의존의 방향성은 오직 위에서 아래로만 내려간다.&lt;br /&gt;일반적으로&amp;nbsp;Presentation, Business, Persistence, DataBase의 4개 표준 레이어로 구성한다.&lt;br /&gt;물론 규모에 따라 병합하기도 하며, 그 이상의 레이어로 구성하기도 한다.&lt;br /&gt;&amp;nbsp;&lt;br /&gt;스프링 기준 대표적인 예시&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style2&quot;&gt;Controller - Service - Domain - Repository&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;각 계층은 어플리케이션 내에서 특정 역할과 관심사(화면 표시, 비즈니스 로직 수행, DB 작업 등) 별로 구분되며,&lt;br /&gt;이는 레이어드 아키텍처의 강력한 기능인 '관심사의 분리(Separation of Concern)'를 의미한다.&lt;br /&gt;&lt;span style=&quot;color: #ee2323;&quot;&gt;특정 계층의 구성 요소는 해당 계층의 관련 기능만 수행해야 한다&lt;/span&gt;는 것이다.&lt;br /&gt;&amp;nbsp;&lt;br /&gt;DDD에서 Layered Architecture를 적용하면 아마 다음과 같은 구조가 일반적으로 사용될 것이다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;824&quot; data-origin-height=&quot;651&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/dlToGo/btsHnWgDShC/6IskqkPouGJdrQtIiZU5E1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/dlToGo/btsHnWgDShC/6IskqkPouGJdrQtIiZU5E1/img.png&quot; data-alt=&quot;https://learn.microsoft.com/ko-kr/dotnet/architecture/microservices/microservice-ddd-cqrs-patterns/infrastructure-persistence-layer-design&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/dlToGo/btsHnWgDShC/6IskqkPouGJdrQtIiZU5E1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FdlToGo%2FbtsHnWgDShC%2F6IskqkPouGJdrQtIiZU5E1%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;824&quot; height=&quot;651&quot; data-origin-width=&quot;824&quot; data-origin-height=&quot;651&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;https://learn.microsoft.com/ko-kr/dotnet/architecture/microservices/microservice-ddd-cqrs-patterns/infrastructure-persistence-layer-design&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이제 DDD 관점에서 Repository를 생각해 보자.&lt;br /&gt;우리는 JPA를 사용하는 세대이므로 JPA를 기준으로 글을 작성하겠다.&lt;/p&gt;
&lt;pre class=&quot;java&quot; data-ke-type=&quot;codeblock&quot; data-ke-language=&quot;java&quot;&gt;&lt;code&gt;public class MemberService {
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;@Autowired
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;private MemberRepository memberRepository;
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;...
}

public interface MemberRepository extends JpaRepository&amp;lt;MemberEntity, Long&amp;gt; {
}&lt;/code&gt;&lt;/pre&gt;
&lt;p style=&quot;text-align: left;&quot; data-ke-size=&quot;size16&quot;&gt;위 코드는 도메인이 인프라를 의존하고 있어 관심사 분리가 되지 않고 있다.&lt;br /&gt;JpaRepository를 상속받은 인터페이스를 그대로 사용하면서 jpa 메서드가 서비스 도메인에 노출되어 내가 구현하지 않은 메서드를 사용할 수 있고, 반환값으로 엔티티를 받기 때문에 구현 기술에 의존하게 되면서 문제가 생긴다.&lt;br /&gt;&amp;nbsp;&lt;br /&gt;서비스 도메인은 인프라의 형태에 의존적이지 않아야 한다.&lt;/p&gt;
&lt;p style=&quot;text-align: left;&quot; data-ke-size=&quot;size14&quot;&gt;&lt;span style=&quot;color: #ee2323;&quot;&gt;인프라의 구현이 jpa이든 mybatis이든 파일 시스템이든 상관없이 서비스의 구현은 동일해야 한다는 말이다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;구현 기술에 대한 의존 없이 도메인을 순수하게 유지하려면 어떻게 해야 할까?&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 부분은 의존성 역전 원칙(DIP, Dependency Inversion Principle)으로 해결할 수 있다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;716&quot; data-origin-height=&quot;197&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/eErgc1/btsHqbkXUls/d76rgHkLhQEYW1sQ5DaIFK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/eErgc1/btsHqbkXUls/d76rgHkLhQEYW1sQ5DaIFK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/eErgc1/btsHqbkXUls/d76rgHkLhQEYW1sQ5DaIFK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FeErgc1%2FbtsHqbkXUls%2Fd76rgHkLhQEYW1sQ5DaIFK%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;716&quot; height=&quot;197&quot; data-origin-width=&quot;716&quot; data-origin-height=&quot;197&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;현재 도메인 영역이 인프라를 의존하며, 고수준 모듈이 저수준 모듈을 의존하고 있는 것으로 보인다.&lt;br /&gt;이는 반대로 저수준 모듈이 고수준 모듈에 의존하게 해야 한다 라는 의미로,&lt;br /&gt;도메인 &amp;rarr; 인프라 의존 관계를 인프라 &amp;rarr; 도메인 의존 관계로 의존의 방향을 역전시키겠다는 이야기다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;828&quot; data-origin-height=&quot;386&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/csmCzT/btsHpMMDgdK/Qe14q3njyBECJjr4AQNdy0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/csmCzT/btsHpMMDgdK/Qe14q3njyBECJjr4AQNdy0/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/csmCzT/btsHpMMDgdK/Qe14q3njyBECJjr4AQNdy0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FcsmCzT%2FbtsHpMMDgdK%2FQe14q3njyBECJjr4AQNdy0%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;828&quot; height=&quot;386&quot; data-origin-width=&quot;828&quot; data-origin-height=&quot;386&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;하지만 한 가지 문제가 생긴다. 코드로 확인해 보자.&lt;/p&gt;
&lt;pre class=&quot;java&quot; data-ke-type=&quot;codeblock&quot; data-ke-language=&quot;java&quot;&gt;&lt;code&gt;// domain layer
public interface MemberRepository {
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;void save(Member member);
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;Member findById(Long id);
}
// pojo class
public class Member {
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;private Long id;
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;private String name;
}


// infrastructure layer
interface MemberJpaRepository extends JpaRepository&amp;lt;MemberEntity, Long&amp;gt; {
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;void save(MemberEntity entity);
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;MemberEntity findById(Long id);
}
// jpa entity class
@Entity
class MemberEntity {
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;@Id
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;private Long id;
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;private String name;
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;위와 같이 도메인에 존재하는 Repository와 인프라에 존재하는 Repository의 형태가 서로 달라져버린다.&lt;br /&gt;그래서 다음과 같이 중간에 Adapter 클래스를 하나 두고, 해당 Adapter가 도메인의 Repository와 인프라의 Repository 사이의 규격을 맞춰주면 된다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;881&quot; data-origin-height=&quot;336&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/cRlauk/btsHpwcdu1k/OcESKsgsAp5miBQNLSkFZ1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/cRlauk/btsHpwcdu1k/OcESKsgsAp5miBQNLSkFZ1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/cRlauk/btsHpwcdu1k/OcESKsgsAp5miBQNLSkFZ1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FcRlauk%2FbtsHpwcdu1k%2FOcESKsgsAp5miBQNLSkFZ1%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;881&quot; height=&quot;336&quot; data-origin-width=&quot;881&quot; data-origin-height=&quot;336&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;pre class=&quot;java&quot; data-ke-type=&quot;codeblock&quot; data-ke-language=&quot;java&quot;&gt;&lt;code&gt;// adapter class (infra layer)
@Repository
public class MemberRepositoryImpl implements MemberRepository {
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;// JpaRepository를 injection하여 필요한 기능만 래핑하여 노출한다. *SOLID 중 ISP 원칙 준수
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;private final MemberJpaRepository memberJpaRepository;

&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;public MemberRepositoryImpl(MemberJpaRepository memberJpaRepository) {
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;this.memberJpaRepository = memberJpaRepository;
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;}

&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;@Override
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;public void save(Member member) {
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;// converting
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;MemberEntity entity = MemberEntity.from(member);

&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;memberJpaRepository.save(entity);
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;}

&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;@Override
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;public Member findById(Long id) {
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;MemberEntity entity = memberJpaRepository.findById(id).orElse(null);
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;// converting
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;return (entity != null) ? new Member(entity.getId(), entity.getName()) : null;
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;}
}

@Service
public class MemberService {
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;// MemberRepository가 jpa를 사용하는지 redis를 사용하는지 전혀 모름
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;private final MemberRepository memberRepository;

&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;public MemberService(MemberRepository memberRepository) {
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;this.memberRepository = memberRepository;
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;}

&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;public void doSomething() {
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;// TODO
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;}

}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;DIP를 이용해서 도메인 모델에 존재하는 Repository를 추상화로 만들고 실제 구현을 infrastructure에서 하게 한다. 도메인 관점에서 &quot;나는 이런 것들을 이렇게 저장할 것이고, 이렇게 불러올 거야!&quot;라는 명세를 만들어놓고 실제 구현 기술에 대한 부분을 분리시킨다는 것이다.&lt;br /&gt;즉, domain layer에서는 저장하는 방법에 대해 관심을 갖고, infrastructure layer 에서는 실제로 어떻게 저장하는지에 대해 관심을 갖는다.&lt;br /&gt;이로써 도메인은 어댑터 뒤에 어떤 기술을 사용하던 상관없이 데이터를 조작하는 데에 필요한 인터페이스만을 바라보고 협력하기 때문에&amp;nbsp;repository의 형태에 의존하지 않으면서 서비스의 구현은 일관성을 가진다.&lt;br /&gt;&amp;nbsp;&lt;br /&gt;이렇게 분리하면 복잡성을 낮추고 확장성을 높이는 이점을 취할 수 있다.&lt;span style=&quot;background-color: #ffffff;&quot;&gt;&lt;span style=&quot;color: #0d0d0d;&quot;&gt;&lt;/span&gt;&lt;/span&gt;&lt;br /&gt;하지만 분리하는 것이 마냥 좋은 것만은 아니며, 다음과 같은 몇 가지 문제들이 있다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;너무 많은 컨버팅 코드&lt;/li&gt;
&lt;li&gt;휴먼 에러&lt;/li&gt;
&lt;li&gt;구현 기술의 강력한 기능 사용 불가&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&lt;span style=&quot;color: #ee2323;&quot;&gt;&lt;b&gt;1. 너무 많은 컨버팅 코드&lt;/b&gt;&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;순수 도메인 객체와 영속성 객체는 분리되어야 한다.&lt;br /&gt;즉, service &amp;harr; repository의 파라미터와 반환 값은 외부에 의존적이지 않도록 컨버팅 작업이 필요하다.&lt;br /&gt;만약, 하나의 애그리거트에 매우 많은 중첩 객체가 존재하면 어떻게 될까?&lt;/p&gt;
&lt;p data-ke-size=&quot;size14&quot;&gt;&lt;span style=&quot;color: #ee2323;&quot;&gt;※ 애그리거트는 간략하게 말하자면, 같은 라이프 사이클을 가지는 관련된 객체들을 모아 하나의 단위로 취급하는 개념이다.&lt;/span&gt;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1280&quot; data-origin-height=&quot;569&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/oqxQS/btsHpk4FNmO/6KGuKqkiAqfbBSfFny0EdK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/oqxQS/btsHpk4FNmO/6KGuKqkiAqfbBSfFny0EdK/img.png&quot; data-alt=&quot;https://devlos.tistory.com/51#google_vignette&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/oqxQS/btsHpk4FNmO/6KGuKqkiAqfbBSfFny0EdK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FoqxQS%2FbtsHpk4FNmO%2F6KGuKqkiAqfbBSfFny0EdK%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;833&quot; height=&quot;370&quot; data-origin-width=&quot;1280&quot; data-origin-height=&quot;569&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;https://devlos.tistory.com/51#google_vignette&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;일반적으로 DDD에서는 하나의 애그리거트를 repository의 대상 엔티티로 삼는다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size14&quot;&gt;&lt;span style=&quot;color: #ee2323;&quot;&gt;Order라는 애그리거트가 존재할 때, 해당 애그리거트를 저장하고 로드하는 repository는 OrderRepository만 존재해야 한다는 소리다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;결국 해당 애그리거트에 포함되는 모든 entity와 value 들에 대해서 transaction consistency를 보장해야 하며, 컨버팅을 하느라 엄청난 시간을 쏟게 된다.&lt;br /&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&lt;span style=&quot;color: #ee2323;&quot;&gt;&lt;b&gt;2. 휴먼 에러&lt;/b&gt;&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;위 컨버팅 문제와 직결된 문제인데, 아래 예시 코드에서 문제점을 찾아보자.&lt;/p&gt;
&lt;pre class=&quot;java&quot; data-ke-type=&quot;codeblock&quot; data-ke-language=&quot;java&quot;&gt;&lt;code&gt;// domain pojo class
public class Order {
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;private Long id;
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;private String orderNum;
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;private List&amp;lt;OrderItem&amp;gt; orderItems;
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;private Long totalPrice;
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;private String address;
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;public Order(Long id, String orderNum, List&amp;lt;OrderItem&amp;gt; orderItems, Long totalPrice, String address) {
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;this.id = id;
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;this.orderNum = orderNum;
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;this.orderItems = orderItems;
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;this.totalPrice = totalPrice;
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;this.address = address;
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;}
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;...
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;위 코드는 domain의 Order이고, 아래는 infra의 Order이다.&lt;/p&gt;
&lt;pre class=&quot;java&quot; data-ke-type=&quot;codeblock&quot; data-ke-language=&quot;java&quot;&gt;&lt;code&gt;// jpa entity class
@Entity
public class OrderEntity {
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;@Id
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;private Long id;
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;private String orderNum;
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;private List&amp;lt;OrderItem&amp;gt; orderItems;
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;private Long totalPrice;
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;public OrderEntity(Long id, String orderNum, List&amp;lt;OrderItem&amp;gt; orderItems, Long totalPrice) {
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;this.id = id;
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;this.orderNum = orderNum;
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;this.orderItems = orderItems;
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;this.totalPrice = totalPrice;
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;}
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;...
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Order객체에 address라는 필드가 추가되었는데, 누군가의 실수로 Entity에는 address가 없다.&lt;br /&gt;이는 어쩌면 당연하겠지만 명시적으로 혹은 코드 상에서 domain과 infra의 연결이 분리되었기 때문에 발생하는 문제이다.&lt;br /&gt;이를 컴파일 단에서 확인할 수 없으니 그만큼 안정성은 떨어질 수 있다.&lt;br /&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&lt;span style=&quot;color: #ee2323;&quot;&gt;&lt;b&gt;3. 구현 기술의 강력한 기능 사용 불가&lt;/b&gt;&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 역시 컨버팅의 연장선이다.&lt;br /&gt;Lazy Loading이나 Dirty Checking 같은 jpa에서 지원하는 기능은 영속성 계층에 의존하는 기능이다.&lt;br /&gt;따라서 컨버팅 이후 해당 기능들을 사용할 수 없게 된다.&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style3&quot;&gt;참고로 Aggregate에 대해서 Lazy Loading이 필요하지 않다고 보는 의견도 다수 존재한다.&lt;br /&gt;aggregate에 value가 필요하면 한 번에 load 되어야 하고 필요하지 않으면 load 되지 않아야 하는데,&lt;br /&gt;lazy loading이 필요하다는 것은 애그리거트의 설계가 잘못되었을 가능성이 있다는 말이다.&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;마무리&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;특정 기술에 의존하지 않는 순수한 도메인 모델 구조를 만들어봤다.&lt;br /&gt;이 구조를 가지면 구현 기술이 변경되더라도 도메인이 받는 영향을 최소화할 수 있다.&lt;br /&gt;하지만 구현 기술이 변경될 일이 잦을까..? &lt;br /&gt;실제로 repository와 도메인 모델의 구현 기술은 거의 변경되지 않는다.&lt;br /&gt;변경이 거의 없는 상황에서 변경을 미리 대비하는 것은 과하다고 생각할 수 있다.&lt;br /&gt;물론 프로젝트의 요구사항이나 규모, 자원 등에 따라 다른 판단이 나올 수 있기 때문에 이 부분은 도메인 모델과 구현 기술을 완전히 분리하면서 얻는 이점과 개발 편의성과 실용성을 가져가면서 구조적인 유연함은 유지할 수 있도록 타협을 하는 것 중 선택해야 하는 문제라고 생각한다.&lt;br /&gt;&amp;nbsp;&lt;br /&gt;&amp;nbsp;&lt;br /&gt;&amp;nbsp;&lt;br /&gt;참고 자료 :&lt;br /&gt;오픈 톡방&lt;br /&gt;&lt;a href=&quot;https://wonit.tistory.com/636&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;&lt;span&gt;[DDD] Repository Pattern 이란, 이론편&lt;/span&gt;&lt;/a&gt;&lt;br /&gt;&lt;a href=&quot;https://wonit.tistory.com/637&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;&lt;span&gt;[DDD] Repository Pattern - 실전편 (Spring 에서 DIP를 통해 Repository의 선언과 구현 분리시키기)&lt;/span&gt;&lt;/a&gt;&lt;/p&gt;</description>
      <category> Architecture</category>
      <category>aggregate</category>
      <category>DDD</category>
      <category>domain</category>
      <category>JPA</category>
      <category>layered architecture</category>
      <category>repository</category>
      <category>service</category>
      <category>Spring data JPA</category>
      <category>구현 기술</category>
      <category>분리</category>
      <author>coco3o</author>
      <guid isPermaLink="true">https://dev-coco.tistory.com/190</guid>
      <comments>https://dev-coco.tistory.com/190#entry190comment</comments>
      <pubDate>Wed, 15 May 2024 19:45:20 +0900</pubDate>
    </item>
    <item>
      <title>[Java] 자바가 Call by Value 방식인 이유</title>
      <link>https://dev-coco.tistory.com/189</link>
      <description>&lt;h3 data-ke-size=&quot;size23&quot;&gt;Intro&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;프로그래밍을 하다 보면 함수 호출 방식인 'Call by Value'와 'Call by Reference' 키워드를 접하게 된다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 두 개념의 차이를 이해하고 특히 Java는 어떻게 'Call by Value' 방식을 사용하는지 알아보자&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;Call by Value / Call by Reference&lt;/h3&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;b&gt;Call by Value(값에 의한 호출)&lt;/b&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;값을 복사하여 처리한다.&lt;/li&gt;
&lt;li&gt;변수의 복사본이 전달되며, 원래 값이 수정되지 않는다.&lt;/li&gt;
&lt;li&gt;실제 인수는 다른 메모리 위치에 생성된다.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;b&gt;Call by Reference(참조에 의한 호출)&lt;/b&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;값의 주소를 참조하여 직접 값에 영향을 준다.&lt;/li&gt;
&lt;li&gt;변수 자체가 전달되며, 원래 값이 수정된다.&lt;/li&gt;
&lt;li&gt;실제 인수는 같은 메모리 위치에 생성된다.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;자바의 Call by Value 동작 방식&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;자바의 데이터 타입은 다음과 같이 크게 두 가지로 나누어진다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;b&gt;원시 타입(primitive type)&lt;/b&gt; - Numeric Type(byte, short, int, float, long, double, char), Boolean Type(boolean)&lt;/li&gt;
&lt;li&gt;&lt;b&gt;참조 타입(reference type)&lt;/b&gt; - Class Type, Interface Type, Array Type, Enum Type, 기타 참조 타입(String 등)&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그래서 메소드 파라미터로 원시 타입을 전달하는 것과 참조 타입을 전달하는 것에는 동작 방식에 차이가 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;원시 타입(primitive type) 전달 방식&lt;/h4&gt;
&lt;pre id=&quot;code_1713601708127&quot; class=&quot;bash&quot; data-ke-language=&quot;bash&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;public class CallByExampleTest {

    @Test
    void primitiveTest() {
        int v = 10;
        
        add(v);
        
        assertThat(v).isEqualTo(10);
    }

    void add(int num) {
        num++;
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;위 코드는 int 타입의 변수 v를 add 함수를 통해 1을 더해주고 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;하지만 결과 값은 여전히 10이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;primitiveTest()의 v 변수와 add()의 num은 아예 연관 없는 변수이기 때문이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;아래 그림으로 보면 한눈에 이해하기 쉬울 것이다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;711&quot; data-origin-height=&quot;416&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/b1TK5c/btsGOcrvGTi/AE3rEErQpmVe4IudtBVc4K/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/b1TK5c/btsGOcrvGTi/AE3rEErQpmVe4IudtBVc4K/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/b1TK5c/btsGOcrvGTi/AE3rEErQpmVe4IudtBVc4K/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fb1TK5c%2FbtsGOcrvGTi%2FAE3rEErQpmVe4IudtBVc4K%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;711&quot; height=&quot;416&quot; data-origin-width=&quot;711&quot; data-origin-height=&quot;416&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;자바에서 변수를 선언하면 기본적으로 Stack 메모리 영역에 할당된다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Stack 영역 내부에 primitiveTest()와 add()의 영역이 각각 나뉘어 있고, 서로 다른 변수가 존재한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그래서 num 값에 1을 더해도 v 변수는 아무런 영향이 없다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;따라서 &lt;b&gt;원시 타입의 전달은 값을 복사해서 전달하는 Call by Value 방식으로 동작한다는 것을 알 수 있다.&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;참조 타입(reference type) 전달 방식&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;참조 타입은 원시 타입과 조금 다르다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;변수는 Stack 영역에 생성되지만, 객체는 Heap 영역에 위치하며, Stack 영역에 있는 변수가 Heap 영역에 있는 객체를 바라보고 있는 형태다.&lt;/p&gt;
&lt;pre id=&quot;code_1713602485449&quot; class=&quot;bash&quot; data-ke-language=&quot;bash&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;public class CallByExampleTest {

    @Test
    void referenceTest() {
        int[] arr = { 10 }; // 주소 : 0x001
        
        add(arr);

        assertThat(arr[0]).isEqualTo(11);
    }

    void add(int[] arrArg) {
        arrArg[0]++; // 주소 : 0x001
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;변수의 참조(주소) 값을 복사해서 전달해 referenceTest() 영역과 add() 영역의 변수들은 모두 동일한 객체의 주소(0x001)를 바라보고 있다.&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;아래 단계별 그림을 보자.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;676&quot; data-origin-height=&quot;417&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/oJNbG/btsGMHTRWsd/7oEs3phLn4TkSLoHAVEkDK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/oJNbG/btsGMHTRWsd/7oEs3phLn4TkSLoHAVEkDK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/oJNbG/btsGMHTRWsd/7oEs3phLn4TkSLoHAVEkDK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FoJNbG%2FbtsGMHTRWsd%2F7oEs3phLn4TkSLoHAVEkDK%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;676&quot; height=&quot;417&quot; data-origin-width=&quot;676&quot; data-origin-height=&quot;417&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;example3.png&quot; data-origin-width=&quot;665&quot; data-origin-height=&quot;407&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/Hfvp8/btsGMMnh2oR/EMyhAy1oBHmoTiqclYikW0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/Hfvp8/btsGMMnh2oR/EMyhAy1oBHmoTiqclYikW0/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/Hfvp8/btsGMMnh2oR/EMyhAy1oBHmoTiqclYikW0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FHfvp8%2FbtsGMMnh2oR%2FEMyhAy1oBHmoTiqclYikW0%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;665&quot; height=&quot;407&quot; data-filename=&quot;example3.png&quot; data-origin-width=&quot;665&quot; data-origin-height=&quot;407&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;example4.png&quot; data-origin-width=&quot;669&quot; data-origin-height=&quot;415&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/ck0qv9/btsGNMT9AwC/wO8ocMHUuLXPmPKB7Xzkg1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/ck0qv9/btsGNMT9AwC/wO8ocMHUuLXPmPKB7Xzkg1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/ck0qv9/btsGNMT9AwC/wO8ocMHUuLXPmPKB7Xzkg1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fck0qv9%2FbtsGNMT9AwC%2FwO8ocMHUuLXPmPKB7Xzkg1%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;669&quot; height=&quot;415&quot; data-filename=&quot;example4.png&quot; data-origin-width=&quot;669&quot; data-origin-height=&quot;415&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;add()에서 생긴 변수가 같은 주소 값을 참조하고 있기 때문에 값이 변경되었다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style2&quot;&gt;결국 주소 값을 넘기고 원본 값이 변경되니까 Call by Reference 아닌가?  싶을 수 있지만,&lt;/blockquote&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;자바의 참조와 Call by Reference의 참조는 약간 다른 의미를 가지고 있다.&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;자바에서 참조의 의미는&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;span style=&quot;color: #ee2323;&quot;&gt;객체가 힙에 저장된 위치를 가리키는 메모리 주소&lt;/span&gt;이다.&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;실제 객체에 대한 참조가 아니라 객체에 접근하고 조작하는 방법이라고 볼 수 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span&gt;Call by Reference는&lt;/span&gt;&lt;span style=&quot;color: #ee2323;&quot;&gt; 참조 자체를 넘기기 때문에 새로운 객체를 할당하면 원본 변수도 영향을 받는다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span&gt;다음 예시 코드를 보자.&lt;/span&gt;&lt;/p&gt;
&lt;pre id=&quot;code_1713605313054&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;public class CallByExampleTest {

    @Test
    void referenceTest() {
        int[] arr = { 10 }; // 0x001
        
        newArr(arr);

        assertThat(arr).isEqualTo(&quot;0x001&quot;);
    }

    void newArr(int[] arrArg) {
        arrArg = new int[]{ 20 }; // 0x002
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;전달받은 값을 새로운 객체로 변경해도 원본 변수는 변하지 않는다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;따라서 &lt;b&gt;참조 타입 역시 Call by Value 방식으로 동작함을 보여준다.&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;newArr()가 종료되고 사용되지 않는 객체(0x002)는 Garbage Collector에 의해 수거될 것이다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;example5.PNG&quot; data-origin-width=&quot;667&quot; data-origin-height=&quot;415&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/cJJ7U8/btsGOeCQuRB/94DbTXWReyuv7HlIoeHYu0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/cJJ7U8/btsGOeCQuRB/94DbTXWReyuv7HlIoeHYu0/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/cJJ7U8/btsGOeCQuRB/94DbTXWReyuv7HlIoeHYu0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FcJJ7U8%2FbtsGOeCQuRB%2F94DbTXWReyuv7HlIoeHYu0%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;667&quot; height=&quot;415&quot; data-filename=&quot;example5.PNG&quot; data-origin-width=&quot;667&quot; data-origin-height=&quot;415&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #333333; text-align: start;&quot;&gt;&lt;span&gt;결국 자바는 항상 Call by Value 방식으로 데이터를 전달하고, 핵심은 &lt;/span&gt;&lt;/span&gt;&lt;span style=&quot;color: #ee2323;&quot;&gt;호출자 변수와 수신자 파라미터는 Stack 영역 내에서 각각 독립적으로 존재하는 다른 변수&lt;/span&gt;라는 것이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;참고 자료&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;a href=&quot;https://stackoverflow.com/questions/40480/is-java-pass-by-reference-or-pass-by-value&quot; target=&quot;_blank&quot; rel=&quot;noopener&amp;nbsp;noreferrer&quot;&gt;https://stackoverflow.com/questions/40480/is-java-pass-by-reference-or-pass-by-value&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://bcp0109.tistory.com/360&quot;&gt;https://bcp0109.tistory.com/360&lt;/a&gt;&amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://mangkyu.tistory.com/322&quot; target=&quot;_blank&quot; rel=&quot;noopener&amp;nbsp;noreferrer&quot;&gt;https://mangkyu.tistory.com/322&lt;/a&gt;&amp;nbsp;&lt;/li&gt;
&lt;/ul&gt;</description>
      <category> Programming/Java</category>
      <category>Call by reference</category>
      <category>call by value</category>
      <category>Heap</category>
      <category>Java</category>
      <category>JVM</category>
      <category>Stack</category>
      <category>메모리</category>
      <category>자바</category>
      <author>coco3o</author>
      <guid isPermaLink="true">https://dev-coco.tistory.com/189</guid>
      <comments>https://dev-coco.tistory.com/189#entry189comment</comments>
      <pubDate>Sat, 20 Apr 2024 19:42:33 +0900</pubDate>
    </item>
    <item>
      <title>VisualVM Remote 연결로 JVM 메모리 사용량 모니터링 하기</title>
      <link>https://dev-coco.tistory.com/188</link>
      <description>&lt;p data-ke-size=&quot;size16&quot;&gt;VisualVM은 JVM 기반 애플리케이션의 성능을 실시간으로 분석하기 위해 주로 사용하는 오픈소스 기반의 GUI 툴이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;운영 환경&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;VisualVM - 2.1.7&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;OpenJDK - 11.0.21&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Linux - CentOS7&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;WAS - Apache Tomcat 9.0.54&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;VisualVM 다운로드&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;JDK 1.7 이상의 경우 JAVA_HOME/bin 밑에 jvjsualvm.exe 파일이 포함 되어 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;별도로 필요할 경우 아래 링크를 통해 다운로드 받자.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;a href=&quot;https://visualvm.github.io/&quot;&gt;https://visualvm.github.io/&lt;/a&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;VisualVM 간단 설명&lt;/h4&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;862&quot; data-origin-height=&quot;599&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/cMRDzo/btsE6H88v20/oP8brCXnXSaPkdtO6D4jl1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/cMRDzo/btsE6H88v20/oP8brCXnXSaPkdtO6D4jl1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/cMRDzo/btsE6H88v20/oP8brCXnXSaPkdtO6D4jl1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FcMRDzo%2FbtsE6H88v20%2FoP8brCXnXSaPkdtO6D4jl1%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;732&quot; height=&quot;509&quot; data-origin-width=&quot;862&quot; data-origin-height=&quot;599&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;Local
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;현재 로컬 컴퓨터에 JVM으로 돌아가는 Application 목록 및 pid&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;Remote
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;로컬 컴퓨터가 아닌 다른 서버에 있는 Application 목록 및 pid&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;VM CoreDumps
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;코어덤프 파일을 열어둔 목록 및 pid&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;Snapshots
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;Application 데이터의 스냅샷 파일 목록&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;VisualVM 원격 연결 방법&lt;/h4&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;1. setenv.sh 파일을 생성하여 JMX Remote 설정을 추가한다.&lt;/p&gt;
&lt;pre id=&quot;code_1708420395153&quot; class=&quot;bash&quot; data-ke-language=&quot;bash&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;vi setenv.sh&lt;/code&gt;&lt;/pre&gt;
&lt;pre id=&quot;code_1708420402225&quot; class=&quot;bash&quot; data-ke-language=&quot;bash&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;# !/bin/bash

CATALINA_OPTS=&quot;-Dcom.sun.management.jmxremote=true \
-Dcom.sun.management.jmxremote.port=1099 \
-Dcom.sun.management.jmxremote.ssl=false \
-Dcom.sun.management.jmxremote.authenticate=false \
-Djava.rmi.server.hostname=127.0.0.1&quot;&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;span style=&quot;font-family: AppleSDGothicNeo-Regular, 'Malgun Gothic', '맑은 고딕', dotum, 돋움, sans-serif; color: #333333; text-align: start;&quot;&gt; -Dcom.sun.management.jmxremote=true \&lt;/span&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;span style=&quot;font-family: AppleSDGothicNeo-Regular, 'Malgun Gothic', '맑은 고딕', dotum, 돋움, sans-serif; color: #333333; text-align: start;&quot;&gt; &lt;span style=&quot;color: #333333; text-align: start;&quot;&gt;모니터링 활성화&lt;/span&gt; &lt;/span&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;font-family: AppleSDGothicNeo-Regular, 'Malgun Gothic', '맑은 고딕', dotum, 돋움, sans-serif; color: #333333; text-align: start;&quot;&gt; -Dcom.sun.management.jmxremote.port=[port] \ &lt;/span&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;span style=&quot;font-family: AppleSDGothicNeo-Regular, 'Malgun Gothic', '맑은 고딕', dotum, 돋움, sans-serif; color: #333333; text-align: start;&quot;&gt; &lt;span style=&quot;color: #333333; text-align: start;&quot;&gt;JMX 원격 접속을 위한 포트 설정&lt;/span&gt; &lt;/span&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;font-family: AppleSDGothicNeo-Regular, 'Malgun Gothic', '맑은 고딕', dotum, 돋움, sans-serif; color: #333333; text-align: start;&quot;&gt; &lt;span style=&quot;color: #333333; text-align: start;&quot;&gt;-Dcom.sun.management.jmxremote.ssl=false \&lt;/span&gt; &lt;/span&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;span style=&quot;font-family: AppleSDGothicNeo-Regular, 'Malgun Gothic', '맑은 고딕', dotum, 돋움, sans-serif; color: #333333; text-align: start;&quot;&gt;&lt;span style=&quot;color: #333333; text-align: start;&quot;&gt; &lt;span style=&quot;color: #333333; text-align: start;&quot;&gt;SSL 접속 설정&lt;/span&gt; &lt;/span&gt;&lt;/span&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;font-family: AppleSDGothicNeo-Regular, 'Malgun Gothic', '맑은 고딕', dotum, 돋움, sans-serif; color: #333333; text-align: start;&quot;&gt;&lt;span style=&quot;color: #333333; text-align: start;&quot;&gt; &lt;span style=&quot;color: #333333; text-align: start;&quot;&gt;-Dcom.sun.management.jmxremote.authenticate=false \&lt;/span&gt; &lt;/span&gt;&lt;/span&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;span style=&quot;font-family: AppleSDGothicNeo-Regular, 'Malgun Gothic', '맑은 고딕', dotum, 돋움, sans-serif; color: #333333; text-align: start;&quot;&gt;&lt;span style=&quot;color: #333333; text-align: start;&quot;&gt;&lt;span style=&quot;color: #333333; text-align: start;&quot;&gt; &lt;span style=&quot;color: #333333; text-align: start;&quot;&gt;접속 인증 설정 (활성화시 인증 관련 추가 설정 필요)&lt;/span&gt; &lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;font-family: AppleSDGothicNeo-Regular, 'Malgun Gothic', '맑은 고딕', dotum, 돋움, sans-serif; color: #333333; text-align: start;&quot;&gt;&lt;span style=&quot;color: #333333; text-align: start;&quot;&gt;&lt;span style=&quot;color: #333333; text-align: start;&quot;&gt; &lt;span style=&quot;color: #333333; text-align: start;&quot;&gt;-Djava.rmi.server.hostname=[ip]&lt;/span&gt; &lt;/span&gt;&lt;/span&gt;&lt;/span&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;span style=&quot;font-family: AppleSDGothicNeo-Regular, 'Malgun Gothic', '맑은 고딕', dotum, 돋움, sans-serif; color: #333333; text-align: start;&quot;&gt;&lt;span style=&quot;color: #333333; text-align: start;&quot;&gt;&lt;span style=&quot;color: #333333; text-align: start;&quot;&gt;&lt;span style=&quot;color: #333333; text-align: start;&quot;&gt; &lt;span style=&quot;color: #333333; text-align: start;&quot;&gt;원격지 ip&lt;/span&gt; &lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;2. 설정한 port 번호는 아래 명령어로 연결 허용 여부를 판단해야 한다.&lt;/p&gt;
&lt;pre id=&quot;code_1708494001396&quot; class=&quot;bash&quot; data-ke-language=&quot;bash&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;natstat -anl | grep [port]&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;만약 막혀 있다면 iptables 설정으로 port를 열어줘야 한다&lt;/p&gt;
&lt;pre id=&quot;code_1708494178015&quot; class=&quot;bash&quot; data-ke-language=&quot;bash&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;# 1099 port 허용
iptables -I INPUT -p tcp --dport 1099 -j ACCEPT
# 추가한 설정 조회
iptables -nL
# 정책 저장
service iptables save
# iptables 재시작
service iptables restart&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;3. Tomcat을 구동한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;4. VisualVM에서 Remote &amp;gt; Add Remote Host... 입력창이 뜨면 원격지의 IP를 입력&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;5. 등록한 Host &amp;gt; Add JMX Connection... 입력창이 뜨면 JVM 등록시 설정한 port 입력한 후 OK&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;6. 완료&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;tempsnip.png&quot; data-origin-width=&quot;1126&quot; data-origin-height=&quot;793&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/dlILYE/btsE7rksG1R/GokZeu9fTw1LLtxWXTNA9k/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/dlILYE/btsE7rksG1R/GokZeu9fTw1LLtxWXTNA9k/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/dlILYE/btsE7rksG1R/GokZeu9fTw1LLtxWXTNA9k/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FdlILYE%2FbtsE7rksG1R%2FGokZeu9fTw1LLtxWXTNA9k%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;1126&quot; height=&quot;793&quot; data-filename=&quot;tempsnip.png&quot; data-origin-width=&quot;1126&quot; data-origin-height=&quot;793&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;</description>
      <category> Server</category>
      <category>CPU</category>
      <category>Heap</category>
      <category>JVM</category>
      <category>threads</category>
      <category>tomcat</category>
      <category>visualvm</category>
      <category>WAS</category>
      <category>메모리</category>
      <category>모니터링</category>
      <author>coco3o</author>
      <guid isPermaLink="true">https://dev-coco.tistory.com/188</guid>
      <comments>https://dev-coco.tistory.com/188#entry188comment</comments>
      <pubDate>Wed, 21 Feb 2024 20:17:35 +0900</pubDate>
    </item>
    <item>
      <title>HATEOAS까지 사용해야 완벽한 RESTful이다.</title>
      <link>https://dev-coco.tistory.com/187</link>
      <description>&lt;p data-ke-size=&quot;size16&quot;&gt;나는 지금까지 REST API는 URI는 정보의 자원만 표현하고, 자원의 행위는 HTTP Method를 통해 명시한다는 것으로만 알고 사용하고 있었다.&lt;br /&gt;위 내용도 물론 맞지만, 그건 반쪽짜리 REST API 였고, HATEOAS까지 알고 사용해야 진정한 REST API를 사용한다 할 수 있다.&lt;br /&gt;그래서 이번엔 HATEOAS가 뭐고 어떤 특징이 있는지 알아보려 한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;HATEOAS (Hypermidia As The Engine Of Application State)&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;HATEOAS(일명 헤이티오스)는 API를 실제로 &quot;RESTful&quot;하게 만드는 REST Appilcation Architecture의 제약 조건이다.&lt;br /&gt;기본적으로 요청에 대해 서버는 응답에 데이터만 클라이언트에게 보내는데,&lt;br /&gt;HATEOAS를 사용하면 응답에 데이터뿐만 아니라 해당 데이터와 관련된 요청에 필요한 URI를 응답에 포함하여 반환하며, &lt;b&gt;이는 REST API를 사용하는 클라이언트가 전적으로 서버와 동적인 상호작용이 가능하도록 해준다.&lt;/b&gt;&lt;br /&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;REST API 구현 단계&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;REST API를 구현하기 위한 단계가 존재하는데, 마지막 단계가 Hypermedia Controls - HATEOAS라는 개념을 통해서 자원에 호출 가능한 API 정보를 자원의 상태를 반영하여 표현하는 것이다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;512&quot; data-origin-height=&quot;303&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/rVPag/btsB16YdFgE/ZcH9M3POv58e7FgCg3JrWK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/rVPag/btsB16YdFgE/ZcH9M3POv58e7FgCg3JrWK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/rVPag/btsB16YdFgE/ZcH9M3POv58e7FgCg3JrWK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FrVPag%2FbtsB16YdFgE%2FZcH9M3POv58e7FgCg3JrWK%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;512&quot; height=&quot;303&quot; data-origin-width=&quot;512&quot; data-origin-height=&quot;303&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;985&quot; data-origin-height=&quot;410&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/cxAVCz/btsB2tlCUwV/xbLCIjMWKiUnKLnEIkSTJ1/img.webp&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/cxAVCz/btsB2tlCUwV/xbLCIjMWKiUnKLnEIkSTJ1/img.webp&quot; data-alt=&quot;https://grapeup.com/blog/how-to-build-hypermedia-api-with-spring-hateoas/#&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/cxAVCz/btsB2tlCUwV/xbLCIjMWKiUnKLnEIkSTJ1/img.webp&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FcxAVCz%2FbtsB2tlCUwV%2FxbLCIjMWKiUnKLnEIkSTJ1%2Fimg.webp&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;985&quot; height=&quot;410&quot; data-origin-width=&quot;985&quot; data-origin-height=&quot;410&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;https://grapeup.com/blog/how-to-build-hypermedia-api-with-spring-hateoas/#&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;b&gt;레벨 0&lt;/b&gt;&lt;br /&gt;HTTP 프로토콜을 사용하여 API를 구현하지만 모든 기능을 활용하지는 않는다. 또한 리소스에 대한 고유 주소는 제공되지 않는다.&lt;br /&gt;method: POST URI: /movie&lt;/li&gt;
&lt;li&gt;&lt;b&gt;레벨 1&lt;/b&gt;&lt;br /&gt;리소스에 대한 고유 식별자가 있지만 리소스에 대한 각 작업에는 고유한 URL이 있다.&lt;br /&gt;method: POST URI: /movie/1/delete&lt;/li&gt;
&lt;li&gt;&lt;b&gt;레벨 2&lt;/b&gt;&lt;br /&gt;동작을 설명하는 동사 대신 HTTP 메소드를 사용한다.&lt;br /&gt;method: DELETE URI: /movie/1&lt;/li&gt;
&lt;li&gt;&lt;b&gt;레벨 3&lt;/b&gt;&lt;br /&gt;HATEOAS가 도입되었다. 간단히 말해서 리소스에 하이퍼미디어를 도입하며, 이를 통해 가능한 작업에 대해 알려주는 응답에 링크를 배치할 수 있으므로 API를 통해 탐색할 수 있는 가능성이 추가된다.&lt;br /&gt;method: DELETE URI: /movie/1&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;요즘 대부분의 프로젝트는 레벨 2를 사용하여 작성된다. 하지만 완벽한 RESTful API를 원한다면 HATEOAS를 고려해야 한다.&lt;br /&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;HATEOAS를 사용한 REST API 응답 형태&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;예를 들어, 게시글을 조회하는 URI가 있다고 가정해보자.&lt;/p&gt;
&lt;pre class=&quot;java&quot; data-ke-type=&quot;codeblock&quot; data-ke-language=&quot;java&quot;&gt;&lt;code&gt;GET https://my-test-server.com/articles&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;기존 REST API application/json 응답 형태&lt;/b&gt;&lt;/p&gt;
&lt;pre class=&quot;java&quot; data-ke-type=&quot;codeblock&quot; data-ke-language=&quot;java&quot;&gt;&lt;code&gt;{
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&quot;id&quot;: 100,
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&quot;subject&quot;: &quot;게시글 100&quot;,
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&quot;content&quot;: &quot;내용&quot;,
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&quot;self&quot;: &quot;https://my-test-server.com/api/v1/articles/100&quot;
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;여기서 해당 글을 조회한 사용자는 다음으로 어떤 행동을 할 수 있을까?&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;다음 게시글 조회&lt;/li&gt;
&lt;li&gt;좋아요&lt;/li&gt;
&lt;li&gt;댓글 달기&lt;/li&gt;
&lt;li&gt;홈 이동&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이런 행동들이 바로 상태 전이(State Transition)가 가능한 것들인데, 이것들을 응답 본문에 넣어줘야 한다는 소리다.&lt;/p&gt;
&lt;pre class=&quot;java&quot; data-ke-type=&quot;codeblock&quot; data-ke-language=&quot;java&quot;&gt;&lt;code&gt;{
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&quot;id&quot;: 100,
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&quot;subject&quot;: &quot;게시글 100&quot;,
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&quot;content&quot;: &quot;내용&quot;,
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&quot;self&quot;: &quot;https://my-test-server.com/api/articles/100&quot;,
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&quot;next&quot;: &quot;https://my-test-server.com/api/articles/101&quot;,
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&quot;like&quot;: &quot;https://my-test-server.com/api/articles/likes&quot;,
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&quot;comment&quot;:&quot;https://my-test-server.com/api/articles/100/comments&quot;,
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&quot;home&quot;: &quot;https://my-test-server.com/&quot;
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;위와 같이 Hypermedia(링크)를 통해서 넣어주면, &lt;span style=&quot;color: #333333;&quot;&gt;클라이언트는 현재의 리소스에서 가능한 액션을 하이퍼미디어 링크를 참조하여 알 수 있고, 이를 따라가며 상태 전이를 쉽게 진행할 수 있다.&lt;/span&gt;&lt;span style=&quot;color: #333333;&quot;&gt;&amp;nbsp;&lt;/span&gt;&lt;br /&gt;&amp;nbsp;&lt;br /&gt;하지만 아직 진정한 REST API라고는 할 수 없으며,&lt;br /&gt;&amp;nbsp;&lt;br /&gt;다음과 같이 링크와 관련된 리소스의 관계를 명확히 표현하는 표준적인 방법을 제공한다.&lt;br /&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&lt;span style=&quot;color: #ee2323;&quot;&gt;HAL&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Hypertext Application Language으로 JSON, XML 코드 내의 외부 리소스에 대한 링크를 추가하기 위한 특별한 데이터 타입이다.&lt;br /&gt;이 HAL 타입을 이용한다면 쉽게 HATEOAS를 달성할 수 있으며, 두 가지의 특징만 이용하면 된다.&lt;br /&gt;1. 리소스 : 일반적인 데이터 필드에 해당한다.&lt;br /&gt;2. 링크 : 하이퍼미디어로 보통 _self 필드가 링크 필드가 된다.&lt;br /&gt;&amp;nbsp;&lt;br /&gt;&lt;b&gt;application/hal+json HATEOAS 응답 형태&lt;/b&gt;&lt;/p&gt;
&lt;pre class=&quot;Java&quot; data-ke-type=&quot;codeblock&quot; data-ke-language=&quot;Java&quot;&gt;&lt;code&gt;{
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;~리소스 영역~
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&quot;id&quot;: 100,
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&quot;subject&quot;: &quot;게시글 100&quot;,
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&quot;content&quot;: &quot;내용&quot;
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;~리소스 영역~
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;~링크 영역~
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&quot;_links&quot; : {
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&quot;self&quot;: {
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&quot;href&quot;: &quot;https://my-test-server.com/api/articles/100&quot;
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;},
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&quot;next&quot;: {
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&quot;href&quot;: &quot;https://my-test-server.com/api/articles/101&quot;
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;},
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&quot;like&quot;: {
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&quot;href&quot;: &quot;https://my-test-server.com/api/articles/likes&quot;
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;},
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&quot;comment&quot;: {
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&quot;href&quot;: &quot;https://my-test-server.com/api/articles/100/comments&quot; 
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;},
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&quot;home&quot;: {
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&quot;href&quot;: &quot;https://my-test-server.com/&quot;
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;}
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;}
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;~링크 영역~
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;HATEOAS를 사용하여 얻게 되는 가장 큰 장점은 서버와 클라이언트를 분리한다는 것이다.&lt;br /&gt;클라이언트는 서버의 상태와 행동을 알 필요가 없으며, 서버 측의 변경에도 클라이언트에선 일일이 대응하지 않아도 된다.&lt;br /&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;마무리&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이로써 우리는 HATEOAS로 보다 RESTful 한 API를 작성하는 방법을 배웠다.&lt;br /&gt;하지만 모든 공공기관 또는 기업에서 사용하지 않는 것처럼 반드시 HATEOAS를 사용해야 하는 것은 아니다.&lt;br /&gt;적재적소에 맞는 REST API를 설계하여 사용하는 게 더욱 중요하며, HATEOAS는 필요에 따라 적절히 사용하도록 하자.&lt;br /&gt;&amp;nbsp;&lt;br /&gt;참고&lt;br /&gt;&lt;a href=&quot;https://wonit.tistory.com/454&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;&lt;span&gt;https://wonit.tistory.com/454&lt;/span&gt;&lt;/a&gt;&lt;br /&gt;&lt;a href=&quot;https://grapeup.com/blog/how-to-build-hypermedia-api-with-spring-hateoas/#&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;&lt;span&gt;https://grapeup.com/blog/how-to-build-hypermedia-api-with-spring-hateoas/#&lt;/span&gt;&lt;/a&gt;&lt;/p&gt;</description>
      <category> Server</category>
      <category>HATEOAS</category>
      <category>REST</category>
      <category>REST API</category>
      <category>RESTful</category>
      <category>헤이티오스</category>
      <author>coco3o</author>
      <guid isPermaLink="true">https://dev-coco.tistory.com/187</guid>
      <comments>https://dev-coco.tistory.com/187#entry187comment</comments>
      <pubDate>Sat, 16 Dec 2023 17:15:23 +0900</pubDate>
    </item>
    <item>
      <title>[Spring] ThreadPoolTaskExecutor 설정으로 비동기 처리 효과적으로 하기</title>
      <link>https://dev-coco.tistory.com/186</link>
      <description>&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;a href=&quot;https://dev-coco.tistory.com/185&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;이전 글&lt;/a&gt;에 이어서 &lt;span style=&quot;letter-spacing: 0px;&quot;&gt;Spring Boot에서 ThreadPoolTaskExecutor 설정을 통해 비동기 처리를 효율적으로 수행하는 방법을 알아보려 한다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;ThreadPoolTaskExecutor&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;ThreadPoolTaskExecutor는 Spring에서 제공해주는 클래스로 자바에서 제공하는 ThreadPoolExecutor를 사용하기 쉽게 만들어 사용하도록 구현 되어 있어 스레드 풀을 쉽고 간단하게 설정하고 관리할 수 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;java.util.concurrent Executor를 최상위 인터페이스로 가진다.&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;Bean 등록&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;우선 ThreadPoolTaskExecutor를 사용하기 위해 Bean으로 등록한다.&lt;/p&gt;
&lt;pre id=&quot;code_1701415547711&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;@Configuration
public class ThreadPoolConfig {

    @Bean(name = &quot;taskExecutor&quot;)
    public Executor taskExecutor() {
        ThreadPoolTaskExecutor taskExecutor = new ThreadPoolTaskExecutor();
        taskExecutor.initialize();

        return taskExecutor;
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;ThreadPoolTaskExecutor를 생성하고 사용할 수 있도록 initialize()를 호출했다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size14&quot;&gt;※ 명시적으로 적지 않아도 빈으로 등록될 때 initialize()한다.&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;Thread 설정&lt;/h4&gt;
&lt;pre id=&quot;code_1701498608267&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;private static final int CORE_POOL_SIZE = 10;
private static final int MAX_POOL_SIZE = 30;

@Bean(name = &quot;taskExecutor&quot;)
public Executor taskExecutor() {
    ThreadPoolTaskExecutor taskExecutor = new ThreadPoolTaskExecutor();
    taskExecutor.setCorePoolSize(CORE_POOL_SIZE);
    taskExecutor.setMaxPoolSize(MAX_POOL_SIZE);
    taskExecutor.initialize();

    return taskExecutor;
}&lt;/code&gt;&lt;/pre&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;b&gt;setCorePoolSize&lt;/b&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;동시에 실행 할 기본 스레드의 수를 설정할 수 있다.&lt;/li&gt;
&lt;li&gt;기본 값은 1이다.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;b&gt;setMaxPoolSize&lt;/b&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;thread-pool의 사용할 수 있는 최대 스레드 수를 설정할 수 있다.&lt;/li&gt;
&lt;li&gt;기본 값은 Integer.MAX_VALUE 이다.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;최초 설정한 스레드의 수만큼 작업하다 더 이상 처리할 수 없을 경우 max size 만큼 스레드가 증가하겠지? 라고 예상할 수 있지만, &lt;span style=&quot;color: #ee2323;&quot;&gt;실제로는 그렇지 않다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;설정한 core size보다 많은 작업 요청이 들어오면 ThreadPoolTaskExecutor는 내부적으로 Integer.MAX_VALUE 만큼 LinkedBlockingQueue를 생성하고 task는 queue에서 대기하게 된다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그리고 queue가 꽉 차게 되면 그 때 max size 만큼 스레드를 생성하여 task를 처리한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;QueueCapacity&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;core size보다 많은 요청이 들어오면 Integer.MAX_VALUE&amp;nbsp;만큼 queue를 생성한다 했는데 이는 기본 설정 값이며,&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;작업에 맞게 queueCapacity 사이즈를 변경하여 사용할 수 있다.&lt;/p&gt;
&lt;pre id=&quot;code_1701498935456&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;private static final int CORE_POOL_SIZE = 10;
private static final int MAX_POOL_SIZE = 30;
private static final int QUEUE_CAPACITY = 100;

@Bean(name = &quot;taskExecutor&quot;)
public Executor taskExecutor() {
    ThreadPoolTaskExecutor taskExecutor = new ThreadPoolTaskExecutor();
    taskExecutor.setCorePoolSize(CORE_POOL_SIZE);
    taskExecutor.setMaxPoolSize(MAX_POOL_SIZE);
    taskExecutor.setQueueCapacity(QUEUE_CAPACITY);
    taskExecutor.initialize();

    return taskExecutor;
}&lt;/code&gt;&lt;/pre&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;b&gt;setQueueCapacity&lt;/b&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;thread-pool executor의 작업 큐의 크기를 설정할 수 있다.&lt;/li&gt;
&lt;li&gt;기본 값은 Integer.MAX_VALUE 이다.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;위와 같이 설정하면 최초 10개의 스레드에서 처리하다가 처리 속도가 밀릴 경우 100개 사이즈의 큐에서 대기하고 그보다 많은 요청이 발생하면 최대 30개의 스레드를 생성해서 처리한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;RejectedExecutionHandler&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;max size만큼 스레드를 생성하고, 설정한 queue가 가득 찬 상태에서 추가 작업이 들어올 경우 &lt;span style=&quot;color: #ee2323;&quot;&gt;RejectedExecutionException 예외&lt;/span&gt;가 발생하게 된다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;더 이상 처리할 수 없다는 오류인데, &lt;span style=&quot;letter-spacing: 0px;&quot;&gt;기본적으로 RejectedExecutionHandler 인터페이스를 구현한 몇 가지 클래스가 제공되며 &lt;/span&gt;&lt;span style=&quot;letter-spacing: 0px;&quot;&gt; 이러한 예외가 발생하지 않도록 우리는 다음과 같이 설정하여 해결할 수 있다.&lt;/span&gt;&lt;/p&gt;
&lt;pre id=&quot;code_1701502357941&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;@Bean(name = &quot;taskExecutor&quot;)
public Executor taskExecutor() {
    ThreadPoolTaskExecutor taskExecutor = new ThreadPoolTaskExecutor();
    taskExecutor.setCorePoolSize(CORE_POOL_SIZE);
    taskExecutor.setMaxPoolSize(MAX_POOL_SIZE);
    taskExecutor.setQueueCapacity(QUEUE_CAPACITY);
    taskExecutor.setRejectedExecutionHandler(new ThreadPoolExecutor.CallerRunsPolicy());
    taskExecutor.initialize();

    return taskExecutor;
}&lt;/code&gt;&lt;/pre&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;b&gt;ThreadPoolExecutor.AbortPolicy&lt;/b&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;기본 설정 값이다.&lt;/li&gt;
&lt;li&gt;reject 발생시 RejectedExecutionException을 발생시킨다.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;b&gt;ThreadPoolExecutor.CallerRunsPolicy&lt;/b&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;shutdown 상태가 아닐 때, ThreadPoolTaskExecutor에 요청한 스레드에서 직접 처리한다.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;b&gt;ThreadPoolExecutor.DiscardPolicy&lt;/b&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;reject된 작업을 무시한다.&lt;/li&gt;
&lt;li&gt;모든 작업이 처리 될 필요가 없을 경우 사용한다.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;b&gt;ThreadPoolExecutor.DiscardOldsetPolicy&lt;/b&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;오래된 작업을 제거하고 reject된 작업을 실행시킨다.&lt;/li&gt;
&lt;li&gt;역시 모든 작업이 처리 될 필요가 없을 경우 사용한다.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;예외와 작업의 누락 없이 모두 처리하려면 &lt;b&gt;CallerRunsPolicy&lt;/b&gt;로 설정하여 사용하자.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;Shutdown&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;위와 같이 별도로 정의한 스레드 풀에서 작업이 이루어지고 있을 때 어플리케이션 종료를 요청하게되면 어플리케이션이 바로 종료된다. 이렇게 되면 아직 처리되지 못한 작업들은 유실되기 때문에 다음과 같이 설정하면&amp;nbsp;작업 유실을 방지할 수 있다.&lt;/p&gt;
&lt;pre id=&quot;code_1701504666478&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;private static final int CORE_POOL_SIZE = 30;
private static final int MAX_POOL_SIZE = 50;
private static final int QUEUE_CAPACITY = 100;
private static final boolean WAIT_TASK_COMPLETE = true;

@Bean(name = &quot;taskExecutor&quot;)
public Executor taskExecutor() {
    ThreadPoolTaskExecutor taskExecutor = new ThreadPoolTaskExecutor();
    taskExecutor.setCorePoolSize(CORE_POOL_SIZE);
    taskExecutor.setMaxPoolSize(MAX_POOL_SIZE);
    taskExecutor.setQueueCapacity(QUEUE_CAPACITY);
    taskExecutor.setRejectedExecutionHandler(new ThreadPoolExecutor.CallerRunsPolicy());
    taskExecutor.setWaitForTasksToCompleteOnShutdown(WAIT_TASK_COMPLETE);
    taskExecutor.initialize();

    return taskExecutor;
}&lt;/code&gt;&lt;/pre&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;b&gt;setWaitForTasksToCompleteOnShutdown&lt;/b&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;span style=&quot;color: #333333; text-align: left;&quot;&gt;&lt;span&gt;&amp;nbsp;&lt;/span&gt;true 설정시 어플리케이션 종료 요청시 queue에 남아 있는 모든 작업들이 완료될 때까지 기다린 후 종료된다.&lt;/span&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;Timeout&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;만약 모든 작업이 처리되길 기다리기 힘든 경우라면 &lt;b&gt;setAwaitTerminationSeconds&lt;/b&gt; 설정을 통해 최대 종료 대기 시간을 설정할 수 있다.&lt;/p&gt;
&lt;pre id=&quot;code_1701505050680&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;private static final int CORE_POOL_SIZE = 30;
private static final int MAX_POOL_SIZE = 50;
private static final int QUEUE_CAPACITY = 100;
private static final boolean WAIT_TASK_COMPLETE = true;
private static final int AWAIT_TERMINATION_SECONDS = 30;

@Bean(name = &quot;taskExecutor&quot;)
public Executor taskExecutor() {
    ThreadPoolTaskExecutor taskExecutor = new ThreadPoolTaskExecutor();
    taskExecutor.setCorePoolSize(CORE_POOL_SIZE);
    taskExecutor.setMaxPoolSize(MAX_POOL_SIZE);
    taskExecutor.setQueueCapacity(QUEUE_CAPACITY);
    taskExecutor.setRejectedExecutionHandler(new ThreadPoolExecutor.CallerRunsPolicy());
    taskExecutor.setWaitForTasksToCompleteOnShutdown(WAIT_TASK_COMPLETE);
    taskExecutor.setAwaitTerminationSeconds(AWAIT_TERMINATION_SECONDS);
    taskExecutor.initialize();

    return taskExecutor;
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;ThreadName&lt;/h4&gt;
&lt;pre id=&quot;code_1701505199084&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;private static final int CORE_POOL_SIZE = 30;
private static final int MAX_POOL_SIZE = 50;
private static final int QUEUE_CAPACITY = 100;
private static final String THREAD_NAME_PREFIX = &quot;executor-&quot;;
private static final boolean WAIT_TASK_COMPLETE = true;
private static final int AWAIT_TERMINATION_SECONDS = 30;

@Bean(name = &quot;taskExecutor&quot;)
public Executor taskExecutor() {
    ThreadPoolTaskExecutor taskExecutor = new ThreadPoolTaskExecutor();
    taskExecutor.setCorePoolSize(CORE_POOL_SIZE);
    taskExecutor.setMaxPoolSize(MAX_POOL_SIZE);
    taskExecutor.setQueueCapacity(QUEUE_CAPACITY);
    taskExecutor.setRejectedExecutionHandler(new ThreadPoolExecutor.CallerRunsPolicy());
    taskExecutor.setWaitForTasksToCompleteOnShutdown(WAIT_TASK_COMPLETE);
    taskExecutor.setAwaitTerminationSeconds(AWAIT_TERMINATION_SECONDS);
    taskExecutor.setThreadNamePrefix(THREAD_NAME_PREFIX);
    taskExecutor.initialize();

    return taskExecutor;
}&lt;/code&gt;&lt;/pre&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;b&gt;setThreadNamePrefix&lt;/b&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;스레드의 이름을 설정한다.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;ThreadPoolTaskExecutor 동작 원리&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;여기까지 내용을 복기하며 ThreadPoolTaskExecutor의 동작 원리를 생각 해보자.&lt;/p&gt;
&lt;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;현재 점유하고 있는 스레드의 수가 corePoolSize만큼 있고, 추가 요청이 오면 LinkedBlockingQueue를 생성하고, 요청을 큐에 넣는다.&lt;/li&gt;
&lt;li&gt;queue에 담긴 요청이 queueCapacity의 수만큼 있을 때 요청이 오면 maxPoolSize 만큼 스레드를 생성하여 처리한다.&lt;/li&gt;
&lt;li&gt;현재 점유하고 있는 스레드의 수가 maxPoolSize 만큼 있고, 큐에 담긴 요청이 queueCapacity의 수 만큼 있을 때 추가 요청이 오면 RejectedExecution 전략에 따라 처리된다.&lt;/li&gt;
&lt;/ol&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;ThreadPoolTaskExecutor 사용법&lt;/h4&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;1. Executor의 execute()&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;다음과 같이 Executor를 주입한 뒤 처리하고자 하는 작업을 Runnable 인터페이스의 run()메소드에 정의하고,&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Executor.execute()의 인자로 넘겨주면 된다.&lt;/p&gt;
&lt;pre id=&quot;code_1701760552966&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;@Slf4j
@Service
public class TestService {

    private final Executor executor;

    public TestService(@Qualifier(&quot;taskExecutor&quot;) Executor executor) {
        this.executor = executor;
    }

    public void executeThreads() {
        Runnable runnable = () -&amp;gt; {
            try {
                log.info(&quot;executing Thread Name .. [{}]&quot;, Thread.currentThread().getName());
                Thread.sleep(1000); // 1초간 정지
            } catch (InterruptedException e) {
                throw new RuntimeException(e);
            }
        };

        for (int i = 0; i &amp;lt; 10; i++) {
            executor.execute(runnable);
        }
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;pre id=&quot;code_1701760579381&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;19:26:42.247 [INFO ] [executor-9] com.coco.demo.TestService - executing Thread Name .. [executor-9]
19:26:42.248 [INFO ] [executor-8] com.coco.demo.TestService - executing Thread Name .. [executor-8]
19:26:42.250 [INFO ] [executor-10] com.coco.demo.TestService - executing Thread Name .. [executor-10]
19:26:42.250 [INFO ] [executor-7] com.coco.demo.TestService - executing Thread Name .. [executor-7]
19:26:42.251 [INFO ] [executor-6] com.coco.demo.TestService - executing Thread Name .. [executor-6]
19:26:42.251 [INFO ] [executor-5] com.coco.demo.TestService - executing Thread Name .. [executor-5]
19:26:42.251 [INFO ] [executor-4] com.coco.demo.TestService - executing Thread Name .. [executor-4]
19:26:42.251 [INFO ] [executor-3] com.coco.demo.TestService - executing Thread Name .. [executor-3]
19:26:42.251 [INFO ] [executor-2] com.coco.demo.TestService - executing Thread Name .. [executor-2]
19:26:42.252 [INFO ] [executor-1] com.coco.demo.TestService - executing Thread Name .. [executor-1]&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;2. CompletableFuture&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;CompletableFuture의&amp;nbsp;xxxAsync가&amp;nbsp;붙은&amp;nbsp;메소드는&amp;nbsp;기본적으로&amp;nbsp;ForkJoinPool의&amp;nbsp;commonPool을&amp;nbsp;사용하며, &lt;br /&gt;두&amp;nbsp;번째&amp;nbsp;인수를&amp;nbsp;받는&amp;nbsp;오버로드&amp;nbsp;메소드에서&amp;nbsp;커스텀한&amp;nbsp;Thread&amp;nbsp;Executor를&amp;nbsp;사용할&amp;nbsp;수&amp;nbsp;있다.&lt;/p&gt;
&lt;pre id=&quot;code_1701762492102&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;public void executeThreads() {
    CompletableFuture&amp;lt;Void&amp;gt; cf = null;

    for(int i = 0; i &amp;lt; 10; i++){
        cf = CompletableFuture.runAsync(() -&amp;gt; {
        try {
            log.info(&quot;executing Thread Name .. [{}]&quot;, Thread.currentThread().getName());
            Thread.sleep(1000); // 1초간 정지
        } catch (InterruptedException e) {
            throw new RuntimeException(e);
        }
        }, executor);
    }

    cf.join();
}&lt;/code&gt;&lt;/pre&gt;
&lt;pre id=&quot;code_1701855603864&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;19:37:16.229 [INFO ] [executor-2] com.coco.demo.TestService - executing Thread Name .. [executor-2]
19:37:16.229 [INFO ] [executor-1] com.coco.demo.TestService - executing Thread Name .. [executor-1]
19:37:16.229 [INFO ] [executor-5] com.coco.demo.TestService - executing Thread Name .. [executor-5]
19:37:16.229 [INFO ] [executor-6] com.coco.demo.TestService - executing Thread Name .. [executor-6]
19:37:16.229 [INFO ] [executor-4] com.coco.demo.TestService - executing Thread Name .. [executor-4]
19:37:16.230 [INFO ] [executor-8] com.coco.demo.TestService - executing Thread Name .. [executor-8]
19:37:16.230 [INFO ] [executor-7] com.coco.demo.TestService - executing Thread Name .. [executor-7]
19:37:16.230 [INFO ] [executor-10] com.coco.demo.TestService - executing Thread Name .. [executor-10]
19:37:16.230 [INFO ] [executor-9] com.coco.demo.TestService - executing Thread Name .. [executor-9]&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;참고&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;a href=&quot;https://docs.spring.io/spring-framework/docs/current/javadoc-api/org/springframework/scheduling/concurrent/ThreadPoolTaskExecutor.html&quot; target=&quot;_blank&quot; rel=&quot;noopener&amp;nbsp;noreferrer&quot;&gt;https://docs.spring.io/spring-framework/docs/current/javadoc-api/org/springframework/scheduling/concurrent/ThreadPoolTaskExecutor.html&lt;/a&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;a href=&quot;https://kapentaz.github.io/spring/Spring-ThreadPoolTaskExecutor-%EC%84%A4%EC%A0%95/#&quot; target=&quot;_blank&quot; rel=&quot;noopener&amp;nbsp;noreferrer&quot;&gt;https://kapentaz.github.io/spring/Spring-ThreadPoolTaskExecutor-%EC%84%A4%EC%A0%95/#&lt;/a&gt;&lt;/p&gt;</description>
      <category> Programming/Java</category>
      <category>Asynchronous</category>
      <category>CompletableFuture</category>
      <category>Executor</category>
      <category>Java</category>
      <category>Spring</category>
      <category>ThreadPoolTaskExecutor</category>
      <category>비동기</category>
      <author>coco3o</author>
      <guid isPermaLink="true">https://dev-coco.tistory.com/186</guid>
      <comments>https://dev-coco.tistory.com/186#entry186comment</comments>
      <pubDate>Tue, 5 Dec 2023 19:46:14 +0900</pubDate>
    </item>
    <item>
      <title>[Java] CompletableFuture 사용법</title>
      <link>https://dev-coco.tistory.com/185</link>
      <description>&lt;h3 data-ke-size=&quot;size23&quot;&gt;Future&lt;/h3&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;Java5부터 사용되던 Future 인터페이스는 java.util.concurrency 패키지에서 비동기 작업의 결과 값을 받는 용도로 사용했다. 하지만&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;span style=&quot;color: #ee2323;&quot;&gt;여러 Future의 결괏값을 조합하거나, 예외를 효과적으로 핸들링할 수가 없었다.&lt;/span&gt;&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;그리고 Future는 오직 get 호출로만 작업 완료가 가능한데,&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;span style=&quot;color: #ee2323;&quot;&gt;get은 작업이 완료될 때까지 대기하는 블로킹호출&lt;/span&gt;이므로 비동기 작업 응답에 추가 작업을 하기 적합하지 않다.&lt;/p&gt;
&lt;pre id=&quot;code_1701065063535&quot; class=&quot;routeros&quot; style=&quot;background-color: #f8f8f8; color: #383a42; text-align: start;&quot; data-ke-type=&quot;codeblock&quot; data-ke-language=&quot;java&quot;&gt;&lt;code&gt;public interface Future&amp;lt;V&amp;gt; {
    ...
    V get() throws InterruptedException, ExecutionException;

    V get(long timeout, TimeUnit unit) throws InterruptedException, ExecutionException, TimeoutException;
}&lt;/code&gt;&lt;/pre&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;get() - 작업이 완료될 때까지 대기&lt;/li&gt;
&lt;li&gt;get(long timeout, TimeUnit unit) - 작업이 완료될 때까지 설정한 시간 동안 대기하며, 시간 내 작업 미완료 시 TimeoutException 발생&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;CompletableFuture&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Java8에서는 이러한 문제들을 모두 해결한 CompletableFuture가 소개되었다.&lt;/p&gt;
&lt;pre id=&quot;code_1699850516683&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;public class CompletableFuture&amp;lt;T&amp;gt; implements Future&amp;lt;T&amp;gt;, CompletionStage&amp;lt;T&amp;gt; {
    ...
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;CompletableFuture 클래스는 Future 인터페이스를 구현함과 동시에 CompletionStage 인터페이스를 구현한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;CompletionStage의 특징을 살펴보면 &lt;span style=&quot;color: #333333; text-align: start;&quot;&gt;CompletableFuture의 장점을 알 수 있다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #ee2323;&quot;&gt; CompletionStage는 결국은 계산이 완료될 것이라는 의미의 약속이다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;계산의 완료는 단일 단계의 완료뿐만 아니라 다른 여러 단계 혹은 다른 여러 단계 중의 하나로 이어질 수 있음도 포함한다.&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;또한,&amp;nbsp;각&amp;nbsp;단계에서&amp;nbsp;발생한&amp;nbsp;에러를&amp;nbsp;관리하고&amp;nbsp;전달할&amp;nbsp;수&amp;nbsp;있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size14&quot;&gt;※ 비동기 연산 Step을 제공해서 체이닝 형태로 조합이 가능하며, 완료 후 콜백이 가능하다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;기본적인 사용 방법&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;- runAsync&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;반환 값이 없는 경우 비동기 작업 실행&lt;/p&gt;
&lt;pre id=&quot;code_1699949433567&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;CompletableFuture&amp;lt;Void&amp;gt; cf = CompletableFuture.runAsync(() -&amp;gt; System.out.println(&quot;Hello World!&quot;));
cf.join(); // Hello World!&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;runAsync()는 Runnable 타입을 파라미터로 전달하기 때문에 어떤 결과 값을 담지 않는다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;- supplyAsync&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;반환 값이 있는 경우 비동기 작업 실행&lt;/p&gt;
&lt;pre id=&quot;code_1699949952868&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;CompletableFuture&amp;lt;String&amp;gt; cf = CompletableFuture.supplyAsync(() -&amp;gt;  &quot;Hello World!&quot;);
System.out.println(cf.join()); // Hello World!&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;반면, supplyAsync()는 supplier 타입을 넘기기 때문에 반환 값이 존재한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;letter-spacing: 0px;&quot;&gt;&lt;span&gt;&lt;span&gt;또한, 결과를 &lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style=&quot;letter-spacing: 0px;&quot;&gt;get() 또는 join() 메소드로 가져올 수 있는데&lt;/span&gt;&lt;span style=&quot;letter-spacing: 0px;&quot;&gt;&amp;nbsp;각&amp;nbsp;&lt;/span&gt;차이점은 다음과 같다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;get() - Future 인터페이스에 정의된 메소드로 &lt;span style=&quot;color: #ee2323;&quot;&gt;checked exception&lt;/span&gt;인 &lt;span style=&quot;color: #409d00; text-align: left;&quot;&gt;InterruptedException&lt;/span&gt;&lt;span style=&quot;text-align: left;&quot;&gt;과 &lt;/span&gt;&lt;span style=&quot;color: #409d00; text-align: left;&quot;&gt;&lt;span style=&quot;color: #409d00;&quot;&gt;ExecutionException&lt;/span&gt;&lt;/span&gt;&lt;span style=&quot;text-align: left;&quot;&gt;을 던지므로 예외 처리 로직이 반드시 필요하다.&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;color: #333333; text-align: start;&quot;&gt;join() - CompletableFuture에 정의되어 있으며, checked&amp;nbsp; exception을 발생시키지 않는 대신&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;/span&gt;&lt;span style=&quot;color: #ee2323;&quot;&gt;unchecked&lt;/span&gt;&lt;span style=&quot;color: #333333; text-align: start;&quot;&gt;&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;/span&gt;&lt;span style=&quot;color: #409d00;&quot;&gt;CompletionException&lt;/span&gt;이 발생된다.&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;일반적으로 join()을 사용하는 것이 권장되지만, 예외 처리에 대한 추가 로직이 필요할 때 혹은 timeout 설정을 해야 하는 경우 get()을 사용하자.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;runAsync와 supplyAsync는 기본적으로 ForkJoinPool의 commonPool()을 사용해 작업을 실행한 스레드를 스레드 풀로부터 얻어 실행시킨다. 만약 원하는 스레드 풀을 사용하려면, ExecutorService를 파라미터로 넘겨주면 된다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;작업 콜백&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;비동기 실행이 끝난 후에 다음과 같이 체이닝 형태로 작성하여 전달 받은 작업 콜백을 실행시켜 준다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;- thenApply&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #333333; text-align: start;&quot;&gt;함수형 인터페이스 Function 타입을 파라미터로 받으며,&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;/span&gt;반환 값을 받아서 다른 값을 반환해주는 콜백이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;thenApply는 앞선 계산의 결과를 콜백 함수로 전달된 Function을 실행한다.&lt;/p&gt;
&lt;pre id=&quot;code_1700531314891&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;CompletableFuture&amp;lt;String&amp;gt; cf = CompletableFuture.supplyAsync(() -&amp;gt; {
    return &quot;hello world!&quot;;
}).thenApply(s -&amp;gt; {
    return s.toUpperCase();
});
System.out.println(cf.join()); // HELLO WORLD!&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;- thenAccept&lt;/b&gt;&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;함수형 인터페이스 Consumer를 파라미터로 받으며, 반환 값을 받아 처리하고 값을 반환하지 않는 콜백이다.&lt;/p&gt;
&lt;pre id=&quot;code_1700558314080&quot; class=&quot;java&quot; style=&quot;background-color: #f8f8f8; color: #383a42; text-align: start;&quot; data-ke-type=&quot;codeblock&quot; data-ke-language=&quot;java&quot;&gt;&lt;code&gt;CompletableFuture&amp;lt;Void&amp;gt; cf = CompletableFuture.supplyAsync(() -&amp;gt; {
    return &quot;hello world!&quot;;
}).thenAccept(System.out::println);

cf.join(); // hello world!&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;- thenRun&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;함수형 인터페이스 Runnable을 파라미터로 받으며, 반환 값을 받지 않고 그냥 다른 작업을 처리하고 값을 반환하지 않는&amp;nbsp; 콜백이다.&lt;/p&gt;
&lt;pre id=&quot;code_1700536099706&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;CompletableFuture&amp;lt;Void&amp;gt; cf = CompletableFuture.supplyAsync(() -&amp;gt; {
    return &quot;hello world!&quot;;
}).thenRun(() -&amp;gt; System.out.println(&quot;hello coco world!&quot;));

cf.join(); // hello coco world!;&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;비동기 작업 콜백&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;- thenApplyAsync&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #0f0f0f; text-align: left;&quot;&gt;앞선 계산의 결과를 콜백 함수로 전달된 &lt;/span&gt;Function&lt;span style=&quot;color: #0f0f0f; text-align: left;&quot;&gt;을 별도의 스레드에서 비동기적으로 실행한다.&lt;/span&gt;&lt;/p&gt;
&lt;pre id=&quot;code_1701053856647&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;CompletableFuture&amp;lt;String&amp;gt; cf = CompletableFuture.supplyAsync(() -&amp;gt; {
    return &quot;hello world!&quot;;
}).thenApplyAsync(s -&amp;gt; {
    return s.toUpperCase();
});
System.out.println(cf.join()); // HELLO WORLD!&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;- thenAcceptAsync&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #0f0f0f; text-align: left;&quot;&gt;앞선 계산의 결과를 콜백 함수로 전달된 &lt;/span&gt;Consumer&lt;span style=&quot;color: #0f0f0f; text-align: left;&quot;&gt;를 별도의 스레드에서 비동기적으로 실행한다.&lt;/span&gt;&lt;/p&gt;
&lt;pre id=&quot;code_1701053866558&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;CompletableFuture&amp;lt;Void&amp;gt; cf = CompletableFuture.supplyAsync(() -&amp;gt; {
    return &quot;hello world!&quot;;
}).thenAcceptAsync(System.out::println);

cf.join(); // hello world!&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;- thenRunAsync&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #0f0f0f; text-align: left;&quot;&gt;앞선 계산의 결과와 상관없이 주어진 작업을 별도의 스레드에서 비동기적으로 실행한다.&lt;/span&gt;&lt;/p&gt;
&lt;pre id=&quot;code_1701053873742&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;CompletableFuture&amp;lt;Void&amp;gt; cf = CompletableFuture.supplyAsync(() -&amp;gt; {
    return &quot;hello world!&quot;;
}).thenRunAsync(() -&amp;gt; System.out.println(&quot;hello coco world!&quot;));

cf.join(); // hello coco world!&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size14&quot;&gt;&lt;span style=&quot;color: #0f0f0f; text-align: left;&quot;&gt;※ xxxAsync가 붙은 메소드는 기본적으로 ForkJoinPool의 commonPool을 사용하며,&amp;nbsp; &lt;br /&gt;두&amp;nbsp;번째&amp;nbsp;인수를&amp;nbsp;받는&amp;nbsp;오버로드&amp;nbsp;메소드에서&amp;nbsp;다른&amp;nbsp;Thread&amp;nbsp;Executor를&amp;nbsp;선택적으로&amp;nbsp;사용할&amp;nbsp;수도&amp;nbsp;있다.&lt;/span&gt;&lt;span style=&quot;color: #0f0f0f; text-align: left;&quot;&gt;&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #0f0f0f; text-align: left;&quot;&gt;비동기 작업 콜백 메소드들은 주로 병렬로 수행되는 작업이나 I/O 작업과 같이 시간이 오래 걸리는 작업을 할 때 유용하게 활용된다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;다른 비동기 작업과 조합하기&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;- thenCompose&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;두 작업을 이어서 실행하도록 조합하며, 앞선 작업의 결과를 받아서 사용할 수 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;함수형 인터페이스 Function을 파라미터로 받는다.&lt;/p&gt;
&lt;pre id=&quot;code_1700555365275&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;CompletableFuture&amp;lt;String&amp;gt; hello = CompletableFuture.supplyAsync(() -&amp;gt; {
    return &quot;Hello&quot;;
});

CompletableFuture&amp;lt;String&amp;gt; helloWorld = hello.thenCompose(this::world);

System.out.println(helloWorld.join()); // Hello World!



private CompletableFuture&amp;lt;String&amp;gt; world(String message) {
    return CompletableFuture.supplyAsync(() -&amp;gt; {
        return message + &quot; &quot; + &quot;World!&quot;;
    });
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;- thenCombine&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;각 작업을 독립적으로 실행하고, 모두 완료되었을 때 결과를 받아서 사용할 수 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;함수형 인터페이스 Function을 파라미터로 받는다.&lt;/p&gt;
&lt;pre id=&quot;code_1700556850156&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;CompletableFuture&amp;lt;String&amp;gt; hello = CompletableFuture.supplyAsync(() -&amp;gt; {
    return &quot;Hello&quot;;
});

CompletableFuture&amp;lt;String&amp;gt; world = CompletableFuture.supplyAsync(() -&amp;gt; {
    return &quot;World!&quot;;
});

CompletableFuture&amp;lt;String&amp;gt; cf = hello.thenCombine(world, (h, w) -&amp;gt; h + &quot; &quot; + w);
System.out.println(cf.join()); // Hello World!&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;- allOf&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;여러 작업들을 동시에 실행하고, 모든 작업 결과에 콜백을 실행한다.&lt;/p&gt;
&lt;pre id=&quot;code_1700633572196&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;CompletableFuture&amp;lt;String&amp;gt; hello = CompletableFuture.supplyAsync(() -&amp;gt; {
    return &quot;Hello&quot;;
});

CompletableFuture&amp;lt;String&amp;gt; world = CompletableFuture.supplyAsync(() -&amp;gt; {
    return &quot;World!&quot;;
});

List&amp;lt;CompletableFuture&amp;lt;String&amp;gt;&amp;gt; futures = List.of(hello, world);
CompletableFuture&amp;lt;List&amp;lt;String&amp;gt;&amp;gt; result = CompletableFuture.allOf(futures.toArray(new CompletableFuture[futures.size()]))
        .thenApply(v -&amp;gt; futures.stream()
                .map(CompletableFuture::join)
                .collect(Collectors.toList()));

System.out.println(result.join()); // [Hello, World!]
result.join().forEach(System.out::println);
// Hello
// World!&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;- anyOf&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;여러 작업들 중에 가장 빨리 끝난 하나의 결과에 콜백을 실행한다.&lt;/p&gt;
&lt;pre id=&quot;code_1700634536924&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;CompletableFuture&amp;lt;String&amp;gt; hello = CompletableFuture.supplyAsync(() -&amp;gt; {
    try {
        Thread.sleep(100);
    } catch (InterruptedException e) {}

    return &quot;Hello&quot;;
});

CompletableFuture&amp;lt;String&amp;gt; world = CompletableFuture.supplyAsync(() -&amp;gt; {
    return &quot;World!&quot;;
});

CompletableFuture&amp;lt;Void&amp;gt; anyOfFuture = CompletableFuture.anyOf(hello, world)
        .thenAccept(System.out::println);

anyOfFuture.join(); // World!&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;에러 핸들링&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Future에서 에러 핸들링 할 수 없던 문제를 CompletableFuture는 어떻게 해결했는지 알아보자.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;- exceptionally&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;발생한 에러를 받아서 예외를 처리한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;함수형 인터페이스 Function을 파라미터로 받는다.&lt;/p&gt;
&lt;pre id=&quot;code_1700725185686&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;CompletableFuture&amp;lt;String&amp;gt; cf = CompletableFuture.supplyAsync(() -&amp;gt; {
    int divisionByZero = 2 / 0;
    return &quot;success&quot;;
}).exceptionally(e -&amp;gt; { // e is wrapped with CompletionException
    return e.toString();
});

System.out.println(cf.join()); // java.util.concurrent.CompletionException: java.lang.ArithmeticException: / by zero&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;- handle&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;(결과값, 에러)를 반환받아 에러가 발생한 경우와 아닌 경우 모두를 처리할 수 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;함수형 인터페이스 BiFunction을 파라미터로 받는다.&lt;/p&gt;
&lt;pre id=&quot;code_1700808513861&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;CompletableFuture&amp;lt;String&amp;gt; cf = CompletableFuture.supplyAsync(() -&amp;gt; {
    int divisionByZero = 2 / 0;
    return &quot;success&quot;;
}).handle((result, e) -&amp;gt; { // e is wrapped with CompletionException
    return e == null ? result : e.toString();
});;

System.out.println(cf.join()); // java.util.concurrent.CompletionException: java.lang.ArithmeticException: / by zero&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;여기까지 비동기 처리를 위한 CompletableFuture 사용 방법에 대해 알아보았고,&amp;nbsp;다음 글에 이어서 ThreadPoolTaskExecutor 설정으로 보다 효과적으로 비동기 처리 하는 방법을 알아보도록 하자.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;참고&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;a href=&quot;https://stackoverflow.com/questions/45490316/completablefuturet-class-join-vs-get&quot;&gt;https://stackoverflow.com/questions/45490316/completablefuturet-class-join-vs-get&lt;/a&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;a href=&quot;https://mangkyu.tistory.com/263&quot; target=&quot;_blank&quot; rel=&quot;noopener&amp;nbsp;noreferrer&quot;&gt;https://mangkyu.tistory.com/263&lt;/a&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;a href=&quot;https://devidea.tistory.com/34&quot; target=&quot;_blank&quot; rel=&quot;noopener&amp;nbsp;noreferrer&quot;&gt;https://devidea.tistory.com/34&lt;/a&gt;&lt;/p&gt;</description>
      <category> Programming/Java</category>
      <category>async</category>
      <category>CompletableFuture</category>
      <category>Java</category>
      <category>비동기</category>
      <category>비동기 프로그래밍</category>
      <category>자바</category>
      <author>coco3o</author>
      <guid isPermaLink="true">https://dev-coco.tistory.com/185</guid>
      <comments>https://dev-coco.tistory.com/185#entry185comment</comments>
      <pubDate>Mon, 27 Nov 2023 20:44:06 +0900</pubDate>
    </item>
    <item>
      <title>[Error] Failed to parse multipart servlet request; nested exception is java.lang.IllegalStateException: The multi-part request contained parameter data (excluding uploaded files) that exceeded the limit for maxPostSize set on the associated connector</title>
      <link>https://dev-coco.tistory.com/184</link>
      <description>&lt;p data-ke-size=&quot;size16&quot;&gt;Multi-part POST 요청을 하는 코드에서 다음과 같은 에러가 발생했다.&lt;br /&gt;&amp;nbsp;&lt;br /&gt;&lt;span style=&quot;color: #ee2323;&quot;&gt;Failed to parse multipart servlet request; nested exception is java.lang.IllegalStateException: The multi-part request contained parameter data (excluding uploaded files) that exceeded the limit for maxPostSize set on the associated connector&lt;/span&gt;&lt;br /&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;원인 분석&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;해당 에러는 멀티파트 요청 파라미터의 제한된 데이터 크기를 초과한 요청을 했을 때 발생하는 에러였고,&lt;br /&gt;프로젝트엔 다음과 같이 멀티파트 관련 설정이 적용되어 있는 상황이었다.&lt;/p&gt;
&lt;pre class=&quot;java&quot; data-ke-type=&quot;codeblock&quot; data-ke-language=&quot;java&quot;&gt;&lt;code&gt;server:
&amp;nbsp;&amp;nbsp;servlet:
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;multipart:
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;max-file-size: 30MB
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;max-request-size: 30MB&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;위 설정을 초과한 요청을 보내는지 확인해 봤는데 요청 데이터 크기는 2194344바이트(2.19MB)였다.&lt;br /&gt;&lt;span style=&quot;color: #333333;&quot;&gt;왜 설정 용량에 한참 못 미치는 요청에 대해 에러가 발생했는지 찾아보았다.&lt;/span&gt;&lt;br /&gt;&amp;nbsp;&lt;br /&gt;&lt;span style=&quot;color: #333333;&quot;&gt;알고보니&amp;nbsp;&lt;/span&gt;&lt;span style=&quot;color: #ee2323;&quot;&gt;max-file-size&lt;/span&gt;&lt;span style=&quot;color: #333333;&quot;&gt; 설정은&amp;nbsp;&lt;/span&gt;&lt;b&gt;요청 파일 하나당 허용 사이즈&lt;/b&gt;&lt;span style=&quot;color: #333333;&quot;&gt;이고,&lt;/span&gt;&lt;br /&gt;&lt;span style=&quot;color: #ee2323;&quot;&gt;max-request-size&lt;/span&gt;&lt;span style=&quot;color: #333333;&quot;&gt; 설정은&lt;/span&gt;&lt;b&gt;multipart/form-data 요청에서 전체 파일의 허용 사이즈&lt;/b&gt;였다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size14&quot;&gt;&lt;s&gt;max-request-size가 전체 데이터&lt;/s&gt;&lt;s&gt;&amp;nbsp;&lt;/s&gt;&lt;s&gt;요청&lt;/s&gt;&lt;s&gt;&amp;nbsp;&lt;/s&gt;&lt;s&gt;사이즈 설정인줄..&lt;/s&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;br /&gt;&lt;span style=&quot;color: #333333;&quot;&gt;즉, 위 속성은 파일 업로드와 관련된 설정이었고, &lt;/span&gt;&lt;span style=&quot;color: #333333;&quot;&gt;&lt;b&gt;나는 파일을 포함한 http form 데이터 크기가 초과되어 에러가 발생한 것&lt;/b&gt;&lt;/span&gt;&lt;span style=&quot;color: #333333;&quot;&gt;이었다.&lt;/span&gt;&lt;br /&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;문제 해결&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;다음과 같이 tomcat의 max-http-form-post-size 속성을 추가했다.&lt;/p&gt;
&lt;pre class=&quot;java&quot; data-ke-type=&quot;codeblock&quot; data-ke-language=&quot;java&quot;&gt;&lt;code&gt;server:
&amp;nbsp;&amp;nbsp;tomcat:
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;max-http-form-post-size: 10MB&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size14&quot;&gt;&lt;span style=&quot;color: #ee2323;&quot;&gt; &lt;/span&gt;※ Spring Boot 2.1.x 버전부터 server.tomcat.max-http-post-size &amp;gt; server.tomcat.max-http-form-post-size로 변경됨&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;br /&gt;&lt;span style=&quot;color: #ee2323;&quot;&gt;max-http-form-post-size&lt;/span&gt;는&lt;b&gt; 파일을 포함한 전체 데이터 &lt;/b&gt;&lt;b&gt;요청의 허용 사이즈를 설정하며, 기본 설정 값은 2MB이다.&lt;/b&gt;&lt;br /&gt;&amp;nbsp;&lt;br /&gt;설정 후 테스트 해보니 정상적으로 처리되었고, 기분 좋게 운영 서버에 반영하니 다시 같은 에러가 발생했다.&lt;br /&gt;tomcat server.xml 외부 설정이 Spring boot application의 내부 설정을 덮어씌우는 것 같았고, &lt;s&gt;(뇌피셜)&lt;/s&gt;&lt;br /&gt;tomcat/config server.xml안에 있는 Connector 태그에 아래 속성을 추가한 뒤 tomcat을 재시작하니 해결되었다.&lt;/p&gt;
&lt;pre class=&quot;java&quot; data-ke-type=&quot;codeblock&quot; data-ke-language=&quot;java&quot;&gt;&lt;code&gt;&amp;lt;Connector ... maxPostSize=&quot;10000000&quot; /&amp;gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;maxPostSize는 바이트 단위로 설정할 수 있고,&amp;nbsp;&quot;-1&quot;으로 설정시 제한을 비활성화할 수 있으며, Tomcat 7.0.63 이전 버전은 &quot;0&quot;으로 설정해야 한다.&lt;br /&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;주의&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;server.xml에는 세 가지 Connector 태그가 존재하며, 사용하고 있는 방식에 맞는 태그에 속성을 추가해야 한다.&lt;/b&gt;&lt;br /&gt;1. &lt;span style=&quot;color: #ee2323;&quot;&gt;기본(8080)&lt;/span&gt;&lt;br /&gt;2. &lt;span style=&quot;color: #ee2323;&quot;&gt;SSL(443)&lt;/span&gt;&lt;br /&gt;3. &lt;span style=&quot;color: #ee2323;&quot;&gt;AJP(아파치와 연동: 8009)&lt;/span&gt;&lt;span style=&quot;color: #ee2323;&quot;&gt;&amp;nbsp;&lt;/span&gt;&lt;/p&gt;
&lt;pre class=&quot;java&quot; data-ke-type=&quot;codeblock&quot; data-ke-language=&quot;java&quot;&gt;&lt;code&gt;&amp;lt;Connector port=&quot;8080&quot; ... /&amp;gt; 
&amp;lt;Connector SSLEnabled=&quot;true&quot; protocol=&quot;HTTP/1.1&quot; ... /&amp;gt; 
&amp;lt;Connector port=&quot;8009&quot; protocol=&quot;AJP/1.3&quot; ... /&amp;gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;예를 들어 SSL(443) 설정을 한 후 443 포트를 이용하여 통신(Submit) 하고 있는데, 기본(8080) 커넥터에만 설정한 경우 설정 값이 적용되지 않음에 주의해야 한다.&amp;nbsp;&lt;br /&gt;&amp;nbsp;&lt;br /&gt;&lt;br /&gt;reference :&amp;nbsp;&lt;br /&gt;&lt;a href=&quot;https://docs.spring.io/spring-boot/docs/current/api/org/springframework/boot/autoconfigure/web/servlet/MultipartProperties.html&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;&lt;span&gt;https://docs.spring.io/spring-boot/docs/current/api/org/springframework/boot/autoconfigure/web/servlet/MultipartProperties.html&lt;/span&gt;&lt;/a&gt;&lt;br /&gt;&lt;a href=&quot;https://docs.spring.io/spring-boot/docs/current/reference/html/application-properties.html#application-properties.server.server.tom&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;&lt;span&gt;https://docs.spring.io/spring-boot/docs/current/reference/html/application-properties.html#application-properties.server.server.tomcat.max-http-form-post-size&lt;/span&gt;&lt;/a&gt;&lt;br /&gt;&lt;a href=&quot;https://tomcat.apache.org/tomcat-9.0-doc/config/http.html#HTTP/1.1_and_HTTP/1.0_Support&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;&lt;span&gt;https://tomcat.apache.org/tomcat-9.0-doc/config/http.html#HTTP/1.1_and_HTTP/1.0_Support&lt;/span&gt;&lt;/a&gt;&lt;br /&gt;&lt;a href=&quot;https://jiurinie.tistory.com/133&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;&lt;span&gt;https://jiurinie.tistory.com/133&lt;/span&gt;&lt;/a&gt;&lt;/p&gt;</description>
      <category> ETC/Error</category>
      <category>error</category>
      <category>Failed to parse multipart servlet request; nested exception is java.lang.IllegalStateException: The multi-part request contained parameter data (excluding uploaded files) that exceeded the limit for maxPostSize set on the associated connector</category>
      <category>multipart</category>
      <category>spring boot</category>
      <category>tomcat</category>
      <author>coco3o</author>
      <guid isPermaLink="true">https://dev-coco.tistory.com/184</guid>
      <comments>https://dev-coco.tistory.com/184#entry184comment</comments>
      <pubDate>Fri, 10 Nov 2023 16:17:21 +0900</pubDate>
    </item>
    <item>
      <title>[Java] 병렬 스트림(ParallelStream) 사용 방법 및 주의사항</title>
      <link>https://dev-coco.tistory.com/183</link>
      <description>&lt;h2 data-ke-size=&quot;size26&quot;&gt;ParallelStream&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Java8에서 등장한 Stream은 병렬 처리를 쉽게 할 수 있도록 메소드를 제공해 준다.&lt;br /&gt;개발자가 직접 스레드 혹은 스레드풀을 생성하거나 관리할 필요 없이 parallelStream(), parallel()만 사용하면 알아서 ForkJoinFramework 관리 방식을 이용하여 작업들을 분할하고, 병렬적으로 처리하게 된다.&lt;br /&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;Fork / Join Framework&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Fork / Join Framework는 작업들을 분할 가능한 만큼 쪼개고, 쪼개진 작업들을 Work Thread를 통해 작업 후 결과를 합치는 과정으로 결과를 만들어 낸다.&lt;br /&gt;즉, 분할 정복(Divide and Conquer) 알고리즘과 흡사하며, Fork를 통해 Task를 분담하고 Join을 통해 결과를 합친다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1032&quot; data-origin-height=&quot;342&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/95MfM/btszjEokemh/IHyNkGGh4sDWdn7jfzmhuK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/95MfM/btszjEokemh/IHyNkGGh4sDWdn7jfzmhuK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/95MfM/btszjEokemh/IHyNkGGh4sDWdn7jfzmhuK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2F95MfM%2FbtszjEokemh%2FIHyNkGGh4sDWdn7jfzmhuK%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;1032&quot; height=&quot;342&quot; data-origin-width=&quot;1032&quot; data-origin-height=&quot;342&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Fork / Join Framework의 중심은 AbstractExecutorService 클래스를 확장한 ForkJoinPool이다.&lt;br /&gt;&amp;nbsp;&lt;br /&gt;ForkJoinPool을 알아보기 위해 &lt;a href=&quot;https://docs.oracle.com/javase/8/docs/api/java/util/concurrent/ForkJoinPool.html&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;&lt;span&gt;JavaDoc&lt;/span&gt;&lt;/a&gt;에서 일부 발췌한 내용이다.&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style3&quot;&gt;An ExecutorService for running ForkJoinTasks. A ForkJoinPool provides the entry point for submissions from non-ForkJoinTask clients,&lt;br /&gt;&lt;br /&gt;as well as management and monitoring operations. A ForkJoinPool differs from other kinds of ExecutorService mainly by virtue of employing work-stealing: all threads in the pool attempt to find and execute tasks submitted to the pool and/or created by other active tasks &lt;br /&gt;(eventually blocking waiting for work if none exist). This enables efficient processing when most tasks spawn other subtasks (as do most ForkJoinTasks), as well as when many small tasks are submitted to the pool from external clients. Especially when setting asyncMode to true in constructors, ForkJoinPools may also be appropriate for use with event-style tasks that are never joined.&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;위 내용을 정리하면 다음과 같다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;다른 종류의 ExecutorService와는 다르게 &lt;span style=&quot;color: #ee2323;&quot;&gt;Work-Stealing 메커니즘을 사용&lt;/span&gt;한다.&lt;/li&gt;
&lt;li&gt;이를 통해 대부분의 Task가 하위 Task를 생성하는 경우, 외부 클라이언트에 의한 Small Task가 많을 경우 효과적일 수 있다.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;508&quot; data-origin-height=&quot;222&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/z1hfY/btszfOeKyLM/aQsvC9w0Bd2kmup5tGUAqk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/z1hfY/btszfOeKyLM/aQsvC9w0Bd2kmup5tGUAqk/img.png&quot; data-alt=&quot;http://www.h-online.com/developer/features/The-fork-join-framework-in-Java-7-1762357.html&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/z1hfY/btszfOeKyLM/aQsvC9w0Bd2kmup5tGUAqk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fz1hfY%2FbtszfOeKyLM%2FaQsvC9w0Bd2kmup5tGUAqk%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;508&quot; height=&quot;222&quot; data-origin-width=&quot;508&quot; data-origin-height=&quot;222&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;http://www.h-online.com/developer/features/The-fork-join-framework-in-Java-7-1762357.html&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;1. task를 보낸다. (submit)&lt;br /&gt;2. inbound queue에 task가 들어가고, A와 B 스레드가 가져다가 task를 처리한다.&lt;br /&gt;3. A와 B는 각자 큐가 있으며, 위 그림의 B처럼 큐에 task가 없으면 A의 task를 steal 하여 처리한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;work-stealing 메커니즘을 사용하기 때문에 CPU 자원이 놀지 않고 최적의 성능을 낼 수 있게 된다.&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size14&quot;&gt;스레드 자신의 task queue로 deque를 사용한다. deque는 양 쪽 끝으로 넣고 뺄 수 있는 구조이며,&lt;br /&gt;각 스레드는 deque의 한쪽 끝에서만 일하고 나머지 반대쪽에서는 task를 훔치러 온 다른 스레드가 접근한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;br /&gt;그리고, &lt;span style=&quot;color: #ee2323;&quot;&gt;Fork / Join Framework의 Work Thread의 수는 해당 어플리케이션이 구동되는 서버의 스펙에 따라 결정&lt;/span&gt;된다.&lt;br /&gt;&lt;b&gt;Runtime.getRuntime().availableProcessors()&lt;/b&gt;으로 JVM에서 이용 가능한 CPU Core 개수를 확인할 수 있으며,&lt;br /&gt;스레드가 N개 생성되었을 때, 하나는 메인 스레드로 스트림을 처리하는 기본 스레드와 나머지 N-1개의 스레드가 ForkJoinPool 스레드이다.&lt;br /&gt;&amp;nbsp;&lt;br /&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;예제 코드로 확인해보기&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;우선 순차적으로 실행 중인 thread 이름을 출력해보자.&lt;/p&gt;
&lt;pre class=&quot;java&quot; data-ke-type=&quot;codeblock&quot; data-ke-language=&quot;java&quot;&gt;&lt;code&gt;public static void main(String[] arg) {

&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;List&amp;lt;Integer&amp;gt; numbers = Arrays.asList(1, 2, 3, 4);

&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;long start = System.currentTimeMillis();
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;numbers.forEach(number -&amp;gt; {
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;try {
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;Thread.sleep(3000);
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;System.out.println(number + &quot;: &quot; + Thread.currentThread().getName());
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;} catch (InterruptedException e) {}
	});
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;long duration = (System.currentTimeMillis() - start);
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;double seconds = duration / 1000.0;

&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;System.out.printf(&quot;Done in %.2f sec\n&quot;, seconds);
}&lt;/code&gt;&lt;/pre&gt;
&lt;pre class=&quot;java&quot; data-ke-type=&quot;codeblock&quot; data-ke-language=&quot;java&quot;&gt;&lt;code&gt;1: main
2: main
3: main
4: main
Done in 12.05 sec&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;위 테스트를 진행했을 때&amp;nbsp;4개의 Elements에 대해서 각각 3초간 delay 되면서 총 &lt;span style=&quot;color: #ee2323;&quot;&gt;12.05초&lt;/span&gt;가 걸렸다.&lt;br /&gt;&amp;nbsp;&lt;br /&gt;다음은 병렬 스트림을 이용하여 실행 중인 thread 이름을 출력하는 메소드를 호출해보자.&lt;br /&gt;사용법은 간단한데, parallelStream() 또는 stream().parallel() 만 붙여주면 된다.&lt;/p&gt;
&lt;pre class=&quot;java&quot; data-ke-type=&quot;codeblock&quot; data-ke-language=&quot;java&quot;&gt;&lt;code&gt;public static void main(String[] arg) {

&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;List&amp;lt;Integer&amp;gt; numbers = Arrays.asList(1, 2, 3, 4);

&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;long start = System.currentTimeMillis();
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;numbers.parallelStream().forEach(number -&amp;gt; {
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;try {
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;Thread.sleep(3000);
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;System.out.println(number + &quot;: &quot; + Thread.currentThread().getName());
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;} catch (InterruptedException e) {}
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;});
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;long duration = (System.currentTimeMillis() - start);
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;double seconds = duration / 1000.0;

&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;System.out.printf(&quot;Done in %.2f sec\n&quot;, seconds);
}&lt;/code&gt;&lt;/pre&gt;
&lt;pre class=&quot;java&quot; data-ke-type=&quot;codeblock&quot; data-ke-language=&quot;java&quot;&gt;&lt;code&gt;3: main
1: ForkJoinPool.commonPool-worker-2
2: ForkJoinPool.commonPool-worker-1
4: ForkJoinPool.commonPool-worker-3
Done in 3.04 sec&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size14&quot;&gt;&lt;span style=&quot;color: #333333;&quot;&gt;※&lt;/span&gt; 현재 사용 중인 PC의 코어 수는 4개이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;메인 스레드를 포함한 4개의 스레드가 병렬 처리되면서&amp;nbsp;&lt;span style=&quot;color: #ee2323;&quot;&gt;12.05&lt;/span&gt;&lt;span style=&quot;color: #ee2323;&quot;&gt;초&lt;/span&gt;가 걸렸던 작업이&lt;span style=&quot;color: #ee2323;&quot;&gt; 3.04초&lt;/span&gt;만에 완료되었다. &lt;br /&gt;그리고 numbers의 size를 5개로 설정하면 결과를 얻는 데까지 6초의 시간이 걸릴 것이다.&lt;br /&gt;4개의 스레드가 동시 작업 할 동안 남은 1개의 작업을 처리하지 못하기 때문이다.&lt;br /&gt;&amp;nbsp;&lt;br /&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;ParallelStream 크기 제어&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;ParallelStream에서 개발자가 임의로 Thread Pool의 크기를 조절하는 방법은 두 가지가 있다.&lt;br /&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;1. Property 설정&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;java.util.concurrent.ForkJoinPool.common.parallelism&amp;nbsp;Property&lt;/b&gt;값을&amp;nbsp;설정하는&amp;nbsp;방법이다.&lt;/p&gt;
&lt;pre class=&quot;java&quot; data-ke-type=&quot;codeblock&quot; data-ke-language=&quot;java&quot;&gt;&lt;code&gt;System.setProperty(&quot;java.util.concurrent.ForkJoinPool.common.parallelism&quot;,&quot;6&quot;);&lt;/code&gt;&lt;/pre&gt;
&lt;pre class=&quot;java&quot; data-ke-type=&quot;codeblock&quot; data-ke-language=&quot;java&quot;&gt;&lt;code&gt;public static void main(String[] arg) {
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;System.setProperty(&quot;java.util.concurrent.ForkJoinPool.common.parallelism&quot;,&quot;6&quot;);

&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;List&amp;lt;Integer&amp;gt; numbers = Arrays.asList(1, 2, 3, 4, 5, 6);

&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;long start = System.currentTimeMillis();
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;numbers.parallelStream().forEach(number -&amp;gt; {
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;try {
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;Thread.sleep(3000);
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;System.out.println(number + &quot;: &quot; + Thread.currentThread().getName());
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;} catch (InterruptedException e) {}
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;});&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;long duration = (System.currentTimeMillis() - start);
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;double seconds = duration / 1000.0;

&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;System.out.printf(&quot;Done in %.2f sec\n&quot;, seconds);
}&lt;/code&gt;&lt;/pre&gt;
&lt;pre class=&quot;java&quot; data-ke-type=&quot;codeblock&quot; data-ke-language=&quot;java&quot;&gt;&lt;code&gt;4: main
3: ForkJoinPool.commonPool-worker-2
2: ForkJoinPool.commonPool-worker-3
1: ForkJoinPool.commonPool-worker-1
6: ForkJoinPool.commonPool-worker-4
5: ForkJoinPool.commonPool-worker-5
Done in 3.07 sec&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #333333; text-align: start;&quot;&gt;이 방법은&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;/span&gt;&lt;b&gt;현재 실행되는 프로세스의 모든 ForkJoinPool의 commonPool에 설정이 적용되며, JVM 전체에 영향&lt;/b&gt;&lt;span style=&quot;color: #333333; text-align: start;&quot;&gt;을 미칠 수 있기 때문에&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;/span&gt;&lt;span style=&quot;color: #ee2323;&quot;&gt;가급적 사용하지 않는 것을 권장&lt;/span&gt;한다. &amp;nbsp;&lt;br /&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;2. ForkJoinPool 설정&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;두 번째 방법은 commonPool을 사용하지 않고 개발자가 정의한 ForkJoinPool을 사용하는 방법이다.&lt;/p&gt;
&lt;pre class=&quot;java&quot; data-ke-type=&quot;codeblock&quot; data-ke-language=&quot;java&quot;&gt;&lt;code&gt;ForkJoinPool forkJoinPool = new ForkJoinPool(6);

forkJoinPool.submit(() -&amp;gt; numbers.parallelStream()
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;.forEach(number -&amp;gt; {
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;try {
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;Thread.sleep(3000);
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;System.out.println(number + &quot;: &quot; + Thread.currentThread().getName());
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;} catch (InterruptedException e) {}
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;})
).get();&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;ForkJoinPool 생성자에 스레드 개수를 지정하여 사용할 수 있으며, 지정한 수만큼 스레드를 이용하여 처리한다.&lt;br /&gt;&amp;nbsp;&lt;br /&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;ParallelStream 사용 전 꼭 알아야 할 주의사항&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;1. Thread Pool을 공유&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;parallelStream은&amp;nbsp;내부적으로&amp;nbsp;common&amp;nbsp;ForkJoinPool을&amp;nbsp;사용하여&amp;nbsp;작업을&amp;nbsp;병렬화&amp;nbsp;시킨다. &lt;br /&gt;parallelStream 별로 Thread Pool을 만드는 게 아니라는 것이다. &lt;br /&gt;&lt;span style=&quot;color: #ee2323;&quot;&gt;별도의 설정이 없다면 하나의 Thread Pool을 모든 parallelStream이 공유&lt;/span&gt;하게 되고,&amp;nbsp; &lt;br /&gt;Thread Pool을 사용하는 다른 Thread에 영향을 줄 수 있으며, 반대로 영향을 받을 수 있다.&lt;br /&gt;&amp;nbsp;&lt;br /&gt;Thread&amp;nbsp;Pool은&amp;nbsp;미리&amp;nbsp;스레드를&amp;nbsp;생성하여&amp;nbsp;보관하고&amp;nbsp;필요할&amp;nbsp;때&amp;nbsp;빌려주고&amp;nbsp;사용하지&amp;nbsp;않으면&amp;nbsp;반납하여&amp;nbsp;Thread의&amp;nbsp;숫자를&amp;nbsp;유지하는&amp;nbsp;역할을&amp;nbsp;한다. &lt;br /&gt;그런데&amp;nbsp;만약&amp;nbsp;Thread를&amp;nbsp;사용 중인&amp;nbsp;곳에서&amp;nbsp;아래&amp;nbsp;이미지처럼&amp;nbsp;Thread를&amp;nbsp;반납하지&amp;nbsp;않고&amp;nbsp;계속&amp;nbsp;점유 중이라면&amp;nbsp;어떻게&amp;nbsp;될까?&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;768&quot; data-origin-height=&quot;590&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/KxdQs/btszvjZHIsU/kSY57yFqtaeNskKCJTpBPK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/KxdQs/btszvjZHIsU/kSY57yFqtaeNskKCJTpBPK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/KxdQs/btszvjZHIsU/kSY57yFqtaeNskKCJTpBPK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FKxdQs%2FbtszvjZHIsU%2FkSY57yFqtaeNskKCJTpBPK%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;431&quot; height=&quot;331&quot; data-origin-width=&quot;768&quot; data-origin-height=&quot;590&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;br /&gt;이렇게&amp;nbsp;되면&amp;nbsp;Thread&amp;nbsp;1,&amp;nbsp;2,&amp;nbsp;3은&amp;nbsp;사용할&amp;nbsp;수&amp;nbsp;없으며&amp;nbsp;Thread&amp;nbsp;4&amp;nbsp;한 개 만을&amp;nbsp;이용해서&amp;nbsp;모든&amp;nbsp;요청을&amp;nbsp;처리하게&amp;nbsp;된다.&amp;nbsp;&lt;br /&gt;특히 Thread 1, 2, 3 이 sleep과 같이 아무런 일을 하지 않으면서 점유를 하고 있다면 이는 문제가 크다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;만약, &lt;b&gt;Thread 4까지 점유 중이게 되면 더 이상 요청은 처리되지 않고 Thread Pool Queue에 쌓이게 되며, 일정시간 이상 되면 요청이 Drop 되는 현상까지 발생&lt;/b&gt;할 것이다. &lt;br /&gt;&lt;br /&gt;병렬 스트림은 Thread Pool을 global하게 공유하기 때문에 만약 A메서드에서 4개의 Thread를 모두 점유하면 다른 병렬 스트림의 요청은 처리되지 않고 대기하게 된다.&lt;br /&gt;또한, blocking &lt;span style=&quot;color: #333333;&quot;&gt;&amp;nbsp;&lt;/span&gt;&lt;span style=&quot;color: #333333;&quot;&gt;I/O&lt;/span&gt; 가 발생하는 작업을 하게 되면 Thread Pool 내부의 스레드들은 block 되며, 이때 Thread Pool을 공유하는 다른 쪽의 병렬 Stream은 스레드를 얻을 때까지 계속해서 기다리게 되면서 문제가 발생한다.&lt;br /&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 문제는 &lt;span style=&quot;color: #ee2323;&quot;&gt;각 parallelStream마다 커스텀(new ForkJoinPool(int n))하여 독립적인 Thread Pool로 분리하여 사용하면 해결&lt;/span&gt;할 수 있다.&lt;br /&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;2. Custom Thread Pool 사용 시 Memory Leak 주의&lt;/h4&gt;
&lt;pre class=&quot;java&quot; data-ke-type=&quot;codeblock&quot; data-ke-language=&quot;java&quot;&gt;&lt;code&gt;ForkJoinPool customForkJoinPool = new ForkJoinPool(Runtime.getRuntime().availableProcessors());&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;※&amp;nbsp;별도의 스레드 풀 생성 시 정석은 실행 중인 CPU 코어 수를 기준으로 생성하는 것이다.&amp;nbsp;&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size14&quot;&gt;물리적인 코어 수를 초과하여 생성할 경우, 생성은 되지만 스레드&amp;nbsp;관리&amp;nbsp;오버헤드와&amp;nbsp;스레드&amp;nbsp;간의&amp;nbsp;빈번한&amp;nbsp;컨텍스트&amp;nbsp;스위칭(Context-Switching)&amp;nbsp;등의&amp;nbsp;문제로&amp;nbsp;성능&amp;nbsp;저하가&amp;nbsp;발생할&amp;nbsp;수&amp;nbsp;있다.&lt;br /&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Parallel Stream 별로 ForkJoinPool을 인스턴스화하여 사용하면 &lt;span style=&quot;color: #ee2323;&quot;&gt;OOME(OutOfMemoryError)&lt;/span&gt;이 발생할 수 있다.&lt;br /&gt;default로 사용되는 Common ForkJoinPool은 정적(static)이기 때문에 메모리 누수가 발생하지 않지만,&lt;br /&gt;Custom 한 ForkJoinPool 객체는 참조 해제되지 않거나, GC(Garbage Collection)로 수집되지 않을 수 있다.&lt;br /&gt;따라서 Custom ForkJoinPool을 사용한 후 다음과 같이 스레드 풀을 명시적으로 종료해야 한다.&lt;/p&gt;
&lt;pre class=&quot;java&quot; data-ke-type=&quot;codeblock&quot; data-ke-language=&quot;java&quot;&gt;&lt;code&gt;ForkJoinPool customForkJoinPool = new ForkJoinPool(Runtime.getRuntime().availableProcessors());
// do something..
customForkJoinPool.shutdown();&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;br /&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 style=&quot;text-align: justify;&quot; data-ke-size=&quot;size20&quot;&gt;3. Collection 별 성능 차이&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;parallelStream은 분할되는 작업의 단위가 균등하게 나누어져야 하며, 나누어지는 작업의 비용이 높지 않아야 순차적 방식보다 효율적으로 이루어질 수 있다.&lt;br /&gt;array, arrayList와 같이 전체 사이즈를 알 수 있는 경우에는 분할 처리가 빠르고 비용이 적게 들지만,&lt;br /&gt;linkedList와 같이 사이즈를 정확히 알 수 없는 데이터 구조는 분할되지 않고 순차 처리를 하므로 성능 효과를 보기 어렵다.&lt;span style=&quot;color: #333333;&quot;&gt;&lt;/span&gt;&lt;br /&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 style=&quot;text-align: justify;&quot; data-ke-size=&quot;size20&quot;&gt;4. 병렬 처리 시 작업의 독립성&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;병렬로 처리되는 작업이 독립적이지 않다면, 수행 성능에 영향이 있을 수 있다.&lt;br /&gt;예를 들어, stream()의 중간 연산 중에 sorted()나 distinct()와 같은 작업을 수행할 경우&lt;br /&gt;내부적으로 상태에 대한 변수를 각 작업들이 공유(synchronized)하게 되어 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size14&quot;&gt;※&lt;span style=&quot;color: #333333;&quot;&gt; &lt;/span&gt;내부적으로 어떤 공용 변수를 만들어 놓고 각 worker들이 이 변수에 접근할 경우, 동기화 작업 등을 통해 변수를 안전하게 유지하며 처리한다. 기존 Thread 작업 시 개발자가 해줘야 했던 동기화 등의 작업을 모두 수행하고 있는 것이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이러한 경우 순차적으로 실행하는 것이 보다 효과적이며,&lt;br /&gt;각각 완전히 분리된 task들에 대해서 병렬로 처리하는 경우에 성능상 이점이 있을 수 있다.&lt;br /&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 style=&quot;text-align: justify;&quot; data-ke-size=&quot;size20&quot;&gt;5. 요소의 수 그리고 요소당 처리 시간&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;컬렉션에 요소의 수가 적고 요소당 처리 시간이 짧으면 순차 처리가 오히려 빠를 수 있다.&lt;br /&gt;병렬 처리는 작업들을 분할(fork)하고 다시 합치는(join) 비용, 스레드 간의 컨텍스트 스위칭 비용도 포함되기 때문이다.&lt;br /&gt;&amp;nbsp;&lt;br /&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;정리하면&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;parallelStream()은 세부 설정이나 복잡한 로직 없이 기존 stream()을 쓰듯 사용할 수 있는 편리함을 제공하지만,&lt;br /&gt;병렬 처리가 무조건 더 나은 결과를 보장한다고 할 순 없다.&lt;br /&gt;&amp;nbsp;&lt;br /&gt;처리 성능에 영향을 미치는 부분들, 분할 및 병합 과정에서의 비용, 멀티 스레드 환경에서의 컨텍스트 스위칭 비용 등에 대해 충분히 고려해야 하기 때문에 신중해야 한다.&lt;br /&gt;&amp;nbsp;&lt;br /&gt;또한,&amp;nbsp; I/O를 기다리는 작업에는 적합하지 않고, (이 경우 CompletableFuture가 적합)&lt;br /&gt;&lt;u&gt;분할이 잘 이루어질 수 있는 데이터 구조 혹은 작업이 독립적이면서 CPU 사용이 높은 작업에 적합하다.&amp;nbsp;&lt;/u&gt;&lt;br /&gt;&amp;nbsp;&lt;br /&gt;특정 로직의 성능 개선을 위해 parallelStream()을 적용하고자 한다면, 이것이 정말로 성능 개선을 해줄 수 있는가 혹 예상치 못한 장애를 발생시키지는 않을까에 대해 충분히 고민하고 적용하는 것이 좋을 것 같다.&lt;br /&gt;&amp;nbsp;&lt;br /&gt;&amp;nbsp;&lt;br /&gt;&amp;nbsp;&lt;br /&gt;Reference :&lt;br /&gt;&lt;a href=&quot;https://www.baeldung.com/java-when-to-use-parallel-stream&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;&lt;span&gt;https://www.baeldung.com/java-when-to-use-parallel-stream &lt;br /&gt;&lt;/span&gt;&lt;/a&gt;&lt;a href=&quot;https://www.baeldung.com/java-8-parallel-streams-custom-threadpool#bd-beware-of-the-memory-leak&quot;&gt;https://www.baeldung.com/java-8-parallel-streams-custom-threadpool#bd-beware-of-the-memory-leak&lt;/a&gt; &lt;br /&gt;&lt;a href=&quot;https://gee.cs.oswego.edu/dl/html/StreamParallelGuidance.html&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;&lt;span&gt;https://gee.cs.oswego.edu/dl/html/StreamParallelGuidance.html&lt;/span&gt;&lt;/a&gt;&lt;a href=&quot;https://www.baeldung.com/java-8-parallel-streams-custom-threadpool#bd-beware-of-the-memory-leak&quot; target=&quot;_self&quot;&gt;&lt;br /&gt;&lt;/a&gt;&lt;span&gt;&lt;/span&gt;&lt;a href=&quot;https://sabarada.tistory.com/102&quot;&gt;https://sabarada.tistory.com/102&lt;/a&gt;&lt;a href=&quot;https://www.baeldung.com/java-8-parallel-streams-custom-threadpool#bd-beware-of-the-memory-leak&quot; target=&quot;_self&quot;&gt;&lt;br /&gt;&lt;/a&gt;&lt;span&gt;&lt;/span&gt;&lt;a href=&quot;https://m.blog.naver.com/tmondev/220945933678&quot;&gt;https://m.blog.naver.com/tmondev/220945933678&lt;/a&gt;&lt;/p&gt;</description>
      <category> Programming/Java</category>
      <category>Java</category>
      <category>Parallel</category>
      <category>parallelstream</category>
      <category>STREAM</category>
      <category>병렬 프로그래밍</category>
      <category>병렬성</category>
      <category>병렬처리</category>
      <category>자바</category>
      <author>coco3o</author>
      <guid isPermaLink="true">https://dev-coco.tistory.com/183</guid>
      <comments>https://dev-coco.tistory.com/183#entry183comment</comments>
      <pubDate>Mon, 6 Nov 2023 20:46:15 +0900</pubDate>
    </item>
  </channel>
</rss>