MATLAB 예외 처리 내용 정리

Updated:

MATLAB의 예외 처리에 대한 중요 내용을 정리한다. 참고한 문서는 다음과 같다.

1. 예외 발생 시키기

error 함수 사용

함수를 작성하다보면 예외를 발생시키고 싶을 때가 있다. 예를 들어 type이 numeric 인 두 인수를 입력받고 합을 계산하여 리턴하는 함수 add.m을 작성한다고 하자. 그리고 add 함수는 두 입력 인수가 numeric이 아니면 에러 메세지를 출력하고, 프로그램이 종료되도록 해야 한다고 하자. MATLAB에서 가장 간단히 예외를 발생시키는 방법은 error 함수를 사용하는 것이다. error 함수의 가장 간단한 형식은 error(ERRMSG)ERRMSG 는 에러가 발생하면 출력하는 메세지이다.

% add.m
function c = add(a, b)
if ~isnumeric(a) || ~isnumeric(b)
    error('Input arguments must be numeric');
end
c = a + b;
end

command window에서 add 함수를 테스트 한다.

>> add(1, 2)
ans =
     3

>> add(1, 'a')
Error using add (line 4)
Input arguments must be numeric

위와 같이 add(1, 'a') 에 의해 에러가 발생하면 작성한 에러 메세지를 출력하면서 프로그램이 종료된다. 에러 메세지 전에 Error using add (line 4) 은 에러가 어느 함수 몇 번째 줄에서 발생했는지 MATLAB이 추적해서 보여주는 추가 정보이다. line 4를 클릭하면 add.m 파일이 열리고 곧바로 네 번째 줄에 커서가 위치한다.

error 함수 형식

MATLAB command window에 help error 를 해 보면 error 함수에 대한 자세한 정보를 볼 수 있다. error 함수의 전형적인 사용법은 다음과 같다.

  • error(ERRMSG)
  • error(ERRMSG, V1, V2, ...)
  • error(MSGID, ERRMSG)
  • error(MSGID, ERRMSG, V1, V2, ...)

error(ERRMSG, V1, V2, ...)sprintf 함수처럼 포맷 에러 메세지를 만들 때 사용한다. 예를 들어 위 add 함수에서 에러가 발생했을 때 입력한 인수의 type이 어떻게 되는지도 출력해 주도록 하려면 아래와 같이 하면 된다.

% add.m
function c = add(a, b)
if ~isnumeric(a) || ~isnumeric(b)
    error('Input arguments must be numeric \n  a=%s, b=%s', ...
        class(a), class(b));
end
c = a + b;
end

위 함수를 테스트 한다.

>> add(1, 'a')
Error using add (line 4)
Input arguments must be numeric
  a=double, b=char

Message identifiers

error(MSGID, ERRMSG) 에서 MSGIDmessage indentifiers라고 한다. 큰 프로그램을 작성하거나 함수 패키지들을 만들 때 많은 예외를 정의하게 된다. 그리고 정의하는 예외들을 체계적으로 잘 분류해 놔야 이후에 예외 처리를 체계적으로 할 수 있다. 이 때 예외를 분류하는데 사용하는 것이 MSGID 이다. MSGID 의 형식은 [component:]component:mnemonic 이다. component는 여러 개를 정의해도 되며, :로 분리된다. MSGID의 마지막은 mnemonic 이다. componentmnemonic은 일반적인 지정자 규칙을 따른다. 즉, 알파벳 대/소문자로 시작하고 그 이후부터 숫자, 알파벳, _로 작성한다.

나만의 수학 라이브러리를 만든다고 가정해보자. 수학 라이브러리 패키지의 이름을 myMathLib 라고 하고 add 함수에서 발생한 입력 인수의 에러를 inpTypeError 라고 하면 add 함수를 아래와 같이 수정하는 것이 좋다.

% add.m
function c = add(a, b)
if ~isnumeric(a) || ~isnumeric(b)
    error('myMathLib:inpTypeError', ...
        'Input arguments must be numeric \n  a=%s, b=%s', ...
        class(a), class(b));
