39
JI 구현 이야기, 리뷰 1 신림프로그래머 최범균, 2014-10-14

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

Embed Size (px)

DESCRIPTION

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

Citation preview

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

JI 구현 이야기, 리뷰 1신림프로그래머 최범균, 2014-10-14

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

내용• 리뷰1 (오늘)

• 시작: 기능 목록 초안, 초기 컴포넌트 식별 • 0.1 개발: 핵심 기능 구현, CLI 구현

• 일부 설계 과정, 일부 구현 결과물

• 리뷰2 (다음) • 0.2 개발: 웹 구현

• 일부 설계/구현 결과물 • 테스트 관련 생각할 거리: 단위 vs 통합, E2E 테스트

2

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

이야기의 시작, 반복(중복)

[커맨드라인] 행렬 분해

[커맨드라인] 사용자용 데이터

생성

[커맨드라인] 아이템용 데이터

생성

[자바] 데이터 변환 후

저장

반복

3

여러 알고리즘에 대해 과정 유사

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

자동화 욕구

4

[커맨드라인] 행렬 분해

[커맨드라인] 사용자용 데이터 생성

[커맨드라인] 아이템용 데이터 생성

[자바] 데이터 변환 후

저장

반복

자동화

[프로세스] 경로, 타입, 숫자 등 많은 설정

단계별 실패 확인 데이터 변환

설정 정보

프로그램

경로, 타입, 숫자 등

과정, 실패 처리 등

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

기능 목록

5

버전 내용

0.1 1개 알고리즘에 대한 서비스 구현 콘솔에서 서비스 실행: 설정 파일 사용

0.2

웹 인터페이스 - 작업 설정 생성 - 작업 설정을 이용해서 백그라운드로 작업 실행 - 실행 내역 추적

0.3 다른 알고리즘 추가

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

0.1v

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

주요 모델

• 분석 실행(프로세스) • 입력: 실행에 필요한 데이터 • 알고리즘 실행: 입력을 이용해서 특정 알고리즘 수행 • 결과 저장: 실행 결과를 원하는 형태로 보관

7

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

0.1 버전 상위 수준 설계 초안

8

주요 인터페이스

핵심 컴포넌트 알고리즘 실행

입력 데이터 경로 제공 결과 보관

설정 파일을 이용해서 분석 기능 실행

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

요구 상세• Mahout을 이용해서 분석 1개 실행

• 로컬에 설치된 Mahout을 사용 • 기능 실행은 쉘스크립트를 이용해서 실행

• Mahout 실행 환경 • 로컬: 로컬 실행, 로컬 입력 파일 • 하둡: 하둡 클러스터(MR) 실행, HDFS 입력 파일

• 로컬에 설치된 하둡을 사용

• 결과 저장 • 로컬에 JSON 파일, (단순 테스트 목적) 콘솔 출력

9

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

요구 상세

10

• Mahout을 이용해서 분석 1개 실행 • 로컬에 설치된 Mahout을 사용 • 기능 실행은 쉘스크립트를 이용해서 실행

• Mahout 실행 환경 • 로컬: 로컬 실행, 로컬 입력 파일 • 하둡: 하둡 클러스터(MR) 실행, HDFS 입력 파일

• 로컬에 설치된 하둡을 사용

• 결과 저장 • 로컬에 JSON 파일, (단순 테스트 목적) 콘솔 출력

JVM에서 외부 쉘을 실행

사용자가 쉘을 마음대로 변경하면 안 되므로, 런타임에 쉘을 동적 생성

Mahout 실행시, 하둡 클러스터 설정 가능하도록

입력 파일로 로컬 경로와 HDFS 경로를 사용할 수 있도록 하둡 결과 파일을 읽어와 처리

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

상위 수준 핵심 로직은

11

입력을 이용함

Mahout을 실행함

결과를 저장함

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

핵심 부분 설계 시작

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…{ }

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

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 = …; } }

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

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) }

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

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 … {…}

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

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

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

추가적인 실패/성공 시나리오를 진행하면서

점진적으로 구현/설계 완성

17

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

핵심 부분 설계 결과

18

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

소속/역할은?

19

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

설계 결과물 모듈 배치 결과

20

인터페이스 추출

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

구현/설계 진행 계속

21

유사한 방식으로 MahoutRunner 콘크리트 클래스의

구현/설계를 점진적으로 진행

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

인터페이스 정의를 어떻게?

22

이 인터페이스의 메서드를 정의하려면?

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

사용자 입장에서 메서드 도출

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안 선택

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

2안으로 구현

24

쉘 실행 결과로 만들어진 파일로부터 데이터를 읽어오는 Iterator 구현 필요

public interface UserItemSource { Iterator<UserItem> iterator(); }

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

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() { … } }

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

26

ResultCreator, *Source, Iterator 구현 관련

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

CLI 부분

27

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

CLI 요구 사항

• 작업 실행에 필요한 정보를 파일로 설정 • JSON 형식 사용

• 콘솔에서 설정 파일 경로를 지정해서 작업 실행 • 예) ji.sh -c recommendation.conf

28

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

핵심은

• 설정 파일을 읽어와

• 알맞게 분석 서비스 객체(예, RecAnalyticService)를 구성한 뒤

• 분석 서비스를 실행

29

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

핵심 기능 구현/설계에 쓰인 테스트 코드(계속)

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(_) } … 계속

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

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 }

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

결과 설계물

32

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

역할?

33

1. 명령행 기본 옵션 검증 및 도움말 출력

2. 설정 파일 읽어와 분석 기능 실행

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

역할 분리

34

1. 명령행 기본 옵션 검증 및 도움말 출력

2. 설정 파일 읽어와 분석 기능 실행

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

설정 파일 읽기 구현(계속)

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; } }

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

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)); } }

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

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; }

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

기타• analytic-service

• 런타임에 쉘 생성: Velocity 이용해서 템플릿 처리

• CLI • 설정 파일 로딩시 플레이스홀더 변환 처리

• ${env.환경변수}, ${자바시스템프로퍼티} • e2e 테스트 (로컬, 하둡 클러스터 환경)

• 메이븐 이용 멀티 프로젝트 사용 • API 위주 서브 프로젝트: spi-* • 주요 서브 프로젝트

• mahout-analytic-service, simple-data-storage • cli

• 배포판 생성 서브 프로젝트: zip 파일로 생성

38

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

끝. 논의시작!

39