1. 우선순위와 계산 순서
연산자 우선순위는 Go가 표현식을 어떤 순서로 계산하는지를 뜻합니다. 여러 연산을 연달아 적었을 때의 이야기죠. 처음에는 “음, 당연한데?” 싶습니다. 그런데 어느 순간 할인 계산이 “이상하게” 되거나, 퍼센트가 마법처럼 바뀌고, 최종 금액이 “흔들리는” 일이 생깁니다. 컴퓨터가 아니라, 괄호 없는 표현식이 문제인 경우가 많습니다.
프로그래밍에서, 그리고 삶에서도 가장 성가신 오류는 즉시 폭발하지 않고 틀렸지만 그럴듯한 결과를 내는 경우입니다. 우선순위와 정수 나눗셈 때문에 바로 그런 버그가 자주 생깁니다. 그래서 지금은 두 가지를 해볼 겁니다. 계산 순서를 이해하고, 결과를 예측하는 법을 익히는 것입니다.
산술 연산자
산술 연산자는 이미 익숙할 겁니다: + - * / %. 지금은 한 번 더 “유리창을 닦듯” 정리하면서 중요한 점을 봅시다. 같은 기호라도 맥락에 따라 다르게 동작할 수 있고, 문맥이 무엇이 일어나는지를 결정합니다. 예를 들어, -5 는 “빼기”가 아니라 단항 마이너스 입니다. 반면 10 - 5 는 뺄셈입니다.
그리고 더 중요한 점: Go에서 산술은 거의 항상 엄격하게 타입이 정해집니다. 즉, int 로 이루어진 식은 int 타입의 결과를 냅니다. 이 점은 나눗셈에서 특히 잘 드러납니다.
“혹시 모르니” 간단한 점검:
package main
import "fmt"
func main() {
a := 10
b := 3
fmt.Printf("a=%d, b=%d\n", a, b) // a=10, b=3
}
연산자 우선순위
2 + 3*4 같은 표현식을 쓰면, Go는 그것을 “지친 사람처럼” 왼쪽에서 오른쪽으로 읽지 않습니다. 규칙대로 읽습니다. 먼저 곱셈, 그다음 덧셈입니다.
지금은 아주 짧은 “학교식” 규칙만 기억하면 충분합니다: 괄호가 가장 강하고, 그다음 * / %, 그다음 + - 입니다. 이것만 알아도 흔한 함정의 90%는 피할 수 있습니다.
아주 간단한 우선순위 표입니다. 지금 필요한 것만 담았습니다:
| 그룹 | 연산자 | 외우는 방법 |
|---|---|---|
| 1 (가장 강함) | |
“괄호가 우선” |
| 2 | |
“곱셈-나눗셈-나머지는 한 팀” |
| 3 (더 약함) | |
“덧셈/뺄셈은 그다음” |
중요: % 연산자(나머지)는 * 와 / 와 같은 우선순위 에 있습니다. 즉, a + b%10 은 a + (b%10) 이지, (a+b)%10 이 아닙니다.
좌결합성
연산자의 우선순위가 같을 때(예: * 와 /), 다음 질문이 생깁니다. “좋아, 그런데 누가 먼저지?” 일반적인 경우 Go는 왼쪽에서 오른쪽으로 계산합니다.
이것을(놀라지 마세요) 결합성 이라고 합니다. 지금은 이것만 기억하면 됩니다: 8 / 2 * 2 는 ((8 / 2) * 2) 로 계산되며, 8 / (2 * 2) 가 아닙니다.
숫자로 직접 보겠습니다:
package main
import "fmt"
func main() {
fmt.Println(8/2*2) // 8
fmt.Println(8/(2*2)) // 2
}
이건 Go의 특별한 특징이라기보다 대부분의 언어에서 자연스러운 수식 계산 방식입니다. 그렇기 때문에 다른 순서를 원한다면, “이미 분명해 보여도” 괄호를 넣어야 합니다.
코드를 더 명확하게 만드는 괄호
현실에서 우선순위는 수학 표처럼 계속 머릿속에 붙들고 있어야 할 대상이 아닙니다. 우선순위는 검증할 줄 알아야 하는 것 이고, 코드는 우선순위가 눈에 보이게 작성해야 합니다.
괄호는 약함의 표시가 아닙니다(가끔 동료들이 그렇게 보이게 만들 뿐이죠). 괄호는 전선에 붙인 라벨과 같습니다. “빨간색은 플러스, 건드리지 마.”
두 버전을 비교해 봅시다:
package main
import "fmt"
func main() {
price := 200
qty := 3
total1 := price*qty + 50
total2 := (price * qty) + 50
fmt.Println(total1, total2) // 650 650
}
둘 다 똑같이 동작하지만, total2 가 더 빨리 읽힙니다. 미래의 “나”가 오늘의 “나”에게 고마워할 겁니다. 이런 일은 삶에서 드물지만, 이럴 때는 정말 그렇습니다.
2. 정수 나눗셈과 나머지
정수 나눗셈
이제 나눗셈 이야기입니다. 많은 사람들이 이 지점에서 “무슨 말이야?!”라고 말하곤 합니다.
두 피연산자가 모두 정수(int, int64 등)라면, / 는 정수 나눗셈 을 수행합니다. 즉, 소수 부분은 그냥 버려집니다.
수학 규칙대로 반올림하지도 않고, 자동으로 float64 로 바꾸지도 않으며, 당신이 “의도한 것”을 추측하지도 않습니다. 그냥 정수 부분만 취합니다.
package main
import "fmt"
func main() {
fmt.Println(7 / 2) // 3
fmt.Println(9 / 3) // 3
fmt.Println(1 / 2) // 0
}
1/2 == 0 인 것은 정수 산술에서는 정상입니다. 그래서 카운터, 인덱스, “상자 몇 개가 꽉 찼는지”, 그리고 코페이카/센트 단위의 금액 계산에 매우 유용합니다.
음수 나눗셈
또 다른 포인트는 음수에서 무슨 일이 일어나는가입니다. Go에서 정수 나눗셈은 0 쪽으로 진행됩니다. 즉, -7/2 는 -3 이며, -4 가 아닙니다.
package main
import "fmt"
func main() {
fmt.Println(-7 / 2) // -3
fmt.Println(-1 / 2) // 0
}
왜 중요할까요? 나머지 % 는 나눗셈과 연결되어 있기 때문입니다. 그리고 마이너스가 들어간 로직(예: 잔액, 부채, 차이)을 다룰 때는, 언어가 실제로 어떻게 동작하는지 미리 알아두는 편이 좋습니다. “금요일 저녁에 내가 원한 방식”이 아니라 말이죠.
나눗셈의 나머지
% 연산자는 나눗셈의 나머지입니다. 정수형에서만 동작하며, 숫자를 초와 분, 달러와 센트, 페이지와 항목처럼 부분으로 나누는 데 아주 유용합니다.
삶의 사실처럼 기억해 두면 좋은 공식이 있습니다:
a 와 b 가 정수라면 다음이 성립합니다: a == (a/b)*b + (a%b)
예제로 확인해 봅시다:
package main
import "fmt"
func main() {
a := 17
b := 5
fmt.Println(a/b, a%b) // 3 2
fmt.Println((a/b)*b + (a%b)) // 17
}
실습: 유로와 센트
정수 나눗셈의 가장 “현실적인” 예시는 돈입니다. float64 로 돈을 다루면 10.0000000002 같은 “꼬리”가 생기기 쉽지만, 정수 코페이카/센트로 다루면 훨씬 엄격합니다.
예를 들어, 우리에게 코페이카(또는 센트)로 된 금액이 있고, 이를 “유로와 센트” 형태로 출력하고 싶다고 해 봅시다.
package main
import "fmt"
func main() {
totalCents := 12345
euros := totalCents / 100
cents := totalCents % 100
fmt.Printf("%d 유로 %02d 센트\n", euros, cents) // 123 유로 45 센트
}
%02d 에 주목하세요. 이것은 단지 출력 형식일 뿐이며, 센트가 항상 두 자리로 보이게 합니다(예: 5 가 아니라 05). 지금은 포맷팅을 별도 주제로 깊게 들어가진 않지만, 이 작은 트릭만으로도 출력이 훨씬 “사람답게” 보입니다.
퍼센트와 연산 순서
퍼센트는 버그의 고전입니다. 이유는 간단합니다. 퍼센트는 거의 항상 “쉬운 공식”처럼 보이지만, 정수에서는 갑자기 나눗셈과 연산 순서에 부딪히기 때문입니다.
예를 들어 보겠습니다. 가격 price = 199 유로, 할인 10% . 유로 단위 할인 금액은 어떻게 계산할까요?
가장 단순한 시도는 보통 이렇게 보입니다: discount := price * 10 / 100. 이것은 꽤 괜찮습니다. 곱셈이 나눗셈보다 먼저 일어나기 때문에 맞는 방향입니다.
하지만 오류는 이렇게 쓸 때 생깁니다: discount := price * (10 / 100). 여기서 (10/100) 은 정수 나눗셈이므로 0 이 됩니다. 그래서 할인은 price*0, 즉 0 이 됩니다.
나란히 보겠습니다:
package main
import "fmt"
func main() {
price := 199
d1 := price * 10 / 100
d2 := price * (10 / 100)
fmt.Println(d1) // 19
fmt.Println(d2) // 0
}
이것은 괄호가 강력한 도구이지만, 나눗셈을 이해하지 못하면 오히려 “더 나쁘게” 만들 수도 있다는 좋은 예입니다.
3. 실습: 주문, 할인, 금액 분해
주문 계산 방식
작은 애플리케이션을 계속 이어가 봅시다. 이름은 가상의 “카페”라고 하겠습니다. 주문 금액, 할인, 최종 금액을 계산합니다. 부동소수점과 싸우지 않도록 모든 값을 코페이카(또는 센트) 단위의 int64 로 저장할 것입니다.
아래는 계산 흐름도입니다. 한 줄에 200자씩 몰아넣지 않도록 도와줍니다.
flowchart TD
A[상품 1개 가격, kopecks] --> C[소계 계산]
B[수량] --> C
C --> D[할인, kopecks]
D --> E[최종 금액, kopecks]
E --> F[달러와 센트]
흐름은 간단합니다. 먼저 소계, 그다음 할인, 그다음 최종 금액, 마지막으로 출력용으로 달러와 센트로 “분해”합니다.
코드: 소계, 할인, 최종 금액
작지만 연결된 예제를 만들어 봅시다. 가격(코페이카 단위), 수량, 할인율을 입력받아, 한 줄에 다 몰지 않고 차근차근 계산합니다.
package main
import (
"fmt"
)
func main() {
var priceK int64
var qty int64
var discountPercent int64
fmt.Scan(&priceK, &qty, &discountPercent)
subtotal := priceK * qty
discount := subtotal * discountPercent / 100
total := subtotal - discount
fmt.Println(total)
}
가장 중요한 줄은 discount := subtotal * discountPercent / 100 입니다. 우선순위 덕분에 이것은 (subtotal * discountPercent) / 100 으로 계산됩니다. 즉, “먼저 곱하고, 그다음 나눈다”는 뜻입니다. 보통 정수 퍼센트 계산에서 원하는 방식입니다.
왜 나눗셈보다 곱셈을 먼저 하는 게 좋은가
이건 단지 “우선순위”만의 문제가 아닙니다. 정수 나눗셈은 소수 부분을 버리므로 정확도가 떨어집니다.
너무 일찍 나누면, 곱하기 전에 이미 정보를 잃을 수 있습니다. 손실은 생각보다 커질 수 있습니다.
코페이카 단위 할인 공식을 비교해 봅시다:
1) subtotal * percent / 100
2) subtotal / 100 * percent
두 식은 절삭 때문에 같지 않습니다.
package main
import "fmt"
func main() {
subtotal := int64(199) // 예제를 단순화한 센트
percent := int64(10)
a := subtotal * percent / 100
b := subtotal / 100 * percent
fmt.Println(a) // 19
fmt.Println(b) // 10
}
두 번째 방식에서는 subtotal/100 이 먼저 1 이 되었습니다. 왜냐하면 199/100 == 1 이기 때문입니다. 그런 다음에야 10 을 곱하게 됩니다.
할인에서 센트는 어디로 가는가
할인을 코페이카로 계산하면, 거의 항상 “수학적으로는” 소수가 나오지만 정수 코페이카에는 소수가 없습니다. 예를 들어 99코페이카의 15% 할인은 14.85코페이카입니다. 0.85는 어떻게 해야 할까요?
이 강의에서는 별도의 반올림 함수를 도입하지 않습니다(그건 별도 주제입니다). 하지만 기본 효과는 이해해야 합니다. 정수에서는 소수 부분이 그냥 사라집니다. 즉, 나눗셈에서는 항상 0 쪽으로 반올림 됩니다.
보겠습니다:
package main
import "fmt"
func main() {
subtotal := int64(99)
percent := int64(15)
discount := subtotal * percent / 100
fmt.Println(discount) // 14
}
이건 Go의 오류가 아닙니다. 단지 선택한 데이터 타입이 정수이기 때문입니다.
금액을 사람 수로 나누고 나머지 계산하기
아주 흔한 상황입니다. N명에게 공평하게 계산서를 나눠야 합니다. 정수 코페이카로 계산하면 “나머지”가 생기고, 그건 누군가가 추가로 내야 하거나, 아니면 카페가 그 센트를 세상에 기부하는 셈이 됩니다.
package main
import "fmt"
func main() {
totalC := 1000 // 10 유로 00 센트
people := 3
perPerson := totalC / people
remainder := totalC % people
fmt.Println(perPerson, remainder) // 333 1
}
여기서는 각 사람이 333센트를 내고, 1센트의 “꼬리”가 남았습니다.
한 줄이 코드를 더 똑똑하게 만들지는 않는다
우선순위를 익히면 이런 유혹이 생깁니다:
total := priceC*qty - priceC*qty*discountPercent/100
컴파일러는 이 식을 그대로 받아들입니다. 하지만 사람(일주일 뒤의 당신을 포함해서)은 천천히 슬퍼지기 시작합니다.
조금 길어도 명확한 편이 낫습니다. 초보자에게 특히 좋은 스타일은 이미 보여 드렸습니다:
package main
import "fmt"
func main() {
priceC := int64(15000)
qty := int64(2)
discountPercent := int64(10)
subtotal := priceC * qty
discount := subtotal * discountPercent / 100
total := subtotal - discount
fmt.Println(total) // 27000
}
숫자 리터럴과 7/2
가끔 혼란은 7 과 2 같은 리터럴이 “그냥 숫자”처럼 보인다는 점에서 더 커집니다. Go에서는 숫자 상수가 처음에는 “이상적인 숫자(타입 없음)”처럼 존재하지만, 일반 식에서 사용되면 기본 타입을 갖게 됩니다. 정수 상수의 기본 타입은 int 이고, 실수 상수의 기본 타입은 float64 입니다.
그래서 7/2 는 int 를 int 로 나누는 것이고, 즉 정수 나눗셈입니다. 반면 7.0/2.0 은 실수 나눗셈입니다.
타입 변환 없이 차이를 보겠습니다(그냥 리터럴만 다르게):
package main
import "fmt"
func main() {
fmt.Println(7 / 2) // 3
fmt.Println(7.0 / 2.0) // 3.5
}
4. 흔한 실수
실수 #1: int / int 에서 “소수”를 기대하기.
int 를 int 로 나누면 int 가 나오고, 소수 부분은 사라집니다. 가장 아쉬운 경우는 작은 수를 나누어 0 이 나온 뒤, 그 결과에 큰 수를 곱하고 “왜 전부 0이지?” 하고 놀라는 경우입니다. 이 문제는 마법이 아니라 이해로 해결합니다. 어떤 산술이 수행되는지, 즉 정수 산술인지 실수 산술인지 알아야 합니다.
실수 #2: 퍼센트의 의미를 바꾸는 괄호를 넣기.
subtotal * percent / 100 은 보통 올바릅니다. 하지만 subtotal * (percent / 100) 는 정수에서는 거의 항상 틀립니다. 왜냐하면 (percent/100) 가 종종 0 이 되기 때문입니다. 괄호는 단순한 장식이 아니라 계산 순서를 바꾸므로, 의도적으로 사용해야 합니다.
실수 #3: % 의 우선순위가 높다는 사실을 잊기.
초보자는 a + b%10 을 “먼저 더하고, 그다음 나머지”로 읽는 경우가 있습니다. 하지만 실제로는 b%10 의 나머지를 먼저 구하고, 그 결과를 a 에 더합니다. 다른 의도였다면 괄호가 필요합니다: (a + b) % 10.
실수 #4: int(x/100*percent) 같은 방식으로 정수에서 “반올림”하려 하기.
x 가 정수라면, x/100 에서 이미 소수 부분이 사라집니다. 그 뒤에 아무리 곱해도 사라진 소수는 돌아오지 않습니다. 보통은 먼저 곱하고 나중에 나누는 편이 더 맞습니다. 이것이 “항상”의 규칙은 아니지만, 정수 산술에서 퍼센트와 비율을 다룰 때 좋은 기본값입니다.
실수 #5: 계산을 한 줄로 몰아넣고 의미를 잃기.
기술적으로 Go는 긴 표현식을 허용하고, 우선순위도 머릿속에 붙잡아 둘 수 있습니다. 하지만 실제 코드에서 더 중요한 것은 표현식의 가독성입니다. 중간 변수들(subtotal, discount, total) 은 “불필요한 코드”가 아니라, 수식을 눈으로 검증할 수 있게 만드는 장치이며, 그만큼 더 신뢰할 수 있게 해 줍니다.
GO TO FULL VERSION