Why

일반적으로 엔지니어는 프로그래밍을 하면서 로직의 구현에 중점을 둡니다.

처음 만들때는 동작 속도는 크게 신경쓰지 않아요.
구현에 집중하기 때문이죠.

시니어 엔지니어가 다른 것 중에 한가지 꼽자면,
같은 일을 해도, 빠르게 동작하는 코드를 만드는 점에서 차이가 있다고 말하고 싶습니다.
여기서 속도는 개발 속도가 빠른게 아니라 만든 코드의 동작 속도를 말해요.

물론 조직 관리 능력과 리더쉽도 있겠지만요.
오늘은 기술적인 이유에 집중 해보겠습니다.

빠르다는 것

같은 일을 처리하는데 시간이 적게 걸린다는 뜻입니다.
프로그래밍의 세계에서 시간은 비용 입니다.

여기 10만건의 데이터를 다운로드 하는 기능이 있습니다.

주니어는 구현에 집중하다보니 생각대로 만듭니다.
결국 완성은 했는데, 아뿔사 다운로드에 3분이나 걸린다고 하네요.

시니어는 다릅니다.
시작 부터 여러 상황을 고려하고 만듭니다.
결국, 다운로드에 10초면 되네요?

둘다 개발 언어도 같고 서버도 같은데요.
18배 빠른 기능이 탄생했습니다.

누가 만든 기능이 좋은 평가를 받을지는 불보듯 뻔하지요.

주니어는 이 상황이 이해가 안되고, 너무 궁금합니다.
기술 스택을 바꿔야 하나 고민 합니다.
근데 스택은 똑같다네요.

주니어는 너무 궁금합니다. 저 형님 언니가 만들면 대체 뭐가 다른지

일단 왜 빠른가

제가 볼때 이유는 몇가지가 있습니다.
오늘은 예제를 통해 그 첫번째를 차이점을 알아볼께요.

예제 문제

여기 100만개의 대한민국 주소 데이터가 있습니다.
여기서 서울 이라는 글자로 시작하는 모든 주소를 찾아서 리턴한다고 가정해보죠.

코테 같은 문제 말고, 현실 문제를 고려 하려고 고심 했습니다.
자- 이제 시니어주니어가 어떻게 다르게 만드는지 봅시다.

(주의: 글 기고 후, 예시 코드의 적절성에 대해 우려감을 받았습니다. 주니어와 시니어의 코드 모두 자세히 보면, 정확성에 문제를 안고 있는 것이 사실 입니다. 다만 제가 말씀드리고 싶은 주제는 이 예시 코드의 적절함이 아니라, 두개 코드의 확연한 접근 방법의 차이에 집중해 주시면 좋겠다 입니다. 이점 참고 해주세요~!)

주니어의 코드

주니어는 이 문제를 이렇게 풀꺼에요.
100만개를 루핑해서, ‘서울’ 이라는 문자열이 포함 됐다면 카운트 하겠죠.
파이썬을 예로들면 아래와 같은 코드가 나옵니다.

for address in addresses:
    if '서울' in address:
        string_count += 1

단순하고, 직관적입니다.
문제는 해결 할거에요.
그러나, 아래 시니어 코드를 보면 생각이 달라질거예요.

시니어는 이렇게 할겁니다.

100만개를 루핑하는 것은 같지만, 해시해서 비교할 겁니다.

def simple_hash(s):
    return sum(ord(c) for c in s)  # 각 문자의 유니코드 값을 더한 값

# '서울'의 해시 값
k = '서울'
khash = simple_hash(k)
klen = len(k)

# 해시로 바꾸고 검사
string_count = 0
for address in addresses:
    # 주소에서 키워드 글자수만큼 앞부분을 해시로 변환하여 검사
    if simple_hash(address[:klen]) == khash:
        string_count += 1

살짝 길어지기도 했는데, 그것보단 왜 이렇게 하는지 궁금할거에요.
하나씩 풀어봅시다.

해시를 왜?

→ 문자를 숫자로 바꾸기 위해서 입니다.

숫자로 바꾸는 이유는?

CPU 안에는 수학 연산에 엄청나게 빠른 ALU가 존재 합니다.
→ 2개 숫자의 비교 연산은 두 숫자의 크기를 비교하는 것으로 A - B 가 0이 아니면 다른 것이므로 ALU를 사용 합니다.
→ 따라서 비교 연산 자체가 엄청나게 빠릅니다.

문자열 앞부분 잘라서 비교하는 이유는?

