Tags dev

비동기 방식이란?

동기 VS 비동기

비동기 방식에 대해서 이야기하기전에 동기와 비동기의 차이부터 짚고 넘어가야 할 것 같다.

1. 동기(Synchronous : 동시에 일어나는)

동기는 말 그대로 동시에 일어난다는 뜻입니다. 요청과 그 결과가 동시에 일어난다는 것입니다.

장점

  • 설계가 간단하고 직관적이다.

단점

  • 결과가 주어질 때까지 아무것도 못하고 대기해야 한다.

2. 비동기(Asynchronous : 동시에 일어나지 않는)

비동기는 동시에 일어나지 않는다는 뜻입니다. 요청과 결과가 동시에 일어나지 않은것이라는 하나의 약속입니다.

단점

  • 설계가 동기방식보다 비교적 복잡하다.

장점

  • 결과가 주어지는데 시간이 걸리더라도 그 시간 동안 다른 작업을 할 수 있다.

동기 방식을 비동기 방식으로 바꾸다

AliExpress 크롤링프로젝트

  1. Issue - CPU 점유율 증가

    • 해당 프로젝트는 초기에 동기방식으로 설계를 진행했는데, CPU 사용량 이슈때문에 비동기방식으로 튜닝을 진행했다.
  2. Approach - Cycle 속도를 늦춰보자

    • 초기 동기방식 설계의 프로그램은 평균 CPU 점유율이 50% 내외를 유지하였다. 이러한 문제 때문에, 실사용에서 프로그램을 구동중에는 다른 작업을 하기 힘들었다. 문제를 해결하기 위해서 Thread.sleep(long delay) 를 이용해서 사이클 속도를 늦추면 어떨까라는 생각에 적용 및 테스트를 진행하였는데 당연하게도 CPU 점유율은 낮아졌다. 30%내외를 유지하였으며 Main thread를 제외하고 3개의 thread가 동작하고 있기 때문에 눈에띄는 효과를 얻을 수 있었다.
  3. Another Issue - 전체 속도 저하

    • Cycle 속도 저하를 통해서 첫 번째 이슈는 처리했다. 하지만, 이 때문에 다른 문제가 생겼다. 자연스레 처리 속도까지 저하된 것이다. 하지만 처리속도를 높이자니 CPU 점유율 issue를 되돌릴 수는 없는 노릇이다.
  4. Approach - 비동기 설계로의 튜닝

    • 처리 속도는 유지하면서, 점유율 문제까지 해결해보자. Java에서는 Thread에 wait과 notify라는 형태의 비동기 방식을 지원한다. 쓰레드와 상태제어 - programmers 를 참고해보면 ‘wait과 notify는 동기화된 블록안에서 사용해야 한다. wait를 만나게 되면 해당 쓰레드는 해당 객체의 모니터링 락에 대한 권한을 가지고 있다면 모니터링 락의 권한을 놓고 대기한다.’

비동기 설계로 바꾼 후

1
2
3
4
5
6
7
8
9
10
11
/*Crawler.java*/
+ private static Crawler instance;

public Crawler() {
setDaemon(true);
+ instance = this;
}

+ public static Crawler getInstance() {
+ return instance;
+ }

+는 Git의 commit 기록을 보며 추가된 부분을 표시한 것이다.
위의 Crawler.java를 보면 어딘가 친숙한 코드를 볼 수 있다. 바로 Singleton pattern이다. 좀 더 정확하게 하자면, Singleton pattern 을 변형하여 같은 코드블럭에 있지 않더라도 접근 할 수 있도록 설계를 수정한 것이다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
/*Writer.java*/

@Override
public void run() {
while (crawlerAlive.get() || !crawledData.isEmpty()) {
+ synchronized (Crawler.getInstance()) {
+ try {
+// asynchronous control
+ if(crawledData.isEmpty())
+ Crawler.getInstance().wait(1000 * 30);
+ } catch (InterruptedException e) {
+ e.printStackTrace();
+ }
+ }
Data d = crawledData.poll();
if (d != null) {
if (wroteCount.get() % 500 == 0)
copyExcel();
writeExcel(d);
wroteCount.getAndIncrement();
}
}
writerAlive = false;
}
dev
11월 3 2018

AliExpress 크롤링 프로젝트

프로젝트 정보

  • 시작일자 : 2018.11.01

  • 사용언어 : Java, JS(아주 조금)

  • 사용기술

    • JAVAFX
    • JAVA-Maven
    • Selenium
  • 개발목적 : 외주

개발 상황 정보

TODO

DONE

  • UI 디자인
  • Reader 구현
  • Crawler 구현
  • ImageResize Util
  • Image Differ 구현
  • Excel Writer 구현
  • 가격 설정 구현
  • 자동종료 + 재시작
  • 수집 완료시 Modal창
  • 비동기 튜닝

