2024. 5. 13. 23:47ㆍ컴퓨터 공학
* 몇 개월 전, 회사에서 문제 해결을 위해서 리팩터링이 있었고, 그 과정에서 에러가 발생했었는데, 그 경험을 공유하고자 이 글을 작성합니다.
저에게 하루 약 120만 건 이상의 요청이 들어오는 서버에 관한 태스크가 주어졌습니다.
서버에 새로운 파이프라인을 추가했고, 이에 대해서 개발환경에서 모든 테스트를 마치고, 실제 운영환경에 배포했습니다.
무사 배포를 기원했지만, 배포하자마자 서버는 죽었다 살아나기를 반복했고, 이 때문에 바로 서버를 롤백했습니다.
저는 매우 의아했습니다. 에러가 발생한 지점의 코드를 저는 고친 것이 없었고, 해당 지점에서 원래 에러가 발생하고 있지 않았기 때문이죠.
그렇게 한 시간 동안 원인을 찾기 위해 로그를 열심히 찾았고, 결국 문제의 원인을 찾아냅니다.
문제 발생의 원인
로그를 보던 중, 이런 로그를 보게 됩니다.
{0xc00000c030, 0x0}
중괄호로 된 것은 struct 같은데,
왜 하나는 주소값이 있고, 하나는 주소값이 없는 것인가...
마침 에러가 발생한 곳은, 모두 인터페이스 nil check를 거친 곳이어서 더 수상했습니다.
그러던 중, 특이한 사실을 알아냈습니다.
바로 인터페이스의 nil이 진짜 nil이 아닐 수도 있다는 사실을요.
package main
import (
"fmt"
)
type A struct {
ID string
}
func makeInt() interface{} {
var a *A
return a
}
func main() {
var a = makeInt()
if a == nil {
fmt.Println("a is nil")
} else {
fmt.Println("a is not nil")
}
}
여기서 어떤 것이 출력될까요?
당연하게 저는 `a is nil`이 출력될 것으로 예상했지만
결과는 `a is not nil`이 출력되었습니다.
왜 그럴까요?
이건 위의 로그에서도 알 수 있는데요,
interface는 내부적으로 type과 value를 가지고 있습니다.
그리고 value만 nil이라면 nil check에 실패하게 되는 것이죠.
에러가 난 코드를 간단하게 재현해 보자면,
package main
import (
"fmt"
)
type A struct {
ID string
}
func makeInt() interface{} {
var a *A
return a
}
func main() {
var a = makeInt()
var ok bool
var b *A
if b, ok = a.(*A); ok {
fmt.Println("ok")
return
}
fmt.Println(b.ID)
}
이렇게 type 변환을 시도했고, nil이라면 실패했을 type 변환에 성공한 것입니다.
이렇게 interface가 nillable 하게 되면, 이런 문제를 야기할 수 있습니다.
그렇기 때문에 반드시 inteface는 nil이 되어서는 안 됩니다.
라이브러리 선택과 제거의 중요성
저는 서버를 수정하는 과정에서 chebyrash/promise라는 라이브러리를 사용하고 있는 코드를 보고,
일반 go 루틴을 사용하는 것과 차이가 없다고 느껴 이를 제거했습니다.
그리고 이것이 문제를 드러나게 했습니다.
promise의 내부 코드를 살펴보면 다음과 같은 코드가 있습니다.
func (p *Promise[T]) handlePanic() {
err := recover()
if err == nil {
return
}
switch v := err.(type) {
case error:
p.reject(v)
default:
p.reject(fmt.Errorf("%+v", v))
}
}
promise 실행 중, 에러가 나면 recover()를 통해서 panic을 잡아주는 로직이 존재했던 것입니다.
더구나 아무런 로그를 남기지 않으니 알 도리가 없었죠.
이를 통해 문제가 발생한 이유를 알아냈습니다.
- 원래 코드에 문제가 있어서 panic을 발생하고 있었으며,
- promise의 recover 때문에 에러가 발생하지 않고 있었고,
- 로그를 남기지 않았기 때문에 문제가 발생하는 줄도 몰랐던 것입니다.
라이브러리를 제거할 때, 내부에 있는 구현부까지 확인했어야 했는데,
제 불찰로 인해 문제가 발생했습니다.
교훈
이 문제를 통해서 얻은 교훈은 다음과 같습니다.
- 언어의 특성을 잘 이해하고 코드를 작성해야한다.
- nillable한 인터페이스를 만들지 말자.
- 로그를 잘 찍자.
'컴퓨터 공학' 카테고리의 다른 글
(디자인 패턴 복습 시리즈) 어댑터 패턴(Adapter pattern) (0) | 2023.02.04 |
---|---|
(Python) FastAPI MVC(1) (0) | 2023.01.20 |
웹 서버 vs 웹 애플리케이션 서버(Web Server vs WAS) (0) | 2023.01.02 |