일반적인 예외 상황 대응

김장훈·2023년 2월 4일
0

아래 서술되는 case 들은 monad 를 사용하기 위한 case를 가정한것입니다.

예외 상황 대응

  • 함수는 일반적으로 return 이 존재하며 우리가 원하는 return 이 나오지 않는 경우를 예외 case라 정의 한다.
  • 그리고 이러한 예외 case 일 경우 일반적으로 아래 2가지 case 로 구분할 수 있다.
  1. return null
  2. throw exepction

return null

  • 예외 상황 발생시 null 을 return 한다. code 아래와 같을 것이다.
const someFunc = () =>{
    try{
      ...
    }catch(error){
      ...
      return null
    } // 또는 if 로 문제상황 확인 후 return null
    return ...
}
const nullableRes = someFunc()
  • 위 처럼 정의된 예외 상황 발생시 null 을 return 하게 될 경우 2개의 문제점이 존재한다.
  1. 해당 function 의 return 값을 사용하는 곳에선 null type 이 용납 안되는 비즈니스 로직인 경우 추가 에러 발생 가능하다.
  • 이는 곧 null type check 가 강제되어야한다는 뜻이며 실수로 check 를 안할 수도 있기에 휴먼 에러의 문제점이 될 수 있다(물론 typescript 를 활용하면 이를 방지할 순 있다 😃)
const otherFunc = (value) => {
  if(value == null){
    ... do 
  }
  do ...
}
  
const nullableRes = someFunc()
const res = otherFunc(nullableRes)
  1. 실제 비즈니스 로직의 결과에 따른 null 값인지 아닌지 확인 불가능하다.
    • 해당 값의 결과가 에러 때문인지 정상 case 에 따른 null 인지 확인 불가
    • 이에 따라서 위 function을 사용하는 상위 레벨에선 예외 상황에 따른 추가대응 등을 하기 어렵다.

Invention of the null-reference a billion dollar mistake (Tony Hoare)

  • 다만 LBYL(Look Before You Leap) 원칙에 따르면 위와 같은 code 가 나쁘다고 할 수 없다.

throw execption

  • 문제 상황 발생시 이를 throw 하여 상위 또는 사용처에서 이를 처리하게 하는 방법이다(EAFP: Easier to Ask Forgiveness Than Permission 스타일)
const someFunc = () =>{
 	try{
      ...
    }catch(error){
      ...
      throw new Error('some error occur, reason ...')
    }
    return ...
}
...

const otherFunction = () => {
  try{
   	const res = someFunc()
  }catch(error){
    console.log(error.message)
    ... doSomthing()
  }
  ...
} 
  • 제일 일반적인 code block 이라 생각되지만 문제점을 보자면 다음과 같다.
  1. 사용처에서 예외 상황 처리가 강제된다. 만약 처리하지 못할 경우 application 을 shut down 할 수도 있다.
    • 그렇기에 trt/catch 가 모든 function 마다 들어가는, 보일러 플레이트가 발생하게 된다.
    • 물론 try / catch 보일러 플레이트 그 자체가 문제라곤 할 수 없다.
  • 별 문제가 없어 보이니 상황을 가정해보자 😃

error 를 throw 안하다가 해야하게 되는 경우

const theOtherFunc = ()={
  return ''
}

const otherFunction = () => {
  try{
   	const res = someFunc()
  }catch(error){
    console.log(error.message)
    ... doSomthing()
  }
  
  theOtherFunc()
  ...
} 
  • theOtherFunc 에서는 어떠한 에러 처리도 하지 않고 있다. 만약 해당 function 에서도 에러 처리가 필요로 해진다면(throw 한다면) 어떻게 해야할까? 당연히 try/catch block 으로 안으로 옮기면 될것이다.
const otherFunction = () => {
  try{
   	const res = someFunc()
    theOtherFunc()
  }catch(error){
    console.log(error.message)
    ... doSomthing()
  }
  ...
} 
  • 그런데 이를 사용하는 곳이 많다면? 그거대로 중노동이 될 것이다.
  • 물론 할 수는 있다. 요새는 ide 가 레퍼런스를 잘 찾아주니까 ... 😃

