SlideShare ist ein Scribd-Unternehmen logo
1 von 51
Downloaden Sie, um offline zu lesen
리팩토링 사례 공유:
스타프로리그 앱
최범균
(트위터: @madvirus, madvirus@madvirus.net)
스타크래프트 프로리그 안드로이드앱
● 스타크래프트 프로리그 결과/순위 제공 앱
● 데이터:
○ 이스포츠 협회 HTML
○ 파싱해서 처리
진행 및 결과
● 진행
○ 앱 제작자 김횽훈님(bluepoet.me)과 코드 리뷰
■ 내용 공유를 허락해주신 용훈님, 땡큐!
○ 2시간 가까이 리팩토링 진행
● 결과
○ 역할에 대한 명확한 정의/분리
○ 파일 캐시 관련 코드의 응집도 높임/커플링 낮춤
○ 그 외 자잘한 코드 정리
진행 과정
● 코드를 보면서 대화를 시작
● 이상한 부분 발견 및 코드 이해
● 리팩토링 진행 (작명, 역할 분리 등)
● 위 과정을 반복
1. 이름
이름이 이상해: 실제 의미 파악
JsoupHtmlParser가
하는 일이 뭐에요?
이스포츠 사이트에서
HTML을 읽어와
파싱해서 Activity가
필요한 데이터를 만들
어요
그럼, Activity가 필요
한 데이터를 제공하
는건가요?
네
아, 그럼 이것들은
StarLeague와 관련
된
DataProvider인 셈
이네요
이름이 이상해: 이름 변경 1/2
HtmlParser -> StarLeagueDataProvider
(구현 방식을 드러내는 HtmlParser에서 실제 역할
을 드러내는 StarLeageDataProvider로 변경)
JsoupHtmlParser -> EsportSiteHtmlDataProvider
(구현기술을 표현하는 Jsoup에서 실제 하는 일의
의도가 드러나는 EsportSiteHtml로 변경)
이름이 이상해: 이름 변경 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) {
...
}
2. 객체 생성: setter > builder
이 부분이 좀 ... 코드 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 자체를 없애기
가 쉽지 않겠는데요,
이 부분이 좀 ... 코드 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 영역의
코드 의미부터 알아
볼까요?
이 부분이 좀 ... 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("");
두 개의 게임 결과 존재 가능
두 게임 모두 없는 경우
한 게임이 있고,
그 게임의 결과 없는 경우
한 게임만 있고,
그 게임의 결과 있는 경우
두 게임 있고,
두 게임 결과 없는 경우
팀이름
경기결과
이 부분이 좀 ... 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. 데이터 생성 코드의 중복
이 부분이 좀 ... 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;
}
}
문자열 생성 규칙
빌더로 이관
이 부분이 좀 ... 빌더 사용하도록 변경
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);
}
메서드 이름으로 의미 향상
데이터 생성 코드 중복 제거
이 부분이 좀 ... 자잘한 수정
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();
}
이 부분이 좀 ... 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();
3. 클래스로 분리
이 부분이 좀 ... 코드 의미 한번 더
@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를
파싱해서 값을 구해
주는 기능 분리
이 부분이 좀 ... 파싱 기능 분리 위한 선
작업
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;
...
이 부분이 좀 ... 파싱 기능 분리
@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;
}
}
중간정리
이름 변경
HTML Document
파싱 기능 분리
ProleagueTotalResult
생성 기능 분리
빌더로 인해 set 메서
드 필요 없어짐
4. 팩토리
콘크리트 클래스에 직접 접근하는 코드
public class ProleagueTotalResultActivity extends Activity {
private StarLeagueDataProvider dataProvider;
private void initialize() {
...
dataProvider = EsportSiteHtmlDataProvider.getInstance();
fileUtils = new FileUtils(getApplicationContext());
...
}
콘크리트 클래스에 직접
접근하네요.
이게 문제가 되나요?
HTML이 아닌 다른 방식
으로 데이터를 가져올 때
잡일이 늘어나요.
일단, 팩토리로 한 번 의
존을 끊어내보죠.
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() {
}
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(); // 콘크리트 클래스에 대한 의존 제거
팩토리 적용으로 직접적인 의존 제거
5. 응집도!!!
팩토리 적용 중, 발견된 문제의 그것
일이 좀 크네 - 이것은 파일 캐시? 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 실행
}
}
}
일이 좀 크네 - 이것은 파일 캐시? 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로 캐시 목적 파일 구분
일이 좀 크네 - 이것은 파일 캐시? 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는
캐시 용도로
생성됨
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());
파일캐시
처리코드
일이 좀 크네 - 파일 캐시 응집도 높이기
● 캐시 관련 코드를 한 곳에 모으기
● 아래 코드에서 캐시 관련 코드 제거하기
○ AsyncDataViewer 및 그 하위 클래스
○ AsyncDataViewer의 하위 타입을 사용하는 Activity들
● 캐시의 구현이 바뀌더라도 나머지 코드는 바
뀌지 않도록
○ 캐시를 적용하지 않아도, DB로 바꿔도
○ 나머지는 영향을 받지 않도록
● 방법은?
일이 좀 크네 - 파일 캐시 응집도 높이기
● 방법은?
○ 캐시 기능을 제공하는 프록시 적용!
캐시 관련 코드를
이 클래스에 모두 모음
팩토리는 EsportSiteHtmlDataProvider
객체가 아닌
FileCacheStarLeagueDataProvider 객
체를 리턴
일이 좀 크네 - 캐시 기능 모으기
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;
}
...
일이 좀 크네 - 캐시 기능 모으기
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;
}
...
일이 좀 크네 - 팩토리 수정
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);
일이 좀 크네 - 다른 코드의 캐시 관련 부
분 제거 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 불필요
일이 좀 크네 - 다른 코드의 캐시 관련 부
분 제거 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 생성 불필요
?
일이 좀 크네 - 다른 코드의 캐시 관련 부
분 제거 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에 캐시 구현이
남아 있음
일이 좀 크네 - 다른 코드의 캐시 관련 부
분 제거 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);
}
일이 좀 크네 - 다른 코드의 캐시 관련 부
분 제거 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);
...
}
중간 정리 이후의 변화 결과
캐시 관련 코드 제거 됨
팩토리를 이용해서 콘크리트 클
래스에 대한 직접적 의존 제거
캐시 관련 코드
한 곳으로 모음
싱글톤 패턴 제거
6. 다음 차례..
정리를 하다 보니 ... 다음 차례는 1/2
public abstract class AsyncDataViewer {
protected Context context;
protected StarLeagueDataProvider dataProvider;
...
public void load(String searchDate) {
viewByUrl(searchDate);
}
}
public class AsyncProleagueTotalResultDataViewer
extends 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);
}
}
상호 의존
템플릿 메서드 기
능 상실
데이터만 남음
정리를 하다 보니 ... 다음 차례는 2/2
public 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에 전달하기 위함
@Override
public List<ProleagueDetailResult> getDetailScore(String
searchDate, int selectedGameSet) {
String detailScoreResult = "";
if (selectedGameSet == 0)
detailScoreResult += getDetailScoreData(searchDate, 41);
else
detailScoreResult += 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
7. 정리
마무리 1/2
● 코드 리팩토링 과정
○ 코드 의미/의도(!) 파악
■ 코드 리뷰 과정
○ 의미/의도에 맞게 변경
○ 변경 과정에서 또 다른 이해 얻음
■ 새로운 이해는 다음 리팩토링의 대상이 됨
● 팩토리 적용 과정 > 싱글톤 패턴 적용 제거
● 팩토리 적용 과정 > 캐시 코드 응집도 문제 도출
● 캐시 분리 과정 > 템플릿 메서드 불필요해짐
○ (이 문서엔 없지만) 테스트로 뒷받침하면 안전
마무리 2/2
● 변화의 폭
○ 점진적 변경
■ 의미 명확하게 이름 변경 (클래스/메서드/변수 등)
● 적용 예: HtmlParser -> StarLeagueDataProvider
■ 구현 중복 제거 (메서드나 클래스로)
● 적용 예: 문자열 생성의 구현 중복을 Builder로 분리
■ 일부 기능 분리 (메서드나 클래스로)
● 적용 예: HTML 파싱을 TotalScoreParser로 분리
■ 콘크리트 클래스에 대한 의존 제거
● 적용 예: StarLeagueDataProviderFactory
○ 큰 폭의 변경
■ 흩어진 캐시 기능을 한 곳에 모으기
광고
내가 만든 코드를 함께 리뷰할 선배 프로그래머가 없나요?
주변 프로그래머들이 너무 바빠서 코드 리뷰할 시간이 없나요?
이런 상황이라면, 고민하지 마시고 연락주세요.
함께 코드를 보고 논의하고 수정하는 시간을 가져보아요~
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. 개발 얘기도 합니다.

Weitere ähnliche Inhalte

Andere mochten auch

자바8 스트림 API 소개
자바8 스트림 API 소개자바8 스트림 API 소개
자바8 스트림 API 소개beom kyun choi
 
도메인구현 KSUG 20151128
도메인구현 KSUG 20151128도메인구현 KSUG 20151128
도메인구현 KSUG 20151128beom kyun choi
 
Tensorflow 101
Tensorflow 101Tensorflow 101
Tensorflow 101GDG Korea
 
Android with dagger_2
Android with dagger_2Android with dagger_2
Android with dagger_2Kros Huang
 
접근성(Accessibility)과 안드로이드
접근성(Accessibility)과 안드로이드접근성(Accessibility)과 안드로이드
접근성(Accessibility)과 안드로이드GDG Korea
 
왜 Swift를 해야할까요?
왜 Swift를 해야할까요?왜 Swift를 해야할까요?
왜 Swift를 해야할까요?선협 이
 
파크히어 Realm 사용 사례
파크히어 Realm 사용 사례파크히어 Realm 사용 사례
파크히어 Realm 사용 사례선협 이
 
GKAC 2014 Nov. - 안드로이드 스튜디오로 생산성 올리기
GKAC 2014 Nov. - 안드로이드 스튜디오로 생산성 올리기GKAC 2014 Nov. - 안드로이드 스튜디오로 생산성 올리기
GKAC 2014 Nov. - 안드로이드 스튜디오로 생산성 올리기GDG Korea
 
같은 유저수, 다른 수익? 모바일 앱의 수익을 높이는 방법
같은 유저수, 다른 수익? 모바일 앱의 수익을 높이는 방법같은 유저수, 다른 수익? 모바일 앱의 수익을 높이는 방법
같은 유저수, 다른 수익? 모바일 앱의 수익을 높이는 방법GDG Korea
 
디자이너 없어도 괜찮아! (feat.Material Design Guide)
디자이너 없어도 괜찮아! (feat.Material Design Guide)디자이너 없어도 괜찮아! (feat.Material Design Guide)
디자이너 없어도 괜찮아! (feat.Material Design Guide)GDG Korea
 
2016 Staccato track3 Android를 더 잘 개발하려면? (MVP, MVVM, Clean Architecture)
2016 Staccato track3 Android를 더 잘 개발하려면? (MVP, MVVM, Clean Architecture)2016 Staccato track3 Android를 더 잘 개발하려면? (MVP, MVVM, Clean Architecture)
2016 Staccato track3 Android를 더 잘 개발하려면? (MVP, MVVM, Clean Architecture)승용 윤
 
Okjsp 13주년 발표자료: 생존 프로그래밍 Test
Okjsp 13주년 발표자료: 생존 프로그래밍 TestOkjsp 13주년 발표자료: 생존 프로그래밍 Test
Okjsp 13주년 발표자료: 생존 프로그래밍 Testbeom kyun choi
 
Best Practices in Media Playback
Best Practices in Media PlaybackBest Practices in Media Playback
Best Practices in Media PlaybackGDG Korea
 
Reinfocement learning
Reinfocement learningReinfocement learning
Reinfocement learningGDG Korea
 
FIrebase를 이용한 호우호우 미니게임 만들기
FIrebase를 이용한 호우호우 미니게임 만들기FIrebase를 이용한 호우호우 미니게임 만들기
FIrebase를 이용한 호우호우 미니게임 만들기GDG Korea
 
안드로이드 데이터 바인딩
안드로이드 데이터 바인딩안드로이드 데이터 바인딩
안드로이드 데이터 바인딩GDG Korea
 
Introduce Android TV and new features from Google I/O 2016
Introduce Android TV and new features from Google I/O 2016Introduce Android TV and new features from Google I/O 2016
Introduce Android TV and new features from Google I/O 2016GDG Korea
 
Hive 입문 발표 자료
Hive 입문 발표 자료Hive 입문 발표 자료
Hive 입문 발표 자료beom kyun choi
 

Andere mochten auch (20)

MVP 패턴 소개
MVP 패턴 소개MVP 패턴 소개
MVP 패턴 소개
 
자바8 스트림 API 소개
자바8 스트림 API 소개자바8 스트림 API 소개
자바8 스트림 API 소개
 
도메인구현 KSUG 20151128
도메인구현 KSUG 20151128도메인구현 KSUG 20151128
도메인구현 KSUG 20151128
 
Tensorflow 101
Tensorflow 101Tensorflow 101
Tensorflow 101
 
Android with dagger_2
Android with dagger_2Android with dagger_2
Android with dagger_2
 
접근성(Accessibility)과 안드로이드
접근성(Accessibility)과 안드로이드접근성(Accessibility)과 안드로이드
접근성(Accessibility)과 안드로이드
 
왜 Swift를 해야할까요?
왜 Swift를 해야할까요?왜 Swift를 해야할까요?
왜 Swift를 해야할까요?
 
파크히어 Realm 사용 사례
파크히어 Realm 사용 사례파크히어 Realm 사용 사례
파크히어 Realm 사용 사례
 
GKAC 2014 Nov. - 안드로이드 스튜디오로 생산성 올리기
GKAC 2014 Nov. - 안드로이드 스튜디오로 생산성 올리기GKAC 2014 Nov. - 안드로이드 스튜디오로 생산성 올리기
GKAC 2014 Nov. - 안드로이드 스튜디오로 생산성 올리기
 
같은 유저수, 다른 수익? 모바일 앱의 수익을 높이는 방법
같은 유저수, 다른 수익? 모바일 앱의 수익을 높이는 방법같은 유저수, 다른 수익? 모바일 앱의 수익을 높이는 방법
같은 유저수, 다른 수익? 모바일 앱의 수익을 높이는 방법
 
디자이너 없어도 괜찮아! (feat.Material Design Guide)
디자이너 없어도 괜찮아! (feat.Material Design Guide)디자이너 없어도 괜찮아! (feat.Material Design Guide)
디자이너 없어도 괜찮아! (feat.Material Design Guide)
 
2016 Staccato track3 Android를 더 잘 개발하려면? (MVP, MVVM, Clean Architecture)
2016 Staccato track3 Android를 더 잘 개발하려면? (MVP, MVVM, Clean Architecture)2016 Staccato track3 Android를 더 잘 개발하려면? (MVP, MVVM, Clean Architecture)
2016 Staccato track3 Android를 더 잘 개발하려면? (MVP, MVVM, Clean Architecture)
 
Flume 훑어보기
Flume 훑어보기Flume 훑어보기
Flume 훑어보기
 
Okjsp 13주년 발표자료: 생존 프로그래밍 Test
Okjsp 13주년 발표자료: 생존 프로그래밍 TestOkjsp 13주년 발표자료: 생존 프로그래밍 Test
Okjsp 13주년 발표자료: 생존 프로그래밍 Test
 
Best Practices in Media Playback
Best Practices in Media PlaybackBest Practices in Media Playback
Best Practices in Media Playback
 
Reinfocement learning
Reinfocement learningReinfocement learning
Reinfocement learning
 
FIrebase를 이용한 호우호우 미니게임 만들기
FIrebase를 이용한 호우호우 미니게임 만들기FIrebase를 이용한 호우호우 미니게임 만들기
FIrebase를 이용한 호우호우 미니게임 만들기
 
안드로이드 데이터 바인딩
안드로이드 데이터 바인딩안드로이드 데이터 바인딩
안드로이드 데이터 바인딩
 
Introduce Android TV and new features from Google I/O 2016
Introduce Android TV and new features from Google I/O 2016Introduce Android TV and new features from Google I/O 2016
Introduce Android TV and new features from Google I/O 2016
 
Hive 입문 발표 자료
Hive 입문 발표 자료Hive 입문 발표 자료
Hive 입문 발표 자료
 

Mehr von beom kyun choi

옛날 웹 개발자가 잠깐 맛본 Vue.js 소개
옛날 웹 개발자가 잠깐 맛본 Vue.js 소개옛날 웹 개발자가 잠깐 맛본 Vue.js 소개
옛날 웹 개발자가 잠깐 맛본 Vue.js 소개beom kyun choi
 
DDD로 복잡함 다루기
DDD로 복잡함 다루기DDD로 복잡함 다루기
DDD로 복잡함 다루기beom kyun choi
 
TDD 발담그기 @ 공감세미나
TDD 발담그기 @ 공감세미나TDD 발담그기 @ 공감세미나
TDD 발담그기 @ 공감세미나beom kyun choi
 
keras 빨리 훑어보기(intro)
keras 빨리 훑어보기(intro)keras 빨리 훑어보기(intro)
keras 빨리 훑어보기(intro)beom kyun choi
 
Tensorflow regression 텐서플로우 회귀
Tensorflow regression 텐서플로우 회귀Tensorflow regression 텐서플로우 회귀
Tensorflow regression 텐서플로우 회귀beom kyun choi
 
파이썬 언어 기초
파이썬 언어 기초파이썬 언어 기초
파이썬 언어 기초beom kyun choi
 
Event source 학습 내용 공유
Event source 학습 내용 공유Event source 학습 내용 공유
Event source 학습 내용 공유beom kyun choi
 
ALS WS에 대한 이해 자료
ALS WS에 대한 이해 자료ALS WS에 대한 이해 자료
ALS WS에 대한 이해 자료beom kyun choi
 
Ji 개발 리뷰 (신림프로그래머)
Ji 개발 리뷰 (신림프로그래머)Ji 개발 리뷰 (신림프로그래머)
Ji 개발 리뷰 (신림프로그래머)beom kyun choi
 
리뷰의 기술 소개
리뷰의 기술 소개리뷰의 기술 소개
리뷰의 기술 소개beom kyun choi
 
스프링 시큐리티 구조 이해
스프링 시큐리티 구조 이해스프링 시큐리티 구조 이해
스프링 시큐리티 구조 이해beom kyun choi
 
자바8 람다식 소개
자바8 람다식 소개자바8 람다식 소개
자바8 람다식 소개beom kyun choi
 
하둡2 YARN 짧게 보기
하둡2 YARN 짧게 보기하둡2 YARN 짧게 보기
하둡2 YARN 짧게 보기beom kyun choi
 
차원축소 훑어보기 (PCA, SVD, NMF)
차원축소 훑어보기 (PCA, SVD, NMF)차원축소 훑어보기 (PCA, SVD, NMF)
차원축소 훑어보기 (PCA, SVD, NMF)beom kyun choi
 
하둡 맵리듀스 훑어보기
하둡 맵리듀스 훑어보기하둡 맵리듀스 훑어보기
하둡 맵리듀스 훑어보기beom kyun choi
 

Mehr von beom kyun choi (20)

옛날 웹 개발자가 잠깐 맛본 Vue.js 소개
옛날 웹 개발자가 잠깐 맛본 Vue.js 소개옛날 웹 개발자가 잠깐 맛본 Vue.js 소개
옛날 웹 개발자가 잠깐 맛본 Vue.js 소개
 
DDD로 복잡함 다루기
DDD로 복잡함 다루기DDD로 복잡함 다루기
DDD로 복잡함 다루기
 
TDD 발담그기 @ 공감세미나
TDD 발담그기 @ 공감세미나TDD 발담그기 @ 공감세미나
TDD 발담그기 @ 공감세미나
 
keras 빨리 훑어보기(intro)
keras 빨리 훑어보기(intro)keras 빨리 훑어보기(intro)
keras 빨리 훑어보기(intro)
 
DDD 준비 서문래
DDD 준비 서문래DDD 준비 서문래
DDD 준비 서문래
 
Tensorflow regression 텐서플로우 회귀
Tensorflow regression 텐서플로우 회귀Tensorflow regression 텐서플로우 회귀
Tensorflow regression 텐서플로우 회귀
 
파이썬 언어 기초
파이썬 언어 기초파이썬 언어 기초
파이썬 언어 기초
 
Event source 학습 내용 공유
Event source 학습 내용 공유Event source 학습 내용 공유
Event source 학습 내용 공유
 
Spring Boot 소개
Spring Boot 소개Spring Boot 소개
Spring Boot 소개
 
ALS WS에 대한 이해 자료
ALS WS에 대한 이해 자료ALS WS에 대한 이해 자료
ALS WS에 대한 이해 자료
 
Ji 개발 리뷰 (신림프로그래머)
Ji 개발 리뷰 (신림프로그래머)Ji 개발 리뷰 (신림프로그래머)
Ji 개발 리뷰 (신림프로그래머)
 
리뷰의 기술 소개
리뷰의 기술 소개리뷰의 기술 소개
리뷰의 기술 소개
 
스프링 시큐리티 구조 이해
스프링 시큐리티 구조 이해스프링 시큐리티 구조 이해
스프링 시큐리티 구조 이해
 
자바8 람다식 소개
자바8 람다식 소개자바8 람다식 소개
자바8 람다식 소개
 
Zookeeper 소개
Zookeeper 소개Zookeeper 소개
Zookeeper 소개
 
하둡2 YARN 짧게 보기
하둡2 YARN 짧게 보기하둡2 YARN 짧게 보기
하둡2 YARN 짧게 보기
 
차원축소 훑어보기 (PCA, SVD, NMF)
차원축소 훑어보기 (PCA, SVD, NMF)차원축소 훑어보기 (PCA, SVD, NMF)
차원축소 훑어보기 (PCA, SVD, NMF)
 
Storm 훑어보기
Storm 훑어보기Storm 훑어보기
Storm 훑어보기
 
HBase 훑어보기
HBase 훑어보기HBase 훑어보기
HBase 훑어보기
 
하둡 맵리듀스 훑어보기
하둡 맵리듀스 훑어보기하둡 맵리듀스 훑어보기
하둡 맵리듀스 훑어보기
 

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

  • 1. 리팩토링 사례 공유: 스타프로리그 앱 최범균 (트위터: @madvirus, madvirus@madvirus.net)
  • 2. 스타크래프트 프로리그 안드로이드앱 ● 스타크래프트 프로리그 결과/순위 제공 앱 ● 데이터: ○ 이스포츠 협회 HTML ○ 파싱해서 처리
  • 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) { ... }
  • 9. 2. 객체 생성: setter > builder
  • 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; } }
  • 22. 중간정리 이름 변경 HTML Document 파싱 기능 분리 ProleagueTotalResult 생성 기능 분리 빌더로 인해 set 메서 드 필요 없어짐
  • 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(); // 콘크리트 클래스에 대한 의존 제거
  • 29. 팩토리 적용 중, 발견된 문제의 그것
  • 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. 중간 정리 이후의 변화 결과 캐시 관련 코드 제거 됨 팩토리를 이용해서 콘크리트 클 래스에 대한 직접적 의존 제거 캐시 관련 코드 한 곳으로 모음 싱글톤 패턴 제거
  • 46. 정리를 하다 보니 ... 다음 차례는 1/2 public abstract class AsyncDataViewer { protected Context context; protected StarLeagueDataProvider dataProvider; ... public void load(String searchDate) { viewByUrl(searchDate); } } public class AsyncProleagueTotalResultDataViewer extends 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. 정리를 하다 보니 ... 다음 차례는 2/2 public 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에 전달하기 위함 @Override public List<ProleagueDetailResult> getDetailScore(String searchDate, int selectedGameSet) { String detailScoreResult = ""; if (selectedGameSet == 0) detailScoreResult += getDetailScoreData(searchDate, 41); else detailScoreResult += 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
  • 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. 개발 얘기도 합니다.