본문 바로가기
Daily Life/Diary

실수할 여지를 주지않기

by devson 2023. 4. 1.

모든 것은 변한다.

서비스도 그렇고 그 서비스의 기반이 되는 소프트웨어도 마찬가지이다.

그러면서 우리는 크고작은 실수를 만들곤 한다.

 

예를 들어

  • DB 테이블에 대한 Model 클래스에는 새로운 필드를 추가하였지만, DB 테이블에는 컬럼을 추가하지 않거나
  • 개발 환경에는 환경 변수 설정을 추가했지만 운영 환경에서는 하지 않거나

 

개발 프로세스가 잘 정립되어있다면 실수를 어느정도 관리할 순 있지만

모든 것이 자동화 되어있지 않는한(심지어 자동화를 하였어도) 사람이 하는 일이다보니 늦든 빠르든 언젠가는 실수가 생긴다.

  • 급하게 일을 끝내려 하거나(이것만 끝내고 집에 가야지~)
  • 관련된 업무와 관련된 모르는 지식이 있거나 (아니 이런게 있었어요?) 

 

그렇지만 나의 멘탈과 안정적인 서비스를 위해서는 크든 사소하든 실수는 되도록 없는게 좋다.

그런 점에서 애초에 실수할 여지를 주지않는 환경을 만드는 것이 정말 중요하다고 생각한다.

 

조엘 온 소프트웨어로 유명한 조엘 스폴스키의 또다른 저서인 모어 조엘 온 소프트웨어에서는 그는 이런 얘기를 한다.

이제부터 프로그래머가 겪는 세 가지 단계를 알려드리겠습니다.

1. 뭐가 깨끗하고 뭐가 더러운지 구분하지 못합니다. 한마디로 똥인지 된장인지 구분 못하는 단계입니다.
2. 눈에 보이는 깨끗함을 이해합니다. 코딩 표준을 준수하는 단계입니다.
3. 얇은 껍질 밑에 감춰진 더러운 냄새를 맡습니다. 이 냄새를 실마리로 더러운 코드를 찾아 바로잡는 단계입니다.

하지만 좀 더 높은 단계도 있습니다. 정말로 여러분이 달성해야 할 단계입니다.

4. 더러운 냄새를 풍기는 코드가 나 좀 고쳐 달라고 저절로 모습을 드러내는 코딩 표준을 만드는 경지에 이른 단계입니다.

- 모어 조엘 온 소프트웨어 : 스물 셋. 틀린 코드를 틀리게 보이도록 만들기

 

뭔가 대단한 얘기를 하는 것 같지만 단순한 원칙으로도 실수의 여지를 없앨 수 있다.

관련해서 몇가지 사례와 경험을 공유한다.

 

if 문에 중괄호 사용하기

(Python과 같은 언어는 제외하고) if 문의 블록에서 코드가 한 줄이라면 중괄호를 생략하는 경우가 종종 있다.

if (someCondition)
    doThis()
 
if (anotherCondition) 
    doThat()
 
// ...

 

하지만 위와 같은 코드에 if 문의 블록에 실행되어야할 코드가 추가되어야하는 경우,

아래와 같이 코딩을 하게 된다면 제대로 동작하는 코드라고 볼 수 없다.

if (someCondition)
    doThis()
    doThis2() // <==
 
if (anotherCondition) 
    doThat()
 
// ...

 

애초에 if 문 블록에 중괄호를 사용했다면 위와 같은 실수가 발생할 여지가 없다.

if (someCondition) {
    doThis()
    doThis2()
}

if (anotherCondition) {
    doThat()
}

// ...

 

 

누가 코딩을 하면서 저런 실수를 하냐고 되물어볼 수 있지만, 실제로 Apple은 위와 같은 실수로 보안적인 위험을 노출시켰던 적이 있다.

