Log Stash

as an Industrial Personnel

Note

TIL: 스칼라(jvm)에서 JS코드 실행하기

SavvyTuna 2017. 10. 9. 19:20

0. tl;dr

JVM에 달려있는 ScriptEngine 클래스를 사용한다.

https://stackoverflow.com/questions/36764639/how-to-run-javascript-code-from-within-scala-jvm

다만 메소드의 갯수나, 하나의 메소드에서 바이트 코드의 길이가 65536을 넘어가는 스크립트 코드는 저 클래스에서 돌릴 수 없다. 따로 방법을 찾아야 함.

1. Response

전부터 비정기적으로 크롤링을 하던 사이트가 하나 있었다. 그 사이트 특성상 한 페이지에서 ajax로 api 호출을 여러번 하는데, 그 단계중의 하나로 어떤 '좌표'값을 받아와 그 값들을 다음 api의 파라메터로 넘겨줘야하는게 있었다.

기존엔 '이래도 되나' 싶을정도로 간단하게 좌표값을 얻어올 수 있었다. 예를들면,

_x = "86fd3dc623705c0e42df1565a6bdaf08", _y = "63367a1dab3659fd5f3896e8a3f1da42" 

이게 서버에서 보내주는 리스폰스였다. 정말 정직하게 _x, _y값들을 던져주기에, 그냥 정규식에서 그룹 캡쳐하는 기능으로 값들을 뽑아내서 쓰고 있었다.

그러다 최근에 갑자기 크롤러가 정상적으로 작동을 안 하길래 살펴보니, 저 '좌표 얻어오는' 단계의 리스폰스가 이렇게 바뀌어 있었다.

 

 

대충 보면 알겠지만 방글방글 웃고 있는 이모티콘(o^_^c)처럼 보이게 난독화시킨 자바스크립트 코드다. (사실 바뀌기 전의 리스폰스도 마찬가지로 자바스크립트였다는 사실을 이 때 깨달았다.)

다른 좌표가 응답으로 와야하는 경우, 응답으로 오는 자바스크립트 코드의 내용도 달라졌다. 당연히 저 리스폰스는 _x, _y 좌표를 계산하는 코드일 것이라고 생각하고 웹 브라우저 콘솔에서 한 번 테스트를 해봤다.

 

 

콘솔을 바로 열었을 땐 당연히 좌표값이 없는 변수였는데,

 

 

위 코드를 복붙, 실행만 하고 다시 찾아보니 좌표값이 세팅된 변수로 나온다.

 

 

 

beautify해서 본 코드. 1~21까지 변수들을 세팅하고 22번줄에서 _x, _y값을 세팅한다.

원래 사이트에서 저 api를 호출하는 부분도 찾아보니 마찬가지로, 저렇게 응답으로 온 자바스크립트 코드를 실행시켜서 웹 브라우저 js vm위에 전역변수값을 세팅해 놓고, 나중에 그 값들을 써먹는 방식으로 사용하고 있었다.

2. Run

그럼 이제 저 자바스크립트 코드를 실행시켜서 _x, _y값만 얻어오면 된다. 크롤링하는 스크립트는 스칼라로 작성해오고 있었기에, 스칼라(jvm)위에서 자바스크립트를 돌릴 수 있는 기상천외한 방법이 혹시 있는지 일단 구글링을 해봤다.

그랬는데 있더라. '자바' VM에서 '자바'스크립트를 구동시켜야 할 일이 얼마나 된다고 만들어놨는지는 모르겠지만 ScriptEngineManager라는 클래스가 자바 라이브러리로 있다는 스택 오버플로우 답변이 찾아졌었다. 나의 경우엔 자바스크립트 코드를 eval() 함수로 평가(실행) 한 다음, get() 메소드로 엔진 내부의 좌표 변수 값들을 가져오게 하면 별 문제 없이 깔끔하게 해결할 수 있을것 같았고, 그대로 해봤는데, 안됐다.

Exception in thread "main" java.lang.RuntimeException: Method code too large! 

문제는 자바스크립트 코드의 길이였다. 문자열의 길이가 길었던게 직접적으로 문제가 된게 아니라, lexer를 돌리고, AST를 만들어서 결과적으로 나온 바이트 코드의 길이가 너무 길었다. 저 ScriptEngineManager는 내부적으로 Nashorn이라는 자바스크립트 엔진을 쓰고 있는데, 이 엔진은 메소드 하나당 바이트 코드 길이를 65536미만으로 제한하고 있었다. 리스폰스로 받아온 코드로 만들어진 바이트 코드의 길이는 12만을 훌쩍 넘기고 있었고. 그래서 안타깝게도 이 방법을 못 쓰게 되었다. 스칼라(또는 자바)에서 쓸 수 있는 다른 js엔진도 없었고, (나중에 보니 모질라의 라이노 엔진이란것도 있었지만) js 구문 분석을 해서 수식을 좀 더 간단하게 치환하긴 싫었고.

'애초에 node.js같은 자바스크립트 기반으로 크롤러를 작성할걸' 하는 후회의 시간을 잠깐 가진 다음, 그냥 깔끔하진 않지만 제일 무책임하면서 확실한 방법을 쓰기로 했다. 스칼라에서 js코드를 파일에 작성해서 넘기든, echo해서 파이프로 넘기든 해서 node.js위에서 실행시키게 한 다음, 결과값만 받아오기로. 그래서 그렇게 했고, 이번엔 (당연히) 됐다.

try {
  // Create a temp file
  val file: File = File.createTempFile(s"${mid}_${eid}", ".tmp")

  // Write the result (JavaScript code) into the file
  val out: PrintWriter = new PrintWriter(file)
  out.println(jsScript)
  out.flush()
  out.close()

  // Invoke Node.js
  val res: String = s"node ${file.getAbsolutePath}" !!
  val arr: Array[String] = res.split("\n")

  file.deleteOnExit()

  // Fetch result
  if (arr.length >= 2)
    Some(arr(0), arr(1))
  else
    None
} catch {
  case e: Exception =>
    None
}

결국 스칼라 코드 사이에 절차적인 코드가 끼어들어가게 되어버렸다.

다른 프로그램에게 의존성이 생기게 되어버려서 깔끔한 방법은 아니지만... 나중에 고쳐야지.

그나저나 왜 리스폰스가 갑자기 바뀌었는지 궁금하다. 원래 크롤러를 파이썬으로 만들었다가 스칼라로 최근에 다시 작성했는데, 그 때 디버깅하느라 api 호출을 많이 해서 그런가...