💻 지나치게 자세히 파헤친 FizzBuzz: Go 언어의 관점 🔗

톰 달링(Tom Dalling)의 고전적인 아티클에서 영감을 받은 이 글에서는 간단한 FizzBuzz 문제를 Go를 사용해 어떻게 극단적으로 오버엔지니어링(과잉 설계)할 수 있는지 탐구합니다.

가장 기본적인 구현에서 출발하여 기본 조건문부터 고차 함수와 동시성(concurrency)에 이르기까지 Go의 언어적 기능들을 살펴보며 점진적으로 리팩터링해 보겠습니다.

📑 목차 🔗


🎯 목표 🔗

출력 결과는 1부터 15까지의 고전적인 FizzBuzz 수열이어야 합니다.

1
2
Fizz
4
Buzz
Fizz
7
8
Fizz
Buzz
11
Fizz
13
14
FizzBuzz

1️⃣ 단순한 접근법 (초급) 🔗

가장 직관적인 방법부터 시작해 봅시다. 제 역할은 해내지만, 3과 5 모두에 대해 나머지 연산(%) 로직을 중복해서 작성해야 합니다.

package main

import "fmt"

func main() {
	for i := 1; i <= 15; i++ {
		if i%3 == 0 && i%5 == 0 {
			fmt.Println("FizzBuzz")
		} else if i%3 == 0 {
			fmt.Println("Fizz")
		} else if i%5 == 0 {
			fmt.Println("Buzz")
		} else {
			fmt.Println(i)
		}
	}
}

2️⃣ DRY 원칙 적용하기 (중복 줄이기) 🔗

숫자 3과 5, 그리고 문자열 "Fizz"와 "Buzz"의 중복을 피하기 위해 결과 문자열을 순차적으로 조합할 수 있습니다. 조건 검사를 마친 후에도 문자열이 비어 있다면 숫자를 그대로 출력합니다.

package main

import "fmt"

func main() {
	for i := 1; i <= 15; i++ {
		res := ""
		
		if i%3 == 0 {
			res += "Fizz"
		}
		if i%5 == 0 {
			res += "Buzz"
		}
		
		if res == "" {
			res = fmt.Sprint(i)
		}
		fmt.Println(res)
	}
}

3️⃣ 구조체를 활용한 매개변수화 (데이터 기반) 🔗

다음으로 규칙을 완전히 외부로 추출해 보겠습니다.

참고: 원문 아티클에서는 이를 위해 map[int]string을 사용했습니다. 하지만 Go는 의도적으로 맵(map)의 순회 순서를 무작위로 섞습니다. 맵을 사용하면 가끔 “FizzBuzz” 대신 “BuzzFizz"가 출력될 수 있습니다! 이를 해결하기 위해 구조체 슬라이스(slice of structs)를 사용하여 실행 순서를 보장합니다.

package main

import "fmt"

type Rule struct {
	Divisor int
	Word    string
}

func run(max int, rules []Rule) []string {
	var all []string
	for i := 1; i <= max; i++ {
		res := ""
		for _, rule := range rules {
			if i%rule.Divisor == 0 {
				res += rule.Word
			}
		}
		if res == "" {
			res = fmt.Sprint(i)
		}
		all = append(all, res)
	}
	return all
}

func main() {
	rules := []Rule{
		{3, "Fizz"},
		{5, "Buzz"},
	}
	
	all := run(15, rules)
	for _, word := range all {
		fmt.Println(word)
	}
}

4️⃣ 함수를 활용한 매개변수화 (클로저) 🔗

데이터를 전달하는 대신 동작(behavior)을 전달하면 어떨까요? 현재 반복 횟수에 따라 문자열 포인터를 수정하는 함수로 Trigger 타입을 정의할 수 있습니다.

package main

import "fmt"

type Trigger func(res *string, i int)

func run(max int, triggers []Trigger) []string {
	var all []string
	for i := 1; i <= max; i++ {
		res := ""
		for _, trigger := range triggers {
			trigger(&res, i)
		}
		all = append(all, res)
	}
	return all
}

func main() {
	all := run(15, []Trigger{
		func(res *string, i int) {
			if i%3 == 0 {
				*res += "Fizz"
			}
		},
		func(res *string, i int) {
			if i%5 == 0 {
				*res += "Buzz"
			}
		},
		func(res *string, i int) {
			if *res == "" {
				*res = fmt.Sprint(i)
			}
		},
	})
	
	for _, word := range all {
		fmt.Println(word)
	}
}

5️⃣ 고차 함수 (Trigger 팩토리) 🔗

익명 함수를 일일이 작성하는 것은 반복적입니다. 함수를 반환하는 팩토리 함수를 작성하여 중복을 제거(DRY)할 수 있습니다. 이는 Go의 클로저(closure) 기능을 활용한 것입니다.

package main

import "fmt"

type Trigger func(res *string, i int)

