[Greenplum & PostgreSQL DB] 오류에 견고한 운영을 위한 PL/R 예외 처리 tryCatch(), error, warning, finally
Greenplum and PostgreSQL Database 2020. 4. 16. 18:15로컬 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; |
많은 도움이 되었기를 바랍니다.
이번 포스팅이 도움이 되었다면 아래의 '공감~'를 꾹 눌러주세요. :-)