많은 개발자들이 경험해봤을 것이다. 로직은 정상적으로 동작하고, 기능도 잘 돌아가지만 정작 실제 트래픽이나 배치 상황에선 터질 듯한 지연과 병목이 발생하는 상황말이다.

 

이런 경우는 단순한 버그가 아니라, 성능 관점의 로직 결함이다.

 

그중에서도 가장 자주 반복되는 특징 5가지를 한번 정리해보자.

 

 

1. 반복문 안에서 DB or 네트워크 호출

나쁜 예 :

List<Long> userIds = getAllUserIds();
for (Long userId : userIds) {
    User user = userRepository.findById(userId); // DB 요청 n번 발생
    process(user);
}

 

위 로직의 문제점은

  • n건마다 DB/network 왕복 요청 발생
  • 1만 건이면 1만 번 DB에 연결한다.
  • N+1 문제로 대표되는 성능 병목

그렇다면 이 로직을 어떻게 개선할 수 있을까?

List<User> users = userRepository.findAllById(userIds); // 한번에 요청
for (User user : users) {
    process(user);
}

 

 

2. Stream API 혹은 람다에서 성능 생각 없이 사용

List<String> emails = users.stream()
    .filter(u -> u.getEmail().endsWith("@gmail.com"))
    .map(u -> u.getEmail())
    .distinct()
    .collect(Collectors.toList());

 

이 로직에서의 문제점은 

  • filter → map → distinct → collect 모두 내부적으로 새로운 리스트를 생성한다
  • 데이터가 많을수록 GC 부하와 메모리 사용량이 급증한다
  • 특히 nested Stream 구조가 있는 경우 O(n^2) 급 성능이 하락한다.

개선 포인트는

  • 대량 처리 시엔 stream 대신 명시적 for문을 사용한다
  • 성능 측정 필요 시 JMH나 System.nanoTime()으로 비교한다

이렇게 2가지가 가능하다.

 

 

3. 컬렉션 구조 선택 미스 (LinkedList vs ArrayList)

List<String> list = new LinkedList<>();
for (int i = 0; i < 1_000_000; i++) {
    list.add(i, "data"); // 중간 삽입
}

 

위 로직은 그냥 봤을땐 문제가 없어 보인다.

 

하지만 문제점이 있다.

  • LinkedList.add(index, element)는 O(n)
  • 실제로는 전체를 다 뒤져야 한다
  • ArrayList 보다 수십 배 느리다

이런 경우

  • 삽입/삭제가 많으면 LinkedList, 조회가 많으면 ArrayList

하지만 대부분의 경우 ArrayList가 빠르다.

 

 

 

4. 쓸데없는 객체 생성 or 스트링 조합

String result = "";
for (int i = 0; i < 10000; i++) {
    result += i; // 매번 새로운 문자열 생성됨
}

 

위 로직에서는 문자열이 계속해서 복사된다 → O(n^2) 그로인해 GC 빈도가 증가하고 CPU가 과도하게 사용된다

 

StringBuilder sb = new StringBuilder();
for (int i = 0; i < 10000; i++) {
    sb.append(i);
}

 

StringBuilder 를 사용해 쓸데없는 문자열 생성을 방지할 수 있다.

 

 

 

5. 캐시 or DB index 미사용

반복해서 부르는 데이터인데 매번 DB를 요청하거나, 검색 쿼리에 인덱스를 미사용하는 경우 → Full Table Scan이 발생할 수 있다.

SELECT * FROM orders WHERE status = 'PENDING';

 

→ status에 인덱스가 없다면 전체 테이블을 검색한다.

 

 

+ Recent posts