func run(max int, triggers []Trigger) []string {
	var all []string
	for i := 1; i <= max; i++ {
		res := ""
		for _, trigger := range triggers {
			trigger(&res, i)
		}
		all = append(all, res)
	}
	return all
}

// create는 커스텀 Trigger 함수를 반환하는 팩토리 역할을 합니다.
func create(number int, word string) Trigger {
	return func(res *string, i int) {
		if i%number == 0 {
			*res += word
		}
	}
}

func main() {
	all := run(15, []Trigger{
		create(3, "Fizz"),
		create(5, "Buzz"),
		func(res *string, i int) {
			if *res == "" {
				*res = fmt.Sprint(i)
			}
		},
	})
	
	for _, word := range all {
		fmt.Println(word)
	}
}

6️⃣ 동시성 적용하기 (채널) 🔗

마지막으로 동시성을 활용하여 Go다운(idiomatic Go) 코드로 만들어 봅시다. 결과를 슬라이스에 모아서 마지막에 한꺼번에 반환하는 대신, 고루틴(goroutine)에서 숫자를 처리하고 채널(channel)을 통해 메인 스레드로 결과를 실시간 스트리밍할 수 있습니다.

run 함수의 chan<- string 시그니처에 주목하세요. 이는 함수가 채널로 데이터를 보내기만 하도록 제한하는 것으로, Go에서 권장되는 좋은 습관입니다.

package main

import "fmt"

type Trigger func(res *string, i int)

func run(max int, out chan<- string, triggers []Trigger) {
	for i := 1; i <= max; i++ {
		res := ""
		for _, trigger := range triggers {
			trigger(&res, i)
		}
		out <- res // 결과를 스트리밍합니다.
	}
	close(out) // 작업이 끝나면 항상 채널을 닫습니다.
}

func create(number int, word string) Trigger {
	return func(res *string, i int) {
		if i%number == 0 {
			*res += word
		}
	}
}

func main() {
	// 버퍼가 없는(unbuffered) 채널 생성
	results := make(chan string)
	
	// 백그라운드 고루틴에서 생성 작업 시작
	go run(15, results, []Trigger{
		create(3, "Fizz"),
		create(5, "Buzz"),
		func(res *string, i int) {
			if *res == "" {
				*res = fmt.Sprint(i)
			}
		},
	})
	
	// 채널을 순회하며 값이 도착하는 대로 읽어들입니다.
	for word := range results {
		fmt.Println(word)
	}
}

🏁 결론 🔗

FizzBuzz 같은 간단한 문제를 오버엔지니어링해 보면서 구조적 루프, 데이터 기반 규칙(그리고 왜 맵의 순회 순서가 중요한지!), 클로저, 고차 함수, 그리고 마지막으로 고루틴과 채널까지 차례대로 살펴보았습니다. Go에서 단 하나의 문제를 해결하는 방법도 이토록 다양하다는 사실을 다시금 일깨워 주는 즐거운 예제입니다!


📚 무료 전자책 다운로드 🔗

카우에의 삶의 방식 오프라인 에디션을 받아보세요—🍏 건강, 💰 부, ❤️ 사랑, 🧘 의미를 최적화하기 위한 실전 가이드입니다.

카우에의 삶의 방식

카우에의 삶의 방식

📬 뉴스레터 구독하기 🔗

더 나은 삶을 위한 새로운 에세이, 도서 요약, 실전 가이드를 발행할 때 알림을 받아보세요. 스팸은 없으며 언제든 구독을 취소할 수 있습니다.


💬 댓글 및 토론 🔗

질문, 의견 또는 제안이 있으신가요? 아래에 생각을 남겨주세요. 댓글은 GitHub Discussions(광고 및 스팸 없음)를 통해 제공됩니다—댓글이나 반응을 남기려면 아래의 GitHub으로 로그인을 클릭하세요(무료 GitHub 계정은 1분 이내에 만들 수 있습니다).



최종 업데이트: 2026. 10. 11..

면책 조항: 본 사이트의 콘텐츠는 정보 제공 및 교육 목적으로만 제공되며 의학, 심리, 재무, 투자, 세무 또는 법률 자문을 구성하지 않습니다. 여기에 제시된 생각, 큐레이션 및 의견은 전적으로 저 개인의 것이며 고용주의 입장을 대변하지 않습니다. 도서, 강연 및 아티클 요약은 독립적인 교육적 해설이며 각 저자의 지적 재산입니다. 편집, 서식 지정 및 요약을 돕기 위해 AI 도구가 사용될 수 있으며, 정확성을 기하기 위해 최선을 다하고 있으나 오류가 포함될 수 있습니다. 개인정보 처리방침: 본 사이트는 선택적 뉴스레터 구독(follow.it), 댓글(giscus / GitHub Discussions) 및 임베디드 미디어(YouTube)를 위해 제3자 서비스를 사용하며, 해당 서비스들은 각자의 개인정보 처리방침에 따라 방문자의 IP 주소와 쿠키를 처리할 수 있습니다.