Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[테크니컬 라이팅] 에버(손채영) 미션 제출합니다. #567

Open
wants to merge 6 commits into
base: helenason
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
30 changes: 30 additions & 0 deletions LEVEL3.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
## 용감한 쫄보
### 부제: 나 혼자 의견이 다른 상황에서 쫄지 않고 이야기하기

"에버는 나랑 생각이 달라도 내가 반박 한 번 하면 바로 쫄잖아."

모 크루로부터 들었던 이야기다. 이 말은 나에게 적당한 충격을 주었다.

같지 않은 생각은 뱉지 않고 스스로 묵살해 버리는 것이 습관이었다. 특히 사적인 자리에서는 더더욱. 내 생각의 옳음을 주장하기보다 상대와의 부드러운 분위기를 형성하기가 나에게는 더 중요했다. 나에게 토론은 불필요한 에너지 낭비였다. 항상 지는 방향을 택했고, 다른 말로는 회피했다. 하지만 사적인 자리에서의 습관은 공적인 자리까지 이어졌다. 다른 의견을 가진 사람을 설득해 본 경험이 적으니 어쩌면 당연하다.

여덟 명과의 페어 프로그래밍을 통해, 한 명을 설득하는 것은 자신 있었다. 나의 주장에는 근거가 있고, 근거들을 풀어 설명할 논리가 있다. 하지만 다수와 나. 다대일의 상황에서 나는 위축되었고 도망가고 싶어 했다. 근거와 논리만으로 설득할 수 없는 무언가가 있다고 생각했다.

---

내가 회피했던 사안은 항상 돌고 돌아 다른 크루의 입을 통해 전해졌다. 언젠가 발견되고 개선될 부분이었다. 다름이 무서워 무시했던 나의 찝찝함은 낭비된 리소스로 돌아왔다. 눈 딱 감고 이야기할걸. 먼저 이야기해서 시간 낭비하지 말걸. 그때부터 나는 내 주장을 아끼지 않기로 했다. 관련하여 유강스 목표를 설정했고, 설레는 마음으로 총대마켓 첫 회의에 참여했다.

첫 회의에 대한 설렘은 실망으로 돌아왔다. 나의 팀은 나와 성향이 달랐다. 나와 달리 그들은 집단 내에서 리스너보다 스피커에 가까운 사람들이었고, 하고 싶은 말을 즉시 꺼내는 것이 익숙해 보였다. 그들이 회의 중 익숙하게 생각하는 포지션 또한 있었을 테다. 부담감 또한 그들을 더욱 그들답게 만들었을 것이다.

처음엔 그런 팀원들이 미웠다. 회의 중 용기 내 말문을 열어도 누군가에 의해 금방 닫혔기 때문에, 그 상황들은 나를 작게 만들었고 나의 말수는 더 줄어들었다. 또는 전혀 반대로, 나의 말을 전하기 위해 팀원들의 말을 무의식적으로 자르기도 했다. 그런 내가 너무 싫었다.

불편함을 느꼈다면? 맞서 싸우면 된다. 간단하다. 그러나 난 그러지 못했다. '내 말 좀 들어줘' 찡찡대고 외치지 못했다. 팀 회고를 통해 감정 없이 정제된 형태로 팀에게 바라는 점을 전했지만, 큰 효과는 없었다. 간접적으로 개선하고자 노력했으나 파급력은 없었다. 역시 감정을 직접 전해야 했을까?

시간이 흘러, 팀 내 작은 파도가 일었다. 나와 비슷한 생각에서 야기된 파도였다. 그 작은 파도는 바람이 불어 큰 파도가 되었고, 그 틈을 타 혼자 외로이 타던 파도까지 쏟아냈다. 팀원들에 대한 미움, 적응하기 어려운 회의 분위기, 지친 채 벗어나 숨고 싶던 마음까지 모조리 털어냈다. 그들은 나의 모든 말을 따뜻하게 받아들였고 오히려 공감했다. 아직도 화성 회의실에서의 그 시간은 나를 울컥하게 한다.

누군가의 상처와 솔직함은 강한 무기가 되었다. 우리는 노력했다. 그라운드 룰을 개선했고 솔직한 분위기를 형성했다. 회의 분위기를 편안하게 만들었고 모두에게 집중했다. 어차피 시간이 흘렀다면 자연스레 개선될 문제였을까? 꼭 누군가 상처를 입고, 누군가의 마음을 드러내야만 개선이 가능한 문제였을까? 맞다. 불편한 지점을 정확히 콕 집어낸 덕분에 좋은 방향으로 해결할 수 있었다고 생각한다.

