 |  |
|
난이도 : 중급 Tom McQueeney, Lead Technical Consultant, Idea Integration
2007 년 12 월 04 일 Java SE 6에 추가된 자바™ 스크립팅 API와 Java SE 5와의 백워드 호환성으로 인해 수십 개의 스크립팅 언어들이 자바 애플리케이션에서 간단하고 일관된 방식으로 런타임 시 호출될 수 있습니다. 본 시리즈 Part 1에서는 API의 기본 기능을 소개했습니다. Part 2에서는 API의 기능을 더욱 자세히 살펴볼 것이며, Ruby, Groovy, JavaScript로 작성된 외부 스크립트들이 런타임 시 실행 및 변경되어 애플리케이션을 중지하거나 재시작 하지 않고도 어떻게 비즈니스 로직을 변경하는지를 설명합니다.
Java SE 6에 추가된 자바 스크립팅 API는 다양한 동적 언어들로 작성된 외부 프로그램들과 코드와 데이터를 공유 및 실행하는 일관된 방식을 제공한다. 자바 애플리케이션에 스크립팅 언어의 힘과 유연성을 더함으로써 스크립팅 언어가 작업을 보다 깨끗하고 간단하고 정밀하게 수행할 수 있도록 한다. 하지만, 자바 스크립팅 API는 수십 개의 스크립팅 언어들이 자바 프로그램에 일관된 방식으로 추가될 수 있게만 하는 것은 아니다. 런타임 시 스크립트가 배치되고, 읽혀지고, 실행될 수 있도록 한다. 이러한 동적인 기능으로 인해, 프로그램이 실행 중인 동안, 스크립트를 수정하여 애플리케이션의 로직을 변경할 수 있다. 이 글에서는 자바 스크립팅 API를 사용하여 외부 스크립트를 호출하여 프로그램의 로직을 동적으로 변경하는 방법을 설명한다. 또한, 한 개 이상의 스크립팅 언어들을 자바 애플리케이션들로 통합할 때 겪는 문제들도 살펴볼 것이다.
Part 1에서는 Hello World 스타일의 애플리케이션을 사용하여 자바 스크립팅 API를 소개했다. 이 글에서 설명할 보다 현실적인 샘플 애플리케이션은 스크립팅 API를 사용하여 Groovy, JavaScript, Ruby로 작성된 외부 스크립트로서 규칙을 정의하는 동적인 규칙 엔진을 만들 것이다. 이 규칙은 특별한 모기지(mortgage) 제품에 대해 신청자가 주택 대출 자격이 있는지 여부를 결정한다. 스크립팅 언어로 비즈니스 규칙을 정의하면 규칙은 더욱 작성하기 쉬워지고, 대출 담당자 같은 비 프로그래머들도 읽기 쉽다. 자바 스크립팅 API로 규칙들을 노출하면, 애플리케이션이 실행 중인 동안 규칙들은 변경될 수 있고, 새로운 모기지 제품들이 추가될 수 있다.
Hello, real world
샘플 애플리케이션은 가상의 회사인 Shaky Ground Financial의 주택 대출 신청을 처리한다. 주택 모기지 산업은 지속적으로 새로운 대출 제품들을 고안하고, 자격과 관련한 규칙을 수정한다. Shaky Ground는 모기지 제품들을 빠르게 추가 및 제거하는 것 외에도, 각 제품에 대해 어떤 사람이 자격이 있는지를 결정하는 비즈니스 규칙들을 빠르게 변경하고 싶어한다.
이때 자바 스크립팅 API가 구원자가 된다. 애플리케이션은 특정 프로퍼티를 구매하려고 하는 대출자가 해당 모기지 론 제품 자격에 맞는지를 결정하는 ScriptMortgageQualifier 클래스로 구성된다. 이 클래스는 Listing 1에 나와있다.
Listing 1. ScriptMortgageQualifier 클래스
// Imports and Javadoc not shown.
public class ScriptMortgageQualifier {
private ScriptEngineManager scriptEngineManager = new ScriptEngineManager();
public MortgageQualificationResult qualifyMortgage(
Borrower borrower,
Property property,
Loan loan,
File mortgageRulesFile
) throws FileNotFoundException, IllegalArgumentException, ScriptException
{
ScriptEngine scriptEngine = getEngineForFile(mortgageRulesFile);
if (scriptEngine == null) {
throw new IllegalArgumentException(
"No script engine on classpath to handle file: " + mortgageRulesFile
);
}
// Make params accessible to scripts by adding to engine's context.
scriptEngine.put("borrower", borrower);
scriptEngine.put("property", property);
scriptEngine.put("loan", loan);
// Make return-value object available to scripts.
MortgageQualificationResult scriptResult = new MortgageQualificationResult();
scriptEngine.put("result", scriptResult);
// Add an object scripts can call to exit early from processing.
scriptEngine.put("scriptExit", new ScriptEarlyExit());
try {
scriptEngine.eval(new FileReader(mortgageRulesFile));
} catch (ScriptException se) {
// Re-throw exception unless it's our early-exit exception.
if (se.getMessage() == null ||
!se.getMessage().contains("ScriptEarlyExitException")
) {
throw se;
}
// Set script result message if early-exit exception embedded.
Throwable t = se.getCause();
while (t != null) {
if (t instanceof ScriptEarlyExitException) {
scriptResult.setMessage(t.getMessage());
break;
}
t = t.getCause();
}
}
return scriptResult;
}
/** Returns a script engine based on the extension of the given file. */
private ScriptEngine getEngineForFile(File f) {
String fileExtension = getFileExtension(f);
return scriptEngineManager.getEngineByExtension(fileExtension);
}
/** Returns the file's extension, or "" if the file has no extension */
private String getFileExtension(File file) {
String scriptName = file.getName();
int dotIndex = scriptName.lastIndexOf('.');
if (dotIndex != -1) {
return scriptName.substring(dotIndex + 1);
} else {
return "";
}
}
/** Internal exception so ScriptEarlyExit.exit can exit scripts early */
private static class ScriptEarlyExitException extends Exception {
public ScriptEarlyExitException(String msg) {
super(msg);
}
}
/** Object passed to all scripts so they can indicate an early exit. */
private static class ScriptEarlyExit {
public void noMessage() throws ScriptEarlyExitException {
throw new ScriptEarlyExitException(null);
}
public void withMessage(String msg) throws ScriptEarlyExitException {
throw new ScriptEarlyExitException(msg);
}
}
} |
이 클래스는 비교적 작다. 모든 비즈니스 결정을 외부 스크립트로 위임하기 때문이다. 각 스크립트는 한 개의 모기지 제품을 나타낸다. 각 스크립트 파일에 있는 코드에는 대출자 유형, 재산, 그 모기지 제품에 적합한 대출을 정의하는 비즈니스 규칙이 포함되어 있다. 이러한 방식으로, 새로운 모기지 제품들은 새로운 스크립트 파일을 스크립트 제품 디렉토리로 복사함으로써 추가될 수 있다. 비즈니스 규칙이 특정 모기지 제품에 자격이 있는 사람에 대해 변경된다면, 스크립트도 이러한 규칙을 반영하며 업데이트 된다.
스크립팅 언어로 모기지 제품 비즈니스 규칙을 작성하는 것은 자바 스크립팅 API의 기능을 보여준다. 또한, 스크립팅 언어는 비 프로그래머들도 읽고, 수정하고, 이해하기 쉽다.
ScriptMortgageQualifier 클래스의 실행 방법
ScriptMortgageQualifier 클래스의 주 메소드는 qualifyMortgage()이다. 이 메소드는 매개변수로서 다음을 받는다:
- 대출자
- 구매할 프로퍼티
- 대출 상세
- 실행 될 스크립트를 포함하고 있는
File 객체
이 메소드의 작업은 비즈니스 엔터티 매개변수를 가지고 스크립트 파일을 실행하고, 모기지 제품에 대해 대출자가 자격이 있는지 여부를 가리키는 결과 객체를 리턴한다. Borrower, Property, Loan에 대한 코드는 여기에서는 보이지 않는다. 이들은 간단한 엔터티 클래스이며, 소스 코드에서 사용할 수 있다. (다운로드).
스크립트 파일을 실행 할 ScriptEngine을 찾기 위해, qualifyMortgage() 메소드는 getEngineForFile() 내부 헬퍼 메소드를 사용한다. getEngineForFile() 메소드는 scriptEngineManager 인스턴스 변수를 사용하여(ScriptEngineManager를 사용하여 클래스 구현 시 초기화 됨) 파일의 주어진 확장으로 스크립트를 처리할 수 있는 스크립트 엔진을 찾는다. getEngineForFile() 메소드는 ScriptEngineManager.getEngineByExtension() 메소드를 사용하여(Listing 1) ScriptEngine을 찾아 리턴한다.
스크립트 엔진이 생기면, qualifyMortgage()는 인커밍 엔터티 매개변수들을 엔진의 콘텍스트로 바인드 하여 스크립트에 사용한다. 처음 세 개의 scriptEngine.put() 호출은 이러한 바인딩을 수행한다. scriptEngine.put()에 대한 네 번째 호출은 새로운 MortgageQualificationResult 자바 객체를 만들고 이것을 스크립트 엔진과 공유한다. qualifyMortgage()가 리턴하는 공유 객체는, 객체에 대한 프로퍼티를 설정함으로써 스크립트가 그 결과를 자바 애플리케이션과 통신할 수 있도록 한다. 스크립트는 result 글로벌 변수를 사용하여 자바 객체에 액세스 한다. 각 스크립트는 이러한 공유 스크립트 객체를 사용하여 자바 프로그램에 결과를 통신하는 책임을 맡고 있다.
scriptEngine.put()으로의 마지막 호출은 내부 헬퍼 클래스 —
ScriptEarlyExit, Listing 1
— 인스턴스가 scriptExit 변수 이름 밑에 있는 스크립트에 사용될 수 있도록 한다. ScriptEarlyExit는 두 개의 간단한 메소드, withMessage()와 noMessage()를 정의하는데, 이것이 하는 일은 예외를 던지는 것이다. 스크립트가 scriptExit.withMessage() 또는 scriptExit.noMessage()를 호출하면, 이 메소드는 ScriptEarlyExitException을 던진다. 스크립트 엔진은 이 예외를 잡으면서, 스크립트 프로세싱을 중지하고, 그 스크립트를 호출했던 eval() 메소드에 ScriptException을 던진다.
이러한 스크립트를 미성숙하게 종료하는 방식은 함수나 메소드 밖에서 처리되는 스크립트에서 리턴하는 일관된 방식을 제공한다. 모든 스크립팅 언어들이 이러한 목적을 위해 문을 제공하는 것은 아니다. 예를 들어, JavaScript로는 탑 레벨 코드를 실행할 때 return 문을 사용할 수 없는데, 이는 이 예제 애플리케이션의 모기지 프로세싱 스크립트가 구현되는 방식이다. scriptExit 공유 객체는 이러한 차이를 메우면서, 어떤 언어의 스크립트라도 대출자가 모기지 제품에 대한 자격이 없다고 결정하자마자 종료된다.
스크립트 엔진의 eval 메소드에 대한 qualifyMortgage에 있는 호출은 try/catch 블록을 사용하여 ScriptException을 잡는다. ScriptException 에러 메시지를 검사함으로써, catch 블록에 있는 코드는 이 스크립트 예외가 ScriptEarlyExitException과 실제 스크립트 에러 중 어떤 것에서 기인했는지를 결정한다. 에러 메시지에 ScriptEarlyExitException 이름이 포함되어 있다면, 이 코드는 모든 것이 계획한 대로 진행되고 있다고 간주하고 스크립트 예외를 무시한다.
자바 스크립팅 API의 스크립트 예외 에러 메시지를 찾는 기술은 엉성하지만, Groovy, JavaScript, Ruby 언어 인터프리터에도 잘 작동한다. 모든 스크립팅 언어 구현들이 호출된 자바 코드에서 던진 자바 예외를 예외 스택에 추가한다면, Throwable.getCause() 메소드를 사용하여 검색 가능하다. JRuby와 Groovy 같은 인터프리터가 수행하지만, 빌트인 Rhino JavaScript 인터프리터는 수행하지 않는다.
코드 실행하기: ScriptMortgageQualifierRunner
ScriptMortgageQualifier 클래스를 테스트 하려면, 테스트 데이터를 사용하여 네 명의 샘플 대출자, 대출자가 구매할 샘플 프로퍼티, 샘플 모기지 론을 나타내야 한다. 샘플 대출자와 프로퍼티와 론을 실행하면, 각 세 개의 스크립트들은 대출자가 스크립트가 나타낸 모기지 제품에 대해 비즈니스 규칙에 부합하는지 여부를 확인한다.
Listing 2는 이러한 테스트 객체들을 생성하고, 디렉토리에서 스크립트 파일을 찾고, Listing 1에서 ScriptMortgageQualifier 클래스를 통해 이들을 실행하는데 사용할 ScriptMortgageQualifierRunner 프로그램의 일부를 보여주고 있다. 이 프로그램의 createGoodBorrower(), createAverageBorrower(), createInvestorBorrower(), createRiskyBorrower(), createProperty(),
createLoan() 헬퍼 메소드는 공간 절약의 차원에서 나타내지 않는다. 이들은 엔터티 객체들을 만들고 테스팅에 알맞은 값을 설정한다. 모든 메소드가 있는 전체 소스 코드는 다운로드 섹션을 참조하라.
Listing 2. ScriptMortgageQualifierRunner 프로그램
// Imports and some helper methods not shown.
public class ScriptMortgageQualifierRunner {
private static File scriptDirectory;
private static Borrower goodBorrower = createGoodBorrower();
private static Borrower averageBorrower = createAverageBorrower();
private static Borrower investorBorrower = createInvestorBorrower();
private static Borrower riskyBorrower = createRiskyBorrower();
private static Property property = createProperty();
private static Loan loan = createLoan();
/**
* Main method to create a File for the directory name on the command line,
* then call the run method if that directory exists.
*/
public static void main(String[] args) {
if (args.length > 0 && args[0].contains("-help")) {
printUsageAndExit();
}
String dirName;
if (args.length == 0) {
dirName = "."; // Current directory.
} else {
dirName = args[0];
}
scriptDirectory = new File(dirName);
if (!scriptDirectory.exists() || !scriptDirectory.isDirectory()) {
printUsageAndExit();
}
run();
}
/**
* Determines mortgage loan-qualification status for four test borrowers by
* processing all script files in the given directory. Each script will determine
* whether the given borrower is qualified for a particular mortgage type
*/
public static void run() {
ScriptMortgageQualifier mortgageQualifier = new ScriptMortgageQualifier();
for(;;) { // Requires Ctrl-C to exit
runQualifications(mortgageQualifier, goodBorrower, loan, property);
runQualifications(mortgageQualifier, averageBorrower, loan, property);
loan.setDownPayment(30000.0); // Reduce down payment to 10%
runQualifications(mortgageQualifier, investorBorrower, loan, property);
loan.setDownPayment(10000.0); // Reduce down payment to 3 1/3%
runQualifications(mortgageQualifier, riskyBorrower, loan, property);
waitOneMinute();
}
}
/**
* Reads all script files in the scriptDirectory and runs them with this borrower's
* information to see if he/she qualifies for each mortgage product.
*/
private static void runQualifications(
ScriptMortgageQualifier mortgageQualifier,
Borrower borrower,
Loan loan,
Property property
) {
for (File scriptFile : getScriptFiles(scriptDirectory)) {
// Print info about the borrower, loan and property.
System.out.println("Processing file: " + scriptFile.getName());
System.out.println(" Borrower: " + borrower.getName());
System.out.println(" Credit score: " + borrower.getCreditScore());
System.out.println(" Sales price: " + property.getSalesPrice());
System.out.println(" Down payment: " + loan.getDownPayment());
MortgageQualificationResult result = null;
try {
// Run the script rules for this borrower on the loan product.
result = mortgageQualifier.qualifyMortgage(
borrower, property, loan, scriptFile
);
} catch (FileNotFoundException fnfe) {
System.out.println(
"Can't read script file: " + fnfe.getMessage()
);
} catch (IllegalArgumentException e) {
System.out.println(
"No script engine available to handle file: " +
scriptFile.getName()
);
} catch (ScriptException e) {
System.out.println(
"Script '" + scriptFile.getName() +
"' encountered an error: " + e.getMessage()
);
}
if (result == null) continue; // Must have hit exception.
// Print results.
System.out.println(
"* Mortgage product: " + result.getProductName() +
", Qualified? " + result.isQualified() +
"\n* Interest rate: " + result.getInterestRate() +
"\n* Message: " + result.getMessage()
);
System.out.println();
}
}
/** Returns files with a '.' other than as the first or last character. */
private static File[] getScriptFiles(File directory) {
return directory.listFiles(new FilenameFilter() {
public boolean accept(File dir, String name) {
int indexOfDot = name.indexOf('.');
// Ignore files w/o a dot, or with dot as first or last char.
if (indexOfDot < 1 || indexOfDot == (name.length() - 1)) {
return false;
} else {
return true;
}
}
});
}
private static void waitOneMinute() {
System.out.println(
"\nSleeping for one minute before reprocessing files." +
"\nUse Ctrl-C to exit..."
);
System.out.flush();
try {
Thread.sleep(1000 * 60);
} catch (InterruptedException e) {
System.exit(1);
}
}
}
|
ScriptMortgageQualifierRunner에 있는 main() 메소드는 스크립트 파일을 읽는 명령행에서 디렉토리를 찾고, 디렉토리가 존재하면 디렉토리의 파일 객체로 정적 변수를 설정하고, run() 메소드를 호출하여 후속 프로세싱을 수행한다.
run() 메소드는 Listing 1의 ScriptMortgageQualifier 클래스를 인스턴스화 한 다음, 무한 루프를 사용하여 네 개의 borrower/loan 시나리오로 내부 runQualifications() 메소드를 호출한다. 무한 루프는 모기지 애플리케이션에서 지속적으로 살아있는 프로세싱을 시뮬레이트 한다. 이 루프로는 처리되고 있는 디렉토리에 스크립트 파일들(모기지 론 제품들)을 추가 또는 수정할 수 있도록 해주고, 이러한 변경 사항들은 애플리케이션을 멈추지 않고 동적으로 적용된다. 애플리케이션의 비즈니스 로직이 외부 스크립트에 있기 때문에 런타임 시 비즈니스 로직을 수정하는 동적인 기능을 제공한다.
runQualifications() 헬퍼 메소드는 ScriptMortgageQualifer.qualifyMortgage로 실제 호출을 하는데, 이는 스크립트 디렉토리에 있는 각 스크립트 파일에 대한 호출이다. 각 호출 앞에는 print 문이 놓여서 처리되는 스크립트 파일과 대출자를 기술하고, 그 뒤에는 대출자가 각 모기지 제품에 대해 자격을 갖추었는지 여부를 보여주는 print 문이 나온다. 자격 심사 결과는 공유된 MortgageQualificationResult 자바 객체의 애트리뷰트에 의해서 결정되는데, 이 스크립팅 코드가 결과를 리턴하는데 사용하는 것이다.
이 글에서 제공하는 소스 코드 ZIP 파일에는 Groovy, JavaScript, Ruby로 작성된 세 개의 샘플 스크립트 파일이 포함된다. 각각은 다른 유형의 30년 모기지 론 제품을 고정 이율로 나타낸다. 스크립트에 있는 코드는 대출자가 이 모기지 유형에 적합한지 여부를 결정하고, 앞서 기술했던 스크립트 엔진 put() 메소드에 제공된 공유 result 글로벌 변수에 메소드를 호출함으로써 결과를 리턴한다. result 글로벌 변수는 MortgageQualificationResult 클래스의 인스턴스이고, Listing 3은 그 일부이다.
Listing 3. MortgageQualificationResult results 클래스
public class MortgageQualificationResult {
private boolean qualified;
private double interestRate;
private String message;
private String productName;
// .. Standard setters and getters not shown.
} |
이 스크립트는 Listing 3의 result 프로퍼티를 설정하여 대출자가 모기지 자격에 맞는지 여부와 이자율을 리턴한다. message와 productName 프로퍼티는 스크립트가 대출이 실패한 이유를 설정하고 관련 제품 명을 리턴한다.
스크립트 파일
실행 ScriptMortgageQualifierRunner에서 온 아웃풋을 보여주기 전에, 프로그램이 실행하는 Groovy, JavaScript, Ruby 스크립트 파일을 보자. Groovy 스크립트의 비즈니스 로직은 비교적 자격을 주기 쉬운 모기지 제품을 정의하지만, 더 높은 재정적 리스크를 의미하는 높은 이율이 있다. JavaScript 스크립트는 정부에서 지원하는 모기지 론을 나타내는데, 대출자의 기타 제약 조건을 갖고 있다. Ruby 스크립트로 된 모기지 제품에는 양호한 신용 내역을 갖고 있는 대출자로 자격을 제한하는 비즈니스 규칙을 포함하고 있다. 이들에게는 낮은 이율로 보상해 준다.
Listing 4는 Groovy 스크립트이다. Groovy를 모르더라도 읽을 수 있을 것이다.
Listing 4. Groovy 모기지 스크립트
/*
This Groovy script defines the "Groovy Mortgage" product.
This product is relaxed in its requirements of borrowers.
There is a higher interest rate to make up for the looser standard.
All borrowers will be approved if their credit history is good, they can
make a down payment of at least 5%, and they either earn more than
$2,000/month or have a net worth (assets minus liabilities) of $25,000.
*/
// Our product name.
result.productName = 'Groovy Mortgage'
// Check for the minimum income and net worth
def netWorth = borrower.totalAssets - borrower.totalLiabilities
if (borrower.monthlyIncome < 2000 && netWorth < 25000) {
scriptExit.withMessage "Low monthly income of ${borrower.monthlyIncome}" +
' requires a net worth of at least $25,000.'
}
def downPaymentPercent = loan.downPayment / property.salesPrice * 100
if (downPaymentPercent < 5) {
scriptExit.withMessage 'Down payment of ' +
"${String.format('%1$.2f', downPaymentPercent)}% is insufficient." +
' 5% minimum required.'
}
if (borrower.creditScore < 600) {
scriptExit.withMessage 'Credit score of 600 required.'
}
// Everyone else qualifies. Find interest rate based on down payment percent.
result.qualified = true
result.message = 'Groovy! You qualify.'
switch (downPaymentPercent) {
case 0..5: result.interestRate = 0.08; break
case 6..10: result.interestRate = 0.075; break
case 11..15: result.interestRate = 0.07; break
case 16..20: result.interestRate = 0.065; break
default: result.interestRate = 0.06; break
}
|
공유 자바 객체에서 값에 액세스 하여 설정하기 위해 스크립트가 사용하는 글로벌 result, borrower,
loan, property 변수들을 주목하라. 이들은 ScriptEngine.put() 메소드로의 호출에 의해 설정된 변수 이름이다.
result.productName =
'Groovy Mortgage' 같은 Groovy 문장에도 주목하라. 이것은 MortgageQualificationResult 객체의 productName 스트링 프로퍼티를 직접 설정한다. Listing 3에서는 이것이 개인 인스턴스 변수가 된다는 것을 분명히 보여주고 있다. 이것은 캡슐화의 위반을 허용하는 자바 스크립팅 API가 아니다. 대신, Groovy와 자바 스크립팅 API와 함께 사용할 수 있는 대부분의 다른 스크립팅 언어 인터프리터들은 공유 자바 객체들과 잘 협력한다. Groovy 문장이 자바 객체에 대한 프라이빗 프로퍼티 값을 설정하거나 읽는다면, Groovy는 JavaBean 계열의 퍼블릭 setter 또는 getter 메소드를 찾아서 사용한다. 예를 들어, result.productName = 'Groovy Mortgage' 문장은 예상 자바 문장인 result.setProductName("Groovy Mortgage")으로 자동으로 변환된다. 이러한 동일한 자바 setter 문장 역시 유효 Groovy이고, 이 스크립트에서 잘 실행되지만, 프로퍼티 할당 문장을 직접 사용하는 것이 더 Groovy 답다.
이제, Listing 5의 JavaScript 모기지 제품을 보자. JavaScript 모기지는 정부 지원 대출을 나타내서 주택 소유를 장려하고 있다. 따라서, 비즈니스 규칙은 이것이 대출자의 최초의 집 구매이고, 대출자가 살아야 한다는 것을 요구한다.
Listing 5. JavaScript 모기지 스크립트
/**
* This script defines the "JavaScript FirstTime Mortgage" product.
* It is a government-sponsored mortgage intended for low-income, first-time
* home buyers without a lot of assets who intend to live in the home.
* Bankruptcies and bad (but not terrible!) credit are OK.
*/
result.productName = 'JavaScript FirstTime Mortgage'
if (!borrower.intendsToOccupy) {
result.message = 'This mortgage is not intended for investors.'
scriptExit.noMessage()
}
if (!borrower.firstTimeBuyer) {
result.message = 'Only first-time home buyers qualify for this mortgage.'
scriptExit.noMessage()
}
if (borrower.monthlyIncome > 4000) {
result.message = 'Monthly salary of $' + borrower.monthlyIncome +
' exceeds the $4,000 maximum.'
scriptExit.noMessage()
}
if (borrower.creditScore < 500) {
result.message = 'Your credit score of ' + borrower.creditScore +
' does not meet the 500 requirement.'
scriptExit.noMessage()
}
// Qualifies. Determine interest rate based on loan amount and credit score.
result.qualified = true
result.message = 'Congratulations, you qualify.'
if (loan.loanAmount > 450000) {
result.interestRate = 0.08 // Big loans and poor credit require higher rate.
} else if (borrower.creditScore < 550) {
result.interestRate = 0.08
} else if (borrower.creditScore < 600) {
result.interestRate = 0.07
} else if (borrower.creditScore < 700) {
result.interestRate = 0.065
} else { // Good credit gets best rate.
result.interestRate = 0.06
} |
이 JavaScript 코드는 자격 심사 메시지를 설정하고 한 문장으로 스크립트를 종료하기 위해 Groovy 스크립트가 사용하는 자바 scriptExit.withMessage() 메소드를 사용할 수 없다는 것에 주목하라. Rhino JavaScript 인터프리터가 던져진 자바 예외를 결과 ScriptException 스택 트레이스에서 삽입된 "cause"로 명시하지 않기 때문이다. 자바 코드에서 던져진 스크립트 예외 메시지는 이 스택 트레이스에서 찾기가 더욱 어렵다. 따라서, Listing 5의 JavaScript 코드는 scriptExit.noMessage()를 호출하여 스크립트 프로세싱이 종료하게 하는 예외를 호출하기 전에 결과 메시지 이유를 개별적으로 설정해야 한다.
Listing 6에 있는 세 번째이자 마지막 모기지 제품은 Ruby로 작성되었다. 20퍼센트 낮춰 불입할 수 있는 좋은 신용 내역을 가진 대출자를 위한 것이다.
Listing 6. Ruby 모기지 스크립트
# This Ruby script defines the "Ruby Mortgage" product.
# It is intended for premium borrowers with its low interest rate
# and 20% down payment requirement.
# Our product name
$result.product_name = 'Ruby Mortgage'
# Borrowers with credit unworthiness do not qualify.
if $borrower.credit_score < 700
$scriptExit.with_message "Credit score of #{$borrower.credit_score}" +
" is lower than 700 minimum"
end
$scriptExit.with_message 'No bankruptcies allowed' if $borrower.hasDeclaredBankruptcy
# Check other negatives
down_payment_percent = $loan.down_payment / $property.sales_price * 100
if down_payment_percent < 20
$scriptExit.with_message 'Down payment must be at least 20% of sale price.'
end
# Borrower qualifies. Determine interest rate of loan
$result.message = "Qualified!"
$result.qualified = true
# Give the best interest rate to the best credit risks.
if $borrower.credit_score > 750 || down_payment_percent > 25
$result.interestRate = 0.06
elsif $borrower.credit_score > 700 && $borrower.totalAssets > 100000
$result.interestRate = 0.062
else
$result.interestRate = 0.065
end
|
 |
