본문 바로가기
JAVA

[Java] 실시간 프로세스 바 구현 방법 (비동기 방식)

by GoodDayDeveloper 2024. 12. 5.
반응형

 

 

 

큰 규모의 사이트를 담당하다보면 응답시간이 긴 트랜잭션을 처리할 경우가 발생하게 됩니다.

 

이럴 경우에 사용자는 트랜잭션이 끝날때까지 사용자들은 한 없이 기다리는 경우가 발생하는데

 

비동기 방식을 통해서 사용자의 경험을 개선할 수 있습니다.

 

 

간단히 구현한 화면입니다.

 

 

 

비동기 방식을 적용하였기에 일정 시간을 따라 프로세스 시간을 체크하여 사용자에게 제공할 수 있습니다.

 

 

응답시간이 긴 트랜잭션을 비동기로 처리할 경우의 이점들을 간단히 작성해보았습니다.

 

 

 

응답성 유지:

 

다른 작업이 트랜잭션이 완료될 때까지 기다리지 않고 계속 실행될 수 있으며 사용자 인터페이스(UI) 응용 프로그램에서 중요한 역할을 합니다.

 

 

자원 효율성:

 

비동기 트랜잭션 처리는 스레드 풀이나 이벤트 루프를 사용하여 자원을 효율적으로 관리할 수 있습니다. 이를 통해 컨텍스트 스위칭 비용을 줄이고 시스템 리소스를 보다 효율적으로 사용할 수 있습니다.

 

 

확장성:

 

비동기 로직은 많은 수의 트랜잭션을 동시에 처리할 수 있도록 도와주어 시스템의 확장성을 높입니다. 특히 서버나 대규모 분산 시스템에서 중요합니다.

 

 

 

비동기 요청을 통하여 서버에서 처리를 하고 다시 스크립트로 처리하는 방식을 설명해보겠습니다.

 

 

 

[Java] 실시간 프로세스 바 구현 방법 (비동기 방식)

 


SCRIPT

 

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
var popup = null 
 