→ 문자열의 앞부분을 길이만큼 잘라서 비교하는 것은 시간 복잡도 때문 입니다.
→ 시간 복잡도로는 O(1) 이 되어, 주소가 아무리 길어도 연산 시간이 일정하게 보장 되겠죠.

그럼, 문자열 처리는 왜 느린데요?

if ‘서울' in address 에서 각 문자를 순차적으로 비교하여 '서울’이라는 단어가 포함되어 있는지 내부적으로 검사하게 됩니다. 이때, 최악의 경우 전체 문자열을 검사해야 하므로 O(n)의 시간 복잡도를 가집니다.
O(n) 에서 n은 입력 문자열의 크기가 되므로 크기가 클수록 시간이 더 걸려서 느리게 됩니다.

결국 시니어는 이렇게 접근한 것이죠.

  • CPU 가 빠르게 계산 할 수 있도록, ALU의 사칙연산을 활용 합니다.
  • 시간복잡도를 떠올리면서, 어떻게 해야 전체 비교하지 않을지 고려 하죠.
  • 결국, 코드CPU에서 어떻게 실행될 지 고려 했습니다.

어때요?
와우! 다르긴 하죠?

주니어 엔지니어 분들께

여러분이 혹시 주니어 개발자라면, 와 이렇게 까지 한다고? 생각 할 수 있습니다.

에이- 그냥 문자열로 비교해도 2초면 되는데, 이정도면 괜찮은 거 아닌가 라고도 생각 할 수 있습니다.

하지만 진가는 이 문제의 건수가 많아 졌을때 발휘됩니다.
100만건이 아니라 1억건 이라면 말이죠.

문자열 비교는 2초가 아니라 20초가 되겠죠.
하지만 숫자 비교는 0.1초1초 가 될 뿐 입니다.

애초에 빨라야 양이 늘어도 유지돼요.

그런데, 만약 요구사항이 1억개에 1초가 아니라 0.1초라면 또 다른 방법을 추가 해야합니다.
그건 앞으로 차차 풀어볼께요.
(저를 팔로우 하시면 가장 빠르게 보실 수 있습니다.)

마무리

좋은 시니어 엔지니어는 빠르게 동작하는 코드를 만듭니다.

많은 이유가 있겠지만, 제가 생각하는 그 이유들을 앞으로 차차 더 알려드릴께요.
오늘 이런 팁을 처음 알았다고 해도 괜찮아요.

처음 부터 시니어인 사람은 없습니다.
매일 같이 연구하고 성장한 결과 위와 같은 코드를 작성할 수 있게 된 것입니다.
노력 하면 누구나 얻을수 있는 것이죠.

궁금하신 내용, 댓글 환영 합니다.
오늘도 읽어 주셔서 감사해요.

아임웹 CTO 매튜 드림.

profile
CTO at Imweb, 20년차 개발 장인, 전) 플레이오토 CTO/창업자

44개의 댓글

comment-user-thumbnail
2024년 6월 20일

안녕하세요! 글 잘 읽었습니다.
그런데 예시에서 주니어가 작성한 코드는 애초에 정확한 구현이 아니지 않나요?

1개의 답글
comment-user-thumbnail
2024년 6월 20일

흥미로운 글 잘 읽었습니다.
댓글을 읽다 저도 의문점이 생겨 댓글 남깁니다.

언어마다 다르겠지만 Kotlin의 경우 startsWith() 함수가 내부적으로 두 문자열의 ByteArray를 비교하는 것으로 최적화되어 있습니다(대부분의 경우 유사한 동작이라고 생각합니다). 이 경우, 위의 문자열의 모든 문자를 유니코드로 바꾼 후 더하여 계산한 Hash 값을 비교하는 것보다 더 빠른 연산 속도를 보여줍니다.

1개의 답글
comment-user-thumbnail
2024년 6월 21일

논리는 이해가 가지만 로직 자체는 적절한 예시가 아닌 것 같아요. Hash 값이 같은 경우가 있으니 시니어의 코드는 오작동이 포함되어있습니다. 속도만큼 중요한 것이 의도한 대로 동작하는 것이니 로직을 조금 바꾸는게 좋을 것 같네요.

좋은 글 감사합니다 :)

1개의 답글
comment-user-thumbnail
2024년 6월 24일

드디어 velog 인기글에 조금은 알아먹을 수 있는 글이 나왔네요. 좋은 글 감사해요!

1개의 답글
comment-user-thumbnail
2024년 6월 24일

댓글들이 이게 맞나.. 기능을 구현하는 과정에서 컴퓨터의 어느 부분을 더 잘 쓰면 좋을지에 대해 알고 구현하는 편이 더 유리하라고 설명하고 있는데 왜 다들 특정 부분 그것도 예시 중의 하나인 것에 꽂혀서 다른 얘기들을 하지..