JRuby 1.0의 $를 잊지 말 것!
Ruby 글로벌-변수 신택스는 Ruby 스크립트에서 공유 자바 객체들에 액세스 할 때 기억해야 한다. 글로벌 변수에 $를 빼면, JRuby 1.0과 현재 JRuby 1.0.1 바이너리 릴리스는 무엇이 잘못되었는지에 대한 어떤 정보도 없이 RaiseException을 던진다. 이 버그는 JRuby 소스 코드 저장소에서 픽스되기 때문에, 향후 바이너리 릴리스 에서도 픽스 될 것이다.
|
|
Listing 6을 읽을 때, 스크립트 엔진 범위에 놓인 공유 자바 객체들이 이름 앞에 $를 붙여 Ruby에서 액세스 되어야 한다. 이것은 Ruby 글로벌 변수 신택스이다. 스크립팅 엔진들은 자바 변수들을 글로벌 변수로서 스크립트 언어들과 공유하기 때문에, Ruby 글로벌 변수 신택스가 사용되어야 한다.
이 리스팅에서, JRuby가 공유 자바 객체들을 호출할 때, Ruby를 자바로 어떻게 자동 변환하는지를 주목하라. 예를 들어, JRuby가 $result.product_name = 'Ruby Mortgage' 같은 개별 단어에 언더스코어의 Ruby 변환을 사용하여 호출되는 자바 객체에 대한 메소드를 본다면, JRuby는 언더스코어가 있는 이름을 찾기에 실패할 경우 혼합 케이스를 사용하는 메소드 이름을 찾는다. 따라서, product_name= Ruby 스타일 메소드 이름은 자바 result.setProductName("Ruby Mortgage") 호출로 정확하게 변환된다.
프로그램 결과
세 개의 모기지 제품 스크립트 파일로 실행될 때 ScriptMortgageQualifierRunner 프로그램의 아웃풋을 보도록 하자. 소스 코드에 포함된 Ant 스크립트를 사용하여 이 프로그램을 실행할 수 있다. Maven을 선호한다면, ZIP 파일의 README.txt 파일에는 Maven으로 프로젝트를 구현 및 실행하는 것에 관련된 도움말이 포함되어 있다. Ant 명령어는 ant run이다. run 태스크는 스크립팅 엔진들과 언어 JAR 파일들이 클래스 경로에 있음을 확인시켜 준다. Listing 7은 Ant의 아웃풋이다.
Listing 7. Ant의 프로그램 결과
> ant run
Buildfile: build.xml
compile:
[mkdir] Created dir: C:\temp\script-article\build-main\classes
[javac] Compiling 10 source files to C:\temp\script-article\build-main\classes
run:
[java] Processing file: GroovyMortgage.groovy
[java] Borrower: Good Borrower
[java] Credit score: 800
[java] Sales price: 300000.0
[java] Down payment: 60000.0
[java] * Mortgage product: Groovy Mortgage, Qualified? true
[java] * Interest rate: 0.06
[java] * Message: Groovy! You qualify.
[java] Processing file: JavaScriptFirstTimeMortgage.js
[java] Borrower: Good Borrower
[java] Credit score: 800
[java] Sales price: 300000.0
[java] Down payment: 60000.0
[java] * Mortgage product: JavaScript FirstTime Mortgage, Qualified? false
[java] * Interest rate: 0.0
[java] * Message: Only first-time home buyers qualify for this mortgage.
[java] Processing file: RubyPrimeMortgage.rb
[java] Borrower: Good Borrower
[java] Credit score: 800
[java] Sales price: 300000.0
[java] Down payment: 60000.0
[java] * Mortgage product: Ruby Mortgage, Qualified? true
[java] * Interest rate: 0.06
[java] * Message: Qualified!
[java] Processing file: GroovyMortgage.groovy
[java] Borrower: Average Borrower
[java] Credit score: 700
[java] Sales price: 300000.0
[java] Down payment: 60000.0
[java] * Mortgage product: Groovy Mortgage, Qualified? true
[java] * Interest rate: 0.06
[java] * Message: Groovy! You qualify.
[java] Processing file: JavaScriptFirstTimeMortgage.js
[java] Borrower: Average Borrower
[java] Credit score: 700
[java] Sales price: 300000.0
[java] Down payment: 60000.0
[java] * Mortgage product: JavaScript FirstTime Mortgage, Qualified? false
[java] * Interest rate: 0.0
[java] * Message: Monthly salary of $4500 exceeds the $4,000 maximum.
[java] Processing file: RubyPrimeMortgage.rb
[java] Borrower: Average Borrower
[java] Credit score: 700
[java] Sales price: 300000.0
[java] Down payment: 60000.0
[java] * Mortgage product: Ruby Mortgage, Qualified? true
[java] * Interest rate: 0.065
[java] * Message: Qualified!
[java] Processing file: GroovyMortgage.groovy
[java] Borrower: Investor Borrower
[java] Credit score: 720
[java] Sales price: 300000.0
[java] Down payment: 30000.0
[java] * Mortgage product: Groovy Mortgage, Qualified? true
[java] * Interest rate: 0.06
[java] * Message: Groovy! You qualify.
[java] Processing file: JavaScriptFirstTimeMortgage.js
[java] Borrower: Investor Borrower
[java] Credit score: 720
[java] Sales price: 300000.0
[java] Down payment: 30000.0
[java] * Mortgage product: JavaScript FirstTime Mortgage, Qualified? false
[java] * Interest rate: 0.0
[java] * Message: This mortgage is not intended for investors.
[java] Processing file: RubyPrimeMortgage.rb
[java] Borrower: Investor Borrower
[java] Credit score: 720
[java] Sales price: 300000.0
[java] Down payment: 30000.0
[java] * Mortgage product: Ruby Mortgage, Qualified? false
[java] * Interest rate: 0.0
[java] * Message: Down payment must be at least 20% of sale price.
[java] Processing file: GroovyMortgage.groovy
[java] Borrower: Risk E. Borrower
[java] Credit score: 520
[java] Sales price: 300000.0
[java] Down payment: 10000.0
[java] * Mortgage product: Groovy Mortgage, Qualified? false
[java] * Interest rate: 0.0
[java] * Message: Down payment of 3.33% is insufficient. 5% minimum required.
[java] Processing file: JavaScriptFirstTimeMortgage.js
[java] Borrower: Risk E. Borrower
[java] Credit score: 520
[java] Sales price: 300000.0
[java] Down payment: 10000.0
[java] * Mortgage product: JavaScript FirstTime Mortgage, Qualified? true
[java] * Interest rate: 0.08
[java] * Message: Congratulations, you qualify.
[java] Processing file: RubyPrimeMortgage.rb
[java] Borrower: Risk E. Borrower
[java] Credit score: 520
[java] Sales price: 300000.0
[java] Down payment: 10000.0
[java] * Mortgage product: Ruby Mortgage, Qualified? false
[java] * Interest rate: 0.0
[java] * Message: Credit score of 520 is lower than 700 minimum
[java] Sleeping for one minute before reprocessing files.
[java] Use Ctrl-C to exit...
|
이 아웃풋은 12개의 섹션이 있다. 이 프로그램은 네 개의 샘플 대출자의 대출 프로파일 각각을 세 개의 스크립트에 제출하여, 대출자가 세 개의 모기지 제품 모두에 적합한지를 확인한다. 프로그램은 1분을 기다리고 모기지 스크립트 프로세싱을 반복한다. 중지되어 있는 동안, 스크립트 파일을 편집하여 비즈니스 규칙을 수정하거나 새로운 스크립트 파일을 스크립트 디렉토리에 추가하여 모기지 제품들을 나타낸다. 이 프로그램은 각 패스 시 스크립트 디렉토리를 조사하고, 이것이 찾은 새로운 스크립트 파일을 처리한다.
예를 들어, 대출 자격이 되는데 필요한 최소 신용 점수를 높여야 한다고 가정해 보자. 1분 중지 동안, src/main/scripts/mortgage-products 디렉토리(Listing 5)에 있는 JavaScriptFirstTimeMortgage.js 스크립트를 편집하여 23 라인에 대한 비즈니스 규칙을 if (borrower.creditScore < 500) {에서 if (borrower.creditScore < 550) {로 바꾼다. 다음 번에 이 규칙이 실행되면, Risk E. Borrower는 더 이상 JavaScript FirstTime Mortgage에 대한 자격을 주지 않는다. 이 대출자의 신용 점수인 520은 현재 너무 낮다. 에러 메시지는 여전히 "Your credit score of 520 does not meet the 500 requirement"이지만, 오래된 에러 메시지들을 즉시 픽스 할 수 있다.
동적인 스크립팅 위험 피하기
런타임 시 프로그램을 변경할 수 있는 기능은 대단한 것이다. 그리고 상당히 위험하다. 새로운 기능과 새로운 비즈니스 규칙들은 애플리케이션을 중지하여 재시작 하지 않고 추가될 수 있다. 새로운 엄청난 버그도 이와 같이 쉽게 추가될 수 있다.
하지만, 실행 애플리케이션을 동적으로 변경하는 것은 중지된 애플리케이션을 변경하는 것 보다 위험하지 않다. 정적인 기술은 이러한 새로운 에러들을 찾기 전에 애플리케이션을 재시작 해야 한다는 것을 의미한다. 좋은 소프트웨어 개발 프랙티스들은 실행 애플리케이션에 대한 — 동적이든 정적이든 — 실행 환경에 도입되기 전에 테스트 되어야 한다고 명시하고 있다. 자바 스크립팅 API는 이러한 규칙을 바꾸지 않는다.
외부 스크립트 파일들은 정식 단위 테스팅의 일부로서 개발 시 테스팅 될 수 있다. JUnit 또는 다른 테스팅 장치들을 mock 자바 객체들과 함께 사용하고, 이 스크립트를 mock과 함께 실행하여 스크립트가 에러 없이 실행되고 기대한 결과를 낼 수 있도록 한다. 애플리케이션 로직을 비 자바 스크립트 파일로 노출하는 것은 그러한 스크립트를 테스트 하지 않는데 대한 이유가 될 수 없다.
여러분이 웹 CGI 스크립트 프로그래머라면, ScriptEngine의 eval() 메소드로 전달하는 것에 주의를 기울여야 한다. 스크립트 엔진은 eval 메소드로 전달된 코드를 즉각 실행한다. 따라서, 여러분은 스트링 또는 믿을 수 없는 소스에서 온 텍스트를 포함하고 있는 Reader 객체를 스크립트 엔진에 절대 전달해서는 안된다.
예를 들어, 스크립팅 API를 사용하여 웹 애플리케이션을 원격에서 감시할 수 있다. 웹 애플리케이션에 대한 상태 정보를 제공하는 주요 자바 객체들로 스크립트 엔진을 접근시켜, 임의의 스크립트 식을 수락하는 간단한 웹 페이지를 만들어서 웹 페이지의 평가와 아웃풋을 위해 스크립트 엔진을 제공할 수 있다. 이러한 방식으로, 실행 자바 객체에 대한 메소드를 쿼리 및 실행하여 애플리케이션의 상태와 건강을 검사한다.
이와 같은 시나리오에서, 웹 페이지로 액세스 하는 누구나 스크립팅 언어에서 사용할 수 있는 어떤 문장이라도 실행할 수 있고 공유 자바 객체에 액세스 할 수 있다. 부주의한 프로그래밍, 잘못된 설정, 보안 허점은 기밀 정보를 권한이 없는 사용자에게 노출하거나, 해커가 System.exit 또는 /bin/rm -fr /와 동일한 스크립팅 문장을 실행할 때 denial-of-service 공격에 애플리케이션을 노출할 수 있다. 다른 강력한 툴과 마찬가지로, 자바 스크립팅 API 역시 주의를 기울여야 한다.
맺음말
이 글에서는 런타임 시 외부 스크립트를 동적으로 읽고 실행하는 자바 애플리케이션의 기능과, 자바 객체에 액세스 하는 스크립트에 초점을 맞춰 설명했다. 자바 스크립팅 API는 다른 기능들도 제공한다. 예를 들면:
- 스크립팅 언어를 사용하여 자바 인터페이스를 구현하고, 다른 자바 인터페이스 레퍼런스처럼 자바 코드에서 스크립트 코드를 호출할 수 있다.
- 스크립트 내부에서 자바 객체들을 인스턴스화 하고 사용하면서, 그러한 객체들을 나중에 자바 애플리케이션에서 사용할 수 있도록 한다.
- 연속적인 실행을 더욱 빠르게 하기 위해 동적인 스크립트가 로딩될 때 사전 컴파일 할 수 있다.
- 스크립트가 사용할 인풋 및 아웃풋 스트림을 설정하면서, 스크립트의 콘솔 인풋 소스로서 파일을 사용하기 쉽도록 하고, 스크립트의 콘솔 아웃풋을 파일이나 다른 스트림에 캡쳐할 수 있다.
- 스크립트가 사용할 수 있는 위치 매개변수들을 명령행 인자로서 설정할 수 있다.
자바 스크립팅 API는 스크립트 엔진 구현자들을 위한 옵션으로서 이러한 기능들을 정의하기 때문에, 모든 스크립팅 엔진들이 이를 제공하는 것은 아니다. 참고자료 섹션에서 더 많은 정보를 얻어가기 바란다.
다운로드 하십시오 | 설명 | 이름 | 크기 | 다운로드 방식 |
|---|
| 소스 코드와 모든 JAR 파일 | java-scripting-part2.zip | 4.5MB | HTTP |
|---|
참고자료 교육
제품 및 기술 얻기
-
Groovy: 최신Groovy 릴리스 다운로드.
-
Java SE 6와 BEA
JRockit: 자바 스크립팅 API를 지원하는 개발 킷과 런타임 환경 및 Mozilla Rhino JavaScript 엔진 포함.
-
스크립팅 프로젝트: java.net의 오픈 소스 Scripting 프로젝트. 20여 개의 스크립트 언어용 스크립트-엔진 인터페이스와 자바 스크립팅 엔진에 대한 링크 제공.
-
Java Platform 1.0 Reference Implementation용 스크립팅: JSR-223 레퍼런스 구현으로, 자바 스크립팅 API가 Java SE 5에서 실행될 수 있도록 하는 세 개의 JAR 파일을 제공한다. . Download and unzip the sjp-1_0-fr-ri.zip 파일을 다운로드 및 압축을 풀어, classpath에 js.jar, script-api.jar, script-js.jar 파일을 둔다.
토론
필자소개  | 
|  | Tom McQueeney는 국립 컨설팅 회사인 Idea Integration의 자바 개발자 겸 애플리케이션 아키텍트이다. Ruby와 Groovy 같은 동적인 언어들을 자바 프로젝트로 통합하여 개발을 더욱 빠르고 효율적으로 만드는데 관심을 갖고 있다. 과거, O'Reilly의 OSCON과 ApacheCon Europe의 연사였으며, Denver Java Users Group의 회장을 역임했다. 자바 인증 아키텍트인 아내와 함께 워싱턴에 살고 있다. |
기사에 대한 평가
|  |