상세 컨텐츠

본문 제목

AutoIT으로 자바 디자인 패턴 '상태 패턴' 구현하기

AutoIt

by techbard 2015. 7. 1. 21:48

본문

반응형
자바로 대표되는 객체지향 프로그래밍에 있어 디자인 패턴이 존재한다. 그중 상태 패턴이라고 하는 것을 AutoIT으로 흉내내 보겠다. (이하 AutoIT을 AU3로 표기)

만일 로직을 구현할 때 수정과 관리가 쉽도록 하려면 어떤 구조를 가져야 할까?


가장 간단하게 구현한다면 함수를 하나 만들어 두고 그 안에 모든 기능을 집어 넣는 것이리라.


하지만 그렇게하면 유지보수가 쉽지 않고 나중에 코드를 다시 보게 되는 경우 이해하기도 어려울 것이다. 무엇보다 If - Then이 겹겹이 쌓이는 구조로 표현할 수 밖에 없는 로직이라면 수정이 필요해도 손댈 수 없을 것이다. 자바의 디자인 코드 중에 이러한 경우를 대비하여 상태 패턴이라는 것을 알려주고 있어 그와 똑같지는 않지만 비슷한 효과를 볼 수 있는 AU3 구현체를 알아본다.


사전 준비가 필요한데


Java 상태 패턴

AU3 코딩 컨벤션


그리고, 몇 가지 내장 Function을 살펴보자.


1. 전역변수 (Global Variable)


말 그대로 전역변수로 코드 내의 어느 위치에서도 액세스 가능하다. 코딩 컨벤션에 따르면 특수한 표기법이 있다. 다른 변수들과 구별이 쉽도록 하기 위함이다.



여기서 우리는 전역변수에 다음번 호출할 함수의 이름을 담을 것이므로 다음과 같이 네이밍한다.

Global $g_sNextRun = ""


2. 열거형 상수 (Enumerates constants)


상수이긴 하지만 일일이 숫자를 지정하지 않아도 알아서 할당해 주는 키워드이다.



여기서 우리는 함수들을 넘나들며 사용할 것이므로 전역적으로 쓰자. 네이밍에서 알 수 있듯이 에러코드로 사용할 것이다.

Global Enum$eError01 = 1, $eError02


3. 함수간 값 전달하기


AU3에서 함수 리턴값을 전달하는 전통적인 방법은 Return 이다. 매우 간단한 계산을 하는 프로그램이라면 모를까 Return으로 변수 하나의 값을 던지는 것은 그다지 실용적이지 않다. 물론 배열도 던질 수 있다. 하지만 AU3에서 배열 index를 사용해서 접근하는 방식 자체가 개수가 많아지면 도저히 몇 번째 인덱스가 원하는 값인지 구분하기 어렵기 때문에 사용하지 않겠다.


함수의 역할에 대해서 생각해 보면 다음의 세 가지 상황이 가능하다.


a. 정상 처리 되어 결과값을 리턴해야 하는 경우

b. 정상 처리가 되지 않아 에러 통지를 해야 하는 경우


많은 경우 정상 처리가 되지 않았을 때 함수 호출자에게 에러 사실을 통지해야 로직이 성립한다. 하지만 에러의 종류가 두 개 이상이라면 Return True / False 이 두 가지만으로 표현할 수 없는 에러의 종류도 있으며, 함수 여러 개를 호출하는 경우 그것이 어떤 함수에서 발생한 'False' 인지 구분하기가 대단히 어렵다.


또한 일반적인 함수의 결과값 되돌려 받기 방법인

$vReturn = FuncA(Param1)

도 사용하다보면 리턴값 변수를 관리해 줘야 하는 부담이 생긴다.


그래서, 여기서는 SetError의 @extended 매크로를 에러코드 대신으로 쓰도록 한다.

Global Enum $eError0 = 0 , $eError1, $eError2


For $i = 1 To 10

$bResult = FuncA()

$iErrorCode = @error