1개의 답글
comment-user-thumbnail
2024년 6월 24일

문자열을 비교하는 연산의 시간 복잡도보다, 애초에 모든 addresses를 순차탐색하지 않도록(예: 인덱싱, 샤딩 등) 그 부분의 시간복잡도를 개선하는 게 더 중요하지 않을까요

3개의 답글
comment-user-thumbnail
2024년 6월 25일

그리고 저 CPU 관련한 부분은 모든 종류(x86, amd64(x86_64), arm, arm64/v8 등)의 CPU에서 동일한가요? 각 CPU 아키텍처마다 차이가 있을지도 궁금합니다

3개의 답글
comment-user-thumbnail
2024년 6월 25일

잘보고 갑니다 :) 글도 재밌고, 댓글은 더 재밌네요

1개의 답글
comment-user-thumbnail
2024년 6월 25일

파이썬의 in 연산을 startswith 메소드로 변경해서 시간 복잡도를 최적화한 것이라면 저도 효율적이라고 생각합니다. (본문에선 해시 연산을 O(1)이라고 언급해주셨는데, 비교 문자열 길이를 무시한다면 startswith도 O(1)이라고 생각합니다. 실제로는 둘 다 O(k, k=비교 문자열 길이)가 아닐까 생각합니다)

하지만 문자열 비교에 사칙 연산을 활용하는 케이스를 보여주고 싶어서 CPU 특성을 이용해 최적화를 하셨으면, 모든 CPU 종류와 특성을 고려하고 실제로 성능 향상이 일어나는지 측정해야 하는데, 그 부분은 개별 시니어 개발자가 일일이 고려하기엔 현실적으로 어렵다고 생각해서 댓글 작성했습니다. 그리고 그런 이유로 인해 일반적으로 컴파일러가 아키텍처에 따른 최적화 부분을 담당하고 있습니다.

저도 코드나 최적화 방식의 정확성에 문제를 제기하는 게 아닙니다. 다만 제가 궁금한 것은 이런 엔지니어링은 추후 cpu 아키텍처가 변경돼면 그에 맞춰서 최적화 구현 코드도 바꿔줘야 하는데, 이런 영역을 개발자가 컴파일러 대신 최적화하는 것이 과연 현실적으로 바람직할지 의문이 들어서 질문드린 것입니다.

1개의 답글
comment-user-thumbnail
2024년 6월 25일

작성자분이 글에서 말하고자하는바가 코드의 내용은 아닌거 같은데 그 부분에서 의견이 많네요 잘봤습니다

1개의 답글
comment-user-thumbnail
2024년 6월 25일

멋지십니다~

1개의 답글
comment-user-thumbnail
2024년 6월 25일

고민을 통한 구현이 필요하다는 걸 느끼네요! 감사합니다.

1개의 답글
comment-user-thumbnail
2024년 6월 26일

재밌는 글 감사합니다~

1개의 답글
comment-user-thumbnail
2024년 6월 26일

진짜 좋은 글이다. 감사합니다 ㅎㅎ

1개의 답글
comment-user-thumbnail
2024년 6월 26일

직관적인 예시로 이해가 잘 되는 것 같습니다.
좋은 글 감사합니다.

1개의 답글
comment-user-thumbnail
2024년 6월 28일

글에 태클이 많은 것 같군요,, 진정한 엔지니어가 되기 위해서 가져야할 좋은 이야기를 써주셨네요. 벨로그 메인에 떠서 읽고갑니다 ㅎㅎ

1개의 답글
comment-user-thumbnail
2024년 6월 29일

와 꿀팁 알아갑니다

1개의 답글
comment-user-thumbnail
2024년 7월 3일

한번씩 놓치게 되는 시간 복잡도에 대해 다시 한번 생각하게 해주는 글이군요.
좋은 글 감사합니다~

답글 달기
comment-user-thumbnail
2024년 10월 12일

좋은 글 감사합니다
그런데 ALU를 사용하기 위해 문자열을 숫자로 바꾸는 것 처럼 말씀하셨는데
문자열이든 해쉬든 실행될때 바이너리코드로 변환되고 ALU를 사용해서 연산을 처리하지 않습니까?
차라리 긴 문자열을 하나하나 비교하는 과정에서 해쉬(정수)로 바꾸면서 한번에 비교 할 수 있다고 하는게 맞지 않을까 생각합니다. (해쉬의 중복값은 배제하더라도)

답글 달기