---

이제는, '쫄지 않고 이야기하자'고 되뇌지 않는다. 더 나은 서비스를 고민하다 보면 무의식중 강하게 주장하는 나를 발견한다. 물론 다수의 앞은 여전히 두렵다. 나의 의견에 여러 반박이 들어오는 상황에서는 나의 근거가 충분히 합당함에도 긴장한다. 하지만 당연하다. 누군가를 설득한 경험이 적다. 애초에 내 의견을 지구 끝까지 피력하기 시작한 시점이 우아한테크코스 레벨2쯤부터이니. 고작 한 계절이다. 98 계절 중 1 계절인데, 나의 몸이 나의 행동을 익숙하게 받아들이는 것이 더 어색하다. 여전히 쫄보이긴 하지만 그래도 꽤 용감해졌음에 만족한다.

팀에 나의 바람은 모두 요구했다. 잘 듣고 경청해줘. 집중해줘. 잘 참여해줘. 그래서, 나는 잘하고 있나? 나도 모르게 팀원들에게 상처가 된 적 없을까? 팀원들에게 불편함을 주고 있진 않은가? 요구한 만큼 스스로를 돌아봐야 한다. 요구한 만큼 책임감을 가져야 한다. 항상 잊지 않기로 하자.
199 changes: 199 additions & 0 deletions LEVEL4.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,199 @@
# 인덱스는 만능일까?

> 이 글은 인덱스가 무엇인지 아는 독자를 타겟으로 작성하였으며, MySQL을 기준으로 설명합니다.

인덱스는 성능 개선에 유용합니다. 누구나 이론적으로는 충분히 이해합니다. 하지만 아직 눈에 띄는 효과를 직접 확인하지 못했습니다. 따라서 직접 1억건의 데이터를 쌓고 실험을 해보고자 합니다. 그 여정을 공유합니다.

## 인덱스란
인덱스는 책의 색인과 같이, 데이터베이스에서 데이터를 효율적으로 검색할 수 있도록 도와주는 데이터 구조입니다. 일반적으로 테이블의 컬럼에 대해 설정되며, 해당 컬럼을 기준으로 데이터의 위치를 빠르게 찾을 수 있게 합니다.

아래는 MySQL 공식문서에서 가져온 문구입니다.

![img_5.png](img_5.png)
> 인덱스는 특정 열 값을 가진 행을 빠르게 찾는 데 사용된다. 인덱스가 없으면 MySQL은 첫번째 행부터 시작하여 전체 표를 읽어야 관련 행을 찾을 수 있다. 표가 클수록 이 비용이 더 많이 든다. 조건 내 컬럼에 대한 인덱스가 있는 경우, MySQL은 모든 데이터를 살펴볼 필요 없이 데이터 파일 내에서 찾을 위치를 빠르게 결정할 수 있다. 이는 모든 행을 순차적으로 읽는 것보다 훨씬 빠르다.

## 인덱스는 데이터를 어떻게 탐색할까

### 이분 탐색 알고리즘
`1부터 100`까지 정렬된 데이터에서 `77`이라는 데이터를 찾기 위해 Y/N로 대답 가능한 질문을 해야 한다고 가정합시다. 우리는 어떤 질문을 던질 수 있을까요?
```
Q. 1인가요?
Q. 2인가요?
Q. 3인가요?
...
```
위 질문을 77번 반복해 답을 얻어낼 수 있습니다.

하지만 과연 이 방식이 효과적일까요? 아니요. 너무 힘이 듭니다.

그렇다면 아래 방식은 어떨까요?
```
Q. 1 ~ 50인가요?
Q. 50 ~ 75인가요?
Q. 75 ~ 87인가요?
...
```
범위를 절반씩 줄여 질문을 해, 찾고자 하는 수의 범위를 좁힙니다. 세 개의 질문으로 벌써 정답인 77에 근접해졌습니다.

위와 같은 흐름으로 원하는 수를 찾는 알고리즘을 `이분 탐색 알고리즘`이라고 합니다.

데이터베이스가 인덱스 테이블에서 데이터를 탐색하는 방법도 마치 이분 탐색 알고리즘과 유사합니다. 이분 탐색 알고리즘은 **정렬**된 데이터를 바탕으로 데이터의 **범위를 좁혀가며** 원하는 데이터를 찾습니다.