그냥 처음부터 다 try/catch 에 넣으면 되지 않음?

  • 충분히 가능하다. 아예 throw 여부와 상관없이 try/catch block 으로 감쌀 수 있다.
  • 그렇다면 또 상황을 상황을 가정해보자 😃
const main = (userId) => {
  try{
   	const user = getUser(userId)
    const parsedUser = parseUser(user)
    const transformUser = transformUser(parseUser)
  }catch(error){
    console.log(error.message)
  }
} 
  • 위 code 를 있는 그대로 이해한다면 3개 function 는 모두 error 를 던질 수 있다(어떤 종류인지는 내부로 들어가지 않는 이상 모른다)
  • 즉 A > B > C 와 같은 pipe line 에서 실패 > ...성공 > 성공 > 실패 나 모두 같은 실패 case 이다. 만약 parseUser 가 실패했을 경우 alarm 을 보내고 그 이후 process 를 진행하고 싶다면 어떻게 작성이 되어야할까?
const main = (userId) => {
  try{
   	const user = getUser(userId)
    const parsedUser = parseUser(user)
    const transformUser = transformUser(parseUser)
  }catch(error){
    console.log(error.message)
    if(error.type == 'parseError'){
      sendAlarm()
      const user = getUser(userId)
      const transformUser = transformUser(user)
    }
    ...
  }
} 

와 같은 식으로 하던가 아니면 아예 code-block 을 분리하는 방법도 가능할 것이다.

const main = (userId) => {
  let user;
  try{
   	user = getUser(userId)
  }catch(error){
	...
  }
  
  try{
    user = parseUser(user)
  }catch(error){
    sendAlarm()
    ...
  }
  
  try{
    const transformUser = transformUser(user)
  }catch(error){
    ...
  }
} 
  • 물론 parseUser 가 예외 상황시 throw error 를 하지 않게 하고 alarm 을 보내는 로직을 안에다 넣는 방법을 생각할 수도 있다.
const parseUser = (user){ // 더이상 error 가 throw 되지 않는다.
  try{
   	...
  }catch(error){
    sendAlarm()
    return user
  }
}
  • 그런데 또 다른 사용처에선 parseUser 의 error 상황이 필요로 하다던지, 아니면 특정 상황에선 alarm 을 보내기 싫다던지 등의 여러 상황이 나올 수 있으며 이렇게 될 경우엔 아래처럼 code 가 바뀔 것이다.
const parseUser = (user, isNeedAlarm){
   try{
   	...
  }catch(error){
    if(isNeedAlarm){
      sendAlarm()
    }
    return user
  } 
}

  
const someOther = (userId)=>{
  const user = getUser(userId)
  const parsedUser = parseUser(user)
  
  if(user.someAttr == parsedUser.someAttr){ // error 발생에 따른 같은 user 인지 확인 ... 
   	console.log('error occur')
  }
}
  • throw 를 안하게 되니 문제 상황을 추정할 수 없다. 그렇기에 저런 상황까지 간다고 하면 return 을 tuple 이나 record 형태로 하여 실패 여부를 표시하게 될 것이다.
const parseUser = (user, isNeedAlarm){
   try{
   	...
    return {user:parsedUser, isSuccess:true}
  }catch(error){
    if(isNeedAlarm){
      sendAlarm()
    }
    return {user:user, isSuccess:false}
  } 
}
  • 뭔가 많이 이상해지고 복잡해진 code 가 되버렸다. 또한 사용처에선 무조건 실패여부를 확인하게 되었다(1번 case 와 같이)

문제는 무엇일까

  • 다시 원래의 code 로 가보자
const main = (userId) => {
  try{
   	const user = getUser(userId)
    const parsedUser = parseUser(user)
    const transformUser = transformUser(parseUser)
  }catch(error){
    console.log(error.message)
  }
} 
  • A > B > C 의 pipe line 에서 각 함수는 정상 또는 예외 상태를 return 한다.
  • 우리는 이를 때에 따라서 적절히 조작하고 싶다.
  • 모나드를 활용하는 code 는 다음장에서 확인해보자.
profile
읽기 좋은 code란 무엇인가 고민하는 백엔드 개발자 입니다.

0개의 댓글