본문 바로가기
Back-end & 알고리즘

코딩테스트부터 대용량 로그 분석까지, 투 포인터(Two Pointers) 패턴의 모든 것

by CodeByJin 2026. 5. 16.
반응형

코딩테스트 문제를 풀다 보면 "분명히 정답은 알겠는데 시간 초과(TLE) 때문에 미치겠다"는 순간이 온다. 특히 배열이나 리스트 같은 선형 자료구조에서 데이터를 뒤지는 상황이 그렇다. 우리는 보통 이럴 때 본능적으로 이중 for문을 돌리려 한다. 하지만 데이터가 10만 개, 100만 개로 늘어나는 순간 서버는 비명을 지르고 당신의 합격 통보서는 멀어진다. 이때 필요한 게 바로 '투 포인터(Two Pointers)'라는 기술이다. 겉보기엔 단순해 보이지만, 실무에서 대용량 트래픽 로그를 분석하거나 실시간 데이터 스트림을 처리할 때 이 개념 하나로 인프라 비용을 수천만 원 아낄 수도 있다.

왜 굳이 두 개의 포인터를 써야 하는가?

우리가 흔히 범하는 실수는 "모든 경우의 수를 다 확인해야 직성이 풀린다"는 강박이다. $O(N^2)$의 시간 복잡도는 N=10^5만 되어도 100억 번의 연산을 수행한다. 2026년 현재 최신 CPU 성능이 아무리 좋아졌어도 1초 안에 이걸 처리하는 건 무리다. 투 포인터는 말 그대로 인덱스를 가리키는 포인터 두 개를 조절하면서 $O(N)$으로 문제를 끝내는 기법이다. 대형마트 오픈런 상황을 생각해보자. 입구와 출구 양쪽에서 인원을 체크하며 특정 조건(예: 매장 내 인원 50명 유지)을 맞추는 것과 같다. 굳이 매번 처음부터 끝까지 사람을 다 셀 필요가 없다는 뜻이다.

투포인터 알고리즘 패턴 - 생성형 AI 이미지

패턴 1: 양 끝에서 중앙으로 좁혀오기 (Sorted Array)

주로 정렬된 배열에서 특정 합(Target Sum)을 찾을 때 사용한다. 왼쪽 끝(left)과 오른쪽 끝(right)에서 시작해서 합이 크면 right를 줄이고, 합이 작으면 left를 키운다.

  • 적용 상황: 두 수의 합, 회문(Palindrome) 검사, 정렬된 상태에서의 중복 제거.
  • 이득: 불필요한 탐색 범위를 즉각적으로 소거한다.
  • 고통: 데이터가 정렬되어 있지 않다면 정렬 비용(O(N \log N))이 추가된다. 정렬된 상태가 보장되지 않는 실시간 스트리밍 데이터에서는 쓰기 까다롭다.
public int[] twoSum(int[] numbers, int target) {
    int left = 0;
    int right = numbers.length - 1;
    
    while (left < right) {
        int sum = numbers[left] + numbers[right];
        if (sum == target) return new int[]{left + 1, right + 1};
        if (sum < target) left++;
        else right--;
    }
    
    return new int[]{-1, -1};
}

실무에서 이거 도입하면 정말 성능이 드라마틱하게 좋아지나요?

질문에 답하자면, **"조건만 맞으면 최소 수십 배에서 수백 배까지 빨라진다"**고 단언할 수 있다. 예를 들어, 우리가 백엔드 아키텍처를 설계하면서 분산된 로그 시스템에서 특정 타임스탬프 범위 내의 이벤트를 추출한다고 가정하자. 이중 루프로 로그를 대조하면 서버 CPU 점유율이 순식간에 90%를 넘기며 Circuit Breaker가 터질 것이다. 하지만 타임스탬프로 정렬된 로그에 투 포인터를 적용하면 메모리 사용량은 일정하게 유지하면서 선형적으로 데이터를 긁어낼 수 있다. 이건 운영 난이도의 문제가 아니라 서비스 생존의 문제다.

패턴 2: 같은 방향으로 움직이는 슬라이딩 윈도우

시작점(start)과 끝점(end)이 모두 인덱스 0에서 출발한다. 특정 조건을 만족할 때까지 end를 이동시켜 윈도우를 키우다가, 조건을 벗어나면 start를 당겨서 윈도우를 줄인다.

 

