# “왜 이렇게 느려?”를 마주했을 때

## 들어가며

서비스를 만들다 보면 어느 순간부터 “이거 왜 이렇게 느리지?” 하는 순간이 온다.\
처음엔 코드 탓을 했다. 쿼리를 잘못 짰나? 로직이 복잡해서 그런가?

근데 조금 더 들여다보면 **정작 느린 건 코드가 아니라 '기다림'일 때**가 많았다. 외부 API 응답, 느린 쿼리, 과도한 반복 로직 같은 것들. 나도 처음엔 이런 걸 잘 몰랐고 막연히 리팩토링하거나 서버 사양을 탓하곤 했다.

이 글은 내가 실무에서 성능 문제를 겪고 그걸 어떻게 관찰하고, 어디서부터 개선해봤는지 **개발자 입장에서 솔직하게 정리한 기록**이다.

아주 특별한 기술은 없다. 그냥 평범한 코드 위에서 조금씩 겪은 이야기들이다.

## “왜 이렇게 느려?” 🤯

느림의 원인은 항상 코드 안에 있는 건 아니다. 대부분은 **기다림**에서 시작된다.

* 응답이 느린 외부 API
* 오래 걸리는 DB 쿼리
* 반복 호출되는 I/O 작업
* 심지어는 로그 출력 하나까지

그런데도 처음엔 보통 로직만 들여다보게 된다. 그래서 진짜 문제는 더 늦게 발견되곤 한다.

**느려졌다면, 어디서 느려졌는지부터 파악해야 한다.** 측정 없이는 개선도 없다.

## 일단 어디가 막혔는지부터 보자

성능 개선은 추측으로 하기엔 너무 위험하다. **직접 측정해서 병목 지점을 찾아내는 것부터 시작**해야 한다.\
Java에서 자주 쓰는 방법은 두 가지다.

### **1. System.currentTimeMillis()**

가장 간단하게 사용할 수 있는 측정 도구다. 특정 코드 블록의 실행 시간을 빠르게 확인할 수 있다.

```java
long start = System.currentTimeMillis();
// 실행할 코드
long end = System.currentTimeMillis();
System.out.println("실행 시간: " + (end - start) + "ms");
```

### **2. StopWatch (Spring)**

작업이 여러 개일 때 **어디서 시간이 많이 소모되는지 파악할 수 있게 도와준다.**

```java
StopWatch stopWatch = new StopWatch();
stopWatch.start("Task 1");
// Task 1 실행
stopWatch.stop();
stopWatch.start("Task 2");
// Task 2 실행
stopWatch.stop();
System.out.println(stopWatch.prettyPrint()); // 결과 출력

// 결과
StopWatch '': running time (millis) = 1502
-----------------------------------------
ms     %     Task name
-----------------------------------------
0501  33%  Task 1
1001  67%  Task 2
```

출력 결과로 어떤 작업이 전체 시간의 몇 %를 차지했는지도 알 수 있다. **시각적으로 병목 구간을 확인하기 좋다.**

병목 구간이 명확해지면 이제 어디를 먼저 손봐야 할지도 보이기 시작한다.

## 기다리느라 느린 거면, 비동기로 보내자

실제로는 느린 게 아니라 **그냥 ‘기다리고 있는’ 경우가 많다.**

외부 API 응답, 파일 I/O, DB 쿼리 등 **CPU는 놀고 있는데, 응답을 기다리느라 전체가 느려진다.**\
이럴 땐 비동기 처리로 병렬화하면 성능이 확 달라진다.

예전에 외부 API 수백 건을 호출하는 작업이 있었다. 처음엔 순차적으로 처리하다 보니, **대기 시간 누적으로 전체 실행 시간이 길어졌다.** 비동기로 전환하면서 모든 요청을 한꺼번에 보내고 기다리는 구조로 바꾸면서 **처리 시간은 크게 줄었다.**

단, 비동기는 무조건 빠른 게 아니다.

* **계산 중심 작업**에 쓰면 오히려 느려질 수 있고
* **너무 많은 요청을 병렬로 보내면** 리소스만 낭비된다

핵심은 이거다

> 지금 이 작업이 **계산을 하고 있는지,**\
> 아니면 **기다리고만 있는지** 구분해야 한다.

기다리는 작업이라면 **비동기나 병렬 처리를 고려할 타이밍이다.**

![](https://velog.velcdn.com/images/sa46lll/post/5b65fa34-8843-445d-865e-3f1d4d45256f/image.png)

## 스레드를 늘렸더니 더 느려졌다?

멀티스레드 = 빠르다? 항상 그런 건 아니다.\
나도 예전에 “스레드를 더 쓰면 빨라지겠지” 싶어서 값을 마구 올렸던 적이 있다. 결과는 예상과 정반대였다. **더 느려졌다.**

이유는 간단했다. **스레드가 많아지면, CPU는 일보다 ‘전환’을 더 많이 하게 된다.** 이걸 **컨텍스트 스위칭 비용**이라고 한다.\
즉, 일을 많이 하는 게 아니라 **누굴 먼저 할지 정하느라 바빠진다.**

그래서 결국 "**스레드를 얼마나 써야 적당한가?"** 라는 질문에 다다른다.

적정 스레드 수는 아래 공식으로 계산할 수 있다:

> `스레드 수 = CPU 코어 수 × (1 + 대기시간 ÷ 서비스시간)`

* 연산 위주 서비스라면 → **CPU 코어 수에 맞추는 게 적절**
* 대기시간이 긴 서비스라면 → **조금 더 많이 쓸 수 있다**

***

### **Virtual Thread**

추가로 Java 19+에선 **가상 스레드(Virtual Thread)** 라는 것도 있다.

* 기존 스레드보다 훨씬 가볍고
* 수천 개도 무리 없이 생성 가능하다
* 비동기 처리와 궁합도 좋다

가볍게 도입해볼 만한 기술이고, 기존 플랫폼 스레드의 한계를 극복할 수 있어서 앞으로 더 자주 보게 될 가능성도 크다.

최근 스프링캠프 2025에서도 버추얼 스레드를 주제로 한 세션이 있었는데, 실제 적용 사례가 꽤 많이 소개된 걸 보고 확실히 요즘 뜨는 주제라는 걸 느꼈다.

관심 있다면 다음 기술 블로그도 참고해볼 만하다.

* [https://tech.kakaopay.com/post/ro-spring-virtual-thread/](https://tech.kakaopay.com/post/ro-spring-virtual-thread/https://techblog.woowahan.com/15398/)
* [https://techblog.woowahan.com/15398/](https://tech.kakaopay.com/post/ro-spring-virtual-thread/https://techblog.woowahan.com/15398/)

## 마무리하며

처음엔 성능 이슈가 뭔가 거창한 문제처럼 느껴졌는데, 막상 겪어보니 **대부분은 기본적인 관찰에서 시작**됐다.

* 어디가 느린지 측정해보고
* 기다리고 있는 부분을 비동기로 바꿔보고
* 스레드 개수도 그냥 늘리지 말고 계산해보고

특별한 기술보다도 **지금 코드를 잘 들여다보는 습관이 더 중요**했던 것 같다.

다음 글에서는 **DB, 캐시, 인프라 쪽에서 겪은 성능 개선 경험**을 정리해보려고 한다. 그때도 삽질은 여전했다.
