로컬 PC 환경에서 공부, 토이 프로젝트 아니면 프로토타입핑을 위해서 작은 데이터셋을 가지고 R로 분석을 진행하는 경우라면 R 코드 상의 오류, 버그 등에 큰 관심을 가지지 않습니다. 왜냐하면 에러나 경고메시지가 났을 때 큰 비용을 들이지 않고 로그를 보고 수정을 하면 되기 때문입니다. 


하지만 R 코드를 활용해서 애플리케이션(application production)을 만들고, 이를 다수의 사용자를 대상으로 시스템 상에서 운영(operation) 을 하는 경우라면 얘기가 달라집니다. R 의 에러, 버그는 서비스를 하는 시스템 전체의 장애를 야기할 수도 있기 때문입니다. 


그래서 R을 위한 R코드가 아니라 서비스를 통한 ROI 창출을 위한 production, operation 이 최종 목표라면 장애, 오류, 예외에 견고한 R 코드를 짜는 것이 꼭 필요합니다. 



[ 오류에 견고한 R 코드를 위해 tryCatch() 를 사용한 예외 처리 ]



이번 포스팅에서는 Greenplum, PostgreSQL에서 PL/R (Procedural Language R) 함수 코드를 짤 때, 오류에 견고한 운영을 위해 tryCatch() 를 이용한 PL/R 예외 처리 방법을 소개하겠습니다. 


먼저 R tryCatch() 함수의 syntax를 살펴보겠습니다. tryCatch() 함수는 안에 expr, error (optional), warning (optional), finally (optional) 의 4개 인자를 원소로 가지는 구조입니다. 


expr 에는 실행할 코드를 써주는데요, required 사항이므로 꼭 써줘야 합니다. 이때 if, else if, else 등의 조건절을 추가해서 분기절을 사용하여 좀더 복잡한 코드를 수행할 수도 있습니다. 


error 에는 위의 expr 의 코드를 평가하는 중에 error가 발생할 경우에 수행할 코드를 써주며, optional 한 부분입니다. 


warning 에는 위의 expr 의 코드를 평가하는 중에 warning 이 발생할 경우에 수행할 코드를 써주며, optional 한 부분입니다. 


finally 에는 위의 expr, error, warning에 상관없이 tryCatch call을 종료하기 전에 항상 수행할 코드를 써줍니다. (가령, R의 temp 객체를 제거한다든지, DB connect 을 close 한다던지, R 코드 수행이 종료되는 날짜/시간을 로그로 남기고 싶다든지...) 


(* Python의 try, except, else, finally 절을 이용한 예외 처리와 비슷합니다. 

  참고 ==> https://rfriend.tistory.com/467 )


[ R tryCatch() syntax ]



tryCatch(

    expr = {

        # Your code here...

        # ...

    },

    error = function(e)

        # (Optional)

        # Do this if an error is caught...

    },

    warning = function(w){

        # (Optional)

        # Do this if an warning is caught...

    },

    finally = {

        # (Optional)

        # Do this at the end before quitting the tryCatch structure...

    }

)


* reference: https://rsangole.netlify.app/post/try-catch/



간단한 예를 들기 위해 두개의 숫자로 나누기를 했을 때 

  (1) 정상적으로 수행되는 경우

  (2) 분모에 '0' 이 있어 별도 메시지를 반환하는 경우

  (3) 분모에 '문자열'이 들어가서 error 가 발생한 경우

의 3가지 유형별로 R tryCatch() 함수를 사용하여 예외처리를 할 수 있도록 PL/R 코드를 짜는 방법을 소개하겠습니다.  (물론 SQL로 두 칼럼을 사용해 나눗셈('/')을 할 수 있습니다. 이 PL/R 코드는 tryCatch 를 소개하기 위한 예제일 뿐입니다)


먼저, 정상적으로 수행되는 경우에 사용할 예제 테이블을 만들어보겠습니다. 



-------------------------------------------------------------------

-- PL/R on Greenplum, PostgreSQL DB

-- : robust PL/R codes using tryCatch(), handling error or warnings

-------------------------------------------------------------------


drop table if exists tbl_sample;

create table tbl_sample (

grp varchar(6) not null

, id integer

, x1 integer

, x2 integer

);


insert into tbl_sample values 

('a', 1, 5, 10)

, ('a', 2, 10, 10)

, ('a', 3, 15, 10)

, ('b', 4, 20, 10)

, ('b', 5, 30, 10)

, ('b', 6, 40, 10);


select * from tbl_sample order by id;


 




  (1) 나눗셈을 하는 PL/R 코드 : 정상 수행되는 경우


원래 두 개의 정수를 input으로 받고 두 정수의 나눗셈 결과를 float8 로 반환하는 것이 맞는데요, 이번 예제에서는 warning message와 error 를 반환하는 PL/R 코드를 어거지로 만들다 보니 returns setof text 로 해서 텍스트를 반환하라고 PL/R 코드를 짰습니다. (혹시 왜 float8 이 아니라 text로 반환받는지 궁금해하시는 분이 계실까봐....) 


아래에 PL/R 코드의 $$ pure R codes block $$ 안에 tryCatch() 함수에 

 - expr : if, else 조건절을 사용하여 분모가 '0' 이면 "Denominator should not be Zero" 경고 메시지를 텍스트로 반환,  분모가 '0'이 아니면 나눗셈 결과를 텍스트로 반환

 - error : expr 코드 평가 중에 error 있으면 error 발생 시점을 메시지로 프린트하고, DB에 에러 메시지 텍스트로 반환

 - warning : expr 코드 평가 중에 warning 있으면 warning 발생 시점을 메시지로 프린트하고, DB에 에러 메시지 텍스트로 반환

 - finally : expr, error, warning 에 상관없이 tryCatch() call 을 종료하기 전에 마지막으로 "All done, quitting." 메시지 프린트

