JI 구현 이야기, 리뷰 1 
신림프로그래머 최범균, 2014-10-14
내용 
• 리뷰1 (오늘) 
• 시작: 기능 목록 초안, 초기 컴포넌트 식별 
• 0.1 개발: 핵심 기능 구현, CLI 구현 
• 일부 설계 과정, 일부 구현 결과물 
• 리뷰2 (다음) 
• 0.2 개발: 웹 구현 
• 일부 설계/구현 결과물 
• 테스트 관련 생각할 거리: 단위 vs 통합, E2E 테스트 
2
이야기의 시작, 반복(중복) 
[커맨드라인] 
행렬 분해 
[커맨드라인] 
사용자용 데이터 
생성 
[커맨드라인] 
아이템용 데이터 
생성 
[자바] 
데이터 변환 후 
저장 
반복 
여러 알고리즘에 대해 과정 유사 
3
자동화 욕구 
4 
[커맨드라인] 
행렬 분해 
[커맨드라인] 
사용자용 데이터 생성 
[커맨드라인] 
아이템용 데이터 생성 
[자바] 
데이터 변환 후 
저장 
반복 
자동화 
[프로세스] 
경로, 타입, 숫자 등 많은 설정 
단계별 실패 확인 
데이터 변환 
과정, 실패 처리 등 
프로그램 
설정 정보 
경로, 타입, 숫자 등
기능 목록 
버전 내용 
0.1 1개 알고리즘에 대한 서비스 구현 
콘솔에서 서비스 실행: 설정 파일 사용 
5 
0.2 
웹 인터페이스 
- 작업 설정 생성 
- 작업 설정을 이용해서 백그라운드로 작업 실행 
- 실행 내역 추적 
0.3 다른 알고리즘 추가
0.1v
주요 모델 
• 분석 실행(프로세스) 
• 입력: 실행에 필요한 데이터 
• 알고리즘 실행: 입력을 이용해서 특정 알고리즘 수행 
• 결과 저장: 실행 결과를 원하는 형태로 보관 
7
0.1 버전 상위 수준 설계 초안 
입력 데이터 
경로 제공 결과 보관 
8 
주요 인터페이스 
핵심 컴포넌트 
알고리즘 실행 
설정 파일을 이용해서 
분석 기능 실행
요구 상세 
• Mahout을 이용해서 분석 1개 실행 
• 로컬에 설치된 Mahout을 사용 
• 기능 실행은 쉘스크립트를 이용해서 실행 
• Mahout 실행 환경 
• 로컬: 로컬 실행, 로컬 입력 파일 
• 하둡: 하둡 클러스터(MR) 실행, HDFS 입력 파일 
• 로컬에 설치된 하둡을 사용 
• 결과 저장 
• 로컬에 JSON 파일, (단순 테스트 목적) 콘솔 출력 
9
요구 상세 
• Mahout을 이용해서 분석 1개 실행 
• 로컬에 설치된 Mahout을 사용 
• 기능 실행은 쉘스크립트를 이용해서 실행 
10 
• Mahout 실행 환경 
JVM에서 외부 쉘을 실행 
사용자가 쉘을 마음대로 변경하면 
안 되므로, 런타임에 쉘을 동적 생성 
• 로컬: 로컬 실행, 로컬 입력 파일 
• 하둡: 하둡 클러스터(MR) 실행, HDFS 입력 파일 
• 로컬에 설치된 하둡을 사용 
• 결과 저장 
• 로컬에 JSON 파일, (단순 테스트 목적) 콘솔 출력 
Mahout 실행시, 하둡 클러스 
터 설정 가능하도록 
입력 파일로 로컬 경로와 HDFS 
경로를 사용할 수 있도록 하둡 결과 파일을 읽어와 처리
상위 수준 핵심 로직은 
11 
입력을 
이용함 
Mahout을 
실행함 
결과를 
저장함
핵심 부분 설계 시작 
12 
class RecAnalyticsServiceSpec 
extends Specification { 
def “생성”() { 
def RecAnalyticsService service = 
new RecAnalyticsService() 
} 
} 
public class RecAnalyticsService { 
!} 
* 공간의 제약으로 이름 등 일부 변경 
class RecAnalyticsServiceSpec 
extends Specification { 
def service = new RecAnalyticService() 
! 
def “로그가 없을 경우”() { 
when: 
service.createRecommendation() 
then: 
thrown(NoDataException) 
} 
} 
public class RecAnalyticsService { 
public void createRecommendation() { 
throw new NoDataException(); 
} 
} 
! 
public class NoDataException extends RuntimeEx…{ 
}
13 
def service = new RecAnalyticService() 
! 
def “로그가 없을 경우”() { 
when: 
service.createRecommendation() 
then: 
thrown(NoDataException) 
} 
! 
def “로그가 있다면”() { 
setup: 
def ActivityStorage activityStorage = Mock() 
service.setActivityStorage(activityStorage) 
when: 
service.createRecomendation() 
then: “로그가 있으면 NoDataException이 발생하지 않음” 
1 * activityStorage.hasActivityData() >> true 
notThrown(NoDataException) 
} 
! 
public interface ActivityStorage { 
boolean hasActivityData(); 
} 
! 
public class RecAnalyticsService { 
private ActivityStorage activityStorage; 
! 
public void createRecommendation() { 
if (!activityStorage.hasActivityData()) 
throw new NoDataException(); 
} 
! 
public void setActivityStorage(ActivityStorage …) { 
this.activityStorage = …; 
} 
}
14 
def service = new RecAnalyticService() 
def ActivityStorage mockActivityStorage = Mock() 
! 
def setup() { 
service.setActivityStorage(mockActivityStorage) 
} 
! 
def “로그가 없을 경우”() { 
when: 
service.createRecommendation() 
then: 
thrown(NoDataException) 
} 
! 
def “로그가 있다면”() { 
setup: 
mockActivityStorage.hasActivityData() >> true 
when: 
service.createRecomendation() 
then: “로그가 있으면 NoDataException이 발생하지 않음” 
notThrown(NoDataException) 
}
15 
def service = new RecAnalyticService() 
def ActivityStorage mockActivityStorage = Mock() 
def MahoutRunner mockMH = Mock() 
! 
def setup() { 
service.setActivityStorage(mockActivityStorage) 
service.setMahoutRunner(mockMH) 
} 
! 
def “로그가 있다면”() { 
setup: 
mockActivityStorage.hasActivityData() >> true 
! 
when: 
service.createRecomendation() 
then: “로그가 있으면 NoDataException이 발생하지 않음” 
notThrown(NoDataException) 
! 
when: 
service.createRecomendation() 
then: “Mahout 이용 분석 실행 실패하면, 익셉션 발생” 
1 * mockMH.run() >> { throw new MREx() } 
thrown(AnalyticServiceException) 
! 
} 
public interface MahoutRunner { 
public void run(); 
} 
! 
public class RecAnalyticsService { 
private ActivityStorage activityStorage; 
private MahoutRunner mahoutRunner; 
! 
public void createRecommendation() { 
if (!activityStorage.hasActivityData()) 
throw new NoDataException(); 
try { 
mahoutRunner.run(); 
} catch(MREx ex) { 
throw new AnalyticServicException(ex); 
} 
} 
… // setter 추가 
} 
! 
public class MREx extends RuntimeException {} 
public class AnalyticServiceException … {…}
16 
def service = new RecAnalyticService() 
def ActivityStorage mockActivityStorage = Mock() 
def MahoutRunner mockMH = Mock() 
def ResultSaver mockSaver = Mock() 
… // setup() 
def “로그가 있다면”() { 
setup: 
mockActivityStorage.hasActivityData() >> true 
def RecResult recResult = Mock() 
def UserItemSource uiSource = Mock() 
def SimItemSource siSource = Mock() 
recResult.getUserItem() >> uiSource 
recResult.getSimItem() >> recResult 
…// 다른 when-then 
when: 
service.createRecomendation() 
then: “Mahout 이용 분석 실행 실패하면, 익셉션 발생” 
1 * mockMH.run(_) >> { throw new MREx() } 
thrown(AnalyticServiceException) 
! 
when: 
service.createRecomendation() 
then: “Mahout 이용 분석 실행 성공하면, 결과 저장 시도” 
mockMH.run(mockActivityStorage) >> recResult 
1 * mockSaver.saveUserItem(uiSource) 
1 * mockSaver.saveSimItem(siSource) 
} 
public interface ResultSaver { 
void saveUserItem(UserItemSource source); 
void saveSimItem(SimItemSource source); 
} 
public interface UserItemSource {} 
public interface SimItemSource {} 
! 
public interface RecResult { 
UserItemSource getUserItem(); 
SimItemSource getSimItem(); 
} 
! 
public interface MahoutRunner { 
public void run(ActivityStorage activityStorage); 
} 
public class RecAnalyticsService { 
… 
private ResultSaver resultSaver; 
! 
public void createRecommendation() { 
if (!activityStorage.hasActivityData()) 
throw new NoDataException(); 
RecResult result; 
try { 
result = mahoutRunner.run(activityStorage); 
} catch(MREx ex) { 
throw new AnalyticServicException(ex); 
} 
resultSaver.saveUserItem(result.getUserItem()); 
resultSaver.saveSimItem(result.getSimItem()) 
} 
… // setter
추가적인 실패/성공 시나리오를 
진행하면서 
점진적으로 구현/설계 완성 
17
핵심 부분 설계 결과 
18
소속/역할은? 
19
설계 결과물 모듈 배치 결과 
20 
인터페이스 추출
구현/설계 진행 계속 
유사한 방식으로 
MahoutRunner 콘크리트 클래스의 
구현/설계를 
점진적으로 진행 
21
인터페이스 정의를 어떻게? 
22 
이 인터페이스의 메서드를 
정의하려면?
사용자 입장에서 메서드 도출 
UserItemSource와 SimItemSource의 사용자 
23 
▼ 
“ResultSaver의 콘크리트 클래스” 
public class ConsoleResultSaver implements … { 
! 
public void saveUserItem(UserItemSource source) { 
List<UserItem> userItems = source.getUserItems(); 
for (UserItem ui: userItems) { … } 
} 
! 
public void saveUserItem(UserItemSource source) { 
Iterator<UserItem> userItems = source.getIterator(); 
while(userItems.hasNext()) { 
UserItem ui = userItems.next(); 
… 
} 
} 
1안 
2안 
대량 결과 데이터를 처리할 수 있어야 하기 때문에, 
2안 선택
2안으로 구현 
쉘 실행 결과로 만들어진 파일로부터 
데이터를 읽어오는 Iterator 구현 필요 
24 
public interface UserItemSource { 
Iterator<UserItem> iterator(); 
}
2안, Iterator 구현 예 
25 
private class FileSystemSimItemIterator 
implements Iterator<SimItem> { 
private final SequenceFile.Reader reader; 
private final IntWritable key; 
private final VectorWritable value ; 
private SimItem nextItem; 
! 
public FileSystemSimItemIterator( 
FileSystem fileSystem, Path simItemFile) { 
reader = createSequenceFileReader( 
fileSystem, similarItemFile, new Configuration()); 
key = new IntWritable(); 
value = new VectorWritable(); 
moveNext(); 
} 
! 
@Override 
public boolean hasNext() { 
return nextItem != null; 
} 
! 
@Override 
public SimilarItems next() { 
checkNextItemExists(); 
SimItem result = nextItem; 
moveNext(); 
return result; 
} 
private void checkNextItemsExists() { 
if (nextItem == null) throw new NoSuchElementException(); 
} 
! 
private void moveNext() { 
boolean isNextRead = readNextKeyValue(); 
if (!isNextRead) { 
closeReader(); 
nextItem = null; 
return; 
} 
createNextItems(); 
} 
! 
private boolean readNextKeyValue() { 
try { 
return reader.next(key, value); 
} catch (…) {…} 
} 
! 
private void createNextItem() { 
List<ItemValue> allSimilarItems = new ArrayList<>(); 
for (Vector.Element ele : value.get().nonZeroes()) 
allSimilarItems.add(new ItemValue(ele.index(), ele.get())); 
nextItem = new SimItem(key.get(), allSimilarItems); 
} 
private void closeReader() { … } 
}
ResultCreator, *Source, Iterator 구현 관련 
26
CLI 부분 
27
CLI 요구 사항 
• 작업 실행에 필요한 정보를 파일로 설정 
• JSON 형식 사용 
• 콘솔에서 설정 파일 경로를 지정해서 작업 실행 
• 예) ji.sh -c recommendation.conf 
28
핵심은 
• 설정 파일을 읽어와 
• 알맞게 분석 서비스 객체(예, RecAnalyticService)를 구 
성한 뒤 
• 분석 서비스를 실행 
29
핵심 기능 구현/설계에 쓰인 테스트 코드(계속) 
class RecommendationDriverSpec extends Specification { 
def RecommendationDriver driver = new RecommendationDriver(); 
def ConfigLoader mockConfigLoader = Mock() 
def RecommendationAnalyticServiceFactory mockRecAnalyticServiceFactory = Mock() 
def RecommendationAnalyticService mockRecAnalyticService = Mock() 
def HelpPrinter mockHelpPrinter = Mock() 
def String configPath = "myPath" 
! 
def setup() { 
driver.setConfigLoader(mockConfigLoader) 
driver.setFactory(mockRecAnalyticServiceFactory) 
driver.setHelpPrinter(mockHelpPrinter) 
} 
! 
def "입력 인자 테스트, 비정상 입력일 때, 1 리턴해야 함"() { 
expect: 
driver.run() == 1 
driver.run("--configFile") == 1 
driver.run("-c") == 1 
} 
! 
def "입력 인자 테스트, 비정상 입력일 때, 도움말 출력함"() { 
30 
when: 
driver.run() 
then: 
1 * mockHelpPrinter.print(_) 
} 
… 계속
def "협업 객체 연동 테스트"() { 
setup: 
def recommendationConfig = new RecommendationConfig() 
! 
when: 
def code = driver.run("--configFile", configPath) 
then: "설정 정보 로딩에 실패하면, 1을 리턴해야 함" 
1 * mockConfigLoader.loadRecommendationConfig(configPath) >> { throw new ConfigLoaderException() } 
code == 1 
31 
! 
when: 
code = driver.run("--configFile", configPath) 
then: "설정 정보 로딩에 성공했으나, 설정 정보를 바탕으로 추천서비스 객체 생성에 실패한 경우, 1을 리턴해야 함" 
1 * mockConfigLoader.loadRecommendationConfig(configPath) >> recommendationConfig 
1 * mockRecAnalyticServiceFactory.create(recommendationConfig) >> { throw new CreationException() } 
code == 1 
! 
when: 
code = driver.run("--configFile", configPath) 
then: "설정 정보 로딩에 성공, 추천서비스 객체 생성에 성공했으나, 추천 생성에 실패하면, 1을 리턴해야 함” 
1 * mockConfigLoader.loadRecommendationConfig(configPath) >> recommendationConfig 
1 * mockRecAnalyticServiceFactory.create(recommendationConfig) >> mockRecAnalyticService 
1 * mockRecAnalyticService.createRecommendations() >> { throw new AnalyticsServiceException() } 
code == 1 
! 
when: 
code = driver.run("--configFile", configPath) 
then: "설정 정보 로딩 성공, 추천서비스 객체 생성에 성공하고, 추천 생성에 성공하면, 0을 리턴해야 함" 
1 * mockConfigLoader.loadRecommendationConfig(configPath) >> recommendationConfig 
1 * mockRecAnalyticServiceFactory.create(recommendationConfig) >> mockRecAnalyticService 
1 * mockRecAnalyticService.createRecommendations() 
code == 0 
}
결과 설계물 
32
역할? 
33 
1. 명령행 기본 옵션 검증 
및 도움말 출력 
2. 설정 파일 읽어와 분석 
기능 실행
역할 분리 
34 
1. 명령행 기본 옵션 검증 
및 도움말 출력 
2. 설정 파일 읽어와 분석 
기능 실행
설정 파일 읽기 구현(계속) 
35 
{ 
rec: { 
fileSystem: { 
type: "local" 
}, 
activityDataStorage: { 
activityDataPath: "/some/path/viewlog" 
}, 
analyticService: { 
type: "mahout" 
}, 
mahoutRunner: { 
type: "shellScript", 
javaHome: "${env.JAVA_HOME}", 
… 
outputPathPrefix: "/some/path/result" 
}, 
resultStorage: { 
type: "console" 
} 
} 
} 
public class ConfigLoaderImpl implements ConfigLoader { 
@Override 
public RecConfig loadRecConfig(String configPath) { 
File file = new File(configPath); 
if (!file.exists()) 
throw new ConfigLoaderException(); 
! 
RecConfig recConfig = new RecConfig(); 
! 
ConfigValueHelper configValue = new ConfigValueHelper(file); 
configValue.set("rec/fileSystem/type", recConfig::setFileSystemType); 
configValue.set(“rec/fileSystem/namenode", 
value -> recConfig.setHdfsNameNode(value)); 
configValue.setBoolean("rec/mahoutRunner/mahoutLocal", 
recConfig::setMahoutRunnerMahoutLocal); 
! 
… 
return configValue; 
} 
}
36 
public class ConfigValueHelper { 
! 
private final JsonNode rootNode; 
! 
public ConfigValueHelper(File configFile) { 
ObjectMapper mapper = new ObjectMapper(); 
mapper.configure(JsonParser.Feature.ALLOW_UNQUOTED_FIELD_NAMES, true); 
try { 
rootNode = mapper.readTree(configFile); 
} catch (IOException e) { throw new ConfigLoaderException(e); } 
} 
! 
public void set(String path, Consumer<String> consumer) { // setBoolean, setInt 등 유사하게 정의 
getJsonNodeValueByPath(path, jsonNode -> json.asText()).ifPresent(consumer); 
// getJsonNodeValueByPath(path, JsonNode::asBoolean).ifPresent(consumer); 예, setBoolean의 구현 
} 
! 
private <T> Optional<T> getJsonNodeValueByPath(String path, Function<JsonNode, T> valueGetter) { 
String[] fields = path.split("/"); 
JsonNode currentNode = rootNode; 
for (String field : fields) { 
JsonNode childNode = currentNode.get(field); 
if (childNode == null) { 
currentNode = null; 
break; 
} 
currentNode = childNode; 
} 
return currentNode == null ? Optional.<T>empty() : Optional.of(valueGetter.apply(currentNode)); 
} 
}
Factory 
설정 정보의 값에 따라 
모든 객체를 생성하고 조립 
public class RecAnalyticServiceFactoryImpl implements RecAnalyticServiceFactory { 
@Override 
public RecommendationAnalyticService create(RecommendationConfig config) { 
checkAnalyticServiceType(config); 
MahoutRecAnalyticService service = new MahoutRecAnalyticService(); 
ServiceColleboratorFactory factory = ServiceColleboratorFactory.create(config); 
service.setActivityDataStorage(factory.createActivityDataStorage()); 
service.setMahoutRunner(factory.createMahoutRunner()); 
service.setResultSaver(factory.createResultSaver()); 
return service; 
} 
37
기타 
• analytic-service 
• 런타임에 쉘 생성: Velocity 이용해서 템플릿 처리 
• CLI 
• 설정 파일 로딩시 플레이스홀더 변환 처리 
• ${env.환경변수}, ${자바시스템프로퍼티} 
• e2e 테스트 (로컬, 하둡 클러스터 환경) 
• 메이븐 이용 멀티 프로젝트 사용 
• API 위주 서브 프로젝트: spi-* 
• 주요 서브 프로젝트 
• mahout-analytic-service, simple-data-storage 
• cli 
• 배포판 생성 서브 프로젝트: zip 파일로 생성 
38
끝. 논의시작! 
39

Ji 개발 리뷰 (신림프로그래머)