end
c = a + b;
end

이제 두 cell 을 입력받고 cell의 내용을 통합한 cell을 리턴하는 함수 mergecell.m 을 작성해 보자. mergecell 함수는 다음과 같다.

% mergecell.m
function c = mergecell(a, b)
if ~iscell(a) || ~iscell(b)
    error('myMathLib:inpTypeError', ...
        'Input arguments must be cell \n  a=%s, b=%s', ...
        class(a), class(b));
end
c = {a{:}, b{:}};
end

mergecell 를 사용해 보자.

>> c = mergecell([1, 2], {'a', 'b'})
Error using mergecell (line 4)
Input arguments must be cell
  a=double, b=cell
 
>> c = mergecell({1, 2}, {'a', 'b'})
c = 
    [1]    [2]    'a'    'b'

>> class(c)
ans =
cell

mergecell 함수를 정의할 때 중요한 것은 MSGIDmyMathLib:inpTypeError 로 정의했다는 것이다. 즉, 내가 정의한 수학 라이브러리 패키지 myMathLib 에서 이런 입력 인수 type check에 관련된 에러를 모두 inpTypeError 로 정의하겠다는 의미이다. 나중에 예외 처리를 위해 try ... catch 문을 사용할 때 이러한 일관된 예외 분류가 도움이 된다.

2. MException 클래스

MException 으로 예외 발생 시키기

앞에서 본 것처럼 간단히 예외를 정의할 때 error 함수를 사용하면 된다. command window에서 아래와 같이 가상의 에러를 발생시켜 보자.

>> error('abc:def', 'error message')
error message

error 함수를 사용한 것과 동일한 효과를 MException 클래스를 사용하여 만들 수 있다. 방식은 아래와 같다.

>> meobj = MException('abc:def', 'error message')
meobj = 
  MException with properties:

    identifier: 'abc:def'
       message: 'error message'
         cause: {}
         stack: [0x1 struct]

>> meobj.throw()
error message

MException 클래스의 형식은 MException(MSGID, ERRMSG, V1, V2, ...) 이고, MException 객체를 리턴한다. MException 객체의 throw 메소드를 호출하면 예외가 발생한다.

MException 객체는 getAccess=public 인 속성 identifier, message, cause, stack 을 가지고 있다. 따라서 객체를 이용하면 내용을 읽을 수 있다.

>> meobj.identifier
ans =
abc:def

>> meobj.message
ans =
error message

>> meobj.cause
ans = 
     {}

>> meobj.stack
ans = 
0x1 struct array with fields:
    file
    name
    line

두 입력 인수의 차이를 계산하는 sub.m 함수를 MException 클래스를 사용하여 작성해 보았다.

% sub.m
function c = sub(a, b)
if ~isnumeric(a) || ~isnumeric(b)
    meObj = MException('myMathLib:inpTypeError', ...
        'Input arguments must be numeric \n  a=%s, b=%s', ...
        class(a), class(b));
    meObj.throw();
end
c = a - b;
end

나중에 나오지만 MException 클래스는 조금 더 다양하게 예외를 컨트롤 할 수 있게 해준다.

MException.last

MException 클래스는 static 메소드 last를 가지고 있다. 이 메소드는 가장 최근에 발생한 catch 하지 않은 예외의 MException 객체를 리턴한다. 이를 이용하여 MATLAB의 여러가지 예외의 identifier 를 확인해 보자.

>> a = [1, 2];
>> a(3);
Index exceeds matrix dimensions.
>> meobj = MException.last;
>> meobj.identifier
ans =
MATLAB:badsubscript

>> sin();
Error using sin
Not enough input arguments.
>> meobj = MException.last;
>> meobj.identifier
ans =
MATLAB:minrhs

>> sin(1,2,3)
Error using sin
Too many input arguments.
>> meobj = MException.last;
>> meobj.identifier
ans =
MATLAB:maxrhs

