회사에서 오랫동안 준비하고 지난 화요일에 큰 배포를 하고 나보니, 

퇴근 후 여유로움을 느끼고 있던 때 쯤, 

지난 겨울에 넥스터즈에서 진행했던 앱에 기능을 넣고 있던 중 갑자기 궁금한 부분이 생겼다. 

 

음악과 관련된 앱이라 soundCloud API를 사용하는 부분이 있는데

soundCloud의 노래들은 각각의 고유한 trackId를 소유하고 있다. 

 

또한 우리가 만든 어드민 툴에서는 신규 노래를 등록할 때 다음과 같은 두 가지의 종류의 URL로 넘겨주고 있다.

1. https://api.soundcloud.com/tracks/607910394

2. https://api.soundcloud.com/tracks/607910394/stream

딱 보면 알겠지만, 마지막에 /stream의 존재 유무이다. 

 

기존에 돌아가던 코드는 아래처럼 노가다성으로 사용하고 있었으며,

String을 매 번 짤라서 사용하기 때문에 메모리를 사용함에 있어서도 매우 비 효율적일 것 같다는 생각이 들었다.

물론 매직 넘버들이 상수화가 안 되어 있어서 가독성도 떨어졌다. 

public String getTrackId(String url) {
    String[] split = StringUtils.split(url, "/");
    int size = split.length;

    if (NumberUtils.isNumber(split[size - 1])) {
        return split[size - 1];
    }

    return split[size - 2];
}

 

맥주 한 캔 하면서 요걸 바라본 결과,

이것보다 정규식을 사용하면 더 빠르게, 더 메모리를 적게 사용하면서 가독성도 높힐 수 있을 것 같다는 생각이 들었고,

생각과 동시에 바로 아래와 같이 코드를 작성했다.

private static final Pattern GET_TRACK_ID_PATTERN = Pattern.compile(".+/(\\d+)(/stream)?");
private static final int TRACK_ID_IDX = 1;
public String getTrackId2(String url) {
    Matcher matcher = GET_TRACK_ID_PATTERN.matcher(url);

    return matcher.find() ? matcher.group(TRACK_ID_IDX) : StringUtils.EMPTY;
}

 

그런 다음.. 바로 아래의 코드로 테스트를 해보았는데

결과는 생각 했던거와 달라서 조금 신기하긴 했다 

@Test
void getTrackIdTimeCheck() {
	//given
	List<String> urlList = IntStream.rangeClosed(0, 1_000_000_000)
			.boxed()
			.map(each -> "https://api.soundcloud.com/tracks/607910394/stream")
			.collect(Collectors.toList());

	//when
	StopWatch stopWatch = new StopWatch();
	stopWatch.start();
	urlList.stream().forEach(SoundCloundUtils::getTrackId);
	stopWatch.stop();

	//then
	log.info("time : {}", stopWatch.getTime());
}

 

5년만에 보는 것 같다