(자세한 내용은 위 글의 원문인 Apple’s #gotofail SSL Security Bug was Easily Preventable을 참조하길 바란다)

 

애초에 중괄호가 있었다면 적어도 이런 실수는 나지 않을 수 있었다.

 

else 사용을 조심하기

if-else 는 개발을 하면서 가장 많이 사용하는 코드 중 하나다.

if-else 로 어떤 조건일 때는 ~~ 코드를 실행하고 그렇지 않은 경우 ~~ 코드를 실행하도록 로직을 배치할 수 있다.

 

그런데 else 는 조건문에 걸리지 않은 모든 경우를 커버하기 때문에,

가끔은 기존 조건문에 새로운 조건을 추가하지 않았음을 인지 못할 때가 있다.

 

아래의 코드는 쇼핑 등급(ShoppingGrade)을 보고 할인율을 정하는 코드이다.

(else 는 직접 사용하지 않아도 아래와 같은 코드도 사실 상 else 를 쓴다고 볼 수 있다.)

enum class ShoppingGrade {
    VIP,
    NORMAL,
}

fun calculateDiscountPercent(grade: ShoppingGrade): BigDecimal {
    if (grade == ShoppingGrade.VIP) {
        return BigDecimal.valueOf(5.5)
    }

    return BigDecimal.ZERO
}

 

서비스를 운영하면서 새로운 쇼핑 등급(ShoppingGrade)GOLD 등급이 추가되었다고 하자.

enum class ShoppingGrade {
    VIP,
    GOLD, // <==
    NORMAL,
}

 

새로운 등급이 추가되었지만 기존 함수에서는 이러한 변경에 대해 아무런 오류를 내지않는다.

컴파일 타임에서도 런타임에서도 말이다.

 

사실 일반적으로 위와 같은 코드를 작성하지만 이와 같은 경우,

개발자가 새로운 타입이 추가된 것에 대해 대응하지 않았음을 알아차리기가 힘들다.

즉, 쇼핑 등급이 GOLD 등급이 되었어도 아무런 할인을 받지 못할 수도 있다.

 

그래서 위와 같이 특정 상태를 기준으로 분기를 해야하는 로직을 작성해야한다면,

아래와 같이 모든 상태를 하나하나 확인하는 코드를 작성할 수 있다.

enum class ShoppingGrade {
    VIP,
    NORMAL,
}

fun calculateDiscountPercent(grade: ShoppingGrade): BigDecimal {
    if (grade == ShoppingGrade.VIP) {
        return BigDecimal.valueOf(5.5)
    }
    
    if (grade == ShoppingGrade.NORMAL) {
        return BigDecimal.ZERO
    }
    
    throw RuntimeException("Unsupported grade: $grade")
}

위와 같이 코딩을 하면 ShoppingGrade에 새로운 타입이 추가되면,

최소한 런타임에서는 새로운 타입 추가에 대해 대응을 하지 않은 것을 알아차릴 수 있다.

(테스트를 잘 작성했다면 PR을 올리기 전에도 알아차릴 수 있을 것이다)

 

Kotlin을 사용한다면 enum과 when(switch)을 조합하면 간결하면서도 컴파일 레벨에서 안전한 코드를 작성할 수 있다.

enum class ShoppingGrade {
    VIP,
    NORMAL,
}

fun calculateDiscountPercent(grade: ShoppingGrade): BigDecimal {
    return when(grade) {
        ShoppingGrade.VIP -> BigDecimal.valueOf(5.5)
        ShoppingGrade.VIP -> BigDecimal.ZERO
    }
}

위 코드에서 ShoppingGrade가 추가되면 바로 컴파일 오류가 난다.

 

이렇듯 else를 사용하지 않음으로써 특정 변경에 대해 내가 대응하지 않았음을 어느정도 인지할 수 있는 수단을 마련하였다.

(else 문이 나쁘다는 의미는 아니고 위와 같은 상태값에 따른 새로운 분기의 가능성이 있다면, 'else 문을 사용해도 괜찮을까?' 고민해보는 것이 좋다고 생각한다)

 

기본값 사용을 조심하기

개발을 하다보면 외부 서비스의 API Key와 같이 외부에 노출되면 안되는 값들을 환경 변수를 통해 받아오는 경우가 있다.

이때 로컬이나 테스트 환경에서는 따로 환경 변수 세팅을 하기 번거로워 아래와 같이 개발용 값을 기본값으로 사용할 때가 있다.

 

아래 코드는 PG사의 결제 API를 호출하는 코드이다.

API를 호출할 때 필요한 API Key를 환경변수를 통해 받아오고 없다면 개발용 API Key를 사용하도록 해놨다.

const parameter = {};
const paymentApiKey = process.env.PAYMENT_API_KEY || 'development_api_key'; // default api key

callPaymentPGApi(parameter, paymentApiKey);

 

하지만 위 코드에는 숨겨진 위험이 있는데, 위 코드가 운영 환경에 배포되었을 때 환경 변수를 설정하지 않더라도 코드가 정상 작동한다는 것이다.

운영 환경에 환경 변수를 설정하지 않았다는 사실을 인지하지 못한채 이 코드가 계속 돌아간다면,

고객이 실제로 서비스에 결제를 했어도 코드에서 개발용 API Key를 사용했기 때문에 고객이 결제한 금액은 곧 환불될 것이다.
(PG사 마다 개발용 API에 대한 정책이 다르겠지만 실제 운영 환경으로써 처리되어야할 것이 처리되지 않는다는 것이다)

 

 

위와 같은 실수를 막기위해서는 애초에 기본값을 사용하지 않는게 안전하다고 생각한다.

아래와 같이 기본값을 제거하고 간단한 assertion을 추가하면 특정 환경에서 환경 변수 설정을 안했는지 런타임에는 파악이 가능하고

최소한 사전에 인지를 하지 못해 생기는 피해는 피할 수 있다.

const parameter = {};
const paymentApiKey = process.env.PAYMENT_API_KEY;
assert(!!paymentApiKey); // <==

callPaymentPGApi(parameter, paymentApiKey);

 

 

이렇듯 운영 환경과 개발 환경에서의 값이 다르게 관리되지 않게되면 서비스 운영 상에 치명적인 여파를 주는 경우엔,

기본값을 사용하는 것을 지양하는 것이 좋다고 생각한다.

 


 

실수할 여지를 주지않기라는 주제에 대해 몇가지 사례와 예제를 얘기해보았다.

 

버그는 개발자의 빠른 퇴근과도 관계가 있지만 더 넓게 봤을 때는 서비스의 신뢰와도 관계가 있다.

개발자로서 좋은 아키텍처 깔끔한 코드도 좋지만 실수할 여지를 주지않는 환경을 만드는 것에도 힘을 쓰면

그만큼 고객이 갖는 서비스에 대한 신뢰 또한 낮아지지 않을 것이며 또한 개발자에게는 유지보수 시 안정감으로 돌아올 것이다.

댓글