이건 직접 운영해 보면 체감이 확 됩니다. 특히 연속된 부분 배열의 최대 합이나 특정 길이를 유지해야 하는 가변 윈도우 문제에서 탁월하다. 동영상 스트리밍이나 주식 호가 데이터처럼 연속성이 중요한 도메인에서 "최근 5분간의 평균 거래량" 같은 지표를 산출할 때 쓴다. 매번 5분치 데이터를 새로 계산하는 게 아니라, 새로 들어온 놈 더하고 나가는 놈 빼주는 식이다.

특징 양 끝 포인터 (Opposite) 슬라이딩 윈도우 (Same)
주요 목적 값 비교 및 타겟팅 연속된 구간의 상태 관리
데이터 상태 정렬 필수인 경우가 많음 정렬 여부보다 연속성이 중요
메모리 부하 매우 낮음 윈도우 상태 저장을 위해 추가 공간 필요할 수 있음

패턴 3: 고속 포인터와 저속 포인터 (Fast & Slow)

이른바 '토끼와 거북이' 알고리즘이다. 연결 리스트(Linked List)에서 순환(Cycle)이 있는지 체크하거나 중간 지점을 찾을 때 쓴다. 한 포인터는 한 칸씩, 다른 하나는 두 칸씩 움직인다. 만약 순환이 있다면 결국 둘은 만난다.

 

실무에서는 메모리 누수를 추적하거나 데이터 구조의 무결성을 검증할 때 응용한다. 예를 들어, 복잡하게 얽힌 객체 참조 관계에서 순환 참조가 발생해 가비지 컬렉터(GC)가 일을 못 하고 있다면? 이런 논리로 참조 트리를 탐색해 루프를 찾아낼 수 있다. 물론 요즘 AI 기반 프로파일링 도구들이 잘 나와 있지만, 밑바닥 원리를 모르면 도구가 뱉는 결과값조차 해석 못 하는 '도구의 노예'가 된다.

백엔드 개발자가 마주할 현실적인 트레이드오프

투 포인터가 만능은 아니다. 코드가 직관적이지 않을 수 있다는 게 최대 단점이다. 단순 for문은 신입 사원도 읽지만, 포인터 두 개가 복잡하게 꼬인 코드는 디버깅 난이도를 올린다. "성능은 $O(N)$인데 가독성은 O(N!)"이 되는 불상사가 생길 수 있다. 또한, 멀티 스레드 환경에서 공유 배열에 투 포인터를 적용하려면 동기화 비용이 발생한다. 이때 발생하는 Lock 경합 때문에 오히려 싱글 스레드보다 느려지는 병목 현상이 생길 수 있으니 주의해야 한다.

결국 어떤 선택을 해야 하는가?

주니어 때는 당장 눈앞의 테스트 케이스를 통과하는 데 급급하겠지만, 시니어의 시선은 달라야 한다. 투 포인터를 쓸지 말지는 다음 기준에 따라 결정하라.

  • 데이터의 크기가 10^5 이상인가? 무조건 도입하라. $O(N^2)$은 자살 행위다.
  • 데이터가 정렬되어 있거나, 정렬 시 이득이 큰가? 양 끝 포인터 패턴을 우선 검토하라.
  • 메모리가 극도로 제한된 환경인가? 추가 배열을 만드는 대신 포인터만 써서 해결하는 투 포인터가 정답이다.
  • 팀원들의 평균적인 숙련도는 어떠한가? 성능 차이가 미미하다면(데이터가 작다면) 가독성 좋은 라이브러리 함수나 내장 메서드를 쓰는 게 유지보수 측면에서 낫다.

기술은 도구일 뿐이다. 투 포인터라는 망치를 들었다고 모든 문제를 못으로 보지 마라. 하지만 적어도 대못을 박아야 할 때 손가락으로 밀어 넣는 바보짓은 하지 말아야 한다. 오늘 정리한 이 패턴들은 코딩테스트뿐만 아니라 당신이 만들 거대한 시스템의 기초 체력이 될 것이다.

 

Spring Security 인증 프로세스: 실무에서 마주하는 구조적 한계와 트레이드오프 분석

보안을 프레임워크에 위임해야 하는 이유: 파편화된 인증 로직의 한계Spring Security가 없던 시절, 혹은 이를 제대로 활용하지 못하는 환경에서는 HTTP 인터셉터나 서블릿 필터 곳곳에 직접 작성한

byteandbit.tistory.com

 

반응형