3. 진행 및 결과
● 진행
○ 앱 제작자 김횽훈님(bluepoet.me)과 코드 리뷰
■ 내용 공유를 허락해주신 용훈님, 땡큐!
○ 2시간 가까이 리팩토링 진행
● 결과
○ 역할에 대한 명확한 정의/분리
○ 파일 캐시 관련 코드의 응집도 높임/커플링 낮춤
○ 그 외 자잘한 코드 정리
4. 진행 과정
● 코드를 보면서 대화를 시작
● 이상한 부분 발견 및 코드 이해
● 리팩토링 진행 (작명, 역할 분리 등)
● 위 과정을 반복
6. 이름이 이상해: 실제 의미 파악
JsoupHtmlParser가
하는 일이 뭐에요?
이스포츠 사이트에서
HTML을 읽어와
파싱해서 Activity가
필요한 데이터를 만들
어요
그럼, Activity가 필요
한 데이터를 제공하
는건가요?
네
아, 그럼 이것들은
StarLeague와 관련
된
DataProvider인 셈
이네요
7. 이름이 이상해: 이름 변경 1/2
HtmlParser -> StarLeagueDataProvider
(구현 방식을 드러내는 HtmlParser에서 실제 역할
을 드러내는 StarLeageDataProvider로 변경)
JsoupHtmlParser -> EsportSiteHtmlDataProvider
(구현기술을 표현하는 Jsoup에서 실제 하는 일의
의도가 드러나는 EsportSiteHtml로 변경)
8. 이름이 이상해: 이름 변경 2/2
public 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 ProleagueTotalResultAdapter
extends ListAdapter {
...
private HtmlParser parser;
...
public ProleagueTotalResultAdapter(Context context,
int layout, List<ProleagueTotalResult> list,
String searchDate,
HtmlParser parser, FileUtils fileUtils) {
...
}
public class ProleagueTotalResultAdapter
extends ListAdapter {
...
private StarLeagueDataProvider dataProvider;
...
public ProleagueTotalResultAdapter(Context context,
int layout, List<ProleagueTotalResult> list,
String searchDate,
StarLeagueDataProvider dataProvider, FileUtils fileUtils) {
...
}
10. 이 부분이 좀 ... 코드 1/2
public class EsportSiteHtmlDataProvider implements StarLeagueDataProvider {
...
@Override
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(" ");
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. 이 부분이 좀 ... 코드 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. 이 부분이 좀 ... 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. 이 부분이 좀 ... 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. 이 부분이 좀 ... 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. 이 부분이 좀 ... 빌더 사용하도록 변경
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. 이 부분이 좀 ... 자잘한 수정
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. 이 부분이 좀 ... 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();
19. 이 부분이 좀 ... 코드 의미 한번 더
@Override
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;
ProleagueTotalResult secondGameResult = null;
if (gameNumbers == 0) {
firstGameResult = ProleagueTotalResult.noGame(0);
secondGameResult = ProleagueTotalResult.noGame(1);
} else {
if (gameNumbers == 3) {
...
}
}
...
}
HTML을 읽어와서
알맞은 데이터를 추출
HTML Document를
파싱해서 값을 구해
주는 기능 분리
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. 이 부분이 좀 ... 파싱 기능 분리
@Override
public 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;
}
}
24. 콘크리트 클래스에 직접 접근하는 코드
public class ProleagueTotalResultActivity extends Activity {
private StarLeagueDataProvider dataProvider;
private void initialize() {
...
dataProvider = EsportSiteHtmlDataProvider.getInstance();
fileUtils = new FileUtils(getApplicationContext());
...
}
콘크리트 클래스에 직접
접근하네요.
이게 문제가 되나요?
HTML이 아닌 다른 방식
으로 데이터를 가져올 때
잡일이 늘어나요.
일단, 팩토리로 한 번 의
존을 끊어내보죠.
25. 1. EsportSiteHtmlDataProvider 생성자를 private에서 public으로 변경
2. 팩토리 클래스 추가
팩토리 구현 1/2
public class StarLeagueDataProviderFactory {
private static StarLeagueDataProvider dataProvider = new EsportSiteHtmlDataProvider();
public static StarLeagueDataProvider create() {
return dataProvider;
}
}
public class EsportSiteHtmlDataProvider implements StarLeagueDataProvider {
...
public EsportSiteHtmlDataProvider() {
}
26. 3. 팩토리 사용하도록 변경 (다른 클래스도 찾아서 변경)
4. EsportSiteHtmlDataProvider의 싱글톤 패턴 제거
팩토리 구현 2/2
public 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(); // 콘크리트 클래스에 대한 의존 제거
30. 일이 좀 크네 - 이것은 파일 캐시? 1/3
public 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. 일이 좀 크네 - 이것은 파일 캐시? 2/3
public 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. 일이 좀 크네 - 이것은 파일 캐시? 3/3
public 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. 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(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 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 ... {
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. 일이 좀 크네 - 파일 캐시 응집도 높이기
● 캐시 관련 코드를 한 곳에 모으기
● 아래 코드에서 캐시 관련 코드 제거하기
○ AsyncDataViewer 및 그 하위 클래스
○ AsyncDataViewer의 하위 타입을 사용하는 Activity들
● 캐시의 구현이 바뀌더라도 나머지 코드는 바
뀌지 않도록
○ 캐시를 적용하지 않아도, DB로 바꿔도
○ 나머지는 영향을 받지 않도록
● 방법은?
35. 일이 좀 크네 - 파일 캐시 응집도 높이기
● 방법은?
○ 캐시 기능을 제공하는 프록시 적용!
캐시 관련 코드를
이 클래스에 모두 모음
팩토리는 EsportSiteHtmlDataProvider
객체가 아닌
FileCacheStarLeagueDataProvider 객
체를 리턴
36. 일이 좀 크네 - 캐시 기능 모으기
FileCacheStarLeagueDataProvider 1/2
public 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. 일이 좀 크네 - 캐시 기능 모으기
FileCacheStarLeagueDataProvider 2/2
public 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. 일이 좀 크네 - 팩토리 수정
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. 일이 좀 크네 - 다른 코드의 캐시 관련 부
분 제거 1/5: AsyncDataViewer
public 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 AsyncProleagueTotalResultDataViewer
extends AsyncDataViewer {
public AsyncProleagueTotalResultDataViewer(
Context context, FileUtils fileUtils, String prefix) {
super(context, fileUtils, prefix);
}
@Override
protected void viewByCacheFile(String searchDate) {
((ProleagueTotalResultActivity) context).
viewListContents(fileUtils.readFile(searchDate + prefix));
}
private AsyncCallback<AsyncData> callback =
new AsyncCallback<AsyncData>() {
@Override
public 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. 일이 좀 크네 - 다른 코드의 캐시 관련 부
분 제거 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;
...
@Override
public 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;
...
@Override
public void onCreate(Bundle savedInstanceState) {
...
fileUtils = new FileUtils(getApplicationContext());
viewer = new AsyncIndividualRankingDataViewer(
this , fileUtils, PREFIX_CACHEFILE);
viewer.load(CalendarUtils.getCurrentDate());
}
캐시 파일 이름 용도
문자열 불필요
캐시에서 사용하기 위한
FileUtils 생성 불필요
?
41. 일이 좀 크네 - 다른 코드의 캐시 관련 부
분 제거 3/5: FileUtils 사용하는 코드 추가 확인
public class ProleagueTotalResultAdapter extends ListAdapter
{
private StarLeagueDataProvider dataProvider;
private FileUtils fileUtils;
public ProleagueTotalResultAdapter(Context context, int
layout,
List<ProleagueTotalResult> list, String searchDate,
StarLeagueDataProvider dataProvider, FileUtils fileUtils) {
...
this.dataProvider = dataProvider;
this.fileUtils = fileUtils;
}
@Override
void 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 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 ...
}
EsportSiteHtmlDataProvider에 캐시 구현이
남아 있음
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. 일이 좀 크네 - 다른 코드의 캐시 관련 부
분 제거 5/5: 캐시 목적 FileUtils 사용 코드 제거
public class ProleagueTotalResultAdapter extends ListAdapter
{
private StarLeagueDataProvider dataProvider;
private FileUtils fileUtils;
public ProleagueTotalResultAdapter(Context context, int
layout,
List<ProleagueTotalResult> list, String searchDate,
StarLeagueDataProvider dataProvider, FileUtils fileUtils) {
...
this.dataProvider = dataProvider;
this.fileUtils = fileUtils;
}
@Override
void 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. 중간 정리 이후의 변화 결과
캐시 관련 코드 제거 됨
팩토리를 이용해서 콘크리트 클
래스에 대한 직접적 의존 제거
캐시 관련 코드
한 곳으로 모음
싱글톤 패턴 제거
49. 마무리 1/2
● 코드 리팩토링 과정
○ 코드 의미/의도(!) 파악
■ 코드 리뷰 과정
○ 의미/의도에 맞게 변경
○ 변경 과정에서 또 다른 이해 얻음
■ 새로운 이해는 다음 리팩토링의 대상이 됨
● 팩토리 적용 과정 > 싱글톤 패턴 적용 제거
● 팩토리 적용 과정 > 캐시 코드 응집도 문제 도출
● 캐시 분리 과정 > 템플릿 메서드 불필요해짐
○ (이 문서엔 없지만) 테스트로 뒷받침하면 안전
50. 마무리 2/2
● 변화의 폭
○ 점진적 변경
■ 의미 명확하게 이름 변경 (클래스/메서드/변수 등)
● 적용 예: HtmlParser -> StarLeagueDataProvider
■ 구현 중복 제거 (메서드나 클래스로)
● 적용 예: 문자열 생성의 구현 중복을 Builder로 분리
■ 일부 기능 분리 (메서드나 클래스로)
● 적용 예: HTML 파싱을 TotalScoreParser로 분리
■ 콘크리트 클래스에 대한 의존 제거
● 적용 예: StarLeagueDataProviderFactory
○ 큰 폭의 변경
■ 흩어진 캐시 기능을 한 곳에 모으기
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. 개발 얘기도 합니다.