### 인덱스 적용 전 데이터 탐색 방법
데이터베이스는 인덱스를 걸지 않은 데이터를 어떻게 탐색할까요?

아래 쿼리를 실행한다고 가정해봅시다.
```sql
select * from 테이블 where 컬럼 = 77;
```

컬럼에 인덱스를 걸지 않은 경우, 데이터베이스는 행 전체를 탐색합니다. 이를 `풀 테이블 스캔`이라 부릅니다. 만약 테이블에 100억개의 데이터가 존재한다면 100억개의 데이터 전부를 탐색해 조건에 맞는 행인지를 판단해야 합니다. 그만큼 많은 시간이 소요되겠죠.

### 인덱스 적용 후 데이터 탐색 방법
위 쿼리의 컬럼에 인덱스를 걸어주면 데이터베이스는 어떻게 데이터를 탐색할까요?

인덱스를 생성하면, 인덱스 테이블이 생성됩니다. 인덱스 테이블은 대상 컬럼의 데이터를 **정렬**하고, 실제 테이블에서의 **위치(포인터) 정보**와 함께 저장합니다.

![img_2.png](img_2.png)

더 명확한 예시로, age 컬럼에 단일 인덱스를 걸어주었다면 아래와 같은 인덱스 테이블이 생성됩니다.

![img_7.png](img_7.png)

위 인덱스 테이블로, 아래 두가지 사실을 확인할 수 있습니다.

1. 인덱스 테이블의 age 컬럼은 정렬되어 있다.
2. 각 행은 실제 테이블에서의 주소값을 가지고 있다.

## 테스트 환경 세팅
그렇다면 인덱스는 모든 상황에서 효과적일까요? 인덱스는 만능이라 어떤 상황에서든 성능이 개선될까요? 1억건의 데이터를 삽입하고 인덱스를 걸어보며 눈으로 직접 확인해봅시다.

member 테이블의 스키마를 아래와 같이 정의하고 1억건의 데이터를 삽입했습니다.
```sql
create table member (
id bigint auto_increment primary key,
nickname varchar(255) not null,
age int not null,
address varchar(255) not null
);
```
![img.png](img.png)

자동으로 생성된 초기 인덱스는 아래와 같습니다. 기본키 id 컬럼에 대해서만 인덱스가 설정되어 있습니다.

![img_1.png](img_1.png)

## 인덱스 성능 비교
인덱스를 걸기 전후의 읽기 성능과 쓰기 성능을 비교해봅시다.

아래 쿼리를 예시로 성능을 측정합니다.

```sql
select * from member where nickname = '사용자77';
```
조건절에서 nickname 컬럼을 사용하고 있기 때문에 nickname 컬럼에 대한 단일 인덱스를 생성했습니다.

**인덱스 적용 과정: 6분 42초**
```sql
index_practice> create index idx_nickname on member(nickname)
[2024-10-01 23:24:24] completed in 6 m 42 s 875 ms
```
데이터 수가 많기 때문에 인덱스 생성에만 약 6분의 시간이 소요되었습니다.

### 읽기 성능
**인덱스 적용 전: 45초**
```sql
index_practice> select * from member where nickname = '사용자77'
[2024-10-01 23:13:51] 100 rows retrieved starting from 1 in 45 s 575 ms (execution: 45 s 552 ms, fetching: 23 ms)
```
**인덱스 적용 후: 45밀리초**
```sql
index_practice> select * from member where nickname = '사용자77'
[2024-10-01 23:25:55] 100 rows retrieved starting from 1 in 70 ms (execution: 45 ms, fetching: 25 ms)
```
인덱스 존재 여부에 따라, 45초에서 45밀리초로 **약 1000배의 성능이 개선**되었습니다.

### 쓰기 성능
**인덱스 적용 전: 13밀리초**
```sql
index_practice> update member set age = age + 1 where nickname = '사용자77'
[2024-10-01 23:42:07] 100 rows affected in 13 ms
```
**인덱스 적용 후: 1분 10초**
```sql
index_practice> update member set age = age + 1 where nickname = '사용자77'
[2024-10-01 23:44:30] 100 rows affected in 1 m 10 s 324 ms
```
인덱스 존재 여부에 따라, 13밀리초에서 1분으로 **약 4500배의 성능이 저하**되었습니다.