$iExtendedCode = @extended


If $iErrorCode = 1 And $iExtendedCode = $eError1 Then ConsoleWrite("FuncA returns Error1." & @CRLF)

If $iErrorCode = 1 And $iExtendedCode = $eError2 Then ConsoleWrite("FuncA returns Error2." & @CRLF)

If Not $iErrorCode = 1 And $bResult Then ConsoleWrite("FuncA runs normally." & @CRLF)

Next


Func FuncA()

Local $iResult = Random(0, 2, 1)

If $iResult = 0 Then

Return True

ElseIf $iResult = 1 Then

Return SetError(1, $eError1)

ElseIf $iResult = 2 Then

Return SetError(1, $eError2)

EndIf

EndFunc


라인번호로 설명을 하기 위해 다시 한 번 코드를 보인다.


- Random(0, 2, 1)

최소값 0에서 최대값 2까지 임의값을 생성하는데 정수형 (flag = 1) 으로 반환한다.

이것은 실행할 때마다 서로 다른 결과를 생성하기 위해서이며 의사적으로 $iResult = 0은 함수 처리 성공, $iResult = 1은 함수 처리 실패로 에러코드 1을 리턴, $iResult = 2는 함수 처리 실패로 에러코드 2를 리턴하는 상황을 만들기 위한 코드이다.


- Return SetError(1, $eError1)

먼저 알아야 할 사실은 @error, @extended가 AU3 내의 매크로라 불리는 형태로 이 값은 언제 어떻게 바뀔지 모른다. 즉 에러코드를 @extended에 세트했다 할지라도 그 값을 따로 저장해 놓지 않으면 참조하는 시점에는 다른 값으로 바뀌어 있을 수도 있다는 점이다. 이 라인은 @error = 1을, @extended = $eError1 상수를 할당한다.


$bResult = FuncA()

$iErrorCode = @error

$iExtendedCode = @extended

FuncA()의 리턴값을 3 가지로 정의했다. 따라서, 4번 라인에서는 명시적으로 Return False를 하지 않는 한 True로 값이 넘어올 것이다.

앞에서 언급했듯이 매크로 값은 시시각각 변하므로 Return을 통해 함수 호출 지점 아래로 되돌아오자마자 다른 변수에 값을 복사했다. 이제 이 값을 가지고 에러 유무를 판단하면 될 것이다.


- 8 ~ 10번 라인

함수에서 에러인 경우 @error = 1로 할당했으므로 먼저 이 조건인지 따진다. 그리고 같은 @error = 1 중에서도 @extended 값을 다르게 할 것이므로 @extended가 상수와 같은지를 따지는 조건 두 개를 and 연산한다. 그러면, 함수에서 리턴하는 조건에 따라 각기 다른 분기를 탈 것이다.

주의할 것은 10번 라인인데 If $bResult 로만 판단하면 Return False를 하지 않았으므로 Return SetError 의 경우에도 True가 리턴된다는 점이다. 따라서 $bResult가 True인지와 $iErrorCode가 1이 아닌 조건을 동시에 만족하는 상황을 나타낸다.


<실행 결과>

FuncA runs normally.

FuncA returns Error1.

FuncA returns Error2.

FuncA returns Error1.

FuncA returns Error1.

FuncA runs normally.

FuncA runs normally.

FuncA returns Error2.

FuncA returns Error1.

FuncA returns Error2.


이로써 함수의 서로 다른 동작 결과에 대해서 반응하는 로직을 구현했다. 그렇다면 정형화된 에러코드가 존재한 상태에서 여러 개의 단위 함수가 존재하고 그것의 처리 결과를 한 곳에서 핸들하고자 할 때는 어떻게 응용할 수 있을까?


AU3의 독특한 내장 함수 하나를 알아보자.


4. 인자로 주어진 함수를 실행하기 (Calls a user-defined function)