Exception in thread "main" java.lang.OutOfMemoryError: Java heap space
	at java.util.Arrays.copyOf(Arrays.java:3181)
	at java.util.ArrayList.grow(ArrayList.java:265)
	at java.util.ArrayList.ensureExplicitCapacity(ArrayList.java:239)
	at java.util.ArrayList.ensureCapacityInternal(ArrayList.java:231)
	at java.util.ArrayList.add(ArrayList.java:462)
	at java.util.stream.Collectors$$Lambda$32/649734728.accept(Unknown Source)
	at java.util.stream.ReduceOps$3ReducingSink.accept(ReduceOps.java:169)
	at java.util.stream.ReferencePipeline$3$1.accept(ReferencePipeline.java:193)
	at java.util.stream.IntPipeline$4$1.accept(IntPipeline.java:250)
	at java.util.stream.Streams$RangeIntSpliterator.forEachRemaining(Streams.java:110)
	at java.util.Spliterator$OfInt.forEachRemaining(Spliterator.java:693)
	at java.util.stream.AbstractPipeline.copyInto(AbstractPipeline.java:482)
	at java.util.stream.AbstractPipeline.wrapAndCopyInto(AbstractPipeline.java:472)
	at java.util.stream.ReduceOps$ReduceOp.evaluateSequential(ReduceOps.java:708)
	at java.util.stream.AbstractPipeline.evaluate(AbstractPipeline.java:234)
	at java.util.stream.ReferencePipeline.collect(ReferencePipeline.java:499)
	at com.music.nexters.utils.SoundCloundUtilsTest.getTrackIdTimeCheck(SoundCloundUtilsTest.java:46)
	at sun.reflect.NativeMethodAccessorImpl.invoke0(Native Method)
	at sun.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:62)
	at sun.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43)
	at java.lang.reflect.Method.invoke(Method.java:498)
	at org.junit.platform.commons.util.ReflectionUtils.invokeMethod(ReflectionUtils.java:686)
	at org.junit.jupiter.engine.execution.MethodInvocation.proceed(MethodInvocation.java:60)
	at org.junit.jupiter.engine.execution.InvocationInterceptorChain$ValidatingInvocation.proceed(InvocationInterceptorChain.java:131)
	at org.junit.jupiter.engine.extension.TimeoutExtension.intercept(TimeoutExtension.java:149)
	at org.junit.jupiter.engine.extension.TimeoutExtension.interceptTestableMethod(TimeoutExtension.java:140)
	at org.junit.jupiter.engine.extension.TimeoutExtension.interceptTestMethod(TimeoutExtension.java:84)
	at org.junit.jupiter.engine.descriptor.TestMethodTestDescriptor$$Lambda$110/1675763772.apply(Unknown Source)
	at org.junit.jupiter.engine.execution.ExecutableInvoker$ReflectiveInterceptorCall.lambda$ofVoidMethod$0(ExecutableInvoker.java:115)
	at org.junit.jupiter.engine.execution.ExecutableInvoker$ReflectiveInterceptorCall$$Lambda$111/1757143877.apply(Unknown Source)
	at org.junit.jupiter.engine.execution.ExecutableInvoker.lambda$invoke$0(ExecutableInvoker.java:105)
	at org.junit.jupiter.engine.execution.ExecutableInvoker$$Lambda$247/824208363.apply(Unknown Source)

 

ㅋㅋㅋ 돌리고 보니 내가 취기가 올라오긴 했나보구나 생각했다

 

아래처럼 다시 수정해서 돌렸는데,

//노가다 로 작성한 코드
@Test
void getTrackIdTimeCheck() {
	//given
	List<String> urlList = IntStream.rangeClosed(0, 100_000_000)
			.boxed()
			.map(each -> "https://api.soundcloud.com/tracks/607910394/stream")
			.collect(Collectors.toList());

	//when
	StopWatch stopWatch = new StopWatch();
	stopWatch.start();
	urlList.stream().forEach(SoundCloundUtils::getTrackId);
	stopWatch.stop();

	//then
	log.info("time : {}", stopWatch.getTime());
}
//time : 20166

 

//정규식 사용한 코드 
@Test
void getTrackIdTimeCheck() {
	//given
	List<String> urlList = IntStream.rangeClosed(0, 100_000_000)
			.boxed()
			.map(each -> "https://api.soundcloud.com/tracks/607910394/stream")
			.collect(Collectors.toList());

	//when
	StopWatch stopWatch = new StopWatch();
	stopWatch.start();
	urlList.stream().forEach(SoundCloundUtils::getTrackId);
	stopWatch.stop();

	//then
	log.info("time : {}", stopWatch.getTime());
}
//time : 56496

 

결과는 정규식이 2배이상 느리다는걸 보여줬다. 

많은 자바 개발자들이 정규식은 run-time 시에 pre-compile 해주면 빠르다고 하긴 하는데.

역시 문자열 속 문자를 찾는건 오래 걸리나보다. 

 

항상 간단한 문자열 속에서 간단한 문자 찾는 코드가 있을 때, 

노가다 코드가 빠를지 ? 정규식이 빠를지 ? 에 대한 해답이 되었다.

맥주 한 잔 한 김에 나와 비슷한 궁금증을 갖고 있는 사람들이 있을 것 같아서 끄적끄적 적어본다.

+ Recent posts