플로우차트
Mandatory
Bonus
식사하는 철학자 문제
식사하는 철학자 문제는 운영체제에서 교착상태(Deadlock)을 설명하기 위한 문제이다.
교착상태
교착상태란 운영체제 또는 소프트웨어의 잘못된 자원 관리로 두 개 이상의 작업이 서로의 작업이 끝나기만을 기다려서 결과적으로 아무 것도 완료되지 못하는 상태를 의미한다. 조금 더 쉽게 표현하면 이러지도 저러지도 못하는 상태이다.
출처: 티스토리
교착상태 발생 조건
1. 상호배제(Mutual exclusion)
한 번에 프로세스 하나만 해당 자원을 사용할 수 있는데, 사용 중인 자원을 다른 프로세스가 사용하려면 요청한 자원이 해제될 때까지 기다려야 한다.
2. 점유대기(Hold and wait)
자원을 가지고 있는 상태에서 다른 프로세스가 사용하는 자원의 반납을 기다려야 한다.
3. 비선점(No preemption)
다른 프로세스에 할당된 자원을 강제로 빼앗을 수 없다.
4. 순환대기(Circular wait)
대기 프로세스의 집합이 순환 형태로 자원을 대기하고 있어야 한다.
교착상태 해결 방법
1. 예방(Prevention)
교착상태 발생 조건 4가지 중에서 하나만 해결한다.
2. 회피(Avoidance)
교착상태의 발생조건을 없애지 않고 교착상태가 발생하지 않도록 알고리즘을 적용한다. 대표적으로 자원 할당 그래프 알고리즘과 은행원 알고리즘이 있다.
3. 회복(Recovery)
교착상태가 발생하는 것을 막지 않고, 교착상태가 발생하면 발생 이후에 문제를 해결한다.
4. 무시(Ignore)
교착상태를 해결할 때 문맥교환(Context switching)에 의한 오버헤드로 성능 저하가 생기는데, 교착상태에 의한 성능저하보다 이를 해결하기 위한 성능 저하가 크면 무시한다.
문제 설명
출처: 위키백과
철학자 N명이 식탁에 둘러 앉으며, 테이블 가운데에 놓인 밥을 먹는다.
철학자는 식사하고, 잠들었다가, 생각하는 것을 반복한다.
철학자는 식사하기 위해서 2개의 포크를 집어야 하며, 전체 포크의 개수는 N개로 철학자의 수와 동일하다.
식사하는 동안 포크는 계속 들고 있으며, 식사를 끝내면 포크를 내려놓고 잠들었다가 생각한다.
포크의 개수는 N개이기 때문에 양 옆에 있는 사람과 포크를 공유해야 한다.
다만, 한 번에 하나의 포크를 집을 수 있으며, 이미 옆사람이 포크를 잡고 있다면 식사가 끝나기를 기다렸다가 포크를 집는다.
교착상태가 발생하는 이유
5명의 철학자가 식사를 한다고 해보자. 그리고 모든 철학자가 왼쪽에 있는 포크를 집는다고 해보자.
그 다음 오른쪽 포크를 잡아야 하는데, 이미 다른 철학자가 잡고 있기 때문에 포크를 내려놓을 때까지 기다려야한다. 하지만 모든 철학자가 그런 상태에 놓여있어서 철학자들이 아무 것도 진행할 수 없게되는 교착상태(Deadlock)가 된다.
철학자들에게 밥 먹이는 방법
철학자마다 순서대로 번호를 매긴다고 해보자.
이 상태에서 해결하는 방법은 아래와 같다.
- 짝수 먼저 식사 시작하기 (홀수는 잠시 잠들었다가 시작)
- 짝수는 왼쪽에 있는 포크를 먼저 집고, 홀수는 오른쪽에 있는 포크를 먼저 집는다.
이번 과제에서는 1번 방법으로 문제를 해결했으며, 교착상태 발생 조건 중에서 순환대기가 발생하지 않도록 구현했다.
Mandatory
구조 설계
- 인자를 파싱해서 저장한다.
- 유효한 인자인지 확인한다.
- 유효하지 않으면 프로그램 종료
- 철학자(스레드)를 생성한다.
- 철학자는 식사 → 수면 → 생각을 반복한다.
- 모니터링 스레드 생성
- 철학자가 식사를 하지 못하거나, 모든 철학자가 식사를 마치면 메인 스레드에서 모든 자원을 회수하고 프로그램을 종료한다.
1. 인자 파싱
1
./philo [철학자의 수, 생존 시간, 식사 소요 시간, 수면 소요 시간, (최소 식사 횟수)]
모든 인자는 양의 정수 값을 가진 int
형 범위 내의 값만 받았다.
파싱을 할 때 숫자가 아닌 값이 들어오거나 음수가 들어오면 -1
로 저장해서 오류가 발생했다는 것을 명시했다.
2. 유효한 인자 확인
0 또는 음수가 들어오는 경우는 프로그램을 바로 종료한다.
특히, 0 이 들어오는 경우는 논리적으로 옳지 않다고 생각했다.
예를 들어, 생존 시간이 0 이라면 생존 자체가 불가능하고, 최소 식사 횟수가 0 이라면 철학자가 전혀 먹지 않아도 된다는 의미로 해석했다.
3. 철학자 생성
반복문을 돌며 철학자의 수만큼 스레드를 생성한다.
포크 할당
철학자의 인덱스는 1부터 시작하고, 포크의 인덱스는 0부터 시작하도록 했다.
이처럼 철학자의 왼쪽 포크는 i
인덱스, 오른쪽 포크는 나머지 연산자를 사용해서 (i + 1) % num_of_philo
인덱스를 잡도록 했다.
즉, 1번 철학자의 왼쪽 포크는 0번 인덱스, 오른쪽 포크는 1번 인덱스를 가지며, 5번 철학자의 왼쪽 포크는 4번 인덱스, 오른쪽 포크는 0번 인덱스를 가진다.
철학자 인덱스 | 1 | 2 | 3 | 4 | 5 |
---|---|---|---|---|---|
포크 인덱스 | 0 | 1 | 2 | 3 | 4 |
이를 코드로 표현하면 아래와 같다.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
#define LEFT 0
#define RIGHT 1
int i;
i = 0;
while (i < num_of_philo) // 철학자의 수만큼 반복
{
...
// 포크 할당
philos[i].forks[LEFT] = &data->forks[i]; // 왼쪽 포크
philos[i].forks[RIGHT] = &data->forks[(i + 1) % num_of_philo]; // 오른쪽 포크
...
i += 1;
}
루틴 함수
철학자 스레드가 수행하는 함수는 아래와 같이 작성했다.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
void *start_routine(void *arg)
{
t_philo *philo;
philo = (t_philo *)arg;
if (philo->idx % 2 == 0) // 짝수이면 잠시 잠들었다가 식사 시작 (= 홀수 먼저 식사)
usleep(500);
while (TRUE)
{
if (is_other_philo_dead(philo->data) == TRUE)
break ;
if (is_all_philo_eaten(philo) == TRUE)
break ;
start_eating(philo);
start_sleeping(philo);
start_thinking(philo);
}
return (arg);
}
짝수 번째 철학자인 경우 잠시 usleep
에 들어갔다가 행동을 반복한다.
그리고 다른 철학자가 죽었거나 모든 철학자가 식사를 마쳤다면 행동을 멈춘다.
4. 모니터링 스레드 생성
메인 스레드에서 모든 철학자의 상태를 확인하는 스레드를 생성했다.
모니터링 스레드는 1번 철학자부터 N번 철학자까지 순서대로 확인하며, 현재 보고 있는 철학자가 죽었는지, 모든 철학자가 식사를 마쳤는지 확인한다.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
void *start_monitoring(void *arg)
{
int i;
t_philo *philos;
i = 0;
philos = (t_philo *)arg;
while (TRUE)
{
if (is_philo_dead(&philos[i]) == TRUE)
{
change_common_data(&philos[i], DIE);
print_state(&philos[i], DIED);
break ;
}
if (is_all_philo_eaten(&philos[i]) == TRUE)
{
change_common_data(&philos[i], EATEN);
break ;
}
i = (i + 1) % philos[i].args.num_of_philo;
}
return (arg);
}
구조도
철학자의 생존 여부를 확인하는 1개의 스레드가 모든 철학자를 확인한다.
모니터링 스레드는 철학자 한 명씩 순차적으로 생존 시간이 지났는지, 최소 식사 횟수를 채웠는지 확인한다.
생존 시간이 지났다면 철학자 스레드들과 공유하는 변수에 생존 여부를 TRUE
로 바꾸어준다. 마찬가지로 모든 철학자들이 최소 식사 횟수를 채웠으면 공유하는 변수에 식사 완료 여부를 TRUE
로 바꾼다.
각 철학자 스레드는 다른 철학자가 죽었는지 감지하기 위해 공통으로 사용하는 변수에 접근해서 데이터를 읽는다. 그리고 최소 식사 횟수를 채우면 공유하는 변수에 식사를 완료한 철학자의 수를 1씩 올린다. 철학자는 행동을 하기 전에 다른 철학자가 죽었는지 아니면 모든 철학자가 식사를 완료했는지 공통 변수에 접근해서 확인한다.
이때, 데이터 레이스가 발생하지 않도록 조심해야 한다. 두 개 이상의 스레드가 동시에 같은 변수에 접근하는데, 하나는 데이터를 읽으려고 하고, 다른 하나는 데이터를 수정하려고 한다면 두 데이터가 서로 다를 수도 있기 때문이다.
예를 들어, 2개의 스레드가 a
라는 int
형 변수에 접근하는데, 1번 스레드가 먼저 a
변수의 값이 2 이라는 것을 확인만 하고, 그 다음 2번 스레드는 변수 a
의 값에 +1 을 한다고 해보자. 그러면 1번 스레드는 a
를 2로 인식하고 있고, 2번 스레드는 a
를 3으로 인식하고 있는 것이다. 따라서 a
라는 동일한 변수에 대해 각 스레드가 다른 데이터를 가지고 있으므로 모든 스레드가 동일한 데이터를 가지고 있다고 말할 수 없다.
그렇기 때문에 모든 스레드가 같은 데이터를 갖기 위해서는 데이터에 대한 접근 및 수정 순서를 순차적으로 해주어야 한다. 데이터 레이스가 발생하지 않으려면 뮤텍스(mutex)를 적절하게 사용해야 하며, 변수의 값을 확인할 때, 변수의 값을 수정할 때 모두 뮤텍스로 변수를 감싸야 한다.
예를 들어, 모니터링 스레드가 수행하는 루틴 함수는 아래와 같이 작성할 수 있다.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
// 모니터링 스레드
// 공통으로 사용하는 변수의 값을 변경
void change_common_data(t_philo *philo, enum e_philo_status status)
{
if (status == DIE)
{
pthread_mutex_lock(&philo->data->dead_flag_lock);
philo->data->is_other_philo_dead = TRUE;
pthread_mutex_unlock(&philo->data->dead_flag_lock);
}
else if (status == EATEN)
{
pthread_mutex_lock(&philo->data->data_lock);
philo->data->is_all_philos_eaten = TRUE;
pthread_mutex_unlock(&philo->data->data_lock);
}
}
// 철학자가 생존했는지 확인
int is_philo_dead(t_philo *philo)
{
int result;
result = FALSE;
pthread_mutex_lock(&philo->time_lock); // 철학자의 식사 시간이 갱신되는 것도 동기화 필요
if (philo->is_alive == FALSE)
{
result = TRUE;
}
pthread_mutex_unlock(&philo->time_lock);
return (result);
}
// 모니터링 스레드의 루틴 함수
void *start_monitoring(void *arg)
{
int i;
t_philo *philos;
i = 0;
philos = (t_philo *)arg;
while (TRUE)
{
if (is_philo_dead(&philos[i]) == TRUE)
{
change_common_data(&philos[i], DIE);
print_state(&philos[i], DIED);
break ;
}
if (is_all_philo_eaten(&philos[i]) == TRUE)
{
change_common_data(&philos[i], EATEN);
break ;
}
i = (i + 1) % philos[i].args.num_of_philo;
}
return (arg);
}
Bonus
Mandatory 에서 데이터 레이스가 발생하지 않도록 뮤텍스(mutex) 를 사용했다면, Bonus 에서는 세마포어(semaphore)를 사용한다.
그리고 철학자를 스레드로 생성하는 것이 아닌 자식 프로세스로 생성(fork()
함수 이용)해야 한다.
세마포어(Semaphore)
세마포어는 뮤텍스와 달리 0 이상 값을 가지며, P연산(sem_wait
)과 V연산(sem_post
)이 가능하다.
P연산은 -1
을, V연산은 +1
을 해주는 연산이라 생각하면 된다.
또한, 세마포어는 초기값을 설정할 수 있기 때문에 뮤텍스와 달리 원하는 횟수만큼 빼거나 더할 수 있다.
그렇기 때문에 Mandatory 에서는 철학자 사이에 포크가 1개씩 놓여있었다면, Bonus 는 모든 포크가 테이블의 한 가운데에 있다고 이해하면 된다.
세마포어의 현재 카운트가 0이면 sem_wait
는 대기하고 있다가 카운트가 1이 되는 순간 실행하는 특징을 갖고 있다. 이는 뮤텍스가 unlock 되면 lock 을 거는 것과 같다.
세마포어는 Linxu 에서는 /dev/shm
또는 /dev/sem
폴더에 저장되는 파일이다. 하지만 Mac OS 에는 해당 폴더가 존재하지 않아서 직접 확인할 수 없었다.
구조도
1. 철학자 모니터링 스레드
스레드와 달리 프로세스는 변수 공유가 되지 않기 때문에 세마포어를 통해서 정보를 주고 받아야 한다.
그래서 프로그램이 종료되어야 하는지 확인하는 모니터링 스레드를 메인 프로세스에서 생성해주었다. 프로그램의 종료를 알리는 finish_semaphore
의 초기값을 0으로 설정하고, sem_wait(finish_semaphore)
를 수행하면 어디선가 sem_post(finish_semaphore)
를 수행하기 전까지 대기한다.
자식 프로세스마다 철학자의 죽음을 감지하는 모니터링 스레드를 하나씩 생성했다. 즉, 철학자가 N명이면 모니터링 스레드도 N개가 된다. 사실 스레드가 많아지기 때문에 자원을 효율적으로 사용하지는 못하긴 하지만, 작동에는 큰 문제가 없었다.
철학자가 죽으면 모니터링 스레드는 sem_post(finish_semaphore)
를 수행한다. 그리고 종료를 감지하는 스레드는 sem_wait
를 실행하면서 자식 프로세스들에게 kill
함수를 사용해서 시그널을 보낸다.
종료 감지 스레드를 코드로 표현하면 다음과 같다.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
// 종료 감지 스레드
int i;
i = 0;
if (sem_wait(data->finish_sem) == 0) // 모니터링 스레드에서 sem_post 수행 시 아래의 코드 실행
{
...
// 자식 프로세스 종료
while (i < data->args.num_of_philo)
{
kill(data->child_process[i], SIGKILL);
i += 1;
}
...
}
2. 식사 완료 감지 스레드
철학자들이 최소 식사 횟수를 모두 채웠는지 감지하기 위해서 메인 프로세스에서 식사 완료 감지 스레드를 생성했다.
마찬가지로 세마포어의 초기값을 0으로 설정하고, 철학자의 수만큼 sem_wait(eaten_semaphore)
를 수행한다. 철학자의 수만큼 반복하는 이유는 철학자 스레드에서 최소 식사 횟수를 모두 채우면 sem_post(eaten_semaphore)
를 수행하기 때문이다.
철학자가 모두 식사를 마치면 sem_post(finish_semaphore)
를 수행해서 전체 프로세스가 종료되도록 알린다.
이를 코드로 표현하면 아래와 같다.
1
2
3
4
5
6
7
8
9
10
// 식사 완료 감지 스레드
int i;
i = 0;
while (i < data->args.num_of_philo)
{
sem_wait(data->eaten_sem);
i += 1;
}
sem_post(data->finish_sem);
3. 종료 감지 스레드
철학자 중 누군가 죽었거나, 식사를 마치면 모든 프로세스를 종료시키는 스레드이다.
최소 식사 횟수를 다 채우지 못하고 철학자가 죽었다면 식사 완료 감지 스레드는 계속 sem_wait
를 하는 상태라서 프로그램이 종료되지 않는다.
그래서 sem_wait(finish_semaphore)
가 실행되면 식사 완료 감지 스레드가 종료될 수 있도록 철학자의 수만큼 sem_post(eaten_semaphore)
를 수행하도록 했다.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
// 종료 감지 스레드
int i;
i = 0;
if (sem_wait(data->finish_sem) == 0)
{
// 식사 완료 감지 스레드 종료
while (i < data->args.num_of_philo)
{
sem_post(data->eaten_sem);
i += 1;
}
i = 0;
// 자식 프로세스 종료
while (i < data->args.num_of_philo)
{
kill(data->child_process[i], SIGKILL);
i += 1;
}
}
테스터
- https://github.com/newlinuxbot/Philosphers-42Project-Tester
- 보너스는 테스터기에서 사용하는 인자값대로 실행하는 것을 추천한다.
- philosophers-visualizer
- 시간의 흐름에 따라 철학자들이 식사, 수면, 생각하는 것을 시각화 해주는 사이트이다.
회고
페어 프로그래밍에 코드 리뷰 곁들이기
이번 철학자 과제도 지난 과제들과 마찬가지로 2명이 팀을 이루어 프로그램을 완성하는 페어 프로그래밍을 활용해서 과제를 진행했다.
다만, 이번에는 다른 분들과 함께 총 5명이 모여 코드 리뷰 스터디를 이루어 진행했다.
Slack 에서 발견한 스터디 모집 게시물
평일 오후 1시에 모여서 각자 작성한 코드를 공유하고, 궁금하거나 이해되지 않는 부분을 서로에게 물어보고 답변하는 방식으로 진행했다.
github 에 함께 공유할 repository 를 생성했고, submodule 로 각자의 repository 를 공유하도록 했다. git submodule 을 이번에 처음 써보았는데, 여러 명이 각자의 작업 내역을 공유하기 좋은 기능이라는 것을 알게 되어 이를 글로 작성하여 스터디원들 뿐만 아니라 다른 분들도 참고할 수 있게 글로 정리하였다. 이 과정에서 git 에 조금 더 익숙해질 수 있었다.
git submodule 사용해서 스터디에 활용하기 [팔만코딩경]
우리는 코드 공동체입니다.
스터디 초반에는 처음 보는 개념과 함수들을 공부해야 하기 때문에 코드 리뷰를 받을 수 있는 부분이 없었다. 대신 과제를 어떤 방식으로 접근할 것인지, 과제에서 요구하는 것이 무엇인지 파악하기 위해 많은 이야기를 나누었다. 그리고 이전에 철학자 과제를 평가해본 적이 없어서 이미 과제를 해결하신 분들에게 어떻게 과제를 해결하면 좋을지 지식 품앗이를 요청하거나, 개인적으로 여쭤보기도 했다.
jujeon 님께서 진행하신 지식 품앗이 참여 후기
만약 처음부터 서브젝트만을 통해 문제를 해결했다면 많이 막막했을 것 같은데, 다른 분들의 도움을 받아서 수월하게 진행할 수 있었다. 그리고 가장 좋은 학습은 다른 분들 평가를 많이 다니면서 과제에 대한 이해도를 높이고, 다양한 관점에서 작성된 코드들을 많이 접해보는 것이라는 것을 느꼈다. 철학자 과제를 하면서 평가를 많이 다녔는데, 한번도 철학자 평가가 안 잡히다가 철학자 과제를 통과하고 나서야 잡혔던 것이 아쉬웠다.
스터디 중반에 접어 들면서 본격적인 코드 리뷰가 가능해졌다. 다른 동료 분들의 코드를 보며 코드에 녹아내려진 생각의 흐름과 논리를 이해했다. 그리고 이해되지 않는 부분은 바로 질문해서 해소하거나, 좋은 의견을 제시함으로써 서로의 코드 퀄리티를 높여갔다.
슬랙에서 주고 받은 정보들
스터디를 좋았던 점을 꼽으라면 하면서 각자 새롭게 알게 된 사실이나, 다른 동료 분들과 이야기하며 얻은 정보를 서로 공유를 했다는 것이다. 매일 만나서 서로 정보와 의견을 나누었기 때문에 점진적으로 과제에 대한 이해도를 높일 수 있었다.
그럼에도 코드 리뷰는 계속 되어야 한다.
사실 매일 만나서 코드 리뷰를 받는 건 어려운 일이다.
어떤 날은 문제점을 해결하지 못해서 코드 1줄조차 수정하지 못해 코드 리뷰를 받을 수 없었던 적도 있었다. 그래서 스터디원들이 아닌 다른 분들에게 발생한 문제에 대해 여쭤보기도하고, 코드를 진찰(?)해달라고 부탁했다.
코드에 마가 꼈습니다.
하지만 무엇이 문제인지, 어디서 발생하는 문제인지 감조차 잡지 못해서 코드에 마가 꼈다고 결론을 내리고 코드를 전부 날리고 다시 작성하기도 했다.
신기한 건 이해한 내용을 바탕으로 다시 처음부터 코드를 작성하면 생각보다 잘 작동한다는 것이었다. 불필요한 변수와 함수를 걷어내고, 생각의 흐름을 정리하다보니 코드를 자연스럽게 작성할 수 있었다. 글을 작성할 때도 생각이 정리되면 알아서 술술 써내려가지는 것처럼 코드도 마찬가지로 개념에 대한 이해가 정립되면 물 흐르듯 쓸 수 있었다.
코드 리뷰를 받을 때는 다른 스터디원 분들의 질문의 본질적인 의도를 파악하기 위해 노력했고, 혹시라도 놓치고 있는 개념이 있는지 파악하고자 했다. 반대로 코드 리뷰를 할 때는 상대방의 코드에서 배울 점이 무엇인지, 개선되면 좋을 것 같은 부분이 있는지, 나의 코드와 비교했을 때 어떤 점이 다르고 어떤 점이 비슷한지 비교하며 자연스럽게 코드를 작성하는 관점을 넓힐 수 있었다.
번뜩이는 아이디어는 가끔 나를 찾아온다.
⚠️ 주의 : 과제에 대한 스포일러가 포함되어 있습니다.
철학자 보너스 과제에서는 뮤텍스가 아닌 세마포어를 사용해서 프로세스 간 통신을 구현해야 한다.
메인 프로세스에서 프로그램의 종료를 감지하는 스레드와 모든 철학자가 식사를 완료했는지 감지하는 스레드로 총 2개의 모니터링을 구현했다.
하지만 철학자가 식사를 하다가 생존 시간이 지나버린 경우에는 모든 철학자가 식사를 완료했는지 감지하는 스레드가 종료되지 않고 계속 대기하는 현상이 발생했다.
이 문제를 해결하기 위해서 프로그램의 종료를 감지하는 스레드가 프로그램을 종료하기 전에 모든 철학자가 식사를 완료했는지 감지하는 스레드가 종료될 수 있도록 처리를 했다.
1
2
3
4
5
6
7
8
9
10
11
12
// 프로그램의 종료를 감지하는 스레드
...
if (식사 종료 감지 == TRUE)
{
// 모든 철학자가 식사를 완료했는지 감지하는 스레드가 종료되도록 처리
while (식사 횟수 감지 스레드가 종료할 때 까지)
{
식사 횟수 += 1
}
...
}
이 문제만 해결하면 과제를 마무리할 수 있었는데, 찰나의 순간에 이런 아이디어를 떠올렸다는 사실에 스스로 놀랐었다.
모든 과제를 이렇게 해결했다면 정말 좋았겠지만, 매번 그렇지는 않아서 이번에 맞이한 행운을 두고두고 기억하고 싶어 함께 적었다.
되돌아보며
이번 과제를 하며 아쉬웠던 점은 처음부터 너무 완벽한 코드를 짜려고 하다보니 코드가 잘 나오지 않았다는 것이다. 그리고 디버깅을 하려고 해도 어디서 문제가 발생했는지 쉽게 파악할 수 없었다.
그래서 일단 작동하는 코드를 최대한 작게 만들어서 테스트를 거치고, 조금씩 덧붙여가면서 완성도를 높이는 것이 훨씬 빠르고 효과적이라는 것을 많이 느꼈다.
그리고 코드 리뷰는 많이 받아볼수록 좋다는 것을 느꼈다. 평가를 받을 때만 코드를 설명하는 것이 아닌 리뷰 시간마다 코드를 설명해야 하기 때문에 실시간 디펜스를 해야 한다. 이 과정에서 평가를 자연스럽게 준비할 수 있게 되고, 발생할 수 있는 예외 상황에 대해서도 충분히 많이 생각해볼 수 있어서 좋았다. 그리고 상대방의 질문을 정확하게 이해하는 것도 결코 쉽지 않기 때문에 코드 리뷰 스터디를 통해 상대방의 말을 이해하는 연습을 충분히 할 수 있었다.
과제를 시작해서 마치기까지 1달이라는 시간이 걸렸지만, 이 과정에서 충분히 많은 것을 공부하고 얻어갈 수 있었다. 누군가 이 과제를 하게 된다면 기꺼이 배운 내용을 공유하고 싶다. 다른 분들이 우리에게 기꺼이 시간을 내어 도와주었던 것처럼.