<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0">
  <channel>
    <title>Java 개발 블로그</title>
    <link>https://lovon.tistory.com/</link>
    <description></description>
    <language>ko</language>
    <pubDate>Mon, 3 Aug 2026 00:01:38 +0900</pubDate>
    <generator>TISTORY</generator>
    <ttl>100</ttl>
    <managingEditor>lovineff</managingEditor>
    <item>
      <title>Jenkins 빌드 후 조치 call url</title>
      <link>https://lovon.tistory.com/198</link>
      <description>&lt;p data-ke-size=&quot;size16&quot;&gt;Jekins 빌드 결과에 따른 api 호출 방법을 작성했습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;실행시 자동 설정되는 환경변수 값을 이용했습니다.&lt;/p&gt;
&lt;pre id=&quot;code_1689309509728&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;# 잡 실행 결과 조회
BUILD_STATUS=$(curl -k --user 유저명:키 --silent http://jenkinsserver:port/job/$JOB_NAME/$BUILD_NUMBER/api/json | jq -r '.result')

# 잡 실행 결과 실패시 api호출
if [ $BUILD_STATUS != 'SUCCESS' ]
then
curl -X POST http://apiserver:8080/api/send/agit \
-H 'Content-Type: application/json' \
-d '{&quot;type&quot;:&quot;&quot;, &quot;channel&quot;:&quot;develop&quot;, &quot;text&quot;:&quot;#JenkinsJobFailMessage\r\n'$JOB_NAME'('$BUILD_NUMBER') '$BUILD_STATUS'\r\nhttp://jenkinsserver:port/job/'$JOB_NAME'/'$BUILD_NUMBER'/console&quot;}'
fi&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;</description>
      <category>SERVER</category>
      <author>lovineff</author>
      <guid isPermaLink="true">https://lovon.tistory.com/198</guid>
      <comments>https://lovon.tistory.com/198#entry198comment</comments>
      <pubDate>Fri, 14 Jul 2023 13:42:33 +0900</pubDate>
    </item>
    <item>
      <title>jenkins algorithm negotiation fail SSH</title>
      <link>https://lovon.tistory.com/197</link>
      <description>&lt;p data-ke-size=&quot;size16&quot;&gt;Jenkins 의 publish over ssh 플러그인 사용시 algorithm negotiation fail 에러 발생에 대해 대응 방법을 설명합니다.&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;최신 버전의 Jenkins 사용시 Publish over SSH 사용중 에러가 발생하는데요.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이에 여러 방안을 찾았으나, 젠킨슨 깃허브에 23년 6월 말 일자로 이슈업되었고 아직 댓글로 안달린것으로 확인했습니다.&lt;/p&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;결론 : 두개의 라이브러리를 다운그레이드&lt;/b&gt;&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&amp;nbsp;&lt;/h4&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;&lt;span style=&quot;background-color: #ffffff; color: #14141f; text-align: start;&quot;&gt;Publish over SSH&lt;/span&gt;&lt;/b&gt;&lt;span style=&quot;background-color: #ffffff; color: #14141f; text-align: start;&quot;&gt;&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;background-color: #ffffff; color: #14141f; text-align: start;&quot;&gt;1.25 -&amp;gt; 1.24&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;b&gt;JSch&amp;nbsp;dependency&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;0.2.8-65.v052c39de79b_2 -&amp;gt; 0.1.55.61.va_e9ee26616e7&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;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;&lt;span style=&quot;background-color: #ffffff; color: #14141f; text-align: start;&quot;&gt;&lt;a href=&quot;https://plugins.jenkins.io/&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;https://plugins.jenkins.io/ &lt;/a&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;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Jenkins 관리 &amp;gt; Plugins &amp;gt; Advanced settings&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;화면의&amp;nbsp; Deploy Plugin &amp;gt; 파일 선택 &amp;gt; Deploy&lt;/p&gt;</description>
      <category>SERVER</category>
      <author>lovineff</author>
      <guid isPermaLink="true">https://lovon.tistory.com/197</guid>
      <comments>https://lovon.tistory.com/197#entry197comment</comments>
      <pubDate>Fri, 14 Jul 2023 13:34:17 +0900</pubDate>
    </item>
    <item>
      <title>젠킨스 제거 (yum)</title>
      <link>https://lovon.tistory.com/196</link>
      <description>&lt;p data-ke-size=&quot;size16&quot;&gt;# yum 패키지 제거&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;sudo&amp;nbsp;yum&amp;nbsp;-y&amp;nbsp;remove&amp;nbsp;jenkins&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;br /&gt;sudo&amp;nbsp;apt-get&amp;nbsp;remove&amp;nbsp;--purge&amp;nbsp;jenkins&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;br /&gt;rm&amp;nbsp;-rf&amp;nbsp;/var/lib/jenkins&lt;br /&gt;rm&amp;nbsp;-rf&amp;nbsp;/root/.jenkins/&lt;/p&gt;</description>
      <category>SERVER/Linux</category>
      <author>lovineff</author>
      <guid isPermaLink="true">https://lovon.tistory.com/196</guid>
      <comments>https://lovon.tistory.com/196#entry196comment</comments>
      <pubDate>Mon, 10 Jul 2023 11:36:27 +0900</pubDate>
    </item>
    <item>
      <title>QueryDsl between</title>
      <link>https://lovon.tistory.com/195</link>
      <description>&lt;p data-ke-size=&quot;size16&quot;&gt;QueryDsl Between 사용 방법&lt;/p&gt;
&lt;pre id=&quot;code_1685444422839&quot; class=&quot;bash&quot; data-ke-language=&quot;bash&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;// 값 설정
String yyyyMMdd = LocalDateTime.now().format(DateTimeFormatter.ofPattern(&quot;yyyyMMdd&quot;));


// 다른부분 생략
jpaQueryFactory
.where(Expressions.asString(yyyyMMdd).between(entity.startDate, entity.endDate))


// 실행 결과
and ('20230530' between p010.START_DATE and p010.END_DATE)&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;</description>
      <category>JPA/queryDsl</category>
      <author>lovineff</author>
      <guid isPermaLink="true">https://lovon.tistory.com/195</guid>
      <comments>https://lovon.tistory.com/195#entry195comment</comments>
      <pubDate>Tue, 30 May 2023 20:02:34 +0900</pubDate>
    </item>
    <item>
      <title>파일 거꾸로 읽기</title>
      <link>https://lovon.tistory.com/194</link>
      <description>&lt;p data-ke-size=&quot;size16&quot;&gt;파일을 마지막 라인부터 읽어와서 문자가 있는 라인을 추출합니다.&lt;/p&gt;
&lt;pre id=&quot;code_1685064322563&quot; class=&quot;bash&quot; data-ke-language=&quot;bash&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;import org.apache.commons.io.input.ReversedLinesFileReader;
import org.junit.jupiter.api.Test;
import org.springframework.util.StringUtils;
import java.io.File;
import java.io.IOException;



String filePath = &quot;test.log&quot;;
String lastLine = &quot;&quot;;

// 파일을 거꾸로 읽습니다.
try (ReversedLinesFileReader reader
	= new ReversedLinesFileReader(new File(filePath))){

	// 공백, 엔터를 제외한 문자가 조회될때까지 라인을 읽습니다.
    while(!StringUtils.hasText(lastLine)){
    	lastLine = reader.readLine();
	    System.out.println(&quot;lastLine = &quot; + lastLine);
    }
} catch (IOException e) {
	e.printStackTrace();
}

System.out.println(&quot;real lastLine = &quot; + lastLine);&lt;/code&gt;&lt;/pre&gt;</description>
      <category>JAVA/JAVA</category>
      <author>lovineff</author>
      <guid isPermaLink="true">https://lovon.tistory.com/194</guid>
      <comments>https://lovon.tistory.com/194#entry194comment</comments>
      <pubDate>Fri, 26 May 2023 10:26:00 +0900</pubDate>
    </item>
    <item>
      <title>파일 압축</title>
      <link>https://lovon.tistory.com/193</link>
      <description>&lt;p data-ke-size=&quot;size16&quot;&gt;기본 압축 방법 (압축파일내 폴더 미생성)&lt;/p&gt;
&lt;pre id=&quot;code_1684977319822&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;public void pack(List&amp;lt;String&amp;gt; zipFileList, String zipFilePath, String zipFileName) throws IOException {
        log.info(&quot;------ zip pack start ------&quot;);
        log.info(&quot;zipFileList.size() = {}&quot;, zipFileList.size());
        log.info(&quot;zipFilePath = {}&quot;, zipFilePath);
        log.info(&quot;zipFileName = {}&quot;, zipFileName);

        // 압축 파일 디렉토리 생성
        Path zipDir = Paths.get(zipFilePath);
        Files.createDirectories(zipDir);

        // 압축파일 생성
        Path p = Files.createFile(Paths.get(zipFilePath + &quot;/&quot; + zipFileName));
        // 압축파일내 파일 추가 작업
        try (ZipOutputStream zs = new ZipOutputStream(Files.newOutputStream(p));){
             zipFileList.forEach(filePath -&amp;gt;{
                 Path path = Paths.get(filePath);
                 if(!Files.isDirectory(path)){
                     log.info(&quot;to Zip file = {}&quot;, filePath);

                     // 압축파일내 파일 생성
                     // ZipEntity 내에 경로 입력시 압축해제시 경로가 그대로 생성된다.
                     ZipEntry zipEntry = new ZipEntry(path.getFileName().toString());
                     try {
                         zs.putNextEntry(zipEntry);
                         Files.copy(path, zs);
                         zs.closeEntry();
                     } catch (IOException e) {
                         System.err.println(e);
                     }
                 }
             });
        }

        log.info(&quot;------ zip file end ------&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;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;파일별 폴더 경로를 생성하기 위한 객체&lt;/p&gt;
&lt;pre id=&quot;code_1684977378472&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;@Getter
@Builder
public class ZipVO {
    private String originFileFullPath;
    private String zipInnerFolderPath;
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;압축 작업&lt;/p&gt;
&lt;pre id=&quot;code_1684977367433&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;public void packByZipVOList(List&amp;lt;ZipVO&amp;gt; zipVOList, String zipFilePath, String zipFileName) throws IOException {
        log.info(&quot;------ zip pack start ------&quot;);
        log.info(&quot;zipVOList.size() = {}&quot;, zipVOList.size());
        log.info(&quot;zipFilePath = {}&quot;, zipFilePath);
        log.info(&quot;zipFileName = {}&quot;, zipFileName);

        Path zipDir = Paths.get(zipFilePath);
        Files.createDirectories(zipDir);

        Path p = Files.createFile(Paths.get(zipFilePath + &quot;/&quot; + zipFileName));
        try (ZipOutputStream zs = new ZipOutputStream(Files.newOutputStream(p))){
            zipVOList.forEach(zipVO -&amp;gt;{
                Path path = Paths.get(zipVO.getOriginFileFullPath());
                if(!Files.isDirectory(path)){
                    log.info(&quot;to Zip file = {}&quot;, zipVO.getOriginFileFullPath());
                    ZipEntry zipEntry = new ZipEntry((StringUtils.hasText(zipVO.getZipInnerFolderPath()) ? &quot;/&quot; + zipVO.getZipInnerFolderPath() : &quot;&quot;) + &quot;/&quot; + path.getFileName());
                    try {
                        zs.putNextEntry(zipEntry);
                        Files.copy(path, zs);
                        zs.closeEntry();
                    } catch (IOException e) {
                        System.err.println(e);
                    }
                }
            });
        }

        log.info(&quot;------ zip file end ------&quot;);
    }&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;테스트 코드로 실제 압축파일 생성 테스트&lt;/p&gt;
&lt;pre id=&quot;code_1684977451695&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;@Test
    void packByZipVO() throws IOException {
        String zipFilePath = &quot;/Users/a/Downloads&quot;;
        String zipFileName = &quot;test.zip&quot;;

        List&amp;lt;ZipVO&amp;gt; zipFileList = new ArrayList&amp;lt;&amp;gt;();
        zipFileList.add(ZipVO.builder()
                .originFileFullPath(&quot;/Users/a/Downloads/202305/202304/a.xlsx&quot;)
                .zipInnerFolderPath(&quot;202304/1&quot;)
                .build());

        zipFileList.add(ZipVO.builder()
                .originFileFullPath(&quot;/Users/a/Downloads/202305/202304/b.xlsx&quot;)
                .zipInnerFolderPath(null)
                .build());

        zipFileList.add(ZipVO.builder()
                .originFileFullPath(&quot;/Users/a/Downloads/202305/202304/c.xlsx&quot;)
                .zipInnerFolderPath(&quot;202304&quot;)
                .build());

        zipFileList.add(ZipVO.builder()
                .originFileFullPath(&quot;/Users/a/Downloads/202305/20230521/d.xlsx&quot;)
                .zipInnerFolderPath(&quot;20230521/1&quot;)
                .build());

        zipFileList.add(ZipVO.builder()
                .originFileFullPath(&quot;/Users/a/Downloads/202305/20230521/e.xlsx&quot;)
                .zipInnerFolderPath(null)
                .build());

        packByZipVOList(zipFileList, zipFilePath, zipFileName);
    }&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;생성 결과 (압축 해제)&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignLeft&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;432&quot; data-origin-height=&quot;488&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/cq0BCX/btshmX6fi1b/VpFSh00fl1ZDghhbbWRuzK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/cq0BCX/btshmX6fi1b/VpFSh00fl1ZDghhbbWRuzK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/cq0BCX/btshmX6fi1b/VpFSh00fl1ZDghhbbWRuzK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fcq0BCX%2FbtshmX6fi1b%2FVpFSh00fl1ZDghhbbWRuzK%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;432&quot; height=&quot;488&quot; data-origin-width=&quot;432&quot; data-origin-height=&quot;488&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;</description>
      <category>JAVA/JAVA</category>
      <author>lovineff</author>
      <guid isPermaLink="true">https://lovon.tistory.com/193</guid>
      <comments>https://lovon.tistory.com/193#entry193comment</comments>
      <pubDate>Thu, 25 May 2023 10:19:58 +0900</pubDate>
    </item>
    <item>
      <title>동적 빈 생성</title>
      <link>https://lovon.tistory.com/192</link>
      <description>&lt;p data-ke-size=&quot;size16&quot;&gt;Spring에서 제공하는 ConfigurableListableBeanFactory 클래스를 이용하여 동적으로 빈 등록이 가능하다.&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;pre id=&quot;code_1625647257709&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;public class DynamicClass {
	// 빈 등록시 생성자 호출여부 확인용
    public DynamicClass() {
        System.out.println(&quot;생성자 호출!&quot;);
    }

    public void func(){
        System.out.println(&quot;Test&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;/p&gt;
&lt;pre id=&quot;code_1625647227132&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;@SpringBootTest
public class DynamicClassTest {
    @Test
    void test(){
        StaticApplicationContext staticApplicationContext = new StaticApplicationContext();

        // 등록 전 빈 조회
        String[] beforeBeans = staticApplicationContext.getBeanNamesForType(DynamicClass.class);
        assertTrue(beforeBeans.length == 0);

        // 빈 등록
        ConfigurableListableBeanFactory beanFactory = staticApplicationContext.getBeanFactory();
        beanFactory.registerSingleton(&quot;dynamicClass&quot;, new DynamicClass());

        // 등록한 빈 조회 (by type)
        String[] afterBeans = staticApplicationContext.getBeanNamesForType(DynamicClass.class);
        assertTrue(afterBeans.length &amp;gt; 0);
        for (String beanName : afterBeans) {
            System.out.println(beanName);
        }

        // 등록한 빈 조회 (by bean name)
        Object dynamicClass = staticApplicationContext.getBean(&quot;dynamicClass&quot;);
        assertNotNull(dynamicClass);
        assertTrue(dynamicClass instanceof DynamicClass);
    }
}&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&gt;&lt;figure class=&quot;imageblock alignLeft&quot; data-origin-width=&quot;242&quot; data-origin-height=&quot;73&quot; data-ke-mobilestyle=&quot;widthOrigin&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/ET668/btq805sOYOW/EKApvLfKCfC9b01E0AEKK0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/ET668/btq805sOYOW/EKApvLfKCfC9b01E0AEKK0/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/ET668/btq805sOYOW/EKApvLfKCfC9b01E0AEKK0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FET668%2Fbtq805sOYOW%2FEKApvLfKCfC9b01E0AEKK0%2Fimg.png&quot; data-origin-width=&quot;242&quot; data-origin-height=&quot;73&quot; data-ke-mobilestyle=&quot;widthOrigin&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;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;</description>
      <category>SpringFramework/Spring</category>
      <author>lovineff</author>
      <guid isPermaLink="true">https://lovon.tistory.com/192</guid>
      <comments>https://lovon.tistory.com/192#entry192comment</comments>
      <pubDate>Wed, 7 Jul 2021 17:40:44 +0900</pubDate>
    </item>
    <item>
      <title>Default Method(인터페이스 디폴트 메소드)</title>
      <link>https://lovon.tistory.com/191</link>
      <description>&lt;p data-ke-size=&quot;size16&quot;&gt;추상 클래스내 함수 구현 처럼 Interface 클래스 내에 함수를 선언 및 구현할 수 있도록 한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Interface 구현 클래스에서 Override 할 수 있다.&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 data-ke-size=&quot;size16&quot;&gt;&quot;하위 호환성&quot; 때문에 등장&lt;/p&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;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;간단 코드로 보기&lt;/p&gt;
&lt;pre id=&quot;code_1625642596203&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;public class DefaultClass{
    public interface TestInterface {
    	// 선언 후 구현까지 해야한다.
        default void defaultFunction(){
            System.out.println(&quot;this is default function&quot;);
        }
    }

    private static class SubClass11 implements TestInterface{
    	// 기존 Interface 함수처럼 Override 가능
        @Override
        public void defaultFunction() {
            System.out.println(&quot;this is SubClass Function&quot;);
        }
    }

    private static class SubClass22 implements TestInterface{	
    	// default Function은 구현하지 않아도 된다.
    }

    public static void main(String[] args) {
        SubClass11 subClass11 = new SubClass11();
        SubClass22 subClass22 = new SubClass22();

        subClass11.defaultFunction();
        subClass22.defaultFunction();
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;실행 결과&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignLeft&quot; data-origin-width=&quot;228&quot; data-origin-height=&quot;52&quot; data-ke-mobilestyle=&quot;widthOrigin&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bjl5Eg/btq83jjVMXB/tYwKcrttmGtnHSzZeOA9Nk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bjl5Eg/btq83jjVMXB/tYwKcrttmGtnHSzZeOA9Nk/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bjl5Eg/btq83jjVMXB/tYwKcrttmGtnHSzZeOA9Nk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fbjl5Eg%2Fbtq83jjVMXB%2FtYwKcrttmGtnHSzZeOA9Nk%2Fimg.png&quot; data-origin-width=&quot;228&quot; data-origin-height=&quot;52&quot; data-ke-mobilestyle=&quot;widthOrigin&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;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;</description>
      <category>JAVA/JAVA</category>
      <author>lovineff</author>
      <guid isPermaLink="true">https://lovon.tistory.com/191</guid>
      <comments>https://lovon.tistory.com/191#entry191comment</comments>
      <pubDate>Wed, 7 Jul 2021 16:25:57 +0900</pubDate>
    </item>
    <item>
      <title>추상클래스를 사용한 통합 배치 관리 프로그램</title>
      <link>https://lovon.tistory.com/190</link>
      <description>&lt;p data-ke-size=&quot;size16&quot;&gt;배치 병렬 처리를 위한 쓰레드 빈 생성 클래스&lt;/p&gt;
&lt;pre id=&quot;code_1625556250466&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;@Configuration
@EnableAsync
public class ThreadConfig {
    @Bean(name=&quot;jobExecutor&quot;)
    public Executor jobExecutor(){
        ThreadPoolTaskExecutor taskExecutor = new ThreadPoolTaskExecutor();
        taskExecutor.setCorePoolSize(5);
        taskExecutor.setMaxPoolSize(10);
        taskExecutor.setQueueCapacity(30);
        taskExecutor.setThreadNamePrefix(&quot;jobThead-&quot;);
        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;p data-ke-size=&quot;size16&quot;&gt;메인 추상 클래스&lt;/p&gt;
&lt;pre id=&quot;code_1625797214141&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;@Setter
@Slf4j
public abstract class AbstractJob extends AbstractJobSub{
    private Long id;
    private String code;                // 코드
    private boolean isActive = false;   // 활성/비활성화
    private String cronRegExp;          // 크론 표현식
    private JobStatusEnum status;       // 잡 상태값
    private Long startWorkTime;         // 잡 시작 시간
    private Long endWorkTime;           // 잡 종료 시간

    /**
     * Job 실행전 수행
     */
    private void start(boolean isActive){
        jobIsWorking = true;
        startWorkTime = System.currentTimeMillis();

        changeJobStatus(id, JobStatusEnum.ING);
        makeJogLogs(id);

        if(isActive){
            logs(&quot;{} 수동 시작되었습니다.&quot;, code);
        }else{
            logs(&quot;{} 시작되었습니다.&quot;, code);
        }
    }

    /**
     * Job 종료 후 수행
     */
    private void end(){
        jobIsWorking = false;
        endWorkTime = System.currentTimeMillis();

        changeJobStatus(id, JobStatusEnum.WAIT);
        logs(&quot;{} 종료되었습니다.\t{}ms 소요되었습니다.&quot;, code, (endWorkTime - startWorkTime));
        jobIsSuccess();
    }

    /**
     * Job 종료 후 수행
     */
    private void interruptedExceptionEnd(){
        jobIsWorking = false;
        endWorkTime = System.currentTimeMillis();

        changeJobStatus(id, JobStatusEnum.WAIT);
        logs(&quot;{} 강제 종료되었습니다.\t{}ms 소요되었습니다.&quot;, code, (endWorkTime - startWorkTime));
        jobIsSuccess();
    }

    /**
     * Job 수행중 에러 발생시 처리
     * @param e: Exception
     */
    private void error(Exception e){
        jobIsWorking = false;
        changeJobStatus(id, JobStatusEnum.FAIL);
        logs(&quot;{} 오류가 발생하였습니다.&quot;, code);
        logs(&quot;{} / &quot; + e.toString(), code);
        jobIsFail();
    }

    /**
     * Job 활성화 상태 여부 검사
     * @return : 잡 활성화 상태 여부
     */
    private boolean checkActive(){
        if(jobIsWorking || status.equals(JobStatusEnum.ING)){
            log.info(&quot;{} 실행중 상태로 중복실행을 방지합니다.&quot;, code);
            return false;
        }

        // JOB 상태 검사
        if(!isActive){
            log.info(&quot;{} 실행 가능한 상태가 아닙니다&quot;, code);
            return false;
        }

        // 크론식 검사
        boolean validExpression = CronSequenceGenerator.isValidExpression(cronRegExp);
        if(!validExpression){
            log.info(&quot;{} cron 표현식이 잘못되었습니다. &amp;gt; {}&quot;, code, cronRegExp);
            return false;
        }

        return true;
    }

    /**
     * Job 실행 시간 검사, true인 경우 잡 수행
     * @return : 잡 수행 가능 여부
     */
    private boolean checkIsWorkTime(){
        // 1초전 날짜로 다음 실행시간을 구하고, 현재 시간과 비교하여 실행여부를 판단한다.
        cronSequenceGenerator = new CronSequenceGenerator(cronRegExp);
        Date nextJobDate = cronSequenceGenerator.next(nowDateMinusOneMinute.getTime());

        Calendar nextJobCalendar = Calendar.getInstance();
        nextJobCalendar.setTime(nextJobDate);
        nextJobCalendar.set(Calendar.MILLISECOND, 0);

        // 같은 시간이라면 실행한다.
        if(nowDate.compareTo(nextJobCalendar) == 0){
            return true;
//        }else{
//            log.info(&quot;{} 실행 가능한 시간이 아닙니다. {} : {}&quot;, code, simpleDateFormat.format(nowDate.getTime()), simpleDateFormat.format(nextJobCalendar.getTime()));
        }

        return false;
    }

    /**
     * 초단위 설정은 0으로 하드코딩
     * @param exp
     */
    public void setCronRegExp(String exp){
        cronRegExp = &quot;0 &quot; + exp;
    }

    /**
     * 실제 Job 수행 전체 로직
     * 활성화 여부 검사, 잡 실행시간 검사 후 통과된 경우 Job 실행
     */
    @Async(&quot;jobExecutor&quot;)
    public void run(){
        try {
            if (checkActive() &amp;amp;&amp;amp; checkIsWorkTime()) {
                start(false);
                job();
                end();
            }
        }catch (InterruptedException interruptedException){
            // 강제 종료시 일반 종료 처리
            interruptedExceptionEnd();
        }catch(Exception e){
            error(e);
            e.printStackTrace();
        }
    }

    /**
     * 즉시 실행 처리
     */
    @Async(&quot;jobExecutor&quot;)
    public void doRun(){
        try {
            if (checkActive()){
                start(true);
                job();
                end();
            }
        }catch (InterruptedException interruptedException){
            // 강제 종료시 일반 종료 처리
            interruptedExceptionEnd();
        }catch(Exception e){
            error(e);
            e.printStackTrace();
        }
    }

    /**
     * 실제 수행되는 Job 구현부
     * @throws Exception
     */
    public abstract void job() throws Exception;
}&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;pre id=&quot;code_1625555965115&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;@Setter
@Slf4j
public class AbstractJobSub {
    // Job 실행 상태 컨트롤용 변수
    public CronSequenceGenerator cronSequenceGenerator;
    public boolean jobIsWorking = false;
    public Calendar nowDateMinusOneMinute = Calendar.getInstance();
    public Calendar nowDate = Calendar.getInstance();

    // JobLogs 테이블 데이터 입력용 변수
    public Long jobLogsId;
    private StringBuilder logText = new StringBuilder();

    @Autowired
    private JobService jobService;

    /**
     * 현재 시간 설정 및 -1분 시간 설정
     * @param date
     */
    public void setNowDate(Calendar date){
        date.set(Calendar.MILLISECOND, 0);  // ms 제거

        nowDate.setTime(date.getTime());
        nowDateMinusOneMinute.setTime(date.getTime());
        nowDateMinusOneMinute.add(Calendar.SECOND, -1); // -1분 처리
    }

    /**
     * Job 실행 상태 수정
     * @param id : Job Id
     * @param jobStatusEnum : Job Status
     */
    public void changeJobStatus(Long id, JobStatusEnum jobStatusEnum){
        jobService.updateStatus(id, jobStatusEnum);
    }

    /**
     * 현재 실행중인 Job 즉시 중지(thread.interrupt 에러 발생)
     */
    public void emergencyStop(){
        Thread thread = Thread.currentThread();
        thread.interrupt();
    }

    /**
     * JobLogs 1로우 생성
     * @param id
     */
    public void makeJogLogs(Long id){
        this.jobLogsId = jobService.makeJobLogs(id);
        resetLogText();
    }

    /**
     * Job 성공시 JobLogs 상태값, Text 수정
     */
    public void jobIsSuccess(){
        jobService.jobIsSuccess(jobLogsId);
        updateLogText();
    }

    /**
     * Job 실패시 JobLogs 상태값, Text 수정
     */
    public void jobIsFail(){
        jobService.jobIsFail(jobLogsId);
        updateLogText();
    }

    /**
     * 빈 로그 텍스트 초기화
     */
    private void resetLogText(){
        this.logText.setLength(0);
    }

    /**
     * 잡 실행 로그를 DB에 적재
     * @param text
     */
    public void logs(String text, Object... arguments){
        LocalDateTime now = LocalDateTime.now();

        logText.append(now.format(DateTimeFormatter.ofPattern(&quot;yyyy-MM-dd HH:mm:ss.SSS&quot;)))
                .append(&quot;\t&quot;)
                .append(Thread.currentThread().getName())
                .append(&quot;\t&quot;)
                .append(makeLogText(text, arguments))
                .append(&quot;\n&quot;);

        log.info(text, arguments);

        // 2000자 이상의 텍스트가 적재 대기중인 경우 적재 후 초기화
        if(logText.length() &amp;gt; 2000){
            updateLogText();
            resetLogText();
        }
    }

    /**
     * log_text 컬럼 update
     */
    private void updateLogText(){
        if(logText.length() &amp;gt; 0){
            jobService.updateLogText(jobLogsId, logText.toString());
        }
    }

    private String makeLogText(String text, Object ...args){
        if(StringUtils.isEmpty(text)){
            return &quot;&quot;;
        }

        if(!text.contains(&quot;{}&quot;)){
            return text;
        }

        StringBuilder sb = new StringBuilder();
        String[] split = text.split(&quot;\\{}&quot;);

        for (int i = 0; i &amp;lt; split.length; i++) {
            if(StringUtils.isEmpty(split[i].replace(&quot;{}&quot;, &quot;&quot;).trim())){
                sb.append(args.length &amp;lt; i + 1 ? &quot;&quot; : args[i]);
            }else{
                sb.append(split[i].replace(&quot;{}&quot;, &quot;&quot;)).append(args.length &amp;lt; i + 1 ? &quot;&quot; : args[i]);
            }
        }

        return sb.toString();
    }
}​&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;JOB 배치 구현 클래스&lt;/p&gt;
&lt;pre id=&quot;code_1625556024131&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;@Slf4j
@Component
public class TestJob1 extends AbstractJob {
    @Override
    public void job() throws Exception {
        for (int i = 0; i &amp;lt; 10; i++) {
            logs(&quot;testjob1 is working&quot;);
            Thread.sleep(500);

            if(i == 5){
                emergencyStop();
            }
        }
    }
}&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;등록된 JOB을 실행시키는 클래스&lt;/p&gt;
&lt;pre id=&quot;code_1625556095712&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;@Slf4j
@Component
@RequiredArgsConstructor
public class MainJob {
    private final ApplicationContext applicationContext;
    private final JobService jobService;
    private final String hostName = SystemUtils.getLocalHostname();

    /**
     * 1분마다 실행되어 조건에 맞는 잡을 실행한다.
     * @throws Exception
     */
    @Scheduled(cron = &quot;0 * * * * *&quot;)
    public void mainJob() throws Exception {
        // 실행 전 활성화 상태 대상 중 기간이 벗어난 목록 수정
        jobService.findExpiredAndActiveJobAndSetNotActive(hostName);

        // 실행 대상 job 목록 조회
        List&amp;lt;JobsModel&amp;gt; allJobsHostEquals = jobService.findWorkJobs(hostName);

        log.info(&quot;{} job size &amp;gt; {}&quot;, hostName, allJobsHostEquals.size());

        Calendar nowDate = Calendar.getInstance();

		// 아래 for문은 병렬 처리가 된다.
        for (JobsModel jobsModel : allJobsHostEquals) {
            // 실행 중 상태이면 수행하지 않음.
            if(jobsModel.getStatus().equals(JobStatusEnum.ING)){
                continue;
            }

            try{
                AbstractJob jobBean = (AbstractJob) applicationContext.getBean(jobsModel.getClassName());
                jobBean.setId(jobsModel.getId());
                jobBean.setNowDate(nowDate);
                jobBean.setCode(jobsModel.getCode());
                jobBean.setActive(jobsModel.getIsActive());
                jobBean.setCronRegExp(jobsModel.getCronRegExp());
                jobBean.setStatus(jobsModel.getStatus());

				// 잡 실행
                jobBean.run();
            }catch(NoSuchBeanDefinitionException noSuchBeanDefinitionException){
                log.error(&quot;존재하지 않는 Bean 이름 입니다. &amp;gt; {}&quot;, jobsModel.getClassName());
            }
        }
    }
}&lt;/code&gt;&lt;/pre&gt;</description>
      <category>SpringFramework/Spring</category>
      <author>lovineff</author>
      <guid isPermaLink="true">https://lovon.tistory.com/190</guid>
      <comments>https://lovon.tistory.com/190#entry190comment</comments>
      <pubDate>Tue, 6 Jul 2021 16:21:38 +0900</pubDate>
    </item>
    <item>
      <title>Cron 정규식 검사</title>
      <link>https://lovon.tistory.com/189</link>
      <description>&lt;pre id=&quot;code_1625555263801&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;import org.springframework.scheduling.support.CronSequenceGenerator;

boolean validExpression = CronSequenceGenerator.isValidExpression(cronRegExp);
if(!validExpression){
	log.info(&quot;{} cron 표현식이 잘못되었습니다. &amp;gt; {}&quot;, code, cronRegExp);
	return false;
}&lt;/code&gt;&lt;/pre&gt;</description>
      <category>SpringFramework/Spring</category>
      <author>lovineff</author>
      <guid isPermaLink="true">https://lovon.tistory.com/189</guid>
      <comments>https://lovon.tistory.com/189#entry189comment</comments>
      <pubDate>Tue, 6 Jul 2021 16:08:06 +0900</pubDate>
    </item>
  </channel>
</rss>