>> [a, b] = sin(1);
Error using sin
Too many output arguments.
>> meobj = MException.last;
>> meobj.identifier
ans =
MATLAB:maxlhs

>> [1, 1] + [2, 3, 4];
Error using  + 
Matrix dimensions must agree.
>> meobj = MException.last;
>> meobj.identifier
ans =
MATLAB:dimagree

위와 같이 MATLAB은 MATLAB component에 mnemonic이 badsubscript, minrhs, maxrhs, maxlhs, dimagree 등으로 예외 id를 잘 분류해 두었다.

stack property

surf 함수에 대한 예외를 살펴보자.

>> surf;
Error using surf (line 49)
Not enough input arguments.
 
>> meobj = MException.last
meobj = 
  MException with properties:
    identifier: 'MATLAB:narginchk:notEnoughInputs'
       message: 'Not enough input arguments.'
         cause: {}
         stack: [1x1 struct]

>> meobj.stack
ans = 
    file: 'C:\Program Files\MATLAB\R2016a\toolbox\matlab\graph3d\surf.m'
    name: 'surf'
    line: 49

surf; 를 입력하면 입력 인수가 불충분하다는 에러가 발생하고 발생한 위치를 surf.m의 49 라인임을 알려준다. line 49를 클릭하면 narginchk(1,inf) 에서 에러가 발생한 것인데, 입력 인수의 개수를 체크하는 함수 narginchk 안에서 에러가 발생한 것이다. 이 예외의 identifier를 보면 MATLAB:narginchk:notEnoughInputs 인데 component에 함수 이름인 narginchk 를 사용했다. MATLAB은 특정 함수 안에 identifier를 작성할 때 함수 이름을 component에 추가하는 분류 방식도 사용하는 것 같다.

stack property는 구조체 배열인데, 위 예외에서 원소 하나가 있음을 알 수 있다. 내용을 확인해 보면 발생한 예외의 경로, 파일명, 줄번호 정보가 있음을 알 수 있다. MATLAB은 특정 함수 안에서 예외가 발생하면 그 발생한 위치의 정보를 stack property에 남겨 둔다.

앞서 작성한 add.m 을 다시 사용해 보면, stack 의 정보를 확인할 수 있다.

>> add(1, 'a')
Error using add (line 4)
Input arguments must be numeric
  a=double, b=char
 
>> meobj = MException.last
meobj = 
  MException with properties:
    identifier: 'myMathLib:inpTypeError'
       message: 'Input arguments must be numeric …'
         cause: {0x1 cell}
         stack: [1x1 struct]

>> meobj.stack
ans = 
    file: 'D:\jobs\190516_matlab_exception\add.m'
    name: 'add'
    line: 4

3. 예외 처리하기

try, catch

예외가 발생하면 에러 메세지를 출력한 후, 프로그램이 종료된다. 자기 혼자만 사용하는 프로그램이라면 이렇게 사용하더라도 큰 문제가 생기지 않겠지만, 다른 사람도 사용하는 프로그램 이라면 예외 발생에 의해 생기는 비정상 종료를 막고 적절히 처리할 수 있어야 한다. 이런 예외 처리를 위해 MATLAB에서는 try, catch 를 사용한다.

아래 calc.m 함수는 앞에서 작성해 둔 add.m 함수를 사용하여 어떤 계산을 하는 함수인데, 예외 처리를 하는 예를 보여준다.

% calc.m
function c = calc
a = 1;
b = '10';

try
    c = add(a, b);
catch meobj
    fprintf('catch %s\n', meobj.identifier);
end
end

MATLAB은 try 블록 안에 있는 코드를 실행한다. 실행 중에 문제가 없으면 catch 블록은 실행하지 않고 end 이후의 코드를 실행한다. 그러나 try 블록을 실행하다가 에러가 발생하면 발생한 시점에서 제어를 catch 블록으로 넘기고 try 블록에서 발생한 예외를 catch 문 옆의 meobj 객체로 저장한다. 입력 인수 b가 문자열이기 때문에try 블록 안에 add(a, b)myMathLib:inpTypeError 에러가 발생한다. 이 예외 정보가 meobj 객체에 저장되고 catch 블록에서 이 예외에 대한 적절한 처리를 하면 된다.

