Thread-Safe 구조체 설계
Coddy GO 여정의 객체 지향 프로그래밍 섹션에 포함된 레슨. 107개 중 65번째.
이제 뮤텍스와 WaitGroup을 이해했으므로, 이를 결합하여 여러 고루틴에서 동시에 안전하게 사용할 수 있는 구조체를 설계해 보겠습니다. 스레드 안전 구조체는 메서드 내부에 동기화를 캡슐화하므로 호출자는 잠금에 대해 걱정할 필요가 없습니다.
패턴은 간단합니다. 구조체에 뮤텍스를 포함하고 공유 상태에 액세스하는 모든 메서드에서 잠급니다:
type SafeCounter struct {
mu sync.Mutex
count int
}
func (c *SafeCounter) Increment() {
c.mu.Lock()
defer c.mu.Unlock()
c.count++
}
func (c *SafeCounter) Value() int {
c.mu.Lock()
defer c.mu.Unlock()
return c.count
}읽기 전용인 Value() 메서드도 뮤텍스를 잠근다는 점에 유의하세요. 이렇게 하지 않으면 한 고루틴이 읽는 동안 다른 고루틴이 쓰기를 수행하여 데이터 경쟁이 발생할 수 있습니다. 쓰기보다 읽기가 훨씬 더 빈번하다면 대신 sync.RWMutex를 사용하고, 읽기 작업에는 RLock()을 호출하세요.
핵심 설계 원칙: 뮤텍스를 비공개로 유지하세요. 소문자 필드 이름(mu)을 사용하면 외부 코드가 필드에 직접 액세스하는 것을 방지할 수 있습니다. 모든 동기화는 메서드를 통해 이루어지므로 스레드 안전성을 완전히 제어할 수 있습니다.
여러 필드가 있는 구조체에서는 일관된 상태를 보장하기 위해 서로 관련된 모든 필드를 동일한 뮤텍스로 보호하세요.
type Account struct {
mu sync.Mutex
balance int
history []string
}
func (a *Account) Deposit(amount int) {
a.mu.Lock()
defer a.mu.Unlock()
a.balance += amount
a.history = append(a.history, fmt.Sprintf("+%d", amount))
}balance와 history는 모두 원자적으로 업데이트됩니다. 어떤 고루틴도 둘 중 하나만 변경되고 다른 하나는 변경되지 않은 일관성 없는 상태를 관찰할 수 없습니다.
챌린지
쉬움struct 메서드 내부에 동기화 기능을 적절히 캡슐화하는 thread-safe bank account 시스템을 만들어 보겠습니다. account는 호출자에게 잠금 세부 정보를 노출하지 않고 동시 입금, 출금 및 balance 확인을 안전하게 처리합니다.
코드를 두 파일에 걸쳐 구성합니다.
account.go: thread-safe bank account를 정의합니다.embedded
sync.Mutex,balancefield (int), 그리고 성공한 모든 작업을 문자열로 기록하는transactionsslice를 포함하는BankAccountstruct를 만듭니다.다음 메서드를 implement합니다.
NewBankAccount(initial int) *BankAccount- 주어진 initial balance와 empty transactions slice를 사용해 새 account를 Create합니다.Deposit(amount int)- amount를 balance에 더하고 transaction을+[amount]로 기록합니다.Withdraw(amount int) bool- sufficient funds가 exist하면 amount를 빼고-[amount]를 기록한 뒤true를 반환합니다. 그렇지 않으면 아무것도 수정하지 않고false를 반환합니다.Balance() int- current balance를 반환합니다.History() []string- transactions slice의 copy를 반환합니다.
struct의 field에 access하는 모든 메서드는 thread safety를 Ensure하기 위해 mutex를 lock해야 합니다. 잠금 해제에는
defer를 사용합니다. mutex와 모든 field는 external code가 메서드를 사용해야 하도록 unexported (소문자) 상태로 유지합니다.main.go: banking operation을 처리하고 thread-safe account를 시연합니다.initial balance를 읽은 다음 operation 수를 읽습니다. 각 operation에 대해 type (
deposit,withdraw또는balance)을 읽고, deposit/withdraw의 경우 amount를 읽습니다.각 operation의 결과를 출력합니다.
deposit:Deposited [amount], Balance: [new balance]를 출력합니다.withdraw: 성공하면Withdrew [amount], Balance: [new balance]를 출력하고, 실패하면Withdrawal failed: insufficient funds를 출력합니다.balance:Current balance: [balance]를 출력합니다.
모든 operation이 끝나면 transaction history를 출력하며, 각 entry는 새 줄에 출력하고 첫 번째 entry에만
History:를 접두사로 붙입니다.
다음 input이 제공됩니다.
- Line 1: Initial balance (integer)
- Line 2: Number of operations (integer)
- Following lines: 각 operation의 type (
deposit,withdraw또는balance) 및 deposit/withdraw의 경우 다음 줄에 amount
예를 들어 다음이 주어졌다고 합시다.
100
5
deposit
50
balance
withdraw
30
withdraw
200
balance출력은 다음과 같아야 합니다.
Deposited 50, Balance: 150
Current balance: 150
Withdrew 30, Balance: 120
Withdrawal failed: insufficient funds
Current balance: 120
History: +50
-30여기서 핵심 원칙은 모든 synchronization이 BankAccount 메서드 내부에 숨겨져 있다는 것입니다. 호출자는 lock에 대해 전혀 생각하지 않고 단순히 Deposit(), Withdraw(), Balance()를 사용합니다. struct가 내부적으로 thread safety를 처리합니다.
직접 해보기
package main
import (
"bufio"
"fmt"
"os"
"strconv"
"strings"
)
func main() {
reader := bufio.NewReader(os.Stdin)
// 초기 잔액 읽기
initialStr, _ := reader.ReadString('\n')
initial, _ := strconv.Atoi(strings.TrimSpace(initialStr))
// 작업 수 읽기
numOpsStr, _ := reader.ReadString('\n')
numOps, _ := strconv.Atoi(strings.TrimSpace(numOpsStr))
// 은행 계좌 생성
account := NewBankAccount(initial)
// 각 작업 처리
for i := 0; i < numOps; i++ {
opType, _ := reader.ReadString('\n')
opType = strings.TrimSpace(opType)
switch opType {
case "deposit":
amountStr, _ := reader.ReadString('\n')
amount, _ := strconv.Atoi(strings.TrimSpace(amountStr))
// TODO: Deposit을 호출하고 결과 출력
// 형식: "Deposited [amount], Balance: [new balance]"
case "withdraw":
amountStr, _ := reader.ReadString('\n')
amount, _ := strconv.Atoi(strings.TrimSpace(amountStr))
// TODO: Withdraw를 호출하고 적절한 결과 출력
// 성공 시: "Withdrew [amount], Balance: [new balance]"
// If failed: "Withdrawal failed: insufficient funds"
_ = amount // 구현할 때 이 줄을 제거하세요
case "balance":
// TODO: Balance를 호출하고 결과 출력
// Format: "Current balance: [balance]"
}
}
// TODO: 거래 내역 출력
// 첫 번째 항목은 "History: " 접두사를 붙여야 합니다
// 이후 항목들은 접두사 없이 새 줄에 있어야 합니다
}
이 레슨에는 짧은 퀴즈가 포함되어 있습니다. 레슨을 시작해 문제를 풀고 진행 상황을 기록하세요.
객체 지향 프로그래밍의 모든 레슨
8에러 처리와 OOP
error 인터페이스사용자 정의 에러 타입에러 래핑 (fmt.Errorf)센티넬 에러errors.Is()와 errors.As()Panic, Defer, Recover요약 - 파일 파서직접 연습해 보세요: 온라인 Go 컴파일러