하도록 짠 코드입니다. 


위에서 작성한 public.tbl_sample 테이블에서 정수형인 x1과 x2 칼럼을 가져다가 array_agg() 해서 plr_divide() PL/R 함수를 실행했으므로 아무런 error나 warning 없이 정상 작동하였습니다. 



-- (case 1) PL/R works well without error or warning

-- define PL/R UDF

drop function if exists plr_divide(int[], int[]);

create or replace function plr_divide(

x1 int[]

, x2 int[]

) 

returns setof text  -- float8

as

$$

divide_tryCatch <- function(x_numerator, x_denominator){

  tryCatch(

    expr = {

      if (x_denominator == 0) {

        message("Denominator should not be Zero")

        return("Denominator should not be Zero")

        } else {

          result <- x_numerator / x_denominator

          return(result)

        }

      }, 

    error = function(e) {

      message("** Error at ", Sys.time(), " **")

      print(e)

      return(e[1])

      }, 

    warning = function(w){

      message("** Warning at ", Sys.time(), " **")

          print(w)

          return(w[1])

          }, 

    finally = {

      message("All done, quitting.")

      }

    )

}

result <- divide_tryCatch(x1, x2)

return(result)

$$ language 'plr';



-- execute PL/R

select 

grp

, unnest(x1_arr) as x1

, unnest(x2_arr) as x2

, plr_divide(x1_arr, x2_arr) as divided 

from (

select 

grp

, array_agg(x1::int) as x1_arr

, array_agg(x2::int) as x2_arr

from tbl_sample

group by grp

) a;






  (2) 나눗셈을 하는 PL/R 코드 : 분모에 '0' 이 들어있어 별도 메시지를 반환하는 경우


다음으로 분모에 '0'이 들어간 경우에 위의 (1)번에서 정의한 plr_divide() PL/R 사용자 정의 함수를 실행시켰을 때 if else 조건절의 '분모가 '0'인 경우 "Denominator should not be Zero" 텍스트 메시지를 DB에 반환하라고 한 사례입니다. 



-- (case 2) PL/R returns a pre-defined message: Non-Zero error

-- execute PL/R UDF

select 

unnest(x1_arr) as x1

, unnest(x2_arr) as x2

, plr_divide(x1_arr, x2_arr) as divided 

from (

select 

array[1] as x1_arr

, array[0] as x2_arr -- '0' in denominator

) a;






  (3) 나눗셈을 하는 PL/R 코드 : 분모에 '문자열'이 들어가서 error 가 발생한 경우


아래 코드는 강제로 error를 발생시키기 위해서 억지로 분모(denominator)에 텍스트 array를 받아서 R로 나눗셈 시 "non-numeric argument to binary operator" 에러 메시지를 반환하도록 한 PL/R 코드입니다. 위의 (1)번에서 짰던 정상적인 경우와는 달리 plr_divide2() PL/R UDF의 'x2' 가 text[] 인 점이 다릅니다. 


error = function(e) {

   # 실행할 코드

    return (e[1])

 } 


에서 에러 객체 'e' 가 리스트 형태이므로 return (e[1]) 처럼 리스트의 [1] 번째 객체를 반환하라고 명시적으로 인덱싱 해올 수 있게끔 [1] 을 e 뒤에 꼭 붙여줘야 합니다. (PL/R 결과 반환 시 text 를 DB에 반환하라고 정의했으므로 return(e[1]) 이 아니라 return(e) 라고 하면 PL/R 실행 시 SQL 에러 발생합니다. 꼼꼼히 안보면 실수하기 쉽습니다.)



-- (case 3) PL/R raises an error and tryCatch runs 'error' part

-- define PL/R UDF

drop function if exists plr_divide2(int[], text[]);

create or replace function plr_divide2(

x1 int[]

, x2 text[]

) 

returns setof text

as

$$

divide_tryCatch <- function(x_numerator, x_denominator){

  tryCatch(

    expr = {

      if (x_denominator == 0) {

        message("Denominator should not be Zero")

        return("Denominator should not be Zero")

        } else {

          result <- x_numerator / x_denominator

          return(result)

          }

      }, 

    error = function(e) {

      message("** Error at ", Sys.time(), " **")

      #print(e)

      return(e[1])

      }

}

result <- divide_tryCatch(x1, x2)

return(result)

$$ language 'plr';



-- execute PL/R : tryCatch() runs 'error' part

select 

unnest(x1_arr) as x1

, unnest(x2_arr) as x2

, plr_divide2(x1_arr, x2_arr) as divided 

from (

select 

array[1] as x1_arr

, array['ggg'] as x2_arr -- it raises an error

) a;





많은 도움이 되었기를 바랍니다. 

이번 포스팅이 도움이 되었다면 아래의 '공감~'를 꾹 눌러주세요. :-)



반응형
Posted by Rfriend

댓글을 달아 주세요

  1. flydodo 2020.04.16 21:32  댓글주소  수정/삭제  댓글쓰기

    500번째 포스팅~! 대단해요~♥

  2. 매드립 2020.04.17 16:53  댓글주소  수정/삭제  댓글쓰기

    이사님, 500포스팅 축하 드립니다.
    항상 잘 보고 있습니다!