calc 함수를 command window에서 실행한 결과는 다음과 같다.

>> calc
catch myMathLib:inpTypeError

프로그램은 예외가 발생하여 비정상 종료가 된 것이 아니라 예외를 받아서 어떤 처리를 한 후 정상 종료 된 것이다.

throw, rethrow, throwAsCaller

calc.m 을 아래와 같이 바꿔 본다.

% calc.m
function c = calc
a = 1;
b = '10';

try
    c = add(a, b); % Line 7
catch meobj
    meobj.throw()  % Line 9
end
end

calc 함수를 command window에서 실행해 보면,

>> calc
Error using calc (line 9)
Input arguments must be numeric
  a=double, b=char
  
>> meobj = MException.last;
>> meobj.stack
ans = 
    file: 'D:\jobs\190516_matlab_exception\calc.m'
    name: 'calc'
    line: 9  

MException 객체는 throw 메소드를 가지고 있는데 이 throw 메소드는 예외를 발생시키면서 호출되는 라인에 대한 정보가 stack property에 저장된다. 그러나 위 예외는 Line 7 에서 발생한 예외를 받아서 예외를 발생시킨 것이기 때문에 stackLine 7 의 정보가 저장되야 한다. 이렇게 하려면 Line 9meobj.rethrow() 로 바꿔야 한다.

바꾼 후 다시 command window에서 calc 함수를 실행해 보면,

>> calc
Error using add (line 4)
Input arguments must be numeric
  a=double, b=char

Error in calc (line 7)
    c = add(a, b); % Line 7
 
>> meobj = MException.last;
>> meobj.stack(1)
ans = 
    file: 'D:\jobs\190516_matlab_exception\add.m'
    name: 'add'
    line: 4

>> meobj.stack(2)
ans = 
    file: 'D:\jobs\190516_matlab_exception\calc.m'
    name: 'calc'
    line: 7

stack property에는 예외 발생 지점의 모든 정보를 저장한다. 위에서 설명한 내용은 MException throw에 나온다.

throwAsCaller는 호출한 함수에서 예외가 발생한 것처럼 표시해 준다. 자세한 내용은 MException throwAsCAller을 참고한다.

cause property

calc.m 을 아래와 같이 바꿔 본다.

% calc.m
function c = calc
a = 1;
b = '10';

try
    c = add(a, b);
catch meobj
    me2 = MException('calc:dummyError', 'dummy error message');
    meobj = meobj.addCause(me2);
    meobj.throw();
end
end

MException 객체는 addCause 메소드를 가지는데, 이 메소드는 cause property인 cell 배열에 MException 객체를 담을 수 있다. 어떤 예외를 처리할 때 또 다른 예외를 정의하고 그것을 cause property에 저장한 후 throw 메소드를 이용해서 다시 발생 시킬 수 있다. 주의 할 점은 MException 객체는 value class 이기 때문에 meobj = meobj.addCause(me2); 처럼 리턴되는 객체를 다시 저장해야 한다.

command window에서 calc 함수를 실행해 보면,

>> calc
Error using calc (line 11)
Input arguments must be numeric
  a=double, b=char

Caused by:
    dummy error message
 
>> meobj = MException.last;
>> meobj
meobj = 
  MException with properties:

    identifier: 'myMathLib:inpTypeError'
       message: 'Input arguments must be numeric …'
         cause: {[1x1 MException]}
         stack: [1x1 struct]

>> meobj.cause{1}
ans = 
  MException with properties:

    identifier: 'calc:dummyError'
       message: 'dummy error message'
         cause: {}
         stack: [0x1 struct]

cause 에 대한 내용은 MException addCause를 참고하라.

Leave a comment