톰 달링(Tom Dalling)의 고전적인 아티클에서 영감을 받은 이 글에서는 간단한 FizzBuzz 문제를 Go를 사용해 어떻게 극단적으로 오버엔지니어링(과잉 설계)할 수 있는지 탐구합니다.
가장 기본적인 구현에서 출발하여 기본 조건문부터 고차 함수와 동시성(concurrency)에 이르기까지 Go의 언어적 기능들을 살펴보며 점진적으로 리팩터링해 보겠습니다.
출력 결과는 1부터 15까지의 고전적인 FizzBuzz 수열이어야 합니다.
1
2
Fizz
4
Buzz
Fizz
7
8
Fizz
Buzz
11
Fizz
13
14
FizzBuzz
가장 직관적인 방법부터 시작해 봅시다. 제 역할은 해내지만, 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)
}
}
}
숫자 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)
}
}
다음으로 규칙을 완전히 외부로 추출해 보겠습니다.
참고: 원문 아티클에서는 이를 위해 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)
}
}
데이터를 전달하는 대신 동작(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)
}
}
익명 함수를 일일이 작성하는 것은 반복적입니다. 함수를 반환하는 팩토리 함수를 작성하여 중복을 제거(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)
}
}
마지막으로 동시성을 활용하여 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 주소와 쿠키를 처리할 수 있습니다.