이 내장 함수가 없다면 상태 패턴을 흉내낼 수는 없다. 이 함수에 주어지는 인자를 변경할 수 있다는 특성을 이용해 조건에 따라 미리 정의된 함수를 실행하도록 구현하자.

먼저 Call은 아래와 같이 사용한다.

Call("함수명", 인자1)


일단 단위 함수마다 인자가 가변적이라면 이를 한줄 코드로 구현해서는 표현할 길이 없긴 하다. 이를 위해서 도움말을 보면 배열 자체를 넘길 수 있다고는 언급하고 있다. 우리는 단위 함수를 만들지만 인자가 필요없는 함수만을 가지고 있다고 가정하자. 이런 경우는 과업을 주로 시스템을 액세스 하는 일로만 쪼갠 경우에 해당할 수 있겠다. 예를 들면, INI 파일을 생성하고 쓰고 읽고 하는 일련의 과업이 있다면 이들을 하나의 함수에 몰아넣을 수도 있겠지만 더 세분화한 과업으로 쪼개서 함수를 만들 수도 있을 것이다. 따라서 최종적으로는 최초 실행하는 함수의 실행 결과에 따라서 다음 번 실행되는 함수가 달라지는 것을 구현하게 되고 이것이 디자인 패턴 중 상태 패턴과 유사한 구조를 가진다.


구조를 전달 할 수 있는 간단한 코드로 보이겠다.

Global $g_NextRun = "A"

Global Enum $eFirtst = 1, $eSecond


_Runner()


Func _Runner()

While 1

Call($g_NextRun)

WEnd


EndFunc


Func A()

ConsoleWrite("A running." & @CRLF)

If Random(0, 1, 1) Then

$g_NextRun = "B"

Return SetError(1, $eFirtst)

Else

$g_NextRun = "C"

EndIf

EndFunc


Func B()

ConsoleWrite("B running." & @CRLF)

$g_NextRun = "C"

Return SetError(1, $eSecond)

EndFunc


Func C()

ConsoleWrite("C running." & @CRLF)

If Random(0, 1, 1) Then

$g_NextRun = "A"

Else

$g_NextRun = "Z"

EndIf

EndFunc


Func Z()

ConsoleWrite("Z running." & @CRLF)

Exit

EndFunc


<실행 결과>

첫번째 실행 결과


A running.

C running.

A running.

C running.

Z running.


두번째 실행 결과


A running.

C running.

Z running.


세번째 실행 결과


A running.

B running.

C running.

A running.

B running.

C running.

Z running.



당연히 실행 결과는 실행할 때마다 다르다. 상태 패턴에서 하고자하는 바가 해당 액션의 실행 결과 = 상태에 따라 다른 로직 흐름을 편하게 만들어 내고자 함에 있다면 AU3에서도 위와 같은 구조를 가질 때 단위 함수가 제 각각 할 일만 정의하고 그 결과에 따라서 다음 번에 실행할 함수 이름을 할당해 주면 되겠다.


이런 구조를 가지는 장점은 당연히 부분 로직을 변경하고자 할 때 해당 함수만 변경하면 되며 호출 관계 또한 그대로 유지한다면 다른 코드는 변경하지 않아도 되는 장점이 있다. 매우 간단한 구조를 가지는 코드에는 함수 하나에 모든 로직을 몰아넣어도 된지만 코딩하는 과정이나 수정할 때 어떤 의도로 작성했는지 한참을 고민해야 하는 경우가 발생한다. 이럴 때 위에 언급한 바와 같이 과업을 여러 단위 과업으로 나누어 함수를 작성하고 그 함수들의 실행 결과에 따른 호출관계만 정의해 주면 된다.



5. 제어권을 한 곳으로 모으기


위의 구조는 훌륭해 보인다!

하지만 Func Z()에서 Exit를 실행해서 실행이 중단되어 버려서 원래 호출자에서 모든 제어권을 가지지 못하는 단점이 있다. 여기서 우리가 왜 SetError를 통한 에러 코드를 가지고 분기를 처리하려고 했는지에 대한 이유가 발생한다. 에러 코드 또한 정의하기 나름이며 Global Enum 으로 상수 정의하는 경우 한 곳에서 관리하기도 편하고 에러 코드 숫자보다 알아보기가 수월하다.


