java에서 tokenize를 할 때 두 개의 방법을 많이 씁니다.
- string tokenizer
- split 메소드
이 둘에 대해서 비교하겠습니다.
string tokenizer의 내부 동작
string tokenizer는 delimeter를 넘겨준다고 했습니다.
![](https://codingdog.pe.kr/wp-content/uploads/2023/08/spl1.png)
먼저, hasMoreTokens로 토큰이 있는지를 검사한다고 하였습니다. 여기서 skipDelimeters를 주목해 주세요.
![](https://codingdog.pe.kr/wp-content/uploads/2023/08/spl2-1.png)
다음, nextToken을 봅시다. 두 메서드에서 공통으로 보이는 것이 무엇인가요? skipDelimiters를 호출하는 것을 볼 수 있어요. 이 함수의 내부로 들어가 봅시다.
![](https://codingdog.pe.kr/wp-content/uploads/2023/08/spl3.png)
[그림 3]에서 보아야 할 것은 딱 2개입니다.
- 249번째 줄의 charAt(position) 부분
- 250번째 줄의 delimeters.indexOf(c) 부분
하나씩 봅시다. 먼저, charAt(position)을 봅시다. 이 메소드는 position 번째에 있는 문자를 가져오는 역할을 해요. 자바는 String 또한 배열로 구현되어 있어요. 그러면 3번째에 있는 문자는 어떻게 가져와요?
![](https://codingdog.pe.kr/wp-content/uploads/2023/08/spl9.png)
배열은 크기가 같은 원소들이 연속으로 모여있는 집합이라 했어요. 기준 위치 알고, 원소 당 크기를 아니까, 상대적인 위치도 금방 계산이 됩니다. 아래 글들에서도 설명을 했어요.
고로, 매우 효율적으로 동작합니다. 문제는 indexOf 입니다. delimeter에 c가 있는지 검사하는데요.
![](https://codingdog.pe.kr/wp-content/uploads/2023/08/spl10.png)
“abcd”에서 ‘a’를 찾는 연산은 상당히 빠르게 동작합니다. 왜? 1번째 위치에 바로 문자 a가 있기 때문입니다. 하지만, ‘e’를 찾는 건 어떨까요?
![](https://codingdog.pe.kr/wp-content/uploads/2023/08/spl11.png)
문자열 전체에 대해서 탐색해 보고 없다고 해야 합니다. 따라서, 최악의 경우 250번째 줄에서 delimeter 문자열의 길이만큼의 복잡도를 가질 수 있어요. 매번 target string의 문자 각각에 대해서요. 이 말은 다시 말하면
구분자가 매우 많은 경우 stringTokenizer는 매우 불리합니다.
target 문자를 순회할 때 마다, delimeter 문자열 전체를 보기 때문입니다.
보통, 구분자의 경우가 많지 않기에, string tokenizer를 써도 무방합니다.
java split 와 string tokenizer 성능 테스트
java split는 regex를 받기 때문에
- tokenizer 보다 복잡한 조건에 대해서 적용 가능합니다.
- 다만, 저의 경우 split 또한
- 특정 문자를 기준으로 토큰화 하는 경우에 많이 씁니다.
- 따라서 regex 또한 문자 집합을 의미하는 []가 들어갔습니다.
그러면 실험을 해 봅시다. 먼저, 테스트를 위한 buildForTest 메소드입니다. 이 메소드에서 쓰는 변수는 3개가 있는데요. 각각에 대한 설명은 아래와 같습니다.
- target은 토큰화를 시킬 문자열을 의미합니다.
- delimiter는 string tokenizer class에 넣을 delimeter를 의미합니다.
- splitPattern은 split에 넣을 정규식을 의미합니다.
![](https://codingdog.pe.kr/wp-content/uploads/2023/08/spl4.png)
1번 테스트에 쓸 데이터입니다. 소문자 집합이 구분자 집합이 됩니다. 그리고, 구분자는 총 5만개를 넣었고, 구분자 사이에 1부터 10까지의 수 중 랜덤하게 넣어놓았습니다.
![](https://codingdog.pe.kr/wp-content/uploads/2023/08/spl5.png)
이렇게 세팅된 데이터를 가지고, StringTokenizer와, split의 시간을 측정합니다. 10번 측정해서 나온 평균 시간을 측정하는데요.
- 1번째로 나온 결과는 String Tokenizer가 평균적으로 걸린 시간
- 2번째로 나온 결과는 split이 평균적으로 걸린 시간
을 의미합니다.
![](https://codingdog.pe.kr/wp-content/uploads/2023/08/spl6.png)
결과를 볼까요? 전자는 7.8, 후자는 6.3이 걸립니다.
![](https://codingdog.pe.kr/wp-content/uploads/2023/08/spl12.png)
심지어 어떤 경우에는 전자보다 후자가 더 걸리는 경우도 있어요. 이는, split의 경우 내부적으로 정규식으로 파싱하기 위한 정보를 추가로 만들기 때문이에요. 문자 집합이 26개여서, split보다 string tokenizer가 월등히 오래 걸릴 거 같았지만, 그렇지 않았던 이유는, 이 정보를 만드는 작업이 가볍지 않았기 때문입니다. 1
![](https://codingdog.pe.kr/wp-content/uploads/2023/08/spl7.png)
반대로, 구분자가 매우 많은 경우에는 이야기가 달라집니다. 44032는 ‘가’, 55203는 ‘힣’을 의미하는데요. delimeter에 가부터 힣까지 다 넣습니다. 그러면 구분자 문자열의 길이가 1.1만자가 넘겠지요. 즉, target 문자열을 token화 시킬 때, 문자 하나 하나 돌 때 마다
- 1.1만자가 넘는 delimeter에 대해 해당 문자가 있는지 검사합니다.
- 만약에, 해당 문자가 없다면, 이미 delimeter는 다 스캔한 상황입니다.
그렇기 때문에, 상당히 비효율적으로 동작하겠지요.
![](https://codingdog.pe.kr/wp-content/uploads/2023/08/spl8.png)
전자가 103.5, 후자가 9.9입니다. 압도적으로 전자가 느렸는데, delimeter가 상당히 크기 때문입니다. 오히려 java split 을 사용한 쪽이 압도적으로 빨랐습니다. 정리하면
- 구분자의 길이가 26 ~ 30자 내외면 string tokenizer로 분리합니다.
- 구분자의 길이가 매우 커지면
- split를 고려하거나
- target 문자열에 있는 구분자를 하나의 문자열로 치환 후 tokenizer를 쓰면 됩니다.
- 예를 들어 구분자가 한글이라면
- 한글인 문자를 모두 ‘가’로 치환한 후에
- ‘가’를 기준으로 분리하면 됩니다.
- 사실 이펙티브 자바에도 나오는 부분입니다. 쓸데없이 객체를 생성하지 말라고. ↩︎