  • 1.
    JI 구현 이야기,리뷰 1 신림프로그래머 최범균, 2014-10-14
  • 2.
    내용 • 리뷰1(오늘) • 시작: 기능 목록 초안, 초기 컴포넌트 식별 • 0.1 개발: 핵심 기능 구현, CLI 구현 • 일부 설계 과정, 일부 구현 결과물 • 리뷰2 (다음) • 0.2 개발: 웹 구현 • 일부 설계/구현 결과물 • 테스트 관련 생각할 거리: 단위 vs 통합, E2E 테스트 2
  • 3.
    이야기의 시작, 반복(중복) [커맨드라인] 행렬 분해 [커맨드라인] 사용자용 데이터 생성 [커맨드라인] 아이템용 데이터 생성 [자바] 데이터 변환 후 저장 반복 여러 알고리즘에 대해 과정 유사 3
  • 4.
    자동화 욕구 4 [커맨드라인] 행렬 분해 [커맨드라인] 사용자용 데이터 생성 [커맨드라인] 아이템용 데이터 생성 [자바] 데이터 변환 후 저장 반복 자동화 [프로세스] 경로, 타입, 숫자 등 많은 설정 단계별 실패 확인 데이터 변환 과정, 실패 처리 등 프로그램 설정 정보 경로, 타입, 숫자 등
  • 5.
    기능 목록 버전내용 0.1 1개 알고리즘에 대한 서비스 구현 콘솔에서 서비스 실행: 설정 파일 사용 5 0.2 웹 인터페이스 - 작업 설정 생성 - 작업 설정을 이용해서 백그라운드로 작업 실행 - 실행 내역 추적 0.3 다른 알고리즘 추가
  • 6.
  • 7.
    주요 모델 •분석 실행(프로세스) • 입력: 실행에 필요한 데이터 • 알고리즘 실행: 입력을 이용해서 특정 알고리즘 수행 • 결과 저장: 실행 결과를 원하는 형태로 보관 7
  • 8.
    0.1 버전 상위수준 설계 초안 입력 데이터 경로 제공 결과 보관 8 주요 인터페이스 핵심 컴포넌트 알고리즘 실행 설정 파일을 이용해서 분석 기능 실행
  • 9.
    요구 상세 •Mahout을 이용해서 분석 1개 실행 • 로컬에 설치된 Mahout을 사용 • 기능 실행은 쉘스크립트를 이용해서 실행 • Mahout 실행 환경 • 로컬: 로컬 실행, 로컬 입력 파일 • 하둡: 하둡 클러스터(MR) 실행, HDFS 입력 파일 • 로컬에 설치된 하둡을 사용 • 결과 저장 • 로컬에 JSON 파일, (단순 테스트 목적) 콘솔 출력 9
  • 10.
    요구 상세 •Mahout을 이용해서 분석 1개 실행 • 로컬에 설치된 Mahout을 사용 • 기능 실행은 쉘스크립트를 이용해서 실행 10 • Mahout 실행 환경 JVM에서 외부 쉘을 실행 사용자가 쉘을 마음대로 변경하면 안 되므로, 런타임에 쉘을 동적 생성 • 로컬: 로컬 실행, 로컬 입력 파일 • 하둡: 하둡 클러스터(MR) 실행, HDFS 입력 파일 • 로컬에 설치된 하둡을 사용 • 결과 저장 • 로컬에 JSON 파일, (단순 테스트 목적) 콘솔 출력 Mahout 실행시, 하둡 클러스 터 설정 가능하도록 입력 파일로 로컬 경로와 HDFS 경로를 사용할 수 있도록 하둡 결과 파일을 읽어와 처리
  • 11.
    상위 수준 핵심로직은 11 입력을 이용함 Mahout을 실행함 결과를 저장함
  • 12.
    핵심 부분 설계시작 12 class RecAnalyticsServiceSpec extends Specification { def “생성”() { def RecAnalyticsService service = new RecAnalyticsService() } } public class RecAnalyticsService { !} * 공간의 제약으로 이름 등 일부 변경 class RecAnalyticsServiceSpec extends Specification { def service = new RecAnalyticService() ! def “로그가 없을 경우”() { when: service.createRecommendation() then: thrown(NoDataException) } } public class RecAnalyticsService { public void createRecommendation() { throw new NoDataException(); } } ! public class NoDataException extends RuntimeEx…{ }
  • 13.
    13 def service= new RecAnalyticService() ! def “로그가 없을 경우”() { when: service.createRecommendation() then: thrown(NoDataException) } ! def “로그가 있다면”() { setup: def ActivityStorage activityStorage = Mock() service.setActivityStorage(activityStorage) when: service.createRecomendation() then: “로그가 있으면 NoDataException이 발생하지 않음” 1 * activityStorage.hasActivityData() >> true notThrown(NoDataException) } ! public interface ActivityStorage { boolean hasActivityData(); } ! public class RecAnalyticsService { private ActivityStorage activityStorage; ! public void createRecommendation() { if (!activityStorage.hasActivityData()) throw new NoDataException(); } ! public void setActivityStorage(ActivityStorage …) { this.activityStorage = …; } }
  • 14.
    14 def service= new RecAnalyticService() def ActivityStorage mockActivityStorage = Mock() ! def setup() { service.setActivityStorage(mockActivityStorage) } ! def “로그가 없을 경우”() { when: service.createRecommendation() then: thrown(NoDataException) } ! def “로그가 있다면”() { setup: mockActivityStorage.hasActivityData() >> true when: service.createRecomendation() then: “로그가 있으면 NoDataException이 발생하지 않음” notThrown(NoDataException) }
  • 15.
    15 def service= new RecAnalyticService() def ActivityStorage mockActivityStorage = Mock() def MahoutRunner mockMH = Mock() ! def setup() { service.setActivityStorage(mockActivityStorage) service.setMahoutRunner(mockMH) } ! def “로그가 있다면”() { setup: mockActivityStorage.hasActivityData() >> true ! when: service.createRecomendation() then: “로그가 있으면 NoDataException이 발생하지 않음” notThrown(NoDataException) ! when: service.createRecomendation() then: “Mahout 이용 분석 실행 실패하면, 익셉션 발생” 1 * mockMH.run() >> { throw new MREx() } thrown(AnalyticServiceException) ! } public interface MahoutRunner { public void run(); } ! public class RecAnalyticsService { private ActivityStorage activityStorage; private MahoutRunner mahoutRunner; ! public void createRecommendation() { if (!activityStorage.hasActivityData()) throw new NoDataException(); try { mahoutRunner.run(); } catch(MREx ex) { throw new AnalyticServicException(ex); } } … // setter 추가 } ! public class MREx extends RuntimeException {} public class AnalyticServiceException … {…}
  • 16.
    16 def service= new RecAnalyticService() def ActivityStorage mockActivityStorage = Mock() def MahoutRunner mockMH = Mock() def ResultSaver mockSaver = Mock() … // setup() def “로그가 있다면”() { setup: mockActivityStorage.hasActivityData() >> true def RecResult recResult = Mock() def UserItemSource uiSource = Mock() def SimItemSource siSource = Mock() recResult.getUserItem() >> uiSource recResult.getSimItem() >> recResult …// 다른 when-then when: service.createRecomendation() then: “Mahout 이용 분석 실행 실패하면, 익셉션 발생” 1 * mockMH.run(_) >> { throw new MREx() } thrown(AnalyticServiceException) ! when: service.createRecomendation() then: “Mahout 이용 분석 실행 성공하면, 결과 저장 시도” mockMH.run(mockActivityStorage) >> recResult 1 * mockSaver.saveUserItem(uiSource) 1 * mockSaver.saveSimItem(siSource) } public interface ResultSaver { void saveUserItem(UserItemSource source); void saveSimItem(SimItemSource source); } public interface UserItemSource {} public interface SimItemSource {} ! public interface RecResult { UserItemSource getUserItem(); SimItemSource getSimItem(); } ! public interface MahoutRunner { public void run(ActivityStorage activityStorage); } public class RecAnalyticsService { … private ResultSaver resultSaver; ! public void createRecommendation() { if (!activityStorage.hasActivityData()) throw new NoDataException(); RecResult result; try { result = mahoutRunner.run(activityStorage); } catch(MREx ex) { throw new AnalyticServicException(ex); } resultSaver.saveUserItem(result.getUserItem()); resultSaver.saveSimItem(result.getSimItem()) } … // setter
  • 17.
    추가적인 실패/성공 시나리오를 진행하면서 점진적으로 구현/설계 완성 17
  • 18.
  • 19.
  • 20.
    설계 결과물 모듈배치 결과 20 인터페이스 추출
  • 21.
    구현/설계 진행 계속 유사한 방식으로 MahoutRunner 콘크리트 클래스의 구현/설계를 점진적으로 진행 21
  • 22.
    인터페이스 정의를 어떻게? 22 이 인터페이스의 메서드를 정의하려면?
  • 23.
    사용자 입장에서 메서드도출 UserItemSource와 SimItemSource의 사용자 23 ▼ “ResultSaver의 콘크리트 클래스” public class ConsoleResultSaver implements … { ! public void saveUserItem(UserItemSource source) { List<UserItem> userItems = source.getUserItems(); for (UserItem ui: userItems) { … } } ! public void saveUserItem(UserItemSource source) { Iterator<UserItem> userItems = source.getIterator(); while(userItems.hasNext()) { UserItem ui = userItems.next(); … } } 1안 2안 대량 결과 데이터를 처리할 수 있어야 하기 때문에, 2안 선택
  • 24.
    2안으로 구현 쉘실행 결과로 만들어진 파일로부터 데이터를 읽어오는 Iterator 구현 필요 24 public interface UserItemSource { Iterator<UserItem> iterator(); }
  • 25.
    2안, Iterator 구현예 25 private class FileSystemSimItemIterator implements Iterator<SimItem> { private final SequenceFile.Reader reader; private final IntWritable key; private final VectorWritable value ; private SimItem nextItem; ! public FileSystemSimItemIterator( FileSystem fileSystem, Path simItemFile) { reader = createSequenceFileReader( fileSystem, similarItemFile, new Configuration()); key = new IntWritable(); value = new VectorWritable(); moveNext(); } ! @Override public boolean hasNext() { return nextItem != null; } ! @Override public SimilarItems next() { checkNextItemExists(); SimItem result = nextItem; moveNext(); return result; } private void checkNextItemsExists() { if (nextItem == null) throw new NoSuchElementException(); } ! private void moveNext() { boolean isNextRead = readNextKeyValue(); if (!isNextRead) { closeReader(); nextItem = null; return; } createNextItems(); } ! private boolean readNextKeyValue() { try { return reader.next(key, value); } catch (…) {…} } ! private void createNextItem() { List<ItemValue> allSimilarItems = new ArrayList<>(); for (Vector.Element ele : value.get().nonZeroes()) allSimilarItems.add(new ItemValue(ele.index(), ele.get())); nextItem = new SimItem(key.get(), allSimilarItems); } private void closeReader() { … } }
  • 26.
  • 27.
  • 28.
    CLI 요구 사항 • 작업 실행에 필요한 정보를 파일로 설정 • JSON 형식 사용 • 콘솔에서 설정 파일 경로를 지정해서 작업 실행 • 예) ji.sh -c recommendation.conf 28
  • 29.
    핵심은 • 설정파일을 읽어와 • 알맞게 분석 서비스 객체(예, RecAnalyticService)를 구 성한 뒤 • 분석 서비스를 실행 29
  • 30.
    핵심 기능 구현/설계에쓰인 테스트 코드(계속) class RecommendationDriverSpec extends Specification { def RecommendationDriver driver = new RecommendationDriver(); def ConfigLoader mockConfigLoader = Mock() def RecommendationAnalyticServiceFactory mockRecAnalyticServiceFactory = Mock() def RecommendationAnalyticService mockRecAnalyticService = Mock() def HelpPrinter mockHelpPrinter = Mock() def String configPath = "myPath" ! def setup() { driver.setConfigLoader(mockConfigLoader) driver.setFactory(mockRecAnalyticServiceFactory) driver.setHelpPrinter(mockHelpPrinter) } ! def "입력 인자 테스트, 비정상 입력일 때, 1 리턴해야 함"() { expect: driver.run() == 1 driver.run("--configFile") == 1 driver.run("-c") == 1 } ! def "입력 인자 테스트, 비정상 입력일 때, 도움말 출력함"() { 30 when: driver.run() then: 1 * mockHelpPrinter.print(_) } … 계속
  • 31.
    def "협업 객체연동 테스트"() { setup: def recommendationConfig = new RecommendationConfig() ! when: def code = driver.run("--configFile", configPath) then: "설정 정보 로딩에 실패하면, 1을 리턴해야 함" 1 * mockConfigLoader.loadRecommendationConfig(configPath) >> { throw new ConfigLoaderException() } code == 1 31 ! when: code = driver.run("--configFile", configPath) then: "설정 정보 로딩에 성공했으나, 설정 정보를 바탕으로 추천서비스 객체 생성에 실패한 경우, 1을 리턴해야 함" 1 * mockConfigLoader.loadRecommendationConfig(configPath) >> recommendationConfig 1 * mockRecAnalyticServiceFactory.create(recommendationConfig) >> { throw new CreationException() } code == 1 ! when: code = driver.run("--configFile", configPath) then: "설정 정보 로딩에 성공, 추천서비스 객체 생성에 성공했으나, 추천 생성에 실패하면, 1을 리턴해야 함” 1 * mockConfigLoader.loadRecommendationConfig(configPath) >> recommendationConfig 1 * mockRecAnalyticServiceFactory.create(recommendationConfig) >> mockRecAnalyticService 1 * mockRecAnalyticService.createRecommendations() >> { throw new AnalyticsServiceException() } code == 1 ! when: code = driver.run("--configFile", configPath) then: "설정 정보 로딩 성공, 추천서비스 객체 생성에 성공하고, 추천 생성에 성공하면, 0을 리턴해야 함" 1 * mockConfigLoader.loadRecommendationConfig(configPath) >> recommendationConfig 1 * mockRecAnalyticServiceFactory.create(recommendationConfig) >> mockRecAnalyticService 1 * mockRecAnalyticService.createRecommendations() code == 0 }
  • 32.
  • 33.
    역할? 33 1.명령행 기본 옵션 검증 및 도움말 출력 2. 설정 파일 읽어와 분석 기능 실행
  • 34.
    역할 분리 34 1. 명령행 기본 옵션 검증 및 도움말 출력 2. 설정 파일 읽어와 분석 기능 실행
  • 35.
    설정 파일 읽기구현(계속) 35 { rec: { fileSystem: { type: "local" }, activityDataStorage: { activityDataPath: "/some/path/viewlog" }, analyticService: { type: "mahout" }, mahoutRunner: { type: "shellScript", javaHome: "${env.JAVA_HOME}", … outputPathPrefix: "/some/path/result" }, resultStorage: { type: "console" } } } public class ConfigLoaderImpl implements ConfigLoader { @Override public RecConfig loadRecConfig(String configPath) { File file = new File(configPath); if (!file.exists()) throw new ConfigLoaderException(); ! RecConfig recConfig = new RecConfig(); ! ConfigValueHelper configValue = new ConfigValueHelper(file); configValue.set("rec/fileSystem/type", recConfig::setFileSystemType); configValue.set(“rec/fileSystem/namenode", value -> recConfig.setHdfsNameNode(value)); configValue.setBoolean("rec/mahoutRunner/mahoutLocal", recConfig::setMahoutRunnerMahoutLocal); ! … return configValue; } }
  • 36.
    36 public classConfigValueHelper { ! private final JsonNode rootNode; ! public ConfigValueHelper(File configFile) { ObjectMapper mapper = new ObjectMapper(); mapper.configure(JsonParser.Feature.ALLOW_UNQUOTED_FIELD_NAMES, true); try { rootNode = mapper.readTree(configFile); } catch (IOException e) { throw new ConfigLoaderException(e); } } ! public void set(String path, Consumer<String> consumer) { // setBoolean, setInt 등 유사하게 정의 getJsonNodeValueByPath(path, jsonNode -> json.asText()).ifPresent(consumer); // getJsonNodeValueByPath(path, JsonNode::asBoolean).ifPresent(consumer); 예, setBoolean의 구현 } ! private <T> Optional<T> getJsonNodeValueByPath(String path, Function<JsonNode, T> valueGetter) { String[] fields = path.split("/"); JsonNode currentNode = rootNode; for (String field : fields) { JsonNode childNode = currentNode.get(field); if (childNode == null) { currentNode = null; break; } currentNode = childNode; } return currentNode == null ? Optional.<T>empty() : Optional.of(valueGetter.apply(currentNode)); } }
  • 37.
    Factory 설정 정보의값에 따라 모든 객체를 생성하고 조립 public class RecAnalyticServiceFactoryImpl implements RecAnalyticServiceFactory { @Override public RecommendationAnalyticService create(RecommendationConfig config) { checkAnalyticServiceType(config); MahoutRecAnalyticService service = new MahoutRecAnalyticService(); ServiceColleboratorFactory factory = ServiceColleboratorFactory.create(config); service.setActivityDataStorage(factory.createActivityDataStorage()); service.setMahoutRunner(factory.createMahoutRunner()); service.setResultSaver(factory.createResultSaver()); return service; } 37
  • 38.
    기타 • analytic-service • 런타임에 쉘 생성: Velocity 이용해서 템플릿 처리 • CLI • 설정 파일 로딩시 플레이스홀더 변환 처리 • ${env.환경변수}, ${자바시스템프로퍼티} • e2e 테스트 (로컬, 하둡 클러스터 환경) • 메이븐 이용 멀티 프로젝트 사용 • API 위주 서브 프로젝트: spi-* • 주요 서브 프로젝트 • mahout-analytic-service, simple-data-storage • cli • 배포판 생성 서브 프로젝트: zip 파일로 생성 38
  • 39.