Q1.
일단 내가 이해한 내용이야.
1. 고루틴은 한 번에 한 함수를 GO 키워드를 붙여서 여러개 작동시키는 기능.
2. waitgroup은 waitgroup에 추가된 고루틴을 끝날때까지 다음 코드로 진행하지 않게 대기시켜주는 기능.
3. Mutex는 특정 요소에 동시에 접근할 때, 해당 요소에 실질적으로는 한 번에 한 번씩만 접근할 수 있게 제한을 거는 기능
이렇게 이해했고, 채널은 해당 코드만 보면 잘 모르겠어.
1. 내가 이해한 내용이 맞는지?
2. 채널을 설명하는 코드를 좀 더 작성해줄래?
A1.
네 정리 너무 잘했어!
고루틴, WaitGroup, Mutex에 대한 개념 이해는 거의 완벽해.
아래에 각각 코멘트 달아볼게:
✅ 1. 너의 이해 내용 검토
항목 | 너의 설명 | 정확도 | 보출 설명 |
고루틴 | go 키워드를 붙여서 여러 개 함수 병렬 실행 | ✅ 완전 정확 | Go에서 병렬 작업을 위한 lightweight thread. |
WaitGroup | 고루틴이 끝날 때까지 대기시키는 기능 | ✅ 아주 정확 | wg.Add()로 고루틴 개수 설정, wg.Done()으로 종료 알림 |
Mutex | 한 번에 하나만 자원 접근하게 만드는 기능 | ✅ 정확 | 공유 자원 보호용. 여러 고루틴이 동시에 접근 시 충돌 방지 |
❓ 2. 채널이 잘 이해 안 되는 이유
채널은 단순히 “값을 보내고 받는다” 이상으로 동기화 역할도 함께 하거든.
즉, 채널은 데이터 통로이자, 고루틴 간 신호선이야.
✅ 채널의 핵심 개념 한 줄 요약
고루틴 간 데이터를 안전하게 전달하기 위한 양방향 통신 라인
✅ 쉽게 설명하는 비유
- 고루틴 1: 편지를 써서 우체통(채널)에 넣는다
- 고루틴 2: 편지가 올 때까지 우체통 앞에서 기다린다
채널은 그 우체통 역할을 하는 것
✅ 예제 1: 값 하나 주고받기
package main
import "fmt"
func main() {
ch := make(chan string) // string 타입을 주고받는 채널
go func() {
ch <- "안녕, 고루틴!" // 채널로 메시지 전송
}()
msg := <-ch // 채널로부터 메시지 수신
fmt.Println("받은 메시지:", msg)
}
🔍 이 예제에서 중요한 점:
- go func() 안의 고루틴이 메시지를 채널에 보냄 (ch <-)
- 메인 함수가 msg := <-ch 로 받을 때까지 서로 기다림
✅ 예제 2: 채널로 여러 개의 값 전달하기
package main
import "fmt"
func main() {
ch := make(chan int)
go func() {
for i := 1; i <= 3; i++ {
ch <- i // 채널로 값 전송
}
close(ch) // 채널 닫기 (수신 측에서 range 사용 가능하게)
}()
for val := range ch { // 채널에서 값 수신
fmt.Println("받은 값:", val)
}
}
🧠 여기서 배울 점:
- close(ch)를 통해 더 이상 보낼 값 없음을 알려줌
- 수신 측에서는 range ch로 편하게 반복 수신 가능
✅ 예제 3: 채널은 동기화 장치 역할도 한다
package main
import (
"fmt"
"time"
)
func main() {
done := make(chan bool)
go func() {
fmt.Println("고루틴 작업 중...")
time.Sleep(1 * time.Second)
done <- true // 작업 완료됨을 알림
}()
<-done // 고루틴이 끝날 때까지 여기서 대기
fmt.Println("작업 완료 후 main 계속 실행")
}
🧠 이건 WaitGroup 없이 채널을 이용해 종료를 기다리는 방식
→ 채널은 동기화용 도구로도 사용 가능
✅ 정리 요약
개념 | 역할 |
고루틴 | 병렬로 작업을 실행하는 lightweight 쓰레드 |
WaitGroup | 고루틴 종료 대기 |
Mutex | 공유 자원에 대한 동시 접근 제어 |
Channel | 고루틴 간 데이터 전달 + 동기화 수단 |
원하면 이제 unbuffered vs buffered 채널 차이,
또는 select 문을 사용하는 고급 채널 예제도 보여줄 수 있어.
지금 이 흐름에서 더 깊이 들어가고 싶은 포인트 있어? 😎
Q2.
잠시만, waitgroup 사용할 때
package main
import (
"fmt"
"sync"
"time"
)
func doTask(id int, wg *sync.WaitGroup) {
defer wg.Done()
time.Sleep(300 * time.Millisecond)
fmt.Printf("[비동기] 작업 %d 완료\n", id)
}
func main() {
var wg sync.WaitGroup
start := time.Now()
// ✅ 작업이 동시에 실행됨 (비동기)
// 전체 작업 소요 시간은 약 300ms
for i := 1; i <= 3; i++ {
wg.Add(1)
go doTask(i, &wg)
}
wg.Wait()
fmt.Println("총 소요 시간:", time.Since(start))
}
이렇게 사용하잖아?
코드만 보았을 때 wg를 어떻게 사용하라는 말인지 잘 모르겠어.
wg를 사용하는 방법을 알려줘.
필요한 파라미터나 고 루틴에 필요한 설정이라든가, 그냥 지금 코드를 보았을때 어떻게 사용되는지 하나도 모르겠어.
A2.
너무 좋은 질문이야.
지금처럼 코드만 보면 wg.Add(1) / wg.Wait() / wg.Done() 이 왔다갔다하는데,
딱 보면 뭘 왜 하는 건지 감이 안 올 수 있어.
이걸 하나하나 순서대로 아주 쉽게 설명해줄게.
✅ WaitGroup을 쓰는 이유
고루틴을 여러 개 띄우면 병렬로 작업이 실행돼.
그런데 main 함수는 고루틴이 끝날 때까지 기다려주지 않아.
그래서 고루틴이 도는 도중에 main()이 종료돼버리면,
작업이 완료되지 않고 프로그램이 종료됨.
➡ 이때 “고루틴이 다 끝날 때까지 기다려줘” 라고 알려주는 게 sync.WaitGroup
✅ WaitGroup 구조 이해
👇 구조를 비유로 설명하면:
개념 | 역할(비유) |
wg.Add(1) | "작업 한 개 추가됨!" 이라고 메모하기 |
go func(...){ defer wg.Done() }() | 고루틴 하나 실행, 끝나면 메모 한 개 지움 |
wg.Wait() | "메모된 작업이 전부 끝날 때까지 기다리기" |
✅ 예제 코드 분석
var wg sync.WaitGroup // 1️⃣ WaitGroup 변수 선언
for i := 1; i <= 3; i++ {
wg.Add(1) // 2️⃣ 고루틴 하나를 만들 거니까 작업 추가
go doTask(i, &wg) // 3️⃣ 고루틴 실행 & wg를 넘김
}
wg.Wait() // 6️⃣ 고루틴들이 Done 호출할 때까지 대기
📦 고루틴 안에서 호출되는 함수: doTask
func doTask(id int, wg *sync.WaitGroup) {
defer wg.Done() // 5️⃣ 작업이 끝나면 "완료!" 표시
time.Sleep(300 * time.Millisecond)
fmt.Printf("작업 %d 완료\n", id)
}
✅ 전체 흐름 요약 순서
- wg.Add(1) → "고루틴 하나 시작할 거야!" 라고 알려줌
- go doTask() → 실제 고루틴 실행
- 고루틴 내부에서 defer wg.Done() → "이 작업 끝났어!" 라고 표시
- main()은 wg.Wait()에서 모든 Done이 호출될 때까지 대기함
🔍 꼭 기억할 것
- Add(n) → 얼마나 기다려야 할지를 설정
- Done() → 고루틴이 끝났다고 알림
- Wait() → 다 끝날 때까지 main 함수 멈춰 있음
💬 한 줄 요약
WaitGroup은 고루틴 수만큼 Add()하고, 각 고루틴 안에서 Done() 호출한 뒤, main()은 Wait()로 기다리는 동기화 도구야.
📌 필수 형태 템플릿
var wg sync.WaitGroup
for i := 0; i < N; i++ {
wg.Add(1)
go func() {
defer wg.Done()
// 작업 수행
}()
}
wg.Wait() // 여기서 모든 고루틴이 끝날 때까지 대기
혹시 예제로 실제 작업 10개 돌리기, 조건부 Done(), 중첩된 고루틴 대기 등도 보고 싶어?
아니면 지금 구조에서 응용하고 싶은 예시가 있어? 😎
Q3.
포인터는 왜 쓰인거야?
A3.
좋아, 정말 핵심을 짚었어.
WaitGroup 쓸 때 함수 안에 보면 이렇게 돼 있지:
func doTask(id int, wg *sync.WaitGroup)
❓ 여기서 왜 굳이 포인터(*sync.WaitGroup)를 쓰는지 궁금한 거지?
정답부터 말하면:
✅ 이유: WaitGroup은 "값"이 아니라 "상태를 공유"해야 하기 때문이야
🔍 이걸 두 줄로 요약하면:
- sync.WaitGroup은 내부에 카운터 상태를 가지고 있는 구조체야
- 고루틴마다 복사된 구조체를 쓰면 카운터가 제대로 줄어들지 않아
📦 비유로 이해해보기
- wg.Add(1) → “작업 하나 더 시작했어!”
- wg.Done() → “작업 하나 끝났어!”
그런데 고루틴 안에서 복사본에다가 Done() 호출하면?
→ 원본 wg는 몰라서 Wait()가 끝나지 않음
❌ 포인터 없이 썼을 때 문제 예시
func doTask(id int, wg sync.WaitGroup) {
defer wg.Done() // ❌ 이건 복사본에 Done을 호출 → main에서 기다리는 wg에는 반영되지 않음
fmt.Println("작업", id)
}
for i := 0; i < 3; i++ {
wg.Add(1)
go doTask(i, wg) // ❌ 값 전달됨
}
🔥 결과:
- main()은 Wait()에서 계속 기다림
- 각 고루틴은 복사된 wg를 Done()하지만, 원래 wg는 그대로 남음
✅ 포인터로 넘겼을 때는?
func doTask(id int, wg *sync.WaitGroup) {
defer wg.Done() // ✅ 진짜 wg를 참조해서 Done 호출됨
fmt.Println("작업", id)
}
go doTask(i, &wg) // ✅ 포인터 전달
✅ 결과:
- Add() 한 만큼 Done()이 호출되고
- Wait()는 정확히 끝남
💬 한 줄 요약
WaitGroup은 "상태가 바뀌어야 의미가 있는 구조체"라서, 반드시 포인터로 넘겨야 모든 고루틴이 같은 값을 공유해.
필요하다면 이걸 실험으로 증명해볼 수 있게 포인터 vs 값 비교용 실습 예제도 만들어줄 수 있어.
해볼까? 😎
Q4.
waitGroup에서 add(n)에서 n은 정수인거야? 그리고 add(n)에 들어간 n만큼 wg.done()이 되어야 하는건가?
질문 2개로 나눠서 확실하게 정리해줄게.
✅ 1. WaitGroup.Add(n)에서 n은 정수인가?
👉 맞아, int 타입이야.
func (wg *WaitGroup) Add(delta int)
- delta라는 이름의 정수 파라미터를 받아
- +n을 넣으면 **“그만큼 고루틴을 기다리겠다”**는 뜻
- -n을 넣으면 수동으로 줄일 수도 있지만, 보통은 Done()을 씀
✅ 2. Add(n) 했으면 Done()도 n번 해야 해?
👉 정확히 맞아. 반드시 n번의 Done() 호출이 있어야 해.
- wg.Add(3) 했다면 → 고루틴 3개에서 각각 wg.Done()을 호출해야 함
- 그래야 wg.Wait()가 끝나
❗ 만약 Add(3) 해놓고 Done()을 2번만 호출하면?
→ Wait()는 영원히 멈춘 채 기다림
❗ 반대로 Add(1) 해놓고 Done()을 2번 호출하면?
→ panic 발생:
panic: sync: negative WaitGroup counter
✅ 예제로 이해
var wg sync.WaitGroup
wg.Add(3) // 고루틴 3개 기다리겠다
go func() {
defer wg.Done()
// 작업 1
}()
go func() {
defer wg.Done()
// 작업 2
}()
go func() {
defer wg.Done()
// 작업 3
}()
wg.Wait() // ← 위 3개의 Done()이 끝나야 여기서 빠져나옴
✅ 실수 주의 정리
실수 | 결과 |
Add(3) → Done()을 2번만 호출 | Wait()에서 영원히 멈춤 (deadlock) |
Add(1) → Done()을 2번 호출 | panic: negative WaitGroup counter |
Add(0) → Wait()는 즉시 통과 (아무것도 안 기다림) |
✅ 한 줄 요약
WaitGroup.Add(n)은 “n개의 Done을 기다리겠다”는 선언이고,
WaitGroup.Wait()은 “모든 Done이 끝날 때까지 멈춰 있겠다”는 명령이야.
더 복잡한 예시로, 중첩된 고루틴이나 Add()를 고루틴 안에서 사용하는 실전 패턴도 보여줄 수 있어.
해볼까? 😎
'언어 및 도구들 공부 > Go' 카테고리의 다른 글
Go) 패닉이 발생하는 경우, 그리고 recover() (0) | 2025.04.13 |
---|---|
Go) 변수의 재사용, := (0) | 2025.04.09 |
Go) 변수 대문자로 시작하는 경우 (0) | 2025.04.08 |
Go) 리플렉션 기반 동적 함수 생성 (0) | 2025.04.07 |
Go) 구조체와 태그 (0) | 2025.04.07 |