### 체감 포인트
위 실험을 통해 체감한 부분은 아래와 같습니다.

1. 장점: 조건절에 있는 컬럼에 인덱스가 걸려있으면 성능이 크게 개선된다.
2. 단점: 인덱스 생성에 꽤 많은 시간 리소스가 든다.
3. 단점: 읽기 성능은 개선되나 쓰기 성능은 저하된다.
4. 기준: 읽기의 성능 개선보다 쓰기의 성능 저하 정도가 더 크다. 쿼리 실행 빈도에 따라 인덱스 적용 여부를 결정하자. 위 예시의 경우, 읽기 쿼리 실행 빈도가 쓰기 쿼리 실행 빈도의 약 4배 이상일 때 인덱스를 걸어야할 것이다.

[//]: # (아래의 쿼리를 통해서도 인덱스를 통한 성능 개선 및 저하를 확인할 수 있습니다.)
[//]: # ()
[//]: # (```)
[//]: # ()
[//]: # (exists select * from member where nickname = ‘사용자77’;)
[//]: # ()
[//]: # (select * from member where age < 25;)
[//]: # ()
[//]: # (select * from member where age <> 25;)
[//]: # ()
[//]: # (select * from member where age = 20 or age = 30 or age = 40 or age = 50 or age = 60;)
[//]: # ()
[//]: # (select * from member where age in &#40;20, 30, 40, 50&#41;;)
[//]: # ()
[//]: # (select * from member where address like ‘서울%’;)
[//]: # ()
[//]: # (select * from member where address like ‘%서울%’;)
[//]: # ()
[//]: # (```)

## 인덱스 적용 실패 사례
앞서 제시한 상황은 이상적인 상황입니다. 애초에 member 테이블은 인덱스의 효과를 설명하기 위해 생성된 테이블이며, 인덱스를 걸기 좋은 쿼리를 제시하였습니다. 그렇다면 실제 프로젝트 환경에서는 어떨까요?

현재 개발 중인 총대마켓 서비스에 사용되는 쿼리입니다.

```sql
SELECT *
FROM offering as o
WHERE (o.discount_rate < 80 OR (o.discount_rate = 80 AND o.id < 900000))
AND (o.discount_rate IS NOT NULL)
AND (o.offering_status <> 'CONFIRMED')
AND ('휴대용' IS NULL OR o.title LIKE '휴대용%' OR o.meeting_address LIKE '휴대용%')
AND (o.is_deleted = false)
ORDER BY o.discount_rate DESC, o.id DESC;
```
```sql
SELECT *
FROM offering as o
WHERE (o.offering_status = 'AVAILABLE' or o.offering_status = 'IMMINENT')
AND (o.id < 900000)
AND ('휴대용' IS NULL OR o.title LIKE '휴대용%' or o.meeting_address LIKE '휴대용%')
AND (o.is_deleted = false)
ORDER BY o.id DESC;
```

위 쿼리를 보고 인덱스를 어떻게 걸어야할지 명확한 답을 찾으셨나요? 저는 어려웠습니다. 인덱스를 탈 수 있는 컬럼은 is_deleted 뿐이었고, 인덱스가 활용되어도 확실한 성능 개선은 없었습니다. 오히려 인덱스를 걸어 성능이 저하된 경우도 많았습니다.

즉, 쿼리 복잡도와 인덱스로 인한 성능 개선율에는 밀접한 연관관계가 있습니다. 따라서 쿼리를 작성하는 시점에서부터 인덱스를 고려해 복잡한 쿼리는 지양하는 것이 좋습니다.

총대마켓 쿼리에 인덱스 적용을 실패한 후, 위 쿼리에 대해서는 최적화를 진행한 상태입니다. 해당 과정 또한 정리 후 여러분께 공유 드리겠습니다.

우아한테크코스 인덱스 강의에서 토미는 이런 말씀을 하셨습니다.
```
인덱스는 선택이 아닌 필수다.
```

그리고 저의 실패를 통한 조언도 하나 몰래 살포시 첨가하며 글을 마무리하겠습니다.
```
쿼리를 작성할 때 인덱스도 함께 고려하라.
```
## 참고
- https://dev.mysql.com/doc/refman/8.4/en/mysql-indexes.html
- https://blog.algomaster.io/p/a-detailed-guide-on-database-indexes
Binary file added img.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added img_1.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added img_2.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added img_5.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added img_7.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.