리팩토링 사례 스타프로리그앱

9,586 views

Published on

리팩토링 사례

Published in: Technology
0 Comments
8 Likes
Statistics
Notes
  • Be the first to comment

No Downloads
Views
Total views
9,586
On SlideShare
0
From Embeds
0
Number of Embeds
8,136
Actions
Shares
0
Downloads
21
Comments
0
Likes
8
Embeds 0
No embeds

No notes for slide

리팩토링 사례 스타프로리그앱

  1. 1. 리팩토링 사례 공유:스타프로리그 앱최범균(트위터: @madvirus, madvirus@madvirus.net)
  2. 2. 스타크래프트 프로리그 안드로이드앱● 스타크래프트 프로리그 결과/순위 제공 앱● 데이터:○ 이스포츠 협회 HTML○ 파싱해서 처리
  3. 3. 진행 및 결과● 진행○ 앱 제작자 김횽훈님(bluepoet.me)과 코드 리뷰■ 내용 공유를 허락해주신 용훈님, 땡큐!○ 2시간 가까이 리팩토링 진행● 결과○ 역할에 대한 명확한 정의/분리○ 파일 캐시 관련 코드의 응집도 높임/커플링 낮춤○ 그 외 자잘한 코드 정리
  4. 4. 진행 과정● 코드를 보면서 대화를 시작● 이상한 부분 발견 및 코드 이해● 리팩토링 진행 (작명, 역할 분리 등)● 위 과정을 반복
  5. 5. 1. 이름
  6. 6. 이름이 이상해: 실제 의미 파악JsoupHtmlParser가하는 일이 뭐에요?이스포츠 사이트에서HTML을 읽어와파싱해서 Activity가필요한 데이터를 만들어요그럼, Activity가 필요한 데이터를 제공하는건가요?네아, 그럼 이것들은StarLeague와 관련된DataProvider인 셈이네요
  7. 7. 이름이 이상해: 이름 변경 1/2HtmlParser -> StarLeagueDataProvider(구현 방식을 드러내는 HtmlParser에서 실제 역할을 드러내는 StarLeageDataProvider로 변경)JsoupHtmlParser -> EsportSiteHtmlDataProvider(구현기술을 표현하는 Jsoup에서 실제 하는 일의의도가 드러나는 EsportSiteHtml로 변경)
  8. 8. 이름이 이상해: 이름 변경 2/2public class ProleagueTotalResultActivity extends Activity{...private HtmlParser parser;...private void initialize() {...parser = JsoupHtmlParser.getInstance();...}}public class ProleagueTotalResultActivity extends Activity {...private StarLeagueDataProvider dataProvider;...private void initialize() {...dataProvider = EsportSiteHtmlDataProvider.getInstance();...}}public class ProleagueTotalResultAdapterextends ListAdapter {...private HtmlParser parser;...public ProleagueTotalResultAdapter(Context context,int layout, List<ProleagueTotalResult> list,String searchDate,HtmlParser parser, FileUtils fileUtils) {...}public class ProleagueTotalResultAdapterextends ListAdapter {...private StarLeagueDataProvider dataProvider;...public ProleagueTotalResultAdapter(Context context,int layout, List<ProleagueTotalResult> list,String searchDate,StarLeagueDataProvider dataProvider, FileUtils fileUtils) {...}
  9. 9. 2. 객체 생성: setter > builder
  10. 10. 이 부분이 좀 ... 코드 1/2public class EsportSiteHtmlDataProvider implements StarLeagueDataProvider {...@Overridepublic String getTotalScore(String searchDate) {Document doc = getDocument(TOTALRESULT_PARSE_URL + searchDate);Elements elements = doc.select("strong");return setTopScore(elements.size(), elements.text());}private String setTopScore(int gameNumbers, String htmlData) {String[] result = htmlData.split(" ");ArrayList<ProleagueTotalResult> list = new ArrayList<ProleagueTotalResult>();ProleagueTotalResult firstGameResult = new ProleagueTotalResult();ProleagueTotalResult secondGameResult = new ProleagueTotalResult();Gson gson = new Gson();if (gameNumbers == 0) {firstGameResult.setClub("");firstGameResult.setResult("해당날짜에는 경기가 없습니다.");secondGameResult.setClub("");secondGameResult.setResult("해당날짜에는 경기가 없습니다.");} else {...이 부분을 개발하면서마음에 안 들었는데요...어떤 부분이요?if-else 부분이 마음에들지 않아요. if-else를없애는 다른 방법이없을까요?이게 HTML 데이터를 기준으로 조건 비교하는 것이어서 if-else 자체를 없애기가 쉽지 않겠는데요,
  11. 11. 이 부분이 좀 ... 코드 2/2} else {if (gameNumbers == 3) {firstGameResult.setClub(result[1] + " vs " + result[2]);firstGameResult.setResult("");secondGameResult.setClub("");secondGameResult.setResult("해당날짜에는 경기가 없습니다.");} else if (gameNumbers == 4) {firstGameResult.setClub(result[1] + " vs " + result[4]);firstGameResult.setResult(result[2] + result[3]);secondGameResult.setClub("");secondGameResult.setResult("해당날짜에는 경기가 없습니다.");} else if (gameNumbers == 6) {...} else {...}}firstGameResult.setRow(0);secondGameResult.setRow(1);list.add(firstGameResult);list.add(secondGameResult);return gson.toJson(list);}그럼, 이 부분의 코드를 좀 더 깔끔하게 만드는 방법이 없을까요?음... 객체 생성 자체를 분리하면 조금 나아질 것도 같아요.어떻게요?일단 if-else 영역의코드 의미부터 알아볼까요?
  12. 12. 이 부분이 좀 ... if-else 부분 코드 의미String[] result = htmlData.split(" ");ProleagueTotalResult firstGameResult = new ProleagueTotalResult();ProleagueTotalResult secondGameResult = new ProleagueTotalResult();if (gameNumbers == 0) {firstGameResult.setClub("");firstGameResult.setResult("해당날짜에는 경기가 없습니다.");secondGameResult.setClub("");secondGameResult.setResult("해당날짜에는 경기가 없습니다.");} else {if (gameNumbers == 3) {firstGameResult.setClub(result[1] + " vs " + result[2]);firstGameResult.setResult("");secondGameResult.setClub("");secondGameResult.setResult("해당날짜에는 경기가 없습니다.");} else if (gameNumbers == 4) {firstGameResult.setClub(result[1] + " vs " + result[4]);firstGameResult.setResult(result[2] + result[3]);secondGameResult.setClub("");secondGameResult.setResult("해당날짜에는 경기가 없습니다.");} else if (gameNumbers == 6) {firstGameResult.setClub(result[1] + " vs " + result[2]);firstGameResult.setResult("");secondGameResult.setClub(result[4] + " vs " + result[5]);secondGameResult.setResult("");두 개의 게임 결과 존재 가능두 게임 모두 없는 경우한 게임이 있고,그 게임의 결과 없는 경우한 게임만 있고,그 게임의 결과 있는 경우두 게임 있고,두 게임 결과 없는 경우팀이름경기결과
  13. 13. 이 부분이 좀 ... if-else 부분의 문제if (gameNumbers == 0) {firstGameResult.setClub("");firstGameResult.setResult("해당날짜에는 경기가 없습니다.");secondGameResult.setClub("");secondGameResult.setResult("해당날짜에는 경기가 없습니다.");} else {if (gameNumbers == 3) {firstGameResult.setClub(result[1] + " vs " + result[2]);firstGameResult.setResult("");secondGameResult.setClub("");secondGameResult.setResult("해당날짜에는 경기가 없습니다.");} else if (gameNumbers == 4) {firstGameResult.setClub(result[1] + " vs " + result[4]);firstGameResult.setResult(result[2] + result[3]);secondGameResult.setClub("");secondGameResult.setResult("해당날짜에는 경기가 없습니다.");} else if (gameNumbers == 6) {firstGameResult.setClub(result[1] + " vs " + result[2]);firstGameResult.setResult("");secondGameResult.setClub(result[4] + " vs " + result[5]);secondGameResult.setResult("");} else {...1. 경기/결과 존재 여부에 따라 다른 처리2. 데이터 생성 코드의 중복
  14. 14. 이 부분이 좀 ... ProleagueTotalResult객체 빌더로 한 번 해 볼까?public class ProleagueTotalResult {...public static class Builder {private static final String NO_MATCHING_STRING = "해당날짜에는경기가 없습니다.";private String name1; private String name2; private boolean nameSet;private String score1; private String score2; private boolean scoreSet;private int order;public Builder order(int order) { this.order = order; return this; }public Builder clubName(String name1, String name2) {this.name1 = name1; this.name2 = name2; nameSet = true;return this;}public Builder result(String score1, String score2) {this.score1 = score1; this.score2 = score2; scoreSet = true;return this;}public ProleagueTotalResult build() { return new ProleagueTotalResult(getClubMatchString(), getResultString(), order); }private String getClubMatchString() {if (nameSet) name1 + " vs " + name2;return "";}private String getResultString() {if (scoreSet) return score1 + score2;return NO_MATCHING_STRING;}}문자열 생성 규칙빌더로 이관
  15. 15. 이 부분이 좀 ... 빌더 사용하도록 변경private String setTopScore(int gameNumbers, String htmlData) {String[] result = htmlData.split(" ");ArrayList<ProleagueTotalResult> list = new ArrayList<ProleagueTotalResult>();ProleagueTotalResult firstGameResult = null;ProleagueTotalResult secondGameResult = null;Gson gson = new Gson();if (gameNumbers == 0) {firstGameResult = new ProleagueTotalResult.Builder().order(0).build();secondGameResult = new ProleagueTotalResult.Builder().order(1).build();} else {if (gameNumbers == 3) {firstGameResult = new ProleagueTotalResult.Builder().order(0).clubName(result[1], result[2]).build();secondGameResult = new ProleagueTotalResult.Builder().order(1).build();} else if (gameNumbers == 4) {firstGameResult = new ProleagueTotalResult.Builder().order(0).clubName(result[1], result[4]).result(result[2], result[3]).build();secondGameResult = new ProleagueTotalResult.Builder().order(1).build();} else if (gameNumbers == 6) {...}}list.add(firstGameResult);list.add(secondGameResult);return gson.toJson(list);}메서드 이름으로 의미 향상데이터 생성 코드 중복 제거
  16. 16. 이 부분이 좀 ... 자잘한 수정private String setTopScore(int gameNumbers, String htmlData) {String[] result = htmlData.split(" ");// ArrayList<ProleagueTotalResult> list = new ArrayList<ProleagueTotalResult>();ProleagueTotalResult firstGameResult = null;ProleagueTotalResult secondGameResult = null;// Gson gson = new Gson(); 제거, 맨 아래 코드에서 필요 시점에 생성if (gameNumbers == 0) {firstGameResult = ProleagueTotalResult.noGame(0); // 게임 없는 경우 생성 코드 간결하게 변경secondGameResult = ProleagueTotalResult.noGame(1); // <-- new ProleagueTotalResult.Builder().order(1).build();} else {if (gameNumbers == 3) {firstGameResult = new ProleagueTotalResult.Builder().order(0).clubName(result[1], result[2]).build();secondGameResult = ProleagueTotalResult.noGame(1);} else if (gameNumbers == 4) {...}}ArrayList<ProleagueTotalResult> list = new ArrayList<ProleagueTotalResult>(); // 생성 시점에 ArrayList 생성list.add(firstGameResult);list.add(secondGameResult);return new Gson().toJson(list); // 필요 시점에 Gson 생성}public static ProleagueTotalResult noGame(int order) {return new ProleagueTotalResult.Builder().order(order).build();}
  17. 17. 이 부분이 좀 ... ProleagueTotalResult의 set* 제거public class ProleagueTotalResult {private String club;private String result;private int row;public ProleagueTotalResult(String sClub, String sResult, int sRow) {club = sClub;result = sResult;row = sRow;}...public int getRow() {return row;}public String getClub() {return club;}public String getResult() {return result;}// setRow/setClub/setResult 메서드 삭제public void setRow(int row) {this.row = row;}빌더로 인해 set 메서드 필요 없어짐!ProleagueTotalResult firstGameResult = new ProleagueTotalResult();firstGameResult.setClub(result[1] + " vs " + result[2]);firstGameResult.setResult("");firstGameResult.setRow(0);firstGameResult = new ProleagueTotalResult.Builder().order(0).clubName(result[1], result[2]).build();
  18. 18. 3. 클래스로 분리
  19. 19. 이 부분이 좀 ... 코드 의미 한번 더@Overridepublic String getTotalScore(String searchDate) {Document doc = getDocument(TOTALRESULT_PARSE_URL + searchDate);Elements elements = doc.select("strong");return setTopScore(elements.size(), elements.text());}private String setTopScore(int gameNumbers, String htmlData) {String[] result = htmlData.split(" ");ProleagueTotalResult firstGameResult = null;ProleagueTotalResult secondGameResult = null;if (gameNumbers == 0) {firstGameResult = ProleagueTotalResult.noGame(0);secondGameResult = ProleagueTotalResult.noGame(1);} else {if (gameNumbers == 3) {...}}...}HTML을 읽어와서알맞은 데이터를 추출HTML Document를파싱해서 값을 구해주는 기능 분리
  20. 20. 이 부분이 좀 ... 파싱 기능 분리 위한 선작업public String getTotalScore(String searchDate) {Document doc = getDocument(TOTALRESULT_PARSE_URL + searchDate);Elements elements = doc.select("strong");return setTopScore(elements.size(), elements.text());}private String setTopScore(int gameNumbers, String htmlData) {String[] result = htmlData.split(" ");ProleagueTotalResult firstGameResult = null;public String getTotalScore(String searchDate) {return setTopScore(getDocument(TOTALRESULT_PARSE_URL + searchDate));}private String getTopScore(Document doc) {Elements elements = doc.select("strong");int gameNumbers = elements.size();String htmlData = elements.text();String[] result = htmlData.split(" ");ProleagueTotalResult firstGameResult = null;...
  21. 21. 이 부분이 좀 ... 파싱 기능 분리@Overridepublic String getTotalScore(String searchDate) {return getTopScore(getDocument(TOTALRESULT_PARSE_URL +searchDate));}private String getTopScore(Document doc) {List<ProleagueTotalResult> list =new TotalScoreParser().parse(doc);return new Gson().toJson(list);}public class TotalScoreParser {public List<ProleagueTotalResult> parse(Document doc) {Elements elements = doc.select("strong");int gameNumbers = elements.size();String htmlData = elements.text();String[] result = htmlData.split(" ");ProleagueTotalResult firstGameResult = null;ProleagueTotalResult secondGameResult = null;if (gameNumbers == 0) {firstGameResult = ProleagueTotalResult.noGame(0);secondGameResult = ProleagueTotalResult.noGame(1);} else {...}List<ProleagueTotalResult> list =new ArrayList<ProleagueTotalResult>();list.add(firstGameResult);list.add(secondGameResult);return list;}}
  22. 22. 중간정리이름 변경HTML Document파싱 기능 분리ProleagueTotalResult생성 기능 분리빌더로 인해 set 메서드 필요 없어짐
  23. 23. 4. 팩토리
  24. 24. 콘크리트 클래스에 직접 접근하는 코드public class ProleagueTotalResultActivity extends Activity {private StarLeagueDataProvider dataProvider;private void initialize() {...dataProvider = EsportSiteHtmlDataProvider.getInstance();fileUtils = new FileUtils(getApplicationContext());...}콘크리트 클래스에 직접접근하네요.이게 문제가 되나요?HTML이 아닌 다른 방식으로 데이터를 가져올 때잡일이 늘어나요.일단, 팩토리로 한 번 의존을 끊어내보죠.
  25. 25. 1. EsportSiteHtmlDataProvider 생성자를 private에서 public으로 변경2. 팩토리 클래스 추가팩토리 구현 1/2public class StarLeagueDataProviderFactory {private static StarLeagueDataProvider dataProvider = new EsportSiteHtmlDataProvider();public static StarLeagueDataProvider create() {return dataProvider;}}public class EsportSiteHtmlDataProvider implements StarLeagueDataProvider {...public EsportSiteHtmlDataProvider() {}
  26. 26. 3. 팩토리 사용하도록 변경 (다른 클래스도 찾아서 변경)4. EsportSiteHtmlDataProvider의 싱글톤 패턴 제거팩토리 구현 2/2public class EsportSiteHtmlDataProvider implements StarLeagueDataProvider {private static EsportSiteHtmlDataProvider instance = new EsportSiteHtmlDataProvider();...public EsportSiteHtmlDataProvider() {}// 아래 코드 제거 시, 컴파일 에러가 발생하는 곳을 찾아서 3번 과정을 마무리 짓는다.public static EsportSiteHtmlDataProvider getInstance() {return instance;}public class ProleagueTotalResultActivity extends Activity {private StarLeagueDataProvider dataProvider;private void initialize() {...// 기존 EsportSiteHtmlDataProvider.getInstance()dataProvider = StarLeagueDataProviderFactory.create(); // 콘크리트 클래스에 대한 의존 제거
  27. 27. 팩토리 적용으로 직접적인 의존 제거
  28. 28. 5. 응집도!!!
  29. 29. 팩토리 적용 중, 발견된 문제의 그것
  30. 30. 일이 좀 크네 - 이것은 파일 캐시? 1/3public abstract class AsyncDataViewer {protected Context context;protected String prefix;protected FileUtils fileUtils;protected StarLeagueDataProvider dataProvider;public AsyncDataViewer(Context context, FileUtils fileUtils, String prefix){this.context = context;this.prefix = prefix;this.fileUtils = fileUtils;dataProvider= StarLeagueDataProviderFactory.create();}protected abstract void viewByCacheFile(String searchDate);protected abstract void viewByUrl(String searchDate);public void load(String searchDate) {if (fileUtils.existFile(searchDate + prefix)) { // 파일이 존재하면,viewByCacheFile(searchDate); // viewByCacheFile 실행} else {viewByUrl(searchDate); // 존재하지 않으면, viewByUrl 실행}}}
  31. 31. 일이 좀 크네 - 이것은 파일 캐시? 2/3public class AsyncProleagueTotalResultDataViewer extends AsyncDataViewer {public AsyncProleagueTotalResultDataViewer(Context context, FileUtils fileUtils, String prefix) {super(context, fileUtils, prefix);}protected void viewByCacheFile(String searchDate) {((ProleagueTotalResultActivity) context).viewListContents(fileUtils.readFile(searchDate + prefix));}protected void viewByUrl(final String searchDate) {Callable<AsyncData> callable = new Callable<AsyncData>() {public AsyncData call() throws Exception {AsyncData asyncData = new AsyncData(searchDate, dataProvider.getTotalScore(searchDate));return asyncData;}};new AsyncExecutor<AsyncData>(context).setCallable(callable).setAsyncCallback(callback).execute();}private AsyncCallback<AsyncData> callback = new AsyncCallback<AsyncData>() {public void onResult(AsyncData result) {((ProleagueTotalResultActivity) context).viewListContents(result.getData());if (CalendarUtils.checkSaveTime(result.getSearchDate())) {fileUtils.saveFile(result.getSearchDate() + prefix, result.getData());}}...};파일이 존재하면 파일에서 읽어와 데이터 전달파일이 없으면dataProvider로 읽어와데이터를 전달한 후,파일에 데이터 저장prefix로 캐시 목적 파일 구분
  32. 32. 일이 좀 크네 - 이것은 파일 캐시? 3/3public class ProleagueTotalResultActivity extends Activity {private static final String PREFIX_CACHEFILE = "_TotalResult.txt";private String searchDate;private FileUtils fileUtils;private StarLeagueDataProvider dataProvider;private AsyncDataViewer viewer;private void initialize() {...fileUtils = new FileUtils(getApplicationContext());viewer = new AsyncProleagueTotalResultDataViewer(this, fileUtils, PREFIX_CACHEFILE);...}public void viewResult(final String selectedDate) {...viewer.load(selectedDate);}public void viewListContents(String jsonData) { // AsyncProleagueTotalResultDataViewer에서 호출...}}AsyncProleagueTotalResultDataViewer가 사용할캐시 파일의 접미사(실제로는 postfix)를ProleagueTotalResultActivity가 제공.다른 DataViewer 구현도 동일한 구성fileUtils는캐시 용도로생성됨
  33. 33. public class ProleagueTotalResultActivity extends Activity {private static final String PREFIX_CACHEFILE = "_TotalResult.txt";private void initialize() {fileUtils = new FileUtils(getApplicationContext());viewer = new AsyncProleagueTotalResultDataViewer(this, fileUtils, PREFIX_CACHEFILE);...}public void viewResult(final String selectedDate) {...viewer.load(selectedDate);일이 좀 크네 - 파일 캐시 관련 코드의 낮은 응집도public abstract class AsyncDataViewer {protected Context context;protected String prefix;protected FileUtils fileUtils;protected StarLeagueDataProvider dataProvider;public AsyncDataViewer(Context context,FileUtils fileUtils, String prefix) {...dataProvider= StarLeagueDataProviderFactory.create();}protected abstract void viewByCacheFile(StringsearchDate);protected abstract void viewByUrl(String searchDate);public void load(String searchDate) {if (fileUtils.existFile(searchDate + prefix)) {viewByCacheFile(searchDate);} else {viewByUrl(searchDate);}}}public class AsyncProleagueTotalResultDataViewerextends AsyncDataViewer {public AsyncProleagueTotalResultDataViewer(Context context, FileUtils fileUtils, String prefix) {super(context, fileUtils, prefix);}protected void viewByCacheFile(String searchDate) {((ProleagueTotalResultActivity) context).viewListContents(fileUtils.readFile(searchDate + prefix));}protected void viewByUrl(final String searchDate) {Callable<AsyncData> callable = new ... {public AsyncData call() throws Exception {AsyncData asyncData = new AsyncData(searchDate, dataProvider.getTotalScore(searchDate));return asyncData;}};new AsyncExecutor<AsyncData>(context)....execute();}private AsyncCallback<AsyncData> callback = new ... {public void onResult(AsyncData result) {((ProleagueTotalResultActivity) context).viewListContents(result.getData());fileUtils.saveFile(result.getSearchDate() + prefix, result.getData());파일캐시처리코드
  34. 34. 일이 좀 크네 - 파일 캐시 응집도 높이기● 캐시 관련 코드를 한 곳에 모으기● 아래 코드에서 캐시 관련 코드 제거하기○ AsyncDataViewer 및 그 하위 클래스○ AsyncDataViewer의 하위 타입을 사용하는 Activity들● 캐시의 구현이 바뀌더라도 나머지 코드는 바뀌지 않도록○ 캐시를 적용하지 않아도, DB로 바꿔도○ 나머지는 영향을 받지 않도록● 방법은?
  35. 35. 일이 좀 크네 - 파일 캐시 응집도 높이기● 방법은?○ 캐시 기능을 제공하는 프록시 적용!캐시 관련 코드를이 클래스에 모두 모음팩토리는 EsportSiteHtmlDataProvider객체가 아닌FileCacheStarLeagueDataProvider 객체를 리턴
  36. 36. 일이 좀 크네 - 캐시 기능 모으기FileCacheStarLeagueDataProvider 1/2public class FileCacheStarLeagueDataProvider implements StarLeagueDataProvider {private static final String PREFIX_DETAILSCORE_CACHEFILE = "_DetailResult.txt";private static final String PREFIX_TEAMRANKING_CACHEFILE = "_TeamRanking.txt";private static final String PREFIX_TOTALSCORE_CACHEFILE = "_TotalResult.txt";private FileUtils fileUtils;private StarLeagueDataProvider realDataProvider;public FileCacheStarLeagueDataProvider(Context context, StarLeagueDataProvider realDataProvider) {this.realDataProvider = realDataProvider;this.fileUtils = new FileUtils(context);}public String getTotalScore(String searchDate) {if (fileUtils.existFile(searchDate + PREFIX_TOTALSCORE_CACHEFILE))return fileUtils.readFile(searchDate + PREFIX_TOTALSCORE_CACHEFILE);String totalScore = realDataProvider.getTotalScore(searchDate);if (CalendarUtils.checkSaveTime(searchDate))fileUtils.saveFile(searchDate + PREFIX_TOTALSCORE_CACHEFILE, totalScore);return totalScore;}...
  37. 37. 일이 좀 크네 - 캐시 기능 모으기FileCacheStarLeagueDataProvider 2/2public List<ProleagueDetailResult> getDetailScore(FileUtils fileUtilsParam, String searchDate, int selectedGameSet) {String cacheFileName = searchDate + "_" + selectedGameSet + PREFIX_DETAILSCORE_CACHEFILE;if (fileUtils.existFile(cacheFileName)) {String detailScoreResult = fileUtils.readFile(cacheFileName);return CommonFunc.getListByVoType(VoType.proleaguedetail, detailScoreResult);}List<ProleagueDetailResult> resultList = realDataProvider.getDetailScore(fileUtilsParam, searchDate, selectedGameSet);if (CalendarUtils.checkSaveTime(searchDate))fileUtils.saveFile(cacheFileName, new Gson().toJson(resultList));return resultList;}public String getTeamRanking() {String searchDate = CalendarUtils.getCurrentDate();if (fileUtils.existFile(searchDate + PREFIX_TEAMRANKING_CACHEFILE))return fileUtils.readFile(searchDate + PREFIX_TEAMRANKING_CACHEFILE);String teamRanking = realDataProvider.getTeamRanking();fileUtils.saveFile(searchDate + PREFIX_TEAMRANKING_CACHEFILE, teamRanking);return teamRanking;}...
  38. 38. 일이 좀 크네 - 팩토리 수정public class StarLeagueDataProviderFactory {private static StarLeagueDataProvider dataProvider = new EsportSiteHtmlDataProvider();public static StarLeagueDataProvider create() {return dataProvider;}}public class StarLeagueDataProviderFactory {private static StarLeagueDataProvider dataProvider = new EsportSiteHtmlDataProvider();public static StarLeagueDataProvider create(Context context) {return new FileCacheStarLeagueDataProvider(context, dataProvider);}}팩토리 코드 수정public abstract class AsyncDataViewer {...public AsyncDataViewer(Context context, FileUtils fileUtils, String prefix) {...dataProvider =StarLeagueDataProviderFactory.create(context);}팩토리 사용 코드 수정public class ProleagueTotalResultActivity extends Activity {...private void initialize() {...dataProvider =StarLeagueDataProviderFactory.create(this);
  39. 39. 일이 좀 크네 - 다른 코드의 캐시 관련 부분 제거 1/5: AsyncDataViewerpublic abstract class AsyncDataViewer {protected Context context;protected String prefix;protected FileUtils fileUtils;protected StarLeagueDataProvider dataProvider;public AsyncDataViewer(Context context, FileUtils fileUtils, String prefix) {this.context = context;this.prefix = prefix;this.fileUtils = fileUtils;dataProvider =StarLeagueDataProviderFactory.create(context);}protected abstract void viewByCacheFile(String searchDate);protected abstract void viewByUrl(String searchDate);public void load(String searchDate) {if (fileUtils.existFile(searchDate + prefix)) {viewByCacheFile(searchDate);} else {viewByUrl(searchDate);}}}public class AsyncProleagueTotalResultDataViewerextends AsyncDataViewer {public AsyncProleagueTotalResultDataViewer(Context context, FileUtils fileUtils, String prefix) {super(context, fileUtils, prefix);}@Overrideprotected void viewByCacheFile(String searchDate) {((ProleagueTotalResultActivity) context).viewListContents(fileUtils.readFile(searchDate + prefix));}private AsyncCallback<AsyncData> callback =new AsyncCallback<AsyncData>() {@Overridepublic void onResult(AsyncData result) {((ProleagueTotalResultActivity) context).viewListContents(result.getData());if (CalendarUtils.checkSaveTime(result.getSearchDate())) {fileUtils.saveFile(result.getSearchDate() +prefix, result.getData());}}더 이상 AsyncDataViewer에서캐시를 처리할 필요 없음캐시 파일 이름용 prefix와 파일처리용 FileUtils 불필요
  40. 40. 일이 좀 크네 - 다른 코드의 캐시 관련 부분 제거 2/5: AsyncDataViewer 생성 부분public class ProleagueTotalResultActivity extends Activity {private static final String PREFIX_CACHEFILE ="_TotalResult.txt";private String searchDate;private void initialize() {,,,dataProvider =StarLeagueDataProviderFactory.create(this);fileUtils = new FileUtils(getApplicationContext());viewer = new AsyncProleagueTotalResultDataViewer(this , fileUtils, PREFIX_CACHEFILE);alarm = new Alarm(this);if (!alarm.isAlarmRegister()) {alarm.registAlarm();}}public void viewListContents(String jsonData) {...adapter = new ProleagueTotalResultAdapter(this, R.layout.row, list,searchDate, dataProvider, fileUtils);...}public class TeamRankingActivity extends Activity {private static final String PREFIX_CACHEFILE ="_TeamRanking.txt";private FileUtils fileUtils;...@Overridepublic void onCreate(Bundle savedInstanceState) {...fileUtils = new FileUtils(getApplicationContext());viewer = new AsyncTeamRankingDataViewer(this , fileUtils, PREFIX_CACHEFILE);viewer.load(CalendarUtils.getCurrentDate());}public class IndividualRankingActivity extends Activity {private static final String PREFIX_CACHEFILE ="_IndividualRanking.txt";private FileUtils fileUtils;...@Overridepublic void onCreate(Bundle savedInstanceState) {...fileUtils = new FileUtils(getApplicationContext());viewer = new AsyncIndividualRankingDataViewer(this , fileUtils, PREFIX_CACHEFILE);viewer.load(CalendarUtils.getCurrentDate());}캐시 파일 이름 용도문자열 불필요캐시에서 사용하기 위한FileUtils 생성 불필요?
  41. 41. 일이 좀 크네 - 다른 코드의 캐시 관련 부분 제거 3/5: FileUtils 사용하는 코드 추가 확인public class ProleagueTotalResultAdapter extends ListAdapter{private StarLeagueDataProvider dataProvider;private FileUtils fileUtils;public ProleagueTotalResultAdapter(Context context, intlayout,List<ProleagueTotalResult> list, String searchDate,StarLeagueDataProvider dataProvider, FileUtils fileUtils) {...this.dataProvider = dataProvider;this.fileUtils = fileUtils;}@Overridevoid setData(final int pos, View convertView) {btn.setOnClickListener(new Button.OnClickListener() {public void onClick(View v) {...List<ProleagueDetailResult> detailResult =dataProvider.getDetailScore(fileUtils, searchDate, data.getRow());...}});}...}public class EsportSiteHtmlDataProvider implementsStarLeagueDataProvider {...public List<ProleagueDetailResult> getDetailScore(FileUtils fileUtils, String searchDate,int selectedGameSet) {String cacheFileName = ....;if (fileUtils.existFile(cacheFileName)) {detailScoreResult = fileUtils.readFile(cacheFileName);} else {...if (CalendarUtils.checkSaveTime(searchDate)) {fileUtils.saveFile(cacheFileName, detailScoreResult);}}return ...}EsportSiteHtmlDataProvider에 캐시 구현이남아 있음
  42. 42. 일이 좀 크네 - 다른 코드의 캐시 관련 부분 제거 4/5: 캐시 목적 FileUtils 사용 코드 제거public class EsportSiteHtmlDataProvider implements StarLeagueDataProvider {...public List<ProleagueDetailResult> getDetailScore(FileUtils fileUtils, String searchDate, int selectedGameSet) {String cacheFileName = ....;if (fileUtils.existFile(cacheFileName)) {detailScoreResult = fileUtils.readFile(cacheFileName);} else {...if (CalendarUtils.checkSaveTime(searchDate)) {fileUtils.saveFile(cacheFileName, detailScoreResult);}}return ...}public interface StarLeagueDataProvider {List<ProleagueDetailResult> getDetailScore(FileUtils fileUtils, String searchDate, int selectedGameSet);}
  43. 43. 일이 좀 크네 - 다른 코드의 캐시 관련 부분 제거 5/5: 캐시 목적 FileUtils 사용 코드 제거public class ProleagueTotalResultAdapter extends ListAdapter{private StarLeagueDataProvider dataProvider;private FileUtils fileUtils;public ProleagueTotalResultAdapter(Context context, intlayout,List<ProleagueTotalResult> list, String searchDate,StarLeagueDataProvider dataProvider, FileUtils fileUtils) {...this.dataProvider = dataProvider;this.fileUtils = fileUtils;}@Overridevoid setData(final int pos, View convertView) {btn.setOnClickListener(new Button.OnClickListener() {public void onClick(View v) {...List<ProleagueDetailResult> detailResult =dataProvider.getDetailScore(fileUtils, searchDate, data.getRow());...}});}...}public class ProleagueTotalResultActivity extends Activity{private String searchDate;private FileUtils fileUtils;...private void initialize() {...fileUtils = new FileUtils(....);.,..}public void viewListContents(String jsonData) {...adapter = new ProleagueTotalResultAdapter(this, R.layout.row, list,searchDate, dataProvider, fileUtils);...}
  44. 44. 중간 정리 이후의 변화 결과캐시 관련 코드 제거 됨팩토리를 이용해서 콘크리트 클래스에 대한 직접적 의존 제거캐시 관련 코드한 곳으로 모음싱글톤 패턴 제거
  45. 45. 6. 다음 차례..
  46. 46. 정리를 하다 보니 ... 다음 차례는 1/2public abstract class AsyncDataViewer {protected Context context;protected StarLeagueDataProvider dataProvider;...public void load(String searchDate) {viewByUrl(searchDate);}}public class AsyncProleagueTotalResultDataViewerextends AsyncDataViewer {public AsyncProleagueTotalResultDataViewer(Context context) {super(context);}...private AsyncCallback<AsyncData> callback =new AsyncCallback<AsyncData>() {public void onResult(AsyncData result) {((ProleagueTotalResultActivity) context).viewListContents(result.getData());}...};}public class ProleagueTotalResultActivity extends Activity {...private StarLeagueDataProvider dataProvider;private AsyncDataViewer viewer;private void initialize() {...viewer =new AsyncProleagueTotalResultDataViewer(this);...}public void viewResult(final String selectedDate) {...viewer.load(selectedDate);}}상호 의존템플릿 메서드 기능 상실데이터만 남음
  47. 47. 정리를 하다 보니 ... 다음 차례는 2/2public class ProleagueTotalResultActivity extends Activity {...private StarLeagueDataProvider dataProvider;private AsyncDataViewer viewer;private void initialize() {...dataProvider =StarLeagueDataProviderFactory.create(this);...}public void viewListContents(String jsonData) {...adapter = new ProleagueTotalResultAdapter(this, R.layout.row, list, searchDate, dataProvider);}}dataProvider를 구하는 이유는Adapter에 전달하기 위함@Overridepublic List<ProleagueDetailResult> getDetailScore(StringsearchDate, int selectedGameSet) {String detailScoreResult = "";if (selectedGameSet == 0)detailScoreResult += getDetailScoreData(searchDate, 41);elsedetailScoreResult += getDetailScoreData(searchDate, 51);return CommonFunc.getListByVoType(VoType.proleaguedetail, detailScoreResult);}private String getDetailScoreData(String searchDate, int selectedGameSet) {...ArrayList<ProleagueDetailResult> list =new ArrayList<ProleagueDetailResult>();int startRow = 3;int endRow = 33;for (int i = startRow; i <= endRow; i += 5) {...ProleagueDetailResult vo = new ProleagueDetailResult();...list.add(vo);}return gson.toJson(list);}List -> String -> List
  48. 48. 7. 정리
  49. 49. 마무리 1/2● 코드 리팩토링 과정○ 코드 의미/의도(!) 파악■ 코드 리뷰 과정○ 의미/의도에 맞게 변경○ 변경 과정에서 또 다른 이해 얻음■ 새로운 이해는 다음 리팩토링의 대상이 됨● 팩토리 적용 과정 > 싱글톤 패턴 적용 제거● 팩토리 적용 과정 > 캐시 코드 응집도 문제 도출● 캐시 분리 과정 > 템플릿 메서드 불필요해짐○ (이 문서엔 없지만) 테스트로 뒷받침하면 안전
  50. 50. 마무리 2/2● 변화의 폭○ 점진적 변경■ 의미 명확하게 이름 변경 (클래스/메서드/변수 등)● 적용 예: HtmlParser -> StarLeagueDataProvider■ 구현 중복 제거 (메서드나 클래스로)● 적용 예: 문자열 생성의 구현 중복을 Builder로 분리■ 일부 기능 분리 (메서드나 클래스로)● 적용 예: HTML 파싱을 TotalScoreParser로 분리■ 콘크리트 클래스에 대한 의존 제거● 적용 예: StarLeagueDataProviderFactory○ 큰 폭의 변경■ 흩어진 캐시 기능을 한 곳에 모으기
  51. 51. 광고내가 만든 코드를 함께 리뷰할 선배 프로그래머가 없나요?주변 프로그래머들이 너무 바빠서 코드 리뷰할 시간이 없나요?이런 상황이라면, 고민하지 마시고 연락주세요.함께 코드를 보고 논의하고 수정하는 시간을 가져보아요~1. 시간/장소: 저녁 시간대, 당산~사당 사이의 커피집2. 준비물: 함께 코드를 볼 수 있는 노트북 및 코드 수정이 가능한 개발도구(이클립스 등)3. 코드 리뷰 가능한 범위: 자바 기반의 코드4. 연락 방법a. 카페 댓글(http://cafe.daum.net/javacan/MsBU/13 글에 댓글)b. 트위터 멘션 또는 DM (@madvirus)c. 이메일 (madvirus@madvirus.net)d. 페이스북 (https://www.facebook.com/beomkyun.choi)5. 개발 얘기도 합니다.

×