회사에서 검색 서비스 개발 및 운영을 담당하고 있는데요.. 업무 중 하나가..
솔루션으로 구매하여 사용하는 엔진의 RAW-LEVEL의 API를 우리 개발자들이
사용하기 편하게 한번 감싸서 검색 API를 개발해 제공하는 업무가 있습니다. 

주로 성능보다는 재사용성이나 사용하는 입장에서 쓰기 편하게
그리고 검색 서버의 IP등의 프로퍼티 값들의 변경에 쉽게 대응 할 수 있고
디버깅을 쉽게 할 수 있도록 하는데 중점을 두고 API를 만들어 제공하고 있는데요..

검색 API라는 것이 단순히 쇼핑몰의 검색결과 페이지에서만 사용되는 것이 아니고
카테고리, 기획전, 리뷰, 모바일, 오픈API등 굉장히 많은 곳에서 사용되게 마련이지요..

그러다보니 의도하지 않은 검색 쿼리들이 유입되는 경우가 있거나
의도적인 악성 검색 쿼리들이 유입되는 경우도 있습니다.

제가 처음 이곳에 왔을 때 이렇게 엔진으로 유입되는 쿼리들을 보고 어디에서 유입되는 쿼리인지
확인 할 수 있는 방법이 전혀 없었습니다. 검색 쿼리를 보고 짐작을 한 뿐이었죠..
("키워드가 있으니 통합검색 페이지겠구나.... 카테고리번호와 키워드가 있으니.. 통합검색아니면 카테고리 페이지겠구나..등)

한번은 검색엔진으로 하루에 검색 request가 50배 이상 들어오는 경우가 있었습니다.
짐작으로 무한루프가 돌고 있는 것 같다라고 생각은 하였고, 실제로 검색 API를 사용하는 곳의 로직에 오류가 있어서
무한루프를 돌면서 검색 API를 호출하고 있었었지만 당시에 이 쿼리가 어디에서 들어오는지 확인 할 방법이 없었습니다.

위의 검색 쿼리를 보고 추론하는 방법으로 예상되는 모든 소스를 뒤져서 수정을 했었죠.

이런 일들을 좀 겪다보니 검색 API를 호출하는 클래스는 대부분 Controller 레벨이고
이 클래스의 이름과 검색 API를 호출하는 메서드를 알면 어느 URL을 통해서 들어오는지도
추적 할 수 있게다 싶은 생각이 들었습니다.

누가 날 부른거요??
 
현재 스레드가 수행되고 있는 클래스의 이름을 확인 하는 법은 비교적 간단합니다.

this.getClass().getName();


하지만 필요한건 나를 호출한 클래스입니다.

우선 검색을 위해 제공되는 API를 SearchService라 하고 이를 호출하는 Controller를
SearchController라 하겠습니다.

  SearchService.java


SearchController.java

SearchConroller의 main 메서드를 실행하면 [search...........] 라고 찍힐 것 입니다.

일반적인 사용패턴이 위와 같기 때문에 SearchService의 search 메서드에 검색 API를 호출한 클래스를 뽑아내는
로직이 들어간다면 어디서 API를 사용하는지 모두 알 수 있을 것 같았습니다.

이 부분을 Throwable을 이용하여 구현하였습니다.


SearchService.java

위 main 메서드를 실행하면 결과는 아래와 같이 나옵니다.

search............
throwable.SearchService[search](11)
throwable.SearchController[search](12)
throwable.SearchController[main](7)


실제로 필요한 것은 바로 이전 호출한 클래스와 메서드의 정보였기 때문에 StackTraceElement[]에서 1번째 index를 뽑아서 (0 아닙니다.) 이를 로깅하도록 하였습니다.

로그파일의 사이즈가 커지고 IO도 다소 늘어날 것이고
어쩌면 new Throwable이라는 것 자체가 부하를 가져 올 수도 있을 것입니다. (사실 이부분은 지금도 어느정도의 영향이 있는지
잘 모르겠습니다.)
하지만 이를 적용하여 얻은 운영상의 이점에 비하면 굉장히 작은 단점이었습니다.

이 로직을 적용한 이후 굉장히 많은 악성쿼리들의 유입 경로를 찾아내어 수정하고 그 패턴을 확인하여
validation을 강화 할 수 있었던 것은 보너스였겠죠.. :D


Posted by 용식
이 주제에 대한 마지막 글입니다.
이전에 Indexing 부분까지 작성하고 까먹고 있었네요.. --ㅋ

이전 포스트까지
문서를 Indexing하고 해당 문서를 저장하여 두는 Store까지 살펴보았습니다.

간단하게 요약하면 문서의 내용을
분석하여 키워드를 추출하고, 그 키워드를 필드별로 해당 키워드를 가진
문서번호(내부적인 unique number)와 연결하여 인덱스 파일에 저장해두는 개념이었습니다.

그렇다면 검색은 어떤 방식일까요?

키워드를 통해 위 인덱스파일에서
문서번호를 검색하여 리턴하는 구조가 될 것 입니다.

  IndexSearcher.java

인덱스파일은 변경이 될 수 있도록 외부에서 인덱스파일을 셋팅 할 수 있도록 하였습니다.
소스의 간소화를 위해 "제목"과 "본문" 필드에 대해서 키워드로 검색을 수행하도록
하드코딩하였습니다.

그리고 and검색을 위해 제목에 대한 검색으로 나온 rowid와 필드에 대한 검색으로 나온 rowid를
체크하여 (loop) 확인하고 있습니다. 실제로는 이런식으로는 안 하겠죠.

그리고 getDoc(long) 메서드가 있습니다. 검색결과로 리턴되는 것은 실제로는 문서ID의 리스트입니다.

이 문서ID에 해당하는 실제 문서를 가져오기 위해 사용되는 메서드입니다.

테스트케이스는 아래와 같습니다.

IndexSearcherTest.java

결과


색인 : -----------------------------------------
필드 명 : 본문
키워드 : 조끼 - rowId : [2]
키워드 : 운동화 - rowId : [0, 1]
키워드 : 좋아요 - rowId : [0, 1]
키워드 : 싫어요 - rowId : [2]
키워드 : 아디다스 - rowId : [1]
키워드 : 나이키 - rowId : [0]
키워드 : 청바지 - rowId : [2]

-----------------------------------------
필드 명 : 제목
키워드 : 운동화 - rowId : [0, 1, 2]
키워드 : 아디다스 - rowId : [1]
키워드 : 나이키 - rowId : [0]
키워드 : 청바지 - rowId : [2]


결과 : [0, 1]
resultDoc1 : Document [rowId=0, fieldList=[Field [name=제목, value=나이키 운동화], Field [name=본문, value=나이키 운동화 좋아요]]]
resultDoc2 : Document [rowId=1, fieldList=[Field [name=제목, value=아디다스 운동화], Field [name=본문, value=아디다스 운동화 좋아요]]]


우선 Document를 3개 생성하여 색인 대상 문서를 만들어냅니다.
그리고 색인을 하게 되죠. 결과를 보시면 추출된 키워드와 키워드별로 연결된 문서ID가 로깅되는 것을
확인 하실 수 있습니다. 본문과 제목필드에 대해서 모두 수행이 되지요.

테스트는 "운동화"라는 검색어로 진행됩니다.

AND검색으로 수행이 되어 최종적으로 0, 1번 문서가 리턴되는 것을 보실 수 있고
해당 ID의 문서를 IndexSearcher의 getDoc(Long) 메서드를 통해서 가져와서 로깅하는 것도 확인 가능합니다.

3번에 걸쳐서 간단하게나마 검색엔진의 자료구조를 구현해보았습니다.

실제 검색엔진에서는
훨씬 복잡하고 많은 기능이 있을테고
무엇보다도 중요한 속도와 대용량 처리를 위한 각종 최적화 작업이
코드에 포함되어 있을 것 입니다.

앞서 말씀드렸듯이 검색파트를 맡고 있으면서 신규로 들어오는
후배들을 위한 공부용 코드임을 인지하여주세요~

예제코드는
https://github.com/need4spd/searchengine 
에서 받아보시거나 확인하실 수 있습니다. 
Posted by 용식
회사에서 검색 파트를 맡고 있으면서
신규로  검색 파트로 들어오는 후배들 교육을 위해 만들었던
예제 소스입니다.

검색이 역인덱스 구조로 되어있고 대략 이런이런 형태로 검색과 색인이 된다..
말로 설명을 하는 것 보다 간단하게 직접 자료구조를 구현해 보는 것도
도움이 될 것 같아서 만들었었습니다.

실제로 자료구조의 구현이라하면 굉장히 어려운데
교육 목적이었기 때문에 자료구조라 부르기도 민망 할 정도로
간략화 시켰습니다. 

검색 엔진의 구현이라고 하였지만 사실
키워드 추출/ 키워드를 통한 색인 및 검색으로 한정지어 간략화시켰기 때문에
검색엔진을 만드는데 있어서 필요한 다른 고려 사항은 하나도 들어가 있지 않습니다.

역인덱스 구조를 Map과 List로 간단하게 구현하였고 (Map의 Key가 키워드, Value가 Document의 ROWID를 담고 있는
List가 되겠죠) 색인/검색/키워드 분석의 예제를
초!! 간략화하여 만들었습니다.

기본적인 API만 사용했기 때문에
자바에 대한 공부에도 도움이 될 것이라 생각합니다.

문장으로부터 키워드를 추출하는 Analyzer와
원문 데이터를 가지고 있는 Repository
색인 데이터를 가지고 있는 IndexFile
검색을 수행하는 Searcher로 구성되어있는 예제입니다.

한번 들어가보겠습니다. 
Posted by 용식
TAG java, 검색
Apache common 프로젝트 중 Pool이라는 것이 있습니다.
Object Pool을 개발 할 수 있는데요..
간단한 예제를 만들어보려고 합니다. 

Pool은 풀링 메카니즘을 쉽게 구현 할 수 있도록 해주는 프로젝트입니다.
DBCP도 이것을 활용해서 구현되어있구요..

기본적으로 Pooling 해서 사용 할 클래스를 하나 만들어 보겠습니다. 

  MyPoolableObject

그리고 필요한 것이 
위 클래스의 객체를 생성하여주는 Factory 클래스입니다.


  MyPoolableObjectFactory

위와 같이 두개의 클래스와 Pool에서 제공되는 GenericObjectPool을 사용하면 간단히 테스트 코드를
만들어 볼 수 있습니다.

  TestPool
결과는 아래와 같이 나옵니다.

 MyPoolableObject@ab95e6count : 1



이렇게만 봐서는 제대로 Pooling이 되는지 알기 어렵습니다.

테스트 코드를 조금만 변경하여 보겠습니다.
  TestPool
결과가 아래와 같이 나옵니다.

i : 0
MyPoolableObjectcount : 1
i : 1
MyPoolableObjectcount : 2
i : 2
MyPoolableObjectcount : 3
i : 3
MyPoolableObjectcount : 4
i : 4
MyPoolableObjectcount : 5
i : 5
MyPoolableObjectcount : 6
i : 6
MyPoolableObjectcount : 7
i : 7
MyPoolableObjectcount : 8
i : 8
MyPoolableObjectcount : 9
i : 9
MyPoolableObjectcount : 10  

결과를 보니 제대로 Pooling이 안되는 것 같네요..
Pooling이 되었다면 static으로 선언한 MyPoolableObject의 count가 전부 1이 되어야 할 것 입니다.
단 한번 객체로 만들어 진 이후에는 계속 Pooling이 되어 사용되었을테니까요.. 하지만 위 결과에서는
count가 지속적으로 증가하고 있고 이 이야기는 바로 매번 객체가 새로 생성되었다는 뜻 입니다.

뭐가 문제일까요?

for문을 보면 GenericObjectPool마저 매번 새로 생성하고 있는 것이 보입니다.
Pool자체가 매번 새롭게 생성되기 때문에 Pooling의 대상이 되는 MyPoolableObject도 매번 새롭게 생성되는 것 입니다.
Pool에 넣어두었지만 Pool자체가 새로 만들어지기 때문이죠..
그래서 아래처럼 코드를 수정해보겠습니다.

  TestPool
Pool은 단 한번만 생성하고 for문으로 돌면서 객체를 Pooling합니다.
결과가 잘 나올 것 같습니다만 실제로 돌려보면 결과는 아래와 같이 나옵니다.

i : 0
MyPoolableObjectcount : 1
i : 1
MyPoolableObjectcount : 2
i : 2
MyPoolableObjectcount : 3
i : 3
MyPoolableObjectcount : 4
i : 4
MyPoolableObjectcount : 5
i : 5
MyPoolableObjectcount : 6
i : 6
MyPoolableObjectcount : 7
i : 7
MyPoolableObjectcount : 8

count는 여전히 매번 증가하고 있으며
더군다나 i가 7에서 프로그램이 그대로 lock에 걸려버립니다.
이건 또 무슨일일까요???

코드를 보면 객체를 가져가는 부분은 있지만 (borrowObject)
사용하고 반납하는 부분이 없습니다.
GenericObjectPool에서의 Pooling max 값이 기본으로 8로 되어 있기 때문에
요청이 올 때마다 MyPoolableObject를 생성하였는데, 8개까지만 생성하고 더 이상
넘겨 줄 것이 없기 때문에 (반환을 하지 않았으므로...)
그대로 멈추고 있는 상황인 것 입니다.

실제 프로그램에서는 위 같은 상황이 되면 어딘가에서 객체를 반환 해 줄 때까지
프로그램이 멈춰있게 될 것 입니다.

그래서 코드를 아래와 같이 다시 한번 수정하여 보겠습니다.



TestPool
returnObject를 추가하였고
결과는 아래와 같습니다.

i : 0
MyPoolableObjectcount : 1
i : 1
MyPoolableObjectcount : 1
i : 2
MyPoolableObjectcount : 1
i : 3
MyPoolableObjectcount : 1
i : 4
MyPoolableObjectcount : 1
i : 5
MyPoolableObjectcount : 1
i : 6
MyPoolableObjectcount : 1
i : 7
MyPoolableObjectcount : 1
i : 8
MyPoolableObjectcount : 1
i : 9
MyPoolableObjectcount : 1

아~!!
이제 좀 제대로 되는 것 같네요.

실제로 사용하려면
여러가지 최적화된 설정도 필요하고
GenericObjectPool도 최소한 JVM안에서는 싱글턴으로 구현 되어야 할 것 입니다.


Posted by 용식

BigDecimal과 기본형

Java 2011.05.13 11:02
난 정수가 아닌 소수점 계산이 필요 할 때
거의 대부분에 있어서 BigDecimal을 사용한다.

결과를 예측하기 쉽고
의도대로 계산식을 구현하기 쉽고
정확하게 나오기 때문에..

이번에 데이터 마이닝 (물론 아주 기초적인 마이닝이지만...)을 하면서도
당연히 BigDecimal을 사용했다.

뭐 느려봐야 얼마나 느리겠어.. 라고 생각하고 있다가..

이번에 속도를 좀 개선하면서
BigDecimal을 double형으로 바꿨더니
속도가 상당히 빨라졌다.

약 3-6배는 빨라진 것 같다.

멀티스레드로 바꾸면서 개선되었던 속도만큼 빨라진듯..

뭐 double, float이라는게 계산을 빠르게 근사치로 하기 위해서
만들어졌다고 하니.... 메모리 사용도 줄어들었을테고..

아무튼..
대용량처리에서는
정확한 값이 필요한 경우가 아니면
그냥 기본형을 사용하자...



 
Posted by 용식

[Java] 오늘의 교훈.

Java 2011.03.30 00:17

위와 같은 코드가 있었다.
 
위와 같은 상태에서 class A에 있던 public enum을 삭제하고
하나의 독립적인 클래스 파일로 E라는 enum 클래스를 생성하였다.

  어차피 public이고 Type도 그대로인지라
컴파일에러는 어디서도 떨어지지 않았고, 변경된 클래스인 A와 새로 만들어진 enum 클래스인 E만을
배포하여 컴파일하였다.

전혀 문제가 없을거라 생각했는데
문제는  User 클래스의 do() 메서드에서 발생하였다.

생각해보면 당연한 것인데...
기존의 E enum 클래스의 return type은
A$E 였고, 새로 뽑아낸 enum 클래스의 Type은 E였다. (기본적으로 앞에 붙는 패키지는 제외..)

결국, 타입이 맞지 않는 오류가 발생하여 위와 같이 기존의 E를 받아 쓰던 클래스들을
전부 다시 컴파일해주었다..

기본적으로 자바에서의 Type이라는 것이 패키지를 포함한 full name일 것인데
평소에 거의 망각하듯이 쓰다보니 아차 하는 상황이 발생하였음....
 
Posted by 용식
앞서 포스트에서는
결과 값을 List<TestDomain>에 담기로 하였습니다.
그래서 중간 중간 나오는 결과를 List<TestDomain>에 담아 두는 로직이
들어가 있었습니다.

하지만, 그렇기 때문에 이미 합산 된 검색어를 찾기 위해서
다시 List에 대해 loop를 돌면서 해당 검색어를 가진 TestDomain을 찾아야 했습니다.
순차 탐색을 하기 때문에 최대 n만큼의 시간이 소요됩니다.

합산 되는 결과가 많아져서 List<TestDomain>의 크기가 커질 수록
탐색 시간도 길어질 것 입니다.

그러면 이 탐색시간을 줄이면 더욱 속도를 올릴 수 있을 것 같습니다.

여러가지 데이터를 탐색하는 알고리즘 중에서 상당히 빠른 방법이 있습니다.

바로 HashCode를 사용한 방법입니다.

이제 350000개의 7일치 검색어를 loop돌면서 (이건 어쩔 수 없다 치고요..)
각 검색어에 대한 검색 횟수의 합을 구한 후 List<TestDomain>에 넣는 것이 아니라
Map<String,TestDomain>에 넣어두겠습니다.

물론, 모든 결과가 Map<String,TestDomain>에 들어 간 이후에는 이를 다시
List<TestDomain>형으로 변경해줘야 합니다.

List<TestDomain>은 이미 사용되고 있는 메서드의 리턴형이기 때문에
이것이 바뀌게 되면 다른 클래스들을 수정해야하는 일이 생깁니다.
내부에서의 리팩토링만 하고 메서드의 signature는 그대로 유지하는 것입니다.

아무튼, 위와 같은 Map -> List 의 형변환을 하는 것이
불필요한 일일지도 모르겠습니다만..
한번 해보면 알겠죠..^^



Test.java 세번째

결과를 담아서 사용 할 Map객체를 생성하고
Map<String,TestDomain> resultMap = new HashMap<String,TestDomain>();

이를 사용해서 각 검색어의 검색횟수를 머지하고 있습니다.

결과는

initData size : 350000
loopCnt  : 350000
sort........
resultList size : 50000
수행시간 : 0초
수행시간 : 63ms

이렇게 나옵니다. 물론 PC마다 조금씩 다르겠지만...

앞선 포스트에서의 여러가지 방법보다 엄청나게 빠른 성능적 향상을 가져 올 수 있었습니다.

ms단위로 떨어졌네요..

가장 먼저 구현되어 있던 Test.java에 비해서
크게 복잡해지지도 않았고, 특별히 다른 라이브러리를 사용하지도 않았습니다.
하지만, 약간의 생각의 전환의 과정을 밟아오면서
조금조금씩 성능을 향상 시킬 수 있었습니다.

포스트를 마무리하기전에
위 내용을 멀티스레드로 구현해보겠습니다.

멀티스레드로 구현하면 더 빠를까요?


KeywordMergetWorker.java
Runnable 인터페이스를 구현한 KeywordMergeWorker입니다.

타겟이 되는 List와 결과를 담을 Map을 생성자에서 받아서
이 List에 대해 일을 하게 됩니다.
사실 여기서는 각각의 List에는 중복된 키워드가 들어있지 않기 때문에
각 KeywordMergeWorker는 단순히 List에 있는 TestDomain을 Map에 담아내는 역할만을
하게 됩니다. 물론 이 KeywordMergeWorker들이 여러개 생성되어 일을 하게 되는 경우에는
상황이 좀 다르겠죠..

그리고 결과를 담을 Map 객체를 병렬처리에 맞게 바꿔줘야 합니다.

Map<String,TestDomain> resultMap = new ConcurrentHashMap<String,TestDomain>();

각각의 KeywordMergeWorker가 자기의 List에서 TestDomain을 꺼내 위 Map에 있는지 없는지 확인 후
작업을 해야하기 때문에 각 thread간의 동기화가 중요합니다. 인위적으로 synchronized를 사용하는 것 보다는
기본적으로 제공하는 라이브러리를 사용하겠습니다.

각 5만개의 키워드를 가지고 있는 List를 전부 합쳐서 총 350000개의 키워드를 가지고 있던 initDatas도 수정해야 합니다.

각각의 스레드가 동일한 크기의 List를 가지고 일 하도록 하는 것도 병렬처리에서는 중요합니다.

총 7개의 스레드가 일을 한다고 가정 할 때
6개의 스레드가 1초만에 일을 끝내고, 1개의 스레드가 10분동안 일을 해야 한다면
결국  그 프로그램의 실행시간은 10분이 되기 때문입니다.

그리고 initDatas의 형을 List<List<TestDomain>> initDatas = new ArrayList<List<TestDomain>>(); 이렇게 수정하고
각 7일치의 List를 add하여 7개의 스레드가 5만개씩의 키워드 List를 가지고 작업을 할 수 있도록 하겠습니다.

실행부는
for(int threadCount = 0; threadCount < 7; threadCount++) {
            KeywordMergeWorker worker = new KeywordMergeWorker(initDatas.get(threadCount), resultMap);
            worker.run();
        }   

이렇게 되겠죠..

제 PC가 쿼드코어이지만 그냥 편의를 위해서
7개의 스레드를 생성하여 돌려보겠습니다


Test.java 병렬처리

병렬처리를 할 때 주의해야 할 것들은 거의 신경쓰지 않은 코드입니다.

내용이 그걸 얘기하려는건 아니니까요..

결과는 어떨까요? 더 빨라졌을까요?

initData size : 7
sort........
resultList size : 50000
수행시간 : 0초
수행시간 : 125ms

오히려 더 느려졌네요..

여러가지 이유가 있겠죠. CPU에  비해 과하게 많은 스레드라던가
동기화를 하기 위해 소요되는 시간이 더 많다던지 하는...

중요한건 병렬처리가 더 좋다 나쁘다에 대한 얘기가 아니라
정말 간단한 하나의 문제를 가지고도 이렇게 여러가지 생각을 해볼 수 있다는 것 입니다.


Posted by 용식
이번에 작성하는 내용은
딱히 어떤 주제를 정하기가 좀 그렇네요..

먼저 이 내용은 어떤 코드가 제일 좋다 나쁘다를 얘기하기 위한 것은 아닙니다.
약간의 생각의 전환이 많이 다른 결과를 가져 올 수도 있다는 것을 이야기 하고 싶어서
작성하는 글 입니다.

제가 최근에 한 소스에서 본 내용을
리팩토링 했던 내용을 적어 보려고 합니다.

7일치의 검색어와 그 검색어의 검색 횟수를 합산하는 프로그램이 있습니다.

일단, 어딘가의 로그에서 하루 단위의 각 검색어와 그 검색어의 검색 횟수는 가져 올 수 있다고 가정하고
하루에 검색횟수 상위 5만개의 검색어를 7일치 가져와서
각 검색어의 검색 횟수를 합산하고 이것을 merge하여 다시 검색 횟수순으로
정렬하여 보여주는 프로그램입니다.

어떻게 보면 map and reduce고요..
어떻게 보면 그냥 group by 검색어 sum(검색횟수) 정도가 될 것 입니다.

아무튼..
raw 데이터가 검색어:검색횟수이기 때문에
이것을 가지고 위의 내용을 구현한 소스가 있었습니다.

일단, 검색어와 검색횟수를 받을 수 있는 domain 클래스를 하나 만들었습니다.


TestDomain.java

검색어는 keyName으로, 검색횟수는 count로 들어갑니다.
이것은 실제 테스트 클래스에서는 INNER CLASS로 사용 될 것 입니다.

초기 데이터 (일 상위 5만개의 7일치 검색어 리스트)는 아래와 같이 셋팅하려 합니다.

초기 데이터 셋팅
하루치 검색어 리스트에 해당하는 list_N 을 7개 만들고
각 리스트에 대해 5만번씩 loop를 돌면서 i를 검색어를 셋팅합니다. (하루 단위의 검색어:검색횟수는 이미 집계되어
있기 때문에 하나의 리스트에 중복되는 검색어는 없다고 가정합니다.)
그리고 각 검색어의 카운트는 3000까지의 정수 중 random으로 나오는 정수를 셋팅합니다.

이렇게 하면 각 list를 add한 initDatas의 사이즈는 350000이 될 것 입니다.

그럼 이제 각 리스트에서 검색어를 뽑아서 어떻게 7일치의 검색어와 검색횟수를 구할 수 있을까요..?

제가 처음 본 소스는 아래와 같이 되어 있었습니다.

일단, merge된 결과를 담을 List를 생성합니다.
List<TestDomain> resultList = new ArrayList<TestDomain>();

그리고, 전체 키워드:검색횟수가 담겨있는 initDatas를 loop 돌면서
resultList를 뒤져서 그 키워드가 resultList에 있으면 각각의 count를 더해서 set하고
resultList에 없으면 resultList에 TestDomain을 add하는 형태로 되어 있었습니다.

기본 룰은 맞는데
문제는 resultList에 검색어가 있냐 없냐를 구분하는 것을
contains를 사용하지 않고(못 했을 수도 있죠..) keyname을 get하여 equals로 비교하고 있었습니다.


Test.java 첫번째 방법
위 소스를 돌려보면..
상황에 따라 조금 다르겠지만

initData size : 350000
loopCnt  : 8750475000
수행시간 : 1110초

이정도가 나오게 됩니다. 2번째 for문에서 break가 얼마나 빨리 실행되냐에 따라서 약간의 차이가 있겠지만
대략 수행시간은 큰 차이가 없을 것 같습니다.

1110초면 약 18분 정도의 시간이 소요됩니다.

그럼 이 코드를 살짝 바꿔보겠습니다.

예전에 OOP에 대한 이야기를 하면서 Object 클래스의 메서드인 equals 메서드에 대한 이야기를 한 적이 있습니다.
(http://devyongsik.tistory.com/290)
List에 어떤 객체가 있냐 없냐는 loop를 돌면서 equals로 계산 할 것이 아니라
equals 메서드를 구현하여 List의 contains 메서드를 활용하자는 이야기였죠.

이것을 바탕으로 TestDomain 클래스에 equals 메서드를 구현하고 이것을 가지고
약간의 소스를 수정해보겠습니다.



Test.java 두번째
TestDomain에 equals 메서드를 구현하고 List의 contains 메서드와 indexOf 메서드를 사용하여
같은 로직을 구현하도록 하였습니다.

initData size : 350000
loopCnt  : 350000
수행시간 : 856초


일단, 성능은 둘째치고라도 for문이 하나로 줄어들었고 이에 따라 loop의 횟수도
줄어들게 됩니다. (하지만 내부적으로 contains와 indexOf 메서드가 결국 해당 object를 찾기 위해서 배열을 loop 돌면서
같은 행동을 하고 있습니다.)

그럼에도 불구하고 속도가 빨라진건 당연히 loop 하나가 사라졌기 때문이겠죠...

하지만 뭐 획기적인 속도 개선은 없는 것 같습니다.

다른 방법이 무엇이 또 있을까요? loop를 줄일 수 있는 방법. 현재는 최대 350000번의 loop를 contains와 indexOf안에서
또 각각 돌고 있기 때문에 이것을 줄이기 위한 방법을 찾아야 할 것 같습니다.

굉장히 간단한 방법이 있죠.. contains 메서드를 indexOf 한번으로 대체하는 것 입니다.



이렇게만 해줘도
결과는 아래와 같이 나옵니다.

initData size : 350000
loopCnt  : 350000
수행시간 : 472초

그럼 또 다른 방법은 없을까요?

-계속-
Posted by 용식


인터페이스에 대한 얘기를 하다가
여기까지 오게 되었네요..

이번 포스트에서는 어노테이션을 사용해서 지금까지 만들었던 심플 프레임워크를 바꿔보려고 합니다.

사실 이번 포스트는 인터페이스와는 큰 관련은 없을지 모르겠습니다.

어노테이션은 @XXX 의 형식으로 사용되는 것으로
가장 대표적인 어노테이션은 @Override 그리고 스프링에서 사용하는 @Controller나
JUnit4 이상에서 사용되는 @Test 정도라고 생각하는데요

이 어노테이션은.. 사전에서는 주석이라고 되어 있습니다. 일반적으로 코드에 주석이라고 하면
컴파일러나 JVM은 읽지 못 하고 사람만 볼 수 있는 주석이라고 생각을 했다면
이 어노테이션은 컴파일러나 JVM에서도 볼 수 있고 활용 할 수 있는 주석이라고 보시면 될 것 같습니다.

어노테이션을 만들면 그 어노테이션의 RetentionPolicy를 정하게 됩니다.

바로 Compile 혹은 Runtime 인데요
위 예시된 어노테이션 중 Override가 Compile 이고
나머지 것들이 Runtime이 될 것 같네요.

예상하셨겠지만
@Override를 잘못 사용하거나 실제로 Override하지 않은 메서드에 저 어노테이션을 사용하면
IDE가 에러를 표시해주죠. 바로 컴파일 단계에서 사용되는 어노테이션입니다. (컴파일 단계 까지만..)

Junit의 @Test나 스피링의 @Controller 어노테이션은 실제 Runtime시에 JVM이 읽고 사용 할 수 있는
어노테이션들입니다.

어노테이션에 대한 것들은 따로 찾아보시면 더 많이 나올거고.. 일단 여기서는
어떻게 프레임워크를 바꿔 볼 것인가.. 인데요..

Class 자체에 어노테이션을 달아서 , 특정 디렉토리의 클래스들을 실시간으로 읽어서
해당 어노테이션이 달려있는 클래스들의 특정 메서드를 실행하는 방법도 있을 것 이고
Junit처럼 메서드 레벨에 어노테이션을 달아서 그 메서드들을 실행시키는 방법도 있을 것 입니다.

여기서 해 보려고 하는 것은 후자입니다.

앞서서 인터페이스를 사용하여 isPassed라는 메서드를 실행하는 MuiltTester를 만들었었는데요..

사용자에 따라서는 하나의 테스터 클래스가 여러 테스트 메서드를 가지고 있고
그 메서드들을 전부 다 실행하여 테스트를 해보고 싶을 경우도 있을 것 입니다.
Junit처럼...

그럼 일단 여기서 사용 할 어노테이션을 만들어 보겠습니다.

MyTest.java


이 어노테이션을 Runtime시에 활용해서 메서드를 실행 하려고 하기 때문에
RetentionPolicy를 RUNTIME으로 설정하였습니다. 어노테이션의 이름은 MyTest고 @MyTest로 사용 될 것 입니다.

이제 테스트 클래스들의 상위 인터페이스를 만들어보겠습니다

ProductTester.java

앞에서는 이 인터페이스에 공통적으로 사용 할 메서드가 선언되어 있었지만
우리는 그것을 어노테이션으로 마킹하여 메서드를 실행시킬 것이기 때문에 여기서는
필요가 없게 됩니다. 그럼에도 불구하고 인터페이스를 만든 이유는 하나의 List에 해당 테스터 클래스들을
전부 넣고 loop를 돌면서 일괄적으로 실행하기 편하도록 하기 위함입니다.

JAVA에 있는 인터페이스 중 Serializable 같은 인터페이스겠네요..

만일 어노테이션을 클래스 레벨에서 선언하고
클래스들을 읽어서 특정 어노테이션이 달린 클래스만 실행 시키도록 한다면
이 인터페이스도 필요 없어지겠죠..

그럼 이제 실제 테스터 클래스들이 어떻게 사용되는지 볼게요..

ProductListCompareTester.java ProductRankScoreTester.java NewServiceTester.java
@MyTest가 쓰인 곳과 메서드의 이름을 보세요.
메서드의 이름이 제각각이라더라도 @MyTest 어노테이션으로 표시를 해준격이 되기 때문에
실시간에 실행 가능한 메서드가 됩니다.
여기서는 테스트 메서드가 하나씩만 쓰여있지만 두개 세개가 되어도 상관 없습니다.

단지 테스트 프레임워크에서 실행하기 원하는 메서드에
@MyTest 어노테이션만 달아주면 됩니다.

대신.. reflect를 이용하여 method를 실행하게 되므로
파라메터 정도는 맞춰주는 것이 여로모로 편할 것 입니다.

그러면 MultiTester를 수정 한 AnnotationTester 클래스를 보겠습니다.

AnnotationTester.java

보시면 List에 들어있는 테스터 클래스들을 가져와서 Class 객체를 얻고
거기서 메서드들을 뽑아냅니다.

그리고 그 메서드들을 탐색하면서 MyTest.class 어노테이션을 가져와서
그 어노테이션이 있으면 그 메서드를 실행하는 로직으로 되어 있습니다

코드가 간단하니까
쉽게 보실 수 있을 것 입니다.

이 어노테이션을 잘 사용하면
여러모로 심플한 코드를 작성 할 수도 있습니다.
스프링에서 xml로 관리 되던 bean 객체들을 어노테이션을 도입하면서
코드의 간결함을 가져 올 수 있었던 것이 대표적인 예라고 생각합니다. (물론 xml이 더 좋다라고 생각하시는 분도 계십니다.. 관리적 측면에서..)

DB 컬럼을 어노테이션을 매칭하여
DB에서 가져 온 데이터를 자동으로 Domain 클래스의 필드와 매핑해서
Domain 객체를 생성 할 수도 있겠고요...

아무튼..
이렇게 해서 최종적으로는
어노테이션을 활용해서 테스트 프레임워크를 만들어보았습니다.

아주 심플하죠..ㅋㅋ

여기서 인터페이스와 어노테이션을 활용 한 방법은
아주 간단하고 수 많은 활용 방법 중 극히 일부분일 뿐입니다.
저도 계속 공부하고 있고요..

이런 것도 있구나.. 하고 느껴 보실 정도면
좋을 것 같습니다.

Posted by 용식
앞선 포스트에서는 Interface를 사용해서 같은 Interface를 구현한 테스터 클래스들을
한번에 실행하여 그 결과를 볼 수 있는 Tester.java 까지 구현하였습니다.


Tester.java
새로운 테스터가 추가되어야 한다면
ProductTester Interface를 구현하여 위 Tester.java 에서 그 클래스를 add해주면 되었습니다.

하지만, 매번 이렇게 테스터가 추가 될 때 마다
클래스를 수정하고, 컴파일해서 배포를 해야 한다면 조금 귀찮은 작업이 될 수도 있습니다.
그리고, 실제로 현장에서 사용되는 테스터 클래스가 한두개가 아닐 수 있는데
그렇게 되면 Tester.java의 클래스는 addTester 메서드로 도배가 될 것 입니다.

그래서 간단하게 프로퍼티를 사용해서 , 테스트 할 테스터 클래스들을 선언하고
이를 이용해서 Tester.java 를 구현해보려고 합니다.

일단 프로퍼티 파일을 만들고 거기다가 테스트 할 테스터 클래스들을 기술합니다.

그리고 이 프로퍼티 파일을 읽어서 해당 테스터 클래스들을 동적으로 생성하고
이를 이용해서 테스트를 수행하는 것이지요..


tester.properties Tester.java
프로퍼티 파일에서 테스터 클래스들을 가져와서 Class.forName 을 통해서 해당 class의 Class instance를 얻고
(관련 글 참조 : http://devyongsik.tistory.com/292) 이를 사용하여 객체를 생성해서 테스트를 수행하는 코드 입니다.
프로퍼티 파일에 대한 stream을 얻는 방법은 위의 두가지 경우처럼 절대경로를 이용하거나, Class의 getResource 메서드를 사용하여 클래스패스에 있는 프로퍼티를 읽어 올 수 있는 방법이 있습니다.
(관련 글 참고 : http://devyongsik.tistory.com/171)

여기서 얘기하고 싶은 것은 저것이 아니기 때문에.. 일단 넘어가고... 아무튼 프로퍼티로 부터 클래스의 full name을 얻어와서
이를 사용해 동적으로 객체를 생성하게 되어 있습니다.
꼭 .properties일 필요는 없고 xml로 만들어서 사용하셔도 되겠죠..

만약에 테스터 클래스가 추가 되어야 한다고 하면 위 상태에서는 소스를 건드릴 것이 아무것도 없습니다

단지, properties 파일에 추가 될 테스터 클래스의 이름을 추가해주기만 하면
알아서 테스트를 실행하게 됩니다.

이정도만 해도 꽤 쓸만 할 것 입니다. (아닐까요..ㅋㅋ)

아직 인터페이스를 사용하는 것이 왜 좋은지 감이 안 오신다면..
위 코드를 인터페이스를 사용하지 않고 한번 구현해보세요.

다음에는 Annotation을 사용해서 만들고자 하는 최종 버전을 작성해 보겠습니다.
Posted by 용식