const startProcess = async () => {
    
    popup = createProgressPopup();
    
    
    var submitObj = new Object();
    submitObj['searchBgnDe'= $("#searchBgnDe").val();
    submitObj['searchEndDe'= $("#searchEndDe").val();
    
    try {
        // 처리 시작 요청
        const response = await fetch('/test/create.do', {
            method: 'POST',
            headers: {
                'Content-Type''application/json'
            },
            body: JSON.stringify(submitObj)
        });
        
        const jobId = await response.text();
        
        // 진행상황 체크 시작
        checkProgress(jobId);
        
    } catch (error) {
        console.error('Error starting process:', error);
    }
    
};
 
 
// 팝업 생성 함수
function createProgressPopup() {
    // 기존 팝업 제거
    const existingPopup = document.querySelector('.popup-overlay');
    if (existingPopup) {
        existingPopup.remove();
    }
 
    // 오버레이 생성
    const overlay = document.createElement('div');
    overlay.className = 'popup-overlay';
    overlay.innerHTML = overlay.innerHTML = '<div class="popup-container"><div class="text-center"><div class="text-xl font-semibold text-gray-800 mb-4">처리 중입니다. 잠시만 기다려주세요.</div><div class="progress-bar mb-3"><div class="inner"></div></div><div id="progressPercentage" class="text-lg font-medium text-gray-700">0%</div></div></div>';
 
    // 문서에 추가
    document.body.appendChild(overlay);
    
    overlay.querySelector('.progress-bar .inner').style.width = '0%';
 
    return {
        progressBar: overlay.querySelector('.progress-bar .inner'),
        progressText: overlay.querySelector('.text-xl'),
        progressPercentage: overlay.querySelector('#progressPercentage'),
        popupContainer: overlay.querySelector('.popup-container'),
        overlay: overlay
    };
}
cs

 

 

우선 저는 두개의 날짜를 가지고 와서 두개의 날짜 사이의 데이터들을 처리하는 로직을 설계하였습니다.

 

우선 createProgressPopup함수를 통해서 팝업 생성을 해줍니다.

 

그리고 비동기 방식을 이용하여 서버에 POST 방식으로 URL을 보내고 데이터를 보냅니다. 

 

서버에서는 ID값을 반환하아는데 그 아이디 값을 가지고

 

checkProgress을 통하여 실시간 체크를 할 수 있는 로직을 타게 됩니다.

 

 

 

JAVA

 

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
@RequestMapping(value="/test/create.do", method = RequestMethod.POST)
@ResponseBody
public ResponseEntity<String>  test1(
        @ModelAttribute("TEST") TEST TEST,
        @RequestBody String filterJSON,
        HttpServletRequest request,
        HttpServletResponse res,
        ModelMap model ) throws Exception {
    
    ObjectMapper mapper = new ObjectMapper();
    test vo = (TEST)mapper.readValue(filterJSON,new TypeReference<TEST>(){ });
 
    Date now = new Date();
    Connection connection = null;
    job_VO job = new job_VO();
    SimpleDateFormat dateFormat = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
    String jobId = String.valueOf(System.currentTimeMillis());
    
    try {
        
        Class.forName("com.mysql.jdbc.Driver");
        connection = DriverManager.getConnection("url""id""password");
        
        connection.setAutoCommit(false);
        
                    
        String sql1 = "INSERT INTO tableName(JOBID,2,3) VALUES(?,?,?)";
        PreparedStatement pstmt1 = connection.prepareStatement(sql1);
        pstmt1.setString(1, jobId);
        pstmt1.setInt(2,  0);
        pstmt1.setString(3"3");
    
        pstmt1.executeUpdate();
        pstmt1.close();
        
        connection.commit();
        
        //날짜 포맷
        DateTimeFormatter formatter2 = DateTimeFormatter.ofPattern("yyyy-MM-dd");
        
        LocalDate startDate = LocalDate.parse(vo.getSearchBgnDe(), formatter2);
        LocalDate endDate = LocalDate.parse(vo.getSearchEndDe(), formatter2);
        
        List<String> dateList = new ArrayList<>();
        
        LocalDate currentDate = startDate;
        while (!currentDate.isAfter(endDate)) {
            // 날짜를 문자열로 변환하여 리스트에 추가
            dateList.add(currentDate.format(formatter2));
            currentDate = currentDate.plusDays(1); // 하루씩 증가
        }
        
        
        for (int i = 0; i < dateList.size(); i++) {
            
    
            //로직 수행
            
            // 진행률 업데이트
            int progress = (i + 1* 100 / dateList.size();
            
            if(progress != 100) {
                String sql2 = "UPDATE tableName SET PROGRESS = ? WHERE JOBID = ?";
                PreparedStatement pstmt2 = connection.prepareStatement(sql2);
                pstmt2.setInt(1,  progress);
                pstmt2.setString(2, jobId);
                pstmt2.executeUpdate();
                pstmt2.close();
            }else {
                job.setSTATUS("COMPLETED");
                
                String sql3 = "UPDATE tableName SET STATUS = ?,ENDTIME = ?,PROGRESS = ? WHERE JOBID = ?";
                PreparedStatement pstmt3 = connection.prepareStatement(sql3);
                pstmt3.setString(1"COMPLETED");
                pstmt3.setString(2, dateFormat.format(now));
                pstmt3.setInt(3,  progress);
                pstmt3.setString(4, jobId);
                pstmt3.executeUpdate();
                pstmt3.close();
            }
            
            connection.commit();
            
            // 처리 중간에 잠시 쉬어줌 (서버 부하 방지)
            Thread.sleep(1000);
        }
 
        
    } catch (Exception e) {
        log.error("Error processing transaction", e);
        
        String sql4 = "UPDATE tableName SET STATUS = ?,ERRORMESSAGE = ?,ENDTIME = ? WHERE JOBID = ?";
        PreparedStatement pstmt4 = connection.prepareStatement(sql4);
        pstmt4.setString(1"FAILED");
        pstmt4.setString(2, e.getMessage());
        pstmt4.setString(3, dateFormat.format(now));
        pstmt4.setString(4, jobId);
        pstmt4.executeUpdate();
        pstmt4.close();
        
        connection.commit();
        connection.setAutoCommit(true);
    }finally {
        try {
            if (connection != null) {
                connection.setAutoCommit(true);
                connection.close();
            }
        } catch (SQLException e) {
            e.printStackTrace();
        }
    }
    
 
    return new ResponseEntity<>(jobId, HttpStatus.OK);
 
}
cs

 

보기 편하기 위해 코드를 전부 붙여서 나열해놓았습니다.

 

구현 메서드의 코드를 가지고 왔는데 구현 메서드에는 @Async 어노테이션이 있어야 비동기 방식이 가능합니다.

 

 

간략히 설명하면 

 

일단 날짜 데이터를 가지고 온 다음에 currentTimeMillis을 통하여 유니크한 ID값을 만들어줍니다.

 

그 다음 Connection 객체를 이용하여 데이터베이스에 접근한 후에 setAutoCommit을 FASLE로 꺼줍니다.

 

그리고 PreparedStatement 객체를 이용하여 쿼리를 실행한 다음 그때마다 Commit을 해줍니다.

 

그래야 실시간으로 데이터베이스 반영이 되고 그것을 체크할 수 있기 때문입니다.

 

반응형

 

JAVA

 

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
@RequestMapping(value="/test/list_{jobId}.do",method = RequestMethod.POST)
@ResponseBody
public String  test2(
        @ModelAttribute("test") test test,
        @PathVariable("jobId"String jobId,
        ModelMap model,
        HttpServletResponse res,HttpServletRequest request
        ) throws Exception {
    
    res.setContentType("text/html; charset=UTF-8");
    PrintWriter out = res.getWriter();
    JSONObject obj = new JSONObject();
    
    try{
 
        //프로세스 아이디값으로 조회
        job_VO progressVO = testService.getProgress(jobId);
 
        obj.put("success",true); 
        obj.put("msg","작업이 완료 되었습니다.");
        obj.put("progressVO",progressVO); 
 
    }catch(Exception e){
 
        obj.put("success",false); 
        obj.put("msg",e.getMessage());
        System.out.println("/test/list_{jobId}.do : " + e.toString());
    }
 
    out.print(obj);
    return null;
 
}
cs

 

 

@PathVariable("jobId") String jobId 을 통해서 jobId을 받아 변수에 저장을 할 수 있습니다.

 

ID값을 받아서 서버에서 프로세스 ID를 조회한 다음 JSON 형태로 프린트해줍니다.

 

 

 

SCRIPT

 

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
const checkProgress = async (jobId) => {
    const progressCheck = setInterval(async () => {
        try {
 
            $.ajax({ 
                url: '/test/list_'+jobId+'.do'
                type: "POST"
                contentType: "application/json;charset=UTF-8",
                dataType : "json",
                progress: true
                }) 
                .done(function(data) {
                    
                if(!data.success){
                    clearInterval(progressCheck);
                    popup.progressText.textContent = res.msg;
                    updatePopupToComplete();
                }
                
                // 프로그레스바 너비 업데이트
                popup.progressBar.style.width = data.progressVO.PROGRESS+'%';
                // 진행 퍼센트 텍스트 업데이트
                popup.progressPercentage.textContent = data.progressVO.PROGRESS+'%';
                
                // 완료 또는 실패 시 체크 중단
                if (data.progressVO.STATUS === 'COMPLETED' || data.progressVO.STATUS === 'FAILED') {
                    clearInterval(progressCheck);
                    if (data.progressVO.STATUS === 'FAILED') {
                        popup.progressText.textContent = '작업이 실패 되었습니다.';
                        updatePopupToComplete();
                    } else {
                        popup.progressText.textContent = '작업이 완료 되었습니다.';
                        updatePopupToComplete();
                    }
                }
                
                }) 
                .fail(function(e) {  
                    alert("FAIL - "+e);
                    clearInterval(progressCheck);
                    popup.progressText.textContent = '작업이 실패 되었습니다.';
                    updatePopupToComplete();
                }) 
                .always(function() {       }); 
            
        } catch (error) {
            clearInterval(progressCheck);
            popup.progressText.textContent = '작업이 실패 되었습니다.';
            updatePopupToComplete();
        }
    }, 2000); 
};
 
 
 
// 팝업 닫기 함수
function closeProgressPopup(overlay) {
    if (overlay) {
        overlay.remove();
    }
}
 
 
function updatePopupToComplete() {
 
    // 닫기 버튼 생성
    const closeBtn = document.createElement('button');
    closeBtn.className = 'close-btn';
    closeBtn.setAttribute('aria-label''닫기');
    closeBtn.innerHTML = '<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke="currentColor"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M6 18L18 6M6 6l12 12" /></svg>';
    
    // 닫기 버튼 이벤트 리스너 추가
    closeBtn.addEventListener('click', () => {
        closeProgressPopup(popup.overlay);
        location.reload();
    });
 
    // 팝업 컨테이너에 닫기 버튼 추가
    popup.popupContainer.appendChild(closeBtn);
    
    
}
cs

 

 

checkProgress을 시작으로 비동기 함수를 실행하게 됩니다.

setInterval을 통해서 지정된 시간마다 함수를 반복해서 호출하는 함수입니다.

즉, clearInterval을 통해 정지하지 않는 이상 무한으로 반복되게 됩니다.


반복 시간을 설정하여 URL을 통해 ID값을 전송을 하고 내려 받은 데이터를 통해서 프로세스바를 업데이트 시켜줍니다.

PROGRESS 변수에 수치를 넣어 업데이트를 시켜주면서 상태값에 따라 실패와 완료로 나뉘게 됩니다.


그리고 updatePopupToComplete 함수를 통해 완료시에 닫기 버튼을 생성해주면 완성이 됩니다.



 

 

반응형

댓글