Successfully reported this slideshow.
We use your LinkedIn profile and activity data to personalize ads and to show you more relevant ads. You can change your ad preferences anytime.

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

신림프로그래머, JI 개발 이야기 리뷰 자료.

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

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

×