JI 구현 이야기, 리뷰 1신림프로그래머 최범균, 2014-10-14
내용• 리뷰1 (오늘)
• 시작: 기능 목록 초안, 초기 컴포넌트 식별 • 0.1 개발: 핵심 기능 구현, CLI 구현
• 일부 설계 과정, 일부 구현 결과물
• 리뷰2 (다음) • 0.2 개발: 웹 구현
• 일부 설계/구현 결과물 • 테스트 관련 생각할 거리: 단위 vs 통합, E2E 테스트
2
이야기의 시작, 반복(중복)
[커맨드라인] 행렬 분해
[커맨드라인] 사용자용 데이터
생성
[커맨드라인] 아이템용 데이터
생성
[자바] 데이터 변환 후
저장
반복
3
여러 알고리즘에 대해 과정 유사
자동화 욕구
4
[커맨드라인] 행렬 분해
[커맨드라인] 사용자용 데이터 생성
[커맨드라인] 아이템용 데이터 생성
[자바] 데이터 변환 후
저장
반복
자동화
[프로세스] 경로, 타입, 숫자 등 많은 설정
단계별 실패 확인 데이터 변환
설정 정보
프로그램
경로, 타입, 숫자 등
과정, 실패 처리 등
기능 목록
5
버전 내용
0.1 1개 알고리즘에 대한 서비스 구현 콘솔에서 서비스 실행: 설정 파일 사용
0.2
웹 인터페이스 - 작업 설정 생성 - 작업 설정을 이용해서 백그라운드로 작업 실행 - 실행 내역 추적
0.3 다른 알고리즘 추가
0.1v
주요 모델
• 분석 실행(프로세스) • 입력: 실행에 필요한 데이터 • 알고리즘 실행: 입력을 이용해서 특정 알고리즘 수행 • 결과 저장: 실행 결과를 원하는 형태로 보관
7
0.1 버전 상위 수준 설계 초안
8
주요 인터페이스
핵심 컴포넌트 알고리즘 실행
입력 데이터 경로 제공 결과 보관
설정 파일을 이용해서 분석 기능 실행
요구 상세• Mahout을 이용해서 분석 1개 실행
• 로컬에 설치된 Mahout을 사용 • 기능 실행은 쉘스크립트를 이용해서 실행
• Mahout 실행 환경 • 로컬: 로컬 실행, 로컬 입력 파일 • 하둡: 하둡 클러스터(MR) 실행, HDFS 입력 파일
• 로컬에 설치된 하둡을 사용
• 결과 저장 • 로컬에 JSON 파일, (단순 테스트 목적) 콘솔 출력
9
요구 상세
10
• Mahout을 이용해서 분석 1개 실행 • 로컬에 설치된 Mahout을 사용 • 기능 실행은 쉘스크립트를 이용해서 실행
• Mahout 실행 환경 • 로컬: 로컬 실행, 로컬 입력 파일 • 하둡: 하둡 클러스터(MR) 실행, HDFS 입력 파일
• 로컬에 설치된 하둡을 사용
• 결과 저장 • 로컬에 JSON 파일, (단순 테스트 목적) 콘솔 출력
JVM에서 외부 쉘을 실행
사용자가 쉘을 마음대로 변경하면 안 되므로, 런타임에 쉘을 동적 생성
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
인터페이스 추출
구현/설계 진행 계속
21
유사한 방식으로 MahoutRunner 콘크리트 클래스의
구현/설계를 점진적으로 진행
인터페이스 정의를 어떻게?
22
이 인터페이스의 메서드를 정의하려면?
사용자 입장에서 메서드 도출
23
UserItemSource와 SimItemSource의 사용자 ▼
“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안으로 구현
24
쉘 실행 결과로 만들어진 파일로부터 데이터를 읽어오는 Iterator 구현 필요
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() { … } }
26
ResultCreator, *Source, Iterator 구현 관련
CLI 부분
27
CLI 요구 사항
• 작업 실행에 필요한 정보를 파일로 설정 • JSON 형식 사용
• 콘솔에서 설정 파일 경로를 지정해서 작업 실행 • 예) ji.sh -c recommendation.conf
28
핵심은
• 설정 파일을 읽어와
• 알맞게 분석 서비스 객체(예, 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 "입력 인자 테스트, 비정상 입력일 때, 도움말 출력함"() { 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 ! 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
37
설정 정보의 값에 따라 모든 객체를 생성하고 조립
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; }
기타• analytic-service
• 런타임에 쉘 생성: Velocity 이용해서 템플릿 처리
• CLI • 설정 파일 로딩시 플레이스홀더 변환 처리
• ${env.환경변수}, ${자바시스템프로퍼티} • e2e 테스트 (로컬, 하둡 클러스터 환경)
• 메이븐 이용 멀티 프로젝트 사용 • API 위주 서브 프로젝트: spi-* • 주요 서브 프로젝트
• mahout-analytic-service, simple-data-storage • cli
• 배포판 생성 서브 프로젝트: zip 파일로 생성
38
끝. 논의시작!
39