Global Enum $eErrorA, $eErrorB, $eErrorC, $eNormalExit


이제 어떤 구조를 가져야 좋을지 생각해 본다.


- 단위 함수의 실행 결과가 정상 결과이면 다음번 함수를 호출한다.

- 단위 함수의 실행 결과가 에러 첫번째이면 해당 에러 코드를 반환한다.

- 단위 함수의 실행 결과가 에러 두번째이면 해당 에러 코드를 반환한다.

- 호출자는 단위 함수의 실행 결과를 받아 조건문에서 처리한다.


Global $g_NextRun = "A"

Global Enum $eFirtst = 1, $eSecond, $eThird, $eNormalExit


_Runner()


Func _Runner()

While 1

Call($g_NextRun)

$iErrorCode = @error

$iExtendedCode = @extended


Select

Case $iErrorCode = 1 And $iExtendedCode = $eFirtst

ConsoleWrite("A running with FirstError." & @CRLF)

Exit

Case $iErrorCode = 1 And $iExtendedCode = $eThird

ConsoleWrite("C running with ThirdError." & @CRLF)

Exit

Case $iErrorCode = 1 And $iExtendedCode = $eNormalExit

ConsoleWrite("Z normal exited." & @CRLF)

Exit

EndSelect

WEnd

EndFunc


Func A()

ConsoleWrite("A running." & @CRLF)

If Random(0, 1, 1) Then

$g_NextRun = "B"

Return SetError(1, $eFirtst)

Else

$g_NextRun = "C"

EndIf

EndFunc


Func B()

ConsoleWrite("B running." & @CRLF)

$g_NextRun = "C"

EndFunc


Func C()

ConsoleWrite("C running." & @CRLF)

If Random(0, 1, 1) Then

Return SetError(1, $eThird)

Else

$g_NextRun = "Z"

EndIf

EndFunc


Func Z()

ConsoleWrite("Z running." & @CRLF)

Return SetError(1, $eNormalExit)

EndFunc


<실행 결과>

첫번째 실행 결과


A running.

A running with FirstError.


두번째 실행 결과


A running.

C running.

Z running.

Z normal exited.


세번째 실행 결과


A running.

C running.

C running with ThirdError.


역시나 실행할 때마다 결과가 달라진다. 이것을 단순히 If - Then 만으로 구현하겠다는 것은 헛 힘만 들이고 구현은 제대로 못하는 결과를 낳게 될 것이다.


더 개선할 방법은 없나?


개선 포인트 1)

단위 함수마다 인자가 사용되고 그 인자가 각기 다를 경우 대응 -> Call 함수에서 무조건 배열로 받도록 정의하고 단위 함수에서는 결과를 배열로 리턴


개선 포인트 2)

다음 실행 함수를 가르키는 전역 변수를 꼭 써야 하나? -> SetError 함수의 세 번째 파라미터는 스트링을 넘길 수가 있으며 여기를 사용하면 될 듯하다.

단, 단위 함수에서는 값을 리턴하지 않고 또 다른 단위 함수에서 다른 함수의 리턴 값을 입력으로 사용하지 않는다는 전제가 있다.


AU3의 도움말을 살펴보면 SetError의 세번째 파라미터는 함수 리턴과 동일한 역할을 한다. 즉, $vResult = Call(FuncA) 와 같을 때 $vResult에 다음번 실행할 함수 이름을 쓸 수 없다는 얘기다. SetError의 세번째 파라미터를 사용한다면 함수 리턴으로 값을 전달할 수는 없다.


반대로 함수간 값 전달이 빈번하지 않다면 이때 전역 변수를 쓰는 것도 고려할 만 하다.






반응형

관련글 더보기

댓글 영역