조건 처리 - 오류·경고를 설계하기
이 장에서 배우는 것
앞 장에서 객체의 형식과 상태를 표현하는 방법을 살펴보았다. 판매·재고 데이터를 객체에 담더라도 입력값이 잘못되거나 파일을 읽지 못하는 상황은 남는다. 이때 함수가 어떤 문제를 알리고, 호출한 코드가 어디까지 복구할지를 정해야 한다. 모든 문제를 오류로 만들면 분석이 자주 끊기고, 모든 문제를 경고로 넘기면 잘못된 결과가 보고서에 섞인다.
R의 조건(condition)은 실행 중 생긴 상황을 전달하는 객체다. 오류·경고·안내 메시지를 조건으로 전달하면 계산과 대응 정책을 분리할 수 있다. 데이터를 검사하는 함수는 문제의 종류와 관련 정보를 알리고, 실행을 관리하는 코드는 중단·보정·기록 중 하나를 선택한다. 이 장에서는 여러 지점의 자료를 처리하면서 한 지점의 실패를 기록하고 나머지 지점의 계산을 이어 가는 프로그램을 만든다.
stop(),warning(),message()를 상황의 심각성과 계산 가능성에 맞게 선택한다.tryCatch()와withCallingHandlers()의 처리 위치와 복귀 방식을 구분한다.- 사용자 조건 클래스에 지점명과 문제 행을 담아 호출자가 판단할 수 있게 한다.
finally와on.exit()로 실행 구간과 함수의 정리 책임을 나눈다.- 의도한 경고를 처리하고, 성공·실패 경로를 직접 검사하는 실행 파일을 완성한다.
문제 상황
동네 편의점 체인에서 매일 지점별 판매 파일을 모은다. A 지점에는 매출이 비어 있는 행이 하나 있고, B 지점은 정상이다. C 지점에는 재고가 음수인 행이 있다. 운영팀은 매출 누락을 이번 집계에서 0으로 보정하되 기록을 남기기로 했다. 음수 재고는 입력 오류인지 반품 처리인지 확인해야 하므로 해당 지점의 집계를 제외하기로 했다.
이 정책은 데이터 자체에서 자동으로 결정되지 않는다. 매출 누락을 0으로 바꾸는 것은 이 프로그램에서 정한 업무 규칙이다. 실제 매출이 없었다는 사실과 값이 누락되었다는 사실은 다르다. 따라서 보정 사실을 경고로 알리고, 경고를 처리하는 쪽에서 화면에 기록한다. 재고가 음수이면 집계를 계속하지 않고 오류를 전달한다.
실행은 C 지점에서 끝나면 안 된다. A와 B의 결과를 반환하고, C의 실패 정보를 따로 보관해야 한다. CSV를 읽는 데 사용한 연결과 임시 파일도 정리해야 한다. 성공한 계산만 검사하는 것으로는 부족하다. 제외된 지점, 경고가 난 행, 정리 코드가 실행되는 위치까지 함께 설계해야 한다.
오류·경고·메시지는 실행 계약이다
stop()은 현재 계산을 계속할 수 없다는 뜻을 전달한다. 처리기가 없으면 오류가 바깥으로 전달되고 실행이 중단된다. warning()은 주의가 필요하지만 현재 계산을 이어 갈 수 있다는 뜻이다. message()는 진행 상황이나 선택된 동작을 알린다. 오류가 아닌 상황이라는 이유만으로 모두 메시지로 만들지는 않는다. 결과 해석에 영향을 주는 보정은 경고로 구분하는 편이 낫다.
| 함수 | 이 예제의 상황 | 기본 동작 | 호출자의 대응 |
|---|---|---|---|
stop() | 음수 재고 발견 | 오류를 전달하며 계산 중단 | 해당 지점 제외 |
warning() | 누락 매출을 0으로 보정 | 경고를 알리고 계산 계속 | 보정 사실 기록 |
message() | 지점 검사 시작 | 안내를 출력하고 계산 계속 | 진행 상황 기록 |
짧은 함수에서는 stop("재고가 음수다")처럼 문자열로 충분할 수 있다. 여러 지점을 묶어 처리할 때는 문자열만으로 부족하다. 어느 지점인지, 어느 행인지, 어떤 규칙을 어겼는지를 호출자가 다시 찾아야 하기 때문이다. 오류 문구를 잘라 정보를 복원하는 방식은 문구를 고칠 때 쉽게 깨진다.
사용자 조건은 메시지와 부가 정보를 가진 객체로 만든다. 이 장에서는 리스트에 message, call, store, rows를 넣고 클래스 벡터를 붙인다. 오류에는 error와 condition, 경고에는 warning과 condition을 포함한다. 앞쪽의 사용자 클래스는 구체적인 문제를 나타내고, 뒤쪽 클래스는 일반적인 처리기에 연결한다.
conditionMessage()는 조건에서 사람이 읽을 설명을 꺼낸다. 프로그램의 분기는 inherits()나 클래스별 처리기로 수행하고, 메시지는 기록에 사용한다. 이 예제는 call = NULL로 조건을 만든다. 화면 기록을 간결하게 하기 위한 선택이다. 재사용할 도구라면 호출 정보를 보관하는 정책도 따로 정할 수 있다.
조건에 붙인 rows는 검사 함수에 전달된 지점 자료 안의 행 번호다. 전체 CSV의 행 번호가 아니다. 이런 기준을 명시하지 않으면 오류 기록을 보고 엉뚱한 원본 행을 수정할 수 있다. 원본 위치가 필요하면 분할 전에 원본 행 번호를 별도 열로 저장하면 된다.
처리기의 위치가 복귀 지점을 바꾼다
처리기(handler)는 조건을 받았을 때 실행할 함수다. tryCatch()의 오류 처리기는 문제가 생긴 계산 구간에서 빠져나온 뒤 실행된다. 처리기의 반환값이 tryCatch() 전체의 값이 된다. 재고 오류를 잡아 실패 목록을 반환하면, 오류가 난 집계 함수의 다음 줄로 돌아가는 것이 아니라 지점 처리 함수가 실패 결과를 반환하게 된다.
withCallingHandlers()의 처리기는 조건이 발생한 문맥에서 실행된다. 경고를 기록하고 원래 계산을 이어 가려는 경우에 적합하다. 다만 처리기에서 값을 반환했다는 이유만으로 기본 경고 출력이 사라지지는 않는다. warning()이 제공하는 muffleWarning 재시작(restart)을 호출해야 기본 출력을 억제한다. 메시지에는 muffleMessage를 사용한다.
여기서 억제한다는 말은 문제를 없던 일로 만든다는 뜻이 아니다. 이 프로그램은 경고를 자체 형식으로 출력한 다음 기본 출력을 억제한다. 따라서 같은 경고가 두 번 보이지 않는다. 다른 종류의 경고는 그대로 전달한다. 예상한 사용자 경고만 처리해야 새롭게 발생한 문제를 놓치지 않는다.
두 도구를 겹쳐 쓰면 역할이 분명해진다. 안쪽의 withCallingHandlers()는 안내와 예상한 경고를 기록한다. 바깥의 tryCatch()는 사용자 재고 오류를 실패 결과로 바꾼다. 파일 형식이나 코드 자체의 문제처럼 예상하지 않은 오류까지 모두 성공처럼 바꾸지는 않는다.
tryCatch(..., warning = function(w) ...)도 사용할 수 있다. 그러나 그 처리기는 경고가 발생한 보호 구간을 빠져나온다. 경고 다음에 있는 계산을 계속해야 한다면 이 차이가 중요하다. 경고 처리기가 숫자 하나를 반환했다고 해서 경고가 발생한 함수의 중간 계산이 그 숫자로 대체되는 것은 아니다.
finally는 tryCatch()의 보호 구간과 처리기 실행이 끝난 뒤 수행할 표현식이다. 성공과 실패 양쪽에서 실행 구간 종료를 기록하는 데 쓸 수 있다. finally의 계산값은 정상 결과를 대체하지 않지만, 그 안에서 새 오류가 발생하면 기존 결과나 오류의 전달을 방해할 수 있다. 또한 같은 tryCatch()에 등록한 처리기들은 finally 실행을 보호하지 않는다.
정리는 자원을 얻은 함수가 책임진다
on.exit()는 현재 함수가 끝날 때 실행할 정리 표현식을 등록한다. 정상 반환뿐 아니라 오류 때문에 함수를 빠져나갈 때도 실행된다. 파일 연결을 연 함수가 닫기까지 책임지도록 만들면, 호출자가 내부 자원의 존재를 알아야 할 필요가 줄어든다.
등록 시점은 자원을 얻은 직후다. 연결을 열고 긴 작업을 한 다음 정리를 등록하면, 그 사이의 오류에는 대응하지 못한다. 정리 작업을 여러 번 등록할 때는 add = TRUE를 사용한다. 기본값으로 다시 호출하면 앞서 등록한 표현식을 대체한다. 여러 정리 작업의 순서가 중요하면 하나의 블록에 순서를 명시하거나 after 인수를 검토한다.
완성 코드에서는 임시 CSV 파일을 만드는 함수가 파일 삭제와 연결 닫기를 맡는다. 파일을 만든 직후 삭제를 등록하고, 읽기 연결을 연 직후 정리 블록에 연결 닫기를 추가한다. 호출자는 CSV 읽기 구간의 종료만 finally로 기록한다. 이 배치는 자원 수명과 실행 구간의 수명을 구분한다.
정리 코드도 가능한 한 단순해야 한다. 정리 중 다시 복잡한 계산을 하거나 오류를 발생시키면 원래 실패 원인을 확인하기 어려워진다. 또 on.exit()는 R이 함수를 빠져나가는 과정에서 실행되는 장치다. 운영체제가 프로세스를 강제로 끝내는 상황까지 보장하는 장치는 아니다.
완성 코드
다음 내용을 main.R로 저장한다. 외부 패키지나 별도의 데이터 파일은 필요 없다. 코드가 임시 CSV를 직접 쓰고 읽은 뒤 지운다. 출력에 임시 경로를 포함하지 않아 실행할 때마다 같은 결과가 나온다. 의도한 경고는 기록 후 처리하므로 실행이 끝난 뒤 별도의 기본 경고가 남지 않는다.
make_store_condition <- function(message, store, rows, classes) {
structure(
list(
message = message,
call = NULL,
store = store,
rows = as.integer(rows)
),
class = classes
)
}
read_demo_csv <- function() {
path <- tempfile(fileext = ".csv")
con <- NULL
on.exit({
if (!is.null(con)) close(con)
unlink(path)
cat("임시 CSV 정리 완료\n")
}, add = TRUE)
input <- data.frame(
store = c("A", "A", "B", "C"),
sales = c(1000, NA, 600, 700),
stock = c(4, 2, 3, -1),
stringsAsFactors = FALSE
)
write.csv(input, path, row.names = FALSE, na = "NA")
con <- file(path, open = "rt")
read.csv(con, stringsAsFactors = FALSE)
}
validate_store <- function(data, store) {
message(sprintf("%s 지점 검사를 시작한다", store))
bad <- which(is.na(data$stock) | data$stock < 0)
if (length(bad) > 0L) {
stop(make_store_condition(
"재고는 결측값 없이 0 이상이어야 한다",
store, bad,
c("store_stock_error", "store_error", "error", "condition")
))
}
missing <- which(is.na(data$sales))
if (length(missing) > 0L) {
warning(make_store_condition(
sprintf("매출 누락 %d행을 0으로 보정한다", length(missing)),
store, missing,
c("store_sales_warning", "warning", "condition")
))
data$sales[missing] <- 0
}
data
}
process_store <- function(data, store) {
tryCatch(
withCallingHandlers({
checked <- validate_store(data, store)
list(
ok = TRUE,
summary = data.frame(
store = store,
sales = sum(checked$sales),
stock = sum(checked$stock),
stringsAsFactors = FALSE
)
)
},
store_sales_warning = function(w) {
cat(sprintf(
"경고 [%s]: %s (지점 내부 행: %s)\n",
w$store, conditionMessage(w),
paste(w$rows, collapse = ",")
))
invokeRestart("muffleWarning")
},
message = function(m) {
cat(sprintf("안내 [%s]: %s\n",
store, trimws(conditionMessage(m))))
invokeRestart("muffleMessage")
}),
store_error = function(e) {
cat(sprintf(
"오류 [%s]: %s (지점 내부 행: %s)\n",
e$store, conditionMessage(e),
paste(e$rows, collapse = ",")
))
list(ok = FALSE, error = e)
},
finally = cat(sprintf("검사 구간 종료 [%s]\n", store))
)
}
main <- function() {
sales <- tryCatch(
read_demo_csv(),
finally = cat("CSV 읽기 구간 종료\n")
)
stores <- sort(unique(sales$store))
results <- lapply(stores, function(store) {
data <- sales[sales$store == store, , drop = FALSE]
process_store(data, store)
})
names(results) <- stores
successful <- vapply(results, function(x) x$ok, logical(1))
summary <- do.call(
rbind,
lapply(results[successful], function(x) x$summary)
)
rownames(summary) <- NULL
stopifnot(
identical(summary$store, c("A", "B")),
identical(as.numeric(summary$sales), c(1000, 600)),
identical(as.numeric(summary$stock), c(6, 3)),
identical(names(results)[!successful], "C"),
inherits(results$C$error, "store_stock_error"),
identical(results$C$error$rows, 1L),
identical(as.numeric(sales$sales), c(1000, NA_real_, 600, 700))
)
cat("집계 결과\n")
for (i in seq_len(nrow(summary))) {
cat(sprintf("%s: 매출 %.0f, 재고 %.0f\n",
summary$store[i], summary$sales[i], summary$stock[i]))
}
cat(sprintf("제외 지점: %s\n",
paste(names(results)[!successful], collapse = ",")))
cat("검사 통과\n")
invisible(list(summary = summary, results = results))
}
main()
예제는 적어도 한 지점이 성공하는 입력을 사용한다. 모든 지점이 실패하면 results[successful]이 비고, 현재 결합식은 NULL을 만든다. 업무용 도구로 확장할 때는 빈 요약 데이터 프레임을 반환할지, 전체 집계 실패를 알릴지 정해야 한다. 입력 열의 존재와 자료형 검사도 별도의 계약으로 추가해야 한다.
줄별 해설
make_store_condition()의 첫 블록은 모든 사용자 조건의 공통 생성 부분이다. rows를 정수로 맞추므로 행 번호 검사에서 자료형 차이가 생기지 않는다. classes는 호출자가 전달한다. 생성 함수가 재고 오류와 매출 경고의 구체적인 업무 의미까지 결정하지 않도록 분리한 것이다.
read_demo_csv()는 임시 경로와 NULL 연결 변수를 먼저 만든다. 정리 블록은 실행 시점의 con 값을 확인한다. 파일을 쓰는 과정에서 실패하면 연결은 아직 없으므로 닫기를 건너뛴다. 연결을 연 뒤 읽기에서 실패하면 연결을 닫고 파일을 지운다. 정상 반환에서도 같은 블록이 실행된다.
write.csv()의 row.names = FALSE는 데이터와 무관한 행 이름 열이 생기는 것을 막는다. na = "NA"는 누락값을 CSV에 명시한다. 이어지는 read.csv()는 이 표기를 결측값으로 읽는다. 읽기 결과가 함수의 반환값으로 정해진 뒤 정리 블록이 실행되므로, 임시 파일이 사라져도 이미 읽은 데이터 프레임은 사용할 수 있다.
validate_store()는 먼저 메시지로 검사 시작을 알린다. 재고 검사의 is.na(data$stock) | data$stock < 0는 재고 누락과 음수를 함께 찾는다. 재고가 잘못되면 즉시 사용자 오류를 전달하므로 이후 매출 보정과 집계는 실행되지 않는다. 이 순서는 집계할 수 없는 지점에 불필요한 보정 경고를 남기지 않게 한다.
매출 누락이 있으면 경고 객체에 지점명과 누락 행을 넣는다. 호출 처리기가 경고를 기록하고 억제하면 warning() 다음 줄로 진행한다. 그때 누락 매출을 0으로 바꾼다. 처리 정책에 따라 경고가 오류로 바뀌거나 다른 처리기가 중단시키면 이 보정 줄에 도달하지 않을 수 있다.
process_store()의 보호 구간은 검사와 집계를 함께 포함한다. 성공하면 ok = TRUE와 요약 표를 가진 목록을 반환한다. 사용자 오류 처리기는 store_error를 받는다. 재고 오류 클래스 벡터에 이 클래스가 포함되어 있으므로 해당 처리기가 실행되고, ok = FALSE와 원래 조건 객체를 반환한다.
경고 처리기는 구체적인 store_sales_warning만 받는다. invokeRestart("muffleWarning")는 이 예제의 warning() 호출이 마련한 재시작을 이용한다. 모든 조건 발생 방식이 같은 재시작을 제공하는 것은 아니므로, 임의의 조건 처리기에서 이 이름을 무조건 호출하는 방식으로 일반화하면 안 된다.
메시지 처리기의 trimws()는 message()가 붙이는 끝의 줄바꿈 등을 제거한다. 출력용 sprintf()가 줄바꿈을 한 번만 붙이므로 빈 줄 없이 일정한 기록이 나온다. finally는 성공 목록이나 실패 목록이 결정된 뒤 지점 검사 구간 종료를 출력한다.
main()은 지점명을 정렬한 후 같은 순서로 처리한다. 입력 행 순서가 바뀌더라도 지점 기록의 순서는 A, B, C로 유지된다. vapply()는 각 결과에서 논리값 하나를 꺼내 성공 여부 벡터를 만든다. 성공한 결과의 요약 표만 rbind로 결합하고 행 이름은 제거한다.
stopifnot()는 A의 보정 결과, B의 정상 결과, C의 제외 여부와 오류 행을 확인한다. 마지막 검사는 원본 sales의 누락값이 그대로인지 확인한다. 보정은 지점 검사 함수 안의 데이터에 적용되었으며 원본 자료를 덮어쓰는 업무 정책은 사용하지 않았다. 마지막의 invisible()는 반환값을 유지하면서 대화형 실행에서 목록이 추가로 출력되는 것을 막는다.
실행 결과
macOS 또는 Linux의 터미널에서 파일을 저장한 디렉터리로 이동한 뒤 실행한다. R은 다음 스크립트를 읽어 실행한다. 별도의 컴파일 명령은 필요 없다.
Rscript main.R
예상 출력은 다음과 같다. 정리 기록이 CSV 읽기 구간 종료보다 먼저 나온다. A의 경고 뒤에는 집계가 이어지고, C의 오류 뒤에는 지점 종료 기록이 나온다.
임시 CSV 정리 완료
CSV 읽기 구간 종료
안내 [A]: A 지점 검사를 시작한다
경고 [A]: 매출 누락 1행을 0으로 보정한다 (지점 내부 행: 2)
검사 구간 종료 [A]
안내 [B]: B 지점 검사를 시작한다
검사 구간 종료 [B]
안내 [C]: C 지점 검사를 시작한다
오류 [C]: 재고는 결측값 없이 0 이상이어야 한다 (지점 내부 행: 1)
검사 구간 종료 [C]
집계 결과
A: 매출 1000, 재고 6
B: 매출 600, 재고 3
제외 지점: C
검사 통과
이 실행에는 난수와 그래프가 없다. 의도한 경고는 기록으로 변환되어 처리되며, 출력 끝에 기본 경고 요약이 붙지 않는다. 검사 조건이 맞지 않거나 예상하지 않은 오류가 생기면 Rscript 실행은 실패한다. 자동 실행에서는 이 실패 상태를 작업 결과와 함께 확인해야 한다.
실무에서 자주 틀리는 것
경고 처리기에서 반환하면 계산이 이어진다고 생각한다
다음 코드는 경고를 기록하지만 결과가 100이 아니다. tryCatch()가 보호 구간을 빠져나오고 처리기의 반환값 0을 결과로 사용한다.
# 의도: 경고를 기록하고 100을 반환한다
value <- tryCatch({
warning("매출 누락을 확인했다")
100
}, warning = function(w) {
cat("경고 기록\n")
0
})
경고 다음 계산을 이어 가려면 호출 처리기를 사용한다. 다음 코드에서는 경고를 기록한 후 기본 출력을 억제하고 100을 반환한다.
value <- withCallingHandlers({
warning("매출 누락을 확인했다")
100
}, warning = function(w) {
cat("경고 기록\n")
invokeRestart("muffleWarning")
})
stopifnot(identical(value, 100))
오류를 잡고 정상 매출 0으로 바꾼다
오류를 모두 0으로 바꾸면 계산 실패와 실제 매출 0을 구별할 수 없다. 아래 코드에서는 파일 오류나 코드 오류도 정상 집계처럼 보일 수 있다.
total <- tryCatch({
checked <- validate_store(data, store)
sum(checked$sales)
}, error = function(e) 0)
복구할 사용자 오류만 받고 성공 여부를 별도로 반환한다. 조건 객체를 보관하면 나중에 제외 사유와 관련 행을 확인할 수 있다.
result <- tryCatch({
checked <- validate_store(data, store)
list(ok = TRUE, total = sum(checked$sales))
}, store_error = function(e) {
list(ok = FALSE, error = e)
})
정리 등록을 작업 끝으로 미룬다
읽기와 닫기 사이에 오류가 발생하면 마지막 줄은 실행되지 않는다. 다음 함수는 닫기를 정상 경로에만 배치했다.
read_local <- function(path) {
con <- file(path, open = "rt")
data <- read.csv(con)
close(con)
data
}
연결을 얻은 직후 정리를 등록한다. 파일을 여는 단계에서 실패하면 연결이 생기지 않았으므로 정리 등록까지 도달하지 않아도 된다.
read_local <- function(path) {
con <- file(path, open = "rt")
on.exit(close(con), add = TRUE)
read.csv(con)
}
오류 문구로 문제 종류를 판별한다
설명 문구는 사용자에게 읽히는 텍스트다. 문구를 다듬거나 언어를 바꾸면 문자열 비교에 의존한 분기가 달라진다.
is_stock_problem <- function(e) {
identical(conditionMessage(e),
"재고는 결측값 없이 0 이상이어야 한다")
}
문제 종류는 클래스로 판별한다. 필요한 지점과 행은 별도 필드에서 꺼낸다.
is_stock_problem <- function(e) {
inherits(e, "store_stock_error")
}
한눈에 보기
| 도구 | 맡는 일 | 복귀 또는 실행 위치 | 주의점 |
|---|---|---|---|
stop() | 계산 불가 전달 | 오류 처리기로 이동 | 실패를 정상값으로 숨기지 않는다 |
warning() | 결과에 영향을 주는 주의 전달 | 기본적으로 계산 계속 | 실행 정책에 따라 오류가 될 수 있다 |
message() | 진행 안내 전달 | 계산 계속 | 결과 데이터와 기록을 구분한다 |
tryCatch() | 실패를 반환값으로 변환 | 보호 구간 밖 | 처리기 반환값이 전체 값이 된다 |
withCallingHandlers() | 발생 위치에서 기록·대응 | 조건 발생 문맥 | 단순 반환만으로 기본 출력이 억제되지 않는다 |
| 사용자 조건 클래스 | 종류와 관련 정보 전달 | 조건 객체에 저장 | 메시지와 기계적 분기 기준을 나눈다 |
finally | 실행 구간 마무리 | 보호 구간과 처리기 실행 후 | 새 오류가 기존 결과를 방해할 수 있다 |
on.exit() | 함수가 소유한 자원 정리 | 현재 함수가 끝날 때 | 자원을 얻은 직후 등록한다 |
이 장의 기능은 모두 base R에 있다. tidyverse 계열의 함수 도구를 사용하더라도 복구 가능한 문제의 범위와 경고 기록 정책은 별도로 정해야 한다. 대응 관계는 아래와 같으며 이 장에서는 설치하거나 실행하지 않는다.
| 이 장의 방식 | 관련 외부 도구 | 관계 |
|---|---|---|
| 성공 여부와 오류를 담은 목록 | purrr::safely() | 결과와 오류를 분리하는 관련 도구다 |
| 실패 때 정한 기본값 반환 | purrr::possibly() | 기본값의 업무 의미를 먼저 정해야 한다 |
| 사용자 오류·경고 전달 | rlang::abort(), rlang::warn() | 조건 생성과 전달을 지원하는 관련 도구다 |
stopifnot() 검사 | testthat | 검사를 조직하고 보고하는 도구이며 base R 기능은 아니다 |
함수의 세부 동작을 확인할 때는 R 조건 처리 공식 설명과 함수 종료 정리 공식 설명을 참고할 수 있다. 다음 장에서는 실행할 식 자체를 값으로 다룬다. 조건 객체에 문제 정보를 담은 것처럼, 계산을 표현하는 객체에도 구조가 있다는 관점으로 이어진다.
연습 문제
- A 지점의 매출을 모두 누락값으로 바꾸면 경고와 요약 매출은 어떻게 달라지는지 설명하라. 현재 정책이 실제 매출 0을 뜻하는지도 설명하라.
- 재고 오류의 클래스를 확인하고 지점명과 행 번호를 검사하는 코드를 작성하라. 오류를 검사하기 위해 의도한 경고나 메시지를 발생시킬 필요는 없다.
- 임시 연결을 연 다음 의도적으로 오류를 발생시키는 함수를 작성하라.
on.exit()가 정리했다는 사실과 바깥 오류 처리기의 실행 순서를 출력으로 확인하라. - 모든 지점이 실패해도 열 이름과 자료형이 정해진 빈 요약 표를 반환하도록 완성 코드의 요약 결합 부분을 수정하라.
정답과 해설
매출 누락이 두 행인 경우
입력의 A 매출 두 값을 모두 NA로 바꾸면 경고는 “매출 누락 2행을 0으로 보정한다”가 되고 지점 내부 행은 1,2가 된다. A의 요약 매출은 0이며 재고는 6이다. 이는 누락값을 집계에서 0으로 취급한 결과다. 실제 판매가 없었다는 사실을 입증하지 않으므로 보고서에는 보정 여부나 누락 행 수를 함께 남기는 편이 낫다.
조건의 구조 검사
조건 생성 함수를 직접 호출하면 신호 발생과 별개로 객체의 구조를 검사할 수 있다. 실패 경로에서 같은 객체가 반환되는지는 완성 코드의 results$C$error 검사로 확인한다.
e <- make_store_condition(
"재고 확인이 필요하다", "C", c(1, 3),
c("store_stock_error", "store_error", "error", "condition")
)
stopifnot(
inherits(e, "error"),
inherits(e, "store_stock_error"),
identical(e$store, "C"),
identical(e$rows, c(1L, 3L)),
identical(conditionMessage(e), "재고 확인이 필요하다")
)
오류 경로의 정리 순서
임시 파일 연결을 만든 뒤 정리를 등록하고 오류를 발생시킨다. 오류가 바깥 처리기에 전달되는 과정에서 함수가 끝나므로 정리 기록이 먼저 나온다.
cleanup_demo <- function() {
con <- file("", open = "w+")
on.exit({
close(con)
cat("연결 정리\n")
}, add = TRUE)
stop("읽기 실패")
}
tryCatch(
cleanup_demo(),
error = function(e) {
cat("오류 처리\n")
invisible(NULL)
}
)
출력은 다음과 같다. 오류를 잡았기 때문에 기본 오류 출력은 나오지 않는다.
연결 정리
오류 처리
성공 지점이 없는 경우
먼저 빈 표의 구조를 정하고 성공 결과가 있을 때만 결합한다. 집계 함수가 매출과 재고를 합계값으로 반환하므로 빈 표의 수치 열도 숫자형으로 만든다. 전체 실패를 작업 실패로 볼지는 이 표 구조와 별개로 결정한다.
summary <- data.frame(
store = character(),
sales = numeric(),
stock = numeric(),
stringsAsFactors = FALSE
)
if (any(successful)) {
summary <- do.call(
rbind,
lapply(results[successful], function(x) x$summary)
)
rownames(summary) <- NULL
}
이 수정 뒤 모든 지점이 실패하면 요약 표는 0행이며 제외 지점 목록은 유지된다. 완성 코드의 stopifnot()는 원래 예제의 A·B 성공을 검사하므로, 전체 실패 입력을 시험할 때는 기대값도 바꿔야 한다. 예를 들어 nrow(summary) == 0L과 !any(successful)를 함께 확인한다.