SCREENSHOT

crawler-ui-capture

문제점(?)

  1. thread를 daemon으로 설정하여도, main thread가 종료될 때, finalize를 호출하지 않는 듯 하다. 그래서, selenium과 chromedriver를 이용하는 특성 때문에 직접적으로 reader.exit() 메소드를 호출 한 후에 main thread를 종료하도록 설정하였다.

  2. aliexpress를 js 사용 없이 확인해보면 lazy - loading이 적용되어있는 것을 확인 할 수 있는데, img 태그에 image-src 라는 속성을 잠시동안 줌으로써 page의 빠른 로딩을 꾀하였다.

  3. java의 (iterable.)forEach method는 for문과 같이 동작하는게 아니라서 break를 쓸 수 없다. 그래서 만든것이 RuntimeException을 상속하여 BreakException이라는 custom Exception을 내부적으로 사용했다. Exception이 발생하면 그것을 catch해 forEach구문을 탈출할 수 있게 한 것이다. 이것이 for/while loop + break 와 비교했을 때 어떤 성능저하를 불러올지는 모르겠지만, 편하게 forEach를 사용하기 위해서 꼭 필요한 존재임은 확실하다.

“BreakException.java”

1
2
3
4
5
6
7
8
9
10
package utils;

public class BreakException extends RuntimeException{

private static final long serialVersionUID = -2431473453374656968L;

public BreakException(String arg) {
super(arg);
}
}

“ImageDiffer.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
try {
if (targetImage != null) {
imgs.forEach(imgName -> {
double diffPercent = ImageDiff.getDifferencePercent(imageDiffPath + "\\" + imgName,
targetImage);
if (diffPercent < 10.00) {
throw new BreakException("same img found :: " + imgName);
}
});
passedCount.getAndIncrement();
String name = getNextImgName();
saveImg(imageSavePath, name, targetImage);
saveImg(imageDiffPath, name, targetImage);
imgs.add(name);
d.setMainImg(name);
// System.out.println("DIFF :: " + d);
passedData.add(d);
}
} catch (BreakException e) {
System.out.println(e.getMessage());
continue;
// 같은 이미지 발견
} catch (IOException e) {
e.printStackTrace();
}

  1. javaFX 관련 문제점
    javaFX Thread가 아닌 Thread (따로 작성한 thread class, 익명 thread class 등..) 에서 javaFX UI에 관련된 명령을 실행할 경우

<java.lang.IllegalStateException: Not on FX application thread; currentThread = Thread-9>

위와 같은 Exception이 발생하게 되어있다. 따라서, 해당 명령을 javaFX Thread가 아닌 곳에서 실행시키고 싶을 경우
다음과 같은 코드로 익명 javaFX thread를 생성 및 실행할 수 있다.

1
2
3
4

Platform.runLater( ()-> { // java lambda expression
doStuffRelatedJavaFx();
});
  1. javaFX application의 재시작 구성

“AECController.java” 에서의 restart() method

1
2
3
4
5
public void restart() {
reader.exit(); // reader 내에 chromeDriver를 다루고 있기 때문에 정상동작을 위해서는 reader를 종료한 후에 동작해야 한다.
saveConfs();
Main.restart();
}

“Main.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

package application;

import javafx.application.Application;
import javafx.application.Platform;
import javafx.stage.Stage;
import javafx.scene.Scene;
import javafx.scene.layout.AnchorPane;
import javafx.fxml.FXMLLoader;


public class Main extends Application {

private static Stage stage; // stage를 종료하기 위해서는 static으로 설정하는것이 좋다.

@Override
public void start(Stage primaryStage) {
try {
AnchorPane root = (AnchorPane)FXMLLoader.load(getClass().getResource("ali.fxml"));
Scene scene = new Scene(root);
primaryStage.setScene(scene);
primaryStage.show();
primaryStage.setResizable(false);
stage = primaryStage;
} catch(Exception e) {
e.printStackTrace();
}
}


public static void restart() {
stage.close();
Platform.runLater( ()->{
new Main().start( new Stage() );
});
}

public static void main(String[] args) {
launch(args);
}
}

COMMENT

[2018-11-03] 개발에 지칠땐 포스팅이 최고인듯 하다.
[2018-11-06] 일단 구현하려했던 것들을 모두 구현하였다. 추가적으로 작업할 것은 남아있지만 이 정도 개발속도면 나름 만족스럽다.
[2018-11-27] 마지막으로 기능 추가요청과 함께, 프로그램 사용에 따른 전체 PC 속도 저하문제에 관한 분석 요청을 해주셨는데 그에 따라 단순 multi-threading 을 비동기방식으로 튜닝하여 CPU usage reducing에 성공하였다.

dev
1
Powered by Hexo
Original Theme Weightless
Owner Dev.secr3t