글을 시작하기에 앞서 해당 시리즈는 Allen Downey, Ben Lauwens의 저서인 Think Julia: How to Think Like a Computer Scientist 를 바탕으로 작성된 글임을 알려드립니다.
이 포스트는 Case Study: Interface Design를 한글로 요약 정리한 글입니다.
사례 연구: 인터페이스 디자인
이번 장에서는 함수들을 설계하는 과정들을 볼 수 있는 사례 연구를 살펴볼 것이다.
이를 진행하기 위해서는 그림을 생성하는 거북이 그래픽(turtle graphics)을 사용해야 한다. 거북이 그래픽은 표준 라이브러리에 포함되어 있지 않기 때문에 해당 모듈을 줄리아 설정에 추가해야 한다.
거북이
모듈(module)은 관련 함수들을 모아 놓은 파일이다. 줄리아는 표준 라이브러리에서 몇 가지 모듈을 제공하고 있으며, 다른 패키지들도 모듈로 추가할 수 있다.
패키지를 추가하는 방법은 다음과 같다. 먼저 REPL 창에 엔터 키를 눌러서 시작한다. 이후 REPL에서 ]
를 입력하면 패키지 설정 모드로 변환된다. 거북이 그래픽을 사용하기 위해서는 아래의 코드를 입력한다.
1 | (v1.0) pkg> add https://github.com/BenLauwens/ThinkJulia.jl |
모듈에 포함되어 있는 함수를 사용하려면, 먼저 using
이라는 명령어를 사용하여 모듈을 가져와야 한다.
1 | julia> using ThinkJulia |
ThinkJulia
모듈은 Luxor.Turtle
이라는 객체를 생성하는 Turtle
함수를 제공하며, 해당 객체는 🐢
라는 변수에 할당된다.
거북이를 만든 후에는 함수를 호출하여 거북이를 이동할 수 있다. 예를 들어 거북이를 앞으로 움직이려면 다음과 같은 코드를 입력하면 된다.
1 | begin |
@svg
키워드는 SVG그림을 그리는 매크로를 실행한다. 매크로는 줄리아의 고급 기능 중 하나이다.forword
의 인수는 🐢와 픽셀 단위의 거리이므로 실제 크기는 각자 디스플레이에 따라 결정된다.
거북이와 함께 호출할 수 있는 다른 함수로는 거북이를 회전시키는 turn
이 있으며, 해당 함수의 첫 번째 인수는 🐢가, 두 번째 인수에는 회전 각도가 들어간다.
또한 각각의 거북이들은 내리거나 올릴 수 있는 펜을 잡고 있다. 펜을 내린다면 거북이가 움직였던 경로들을 남기며, 반대로 펜을 올리면 경로들을 남기지 않는다. 즉, 펜이 내려간 상태에서 거북이가 앞으로 움직이면 거북이가 앞으로 간 경로가 남겨지는 것이다. 이런 함수를 penup
, pendown
이라고 부른다.
1 | 🐢 = Turtle() |
간단한 반복
다음과 같이 한번 써보자.
1 | 🐢 = Turtle() |
위의 예시는 사각형을 그리는 코드이다.
우리는 for
문을 사용하여 위의 예시와 같은 똑같은 작업을 더 간단하게 수행할 수 있다.
1 | julia> for i in 1:4 |
해당 예시는 for
문의 가장 간단한 예시이다. for
문에 대한 자세한 내용은 다른 장에서 더 살펴볼 예정이다.
지금부터 위의 간단한 for
문을 이용해서 사각형을 그려보자.
1 | 🐢 = Turtle() |
for
문의 문법은 함수를 정의하는 것과 비슷하다. 헤더(header)와 본문이 있으며, 마지막에는 키워드 end
를 사용한다.
또한 for
문은 본문을 헤더에 지정된 수만큼 반복하므로 루프(loop)라고도 부른다. 위의 예시의 경우는 (i in 1:4
처럼 4번이라고 지정했음) 4번 반복 실행한다.
예시 풀어보기
이번 장에서는 거북이를 사용하여 여러 가지 연습을 해볼 것이다.
- 거북이를
t
매개 변수로 사용하는square()
를 작성하시오.
1 | function square() |
t
를sqaure()
의 인수로 전달하는 함수를 작성한 후, 매크로를 실행하시오.
1 | function square(t) |
- 변의 길이가
len
이 되도록 본문을 수정한 후,len
이라는 다른 매개 변수를square()
에 추가하시오.
1 | function square(t, len) |
square()
의 복사본을 만들고 이름을polygon()
으로 변경하시오. 그 후 새로운 매개변수n
을 추가하고 본문을 수정하시오. (n의 외부각도는 360/n 으로 수정하면 된다.)
1 | function polygon(t,len,n) |
t
와 반지름r
를 매개 변수로 하는circle()
를 작성하시오.
1 | function circle(t, r) |
캡슐화
첫 번째 연습에서는 사각형 그리기 코드를 함수 정의에 넣은 후 거북이를 매개 변수를 전달하여 함수를 호출하도록 요청한다. 해당 코드는 다음과 같다.
1 | function square(t) |
square()
가장 안쪽에 있는 명령문 forward
와 turn
은 for
문에 속해있다는 것을 나타내기 위해 두 번 들여쓰기로 작성한다.
위의 예시와 같이 코드 조각들을 함수로 감싸는 것을 캡슐화(Encapsulation)라고 한다. 캡슐화의 이점 중 하나는 코드에 이름을 첨부하여 프로그램 과정을 명확하게 정리할 수 있다는 것이다. 또한 한 번 캡슐화를 진행한 이후에는 코드를 재사용할 때 함수 하나만 호출하면 되기 때문에 간편하다.
일반화
다음으로는 len
이라는 매개 변수를 추가하는 것이다. 코드는 아래와 같다.
1 | function square(t, len) |
함수에 매개 변수를 추가하는 것은 해당 함수를 보다 다양한 환경에서 일반적으로 사용할 수 있게 만들어주기 때문에 일반화(Generalization)라고 한다. 매개 변수 len
을 추가함으로써 square()
는 이제 사각형의 길이를 조정할 수 있게 되었다.
1 | function polygon(t, n, len) |
위의 코드는 칠각형을 그리는 코드 예시이다.
인터페이스 디자인
다음 단계는 반지름 r
를 매개 변수로 하는 circle()
를 작성하는 것이다. 다음은 다각형 함수인 polygon()
을 사용하여 20면 다각형을 그리는 코드이다.
1 | function circle(t, r) |
첫 번째 줄은 반지름이 r
인 원의 둘레를 계산한다. n
은 각도가 변형되는 수이며, len
은 선분의 길이이기 때문에 둘레를 n
으로 나눠 정의한다. 그러면 20면 다각형을 그릴 수 있다.
위의 해결안은 문제는 n
이 상수라는 것이다. 즉, 매우 큰 원의 경우 선분이 너무 길고 작은 원의 경우는 작은 선분을 그리는데 시간이 낭비된다. 따라서 n
을 매개 변수로 사용하여 일반화한다면, 이 문제를 해결할 수 있다.
하지만 이런 일반화는 사용자에게 더 많은 제어권을 부여하지만 인터페이스는 덜 깨끗해진다.
함수의 인터페이스(interface)는 어떻게 사용되는지에 대한 요약이다. 구체적으로 인터페이스는 매개 변수는 무엇인지, 반환 값은 무엇인지 등의 내용들을 포함하고 있다고 보면 된다.
보통 사용자가 불필요한 세부 사항을 처리할 필요 없이 작업이 수행되는 함수들을 인터페이스가 깨끗하다고 말한다.
예를 들어, 위의 예제에서 r
은 그릴 원을 지정하므로 인터페이스에 속한다. 하지만 n
은 원을 그리는 방법에 대한 세부 사항과 관련이 있기 때문에 인터페이스에 속하지 않는다.
따라서 인터페이스를 어지럽히지 말고 둘레에 따라 적절한 n
값을 선택하는게 좋다.
1 | function circle(t, r) |
위의 코드는 len
의 값이 3에 가까울 수 있도록 n
을 자동 설정한 코드다. len
의 값은 호선의 길이로서 3정도가 원 크기와 상관없이 가장 적합하기에 3으로 결정하였다.
n
값에 3을 더해준 것은 다각형의 선분이 적어도 3 이상이라는 것을 보장한다.
리팩토링
이전 장에서 circle()
를 설계할 때는 polygon()
를 재사용할 수 있었다. 하지만 arc()
는 지금까지 만들었던 함수들을 이용할 수 없다.
한가지 대안으로는 다각형의 사본을 만들고, 호선으로 변환하는 것이다. 결과는 다음 코드와 같다.
1 | function arc(t, r, angle) |
이 함수의 후반부는 polygon()
와 유사해보이지만 핵심요소인 인터페이스를 변경했기 때문에 재사용한 것이라고 볼 수는 없다. 따라서 위의 예시는 angle
이라는 변수를 추가하여 polygon()
을 일반화하였기 때문에 해당 함수는 더 이상 다각형(polygon)
라는 이름을 가질 수 없다.
지금부터는 더 일반적으로 사용할 수 있는 polyline()
을 만들어보자.
1 | function polyline(t, n, len, angle) |
위의 polyline()
을 이용하여 polygon()
과 arc()
을 더 간단하게 캡슐화할 수 있다.
1 | function polygon(t, n, len) |
마지막으로 arc()
을 이용하여 circle()
를 간단하게 표현할 수 있다.
1 | function circle(t, r) |
지금까지 복잡했던 코드들을 일반화하고 캡슐화하여 정리하였다. 이와같이 인터페이스를 개선하고 코드 재사용을 위해 프로그램을 정리하는 과정을 리팩토링이라고 한다. 이 경우는 호를 구하는 arc()
과 다각형을 구하는 polygon()
의 유사점을 발견하여 polyline()
으로 정리하였다.
프로그램을 미리 계획한 경우에는 먼저 polyline()
을 작성하여 리팩토링을 생략했겠지만, 대부분 프로젝트 시작시에 모든 인터페이스를 설계하지 못한다. 따라서 리팩토링을 하는 법을 알아두는 것도 중요하다.
개발 계획
개발 계획은 프로그램을 만드는 프로세스이다. 이 사례 연구에서 사용한 프로세스는 “캡슐화 및 일반화”이며, 이 프로세스의 단계는 다음과 같다.
- 함수 정의 없이 작은 프로그램을 작성하기.
- 일단 프로그램이 작동하면, 일관된 부분을 찾아서 함수로 캡슐화하고 이름을 지정하기
- 함수에 적절한 매개 변수를 추가하여 함수를 일반화하기
- 프로그램 기능이 원하는만큼 나올 때까지 1-3단계를 반복하기
- 리팩토링을 통해 프로그램을 개선하기 (캡슐화와 일반화 다시 점검)
이 프로세스에는 몇 가지 단점이 있기 때문에, 이후 단점에 대해 알아보고 대안을 논의할 것이다. 하지만 프로그램을 함수로 설정하는 방법을 모르는 경우에는 유용하게 사용할 수 있는 방법이다.
독스트링(Docstring)
독스트링(Docstring)의 ‘Doc’은 ‘documentation’의 줄임말이며, 이는 함수 앞에 인터페이스를 설명하는 설명을 의미한다. 아래 예시를 보자.
1 | """ |
위의 예시와 같은 문법(“””
)을 사용하여 함수 앞에 작성하며, 이후 REPL에서 ?
를 입력하고 함수의 이름을 치면 문서를 확인할 수 있다.
1 | help?> polyline |
독스트링에는 해당 함수를 사용하기 위한 필수적인 정보가 포함되어 있다. 따라서 팀 단위로 개발을 하는 경우에는 만든 함수에 독스트링을 붙여 다른 사람들도 쉽게 사용할 수 있도록 하는 것이 좋다.
디버깅
인터페이스는 함수와 사용자 사이의 계약과 같다. 사용자는 특정 매개 변수를 제공하고 함수는 특정 작업을 수행한다.
예를 들어 polyline()
에서는 4개의 인수가 필요하다. t는 Turtle()
이어야 하며 n
은 정수여야 하고, len
은 양수여야 한다. angle
은 각도로 인식되어야 하며, 도 단위로 설정되어 있다.
이런 요구사항들은 함수가 실행되기 전에 true
로 간주되기 때문에 전제조건(preconditions)이라고 한다. 반대로 함수 작동 끝에 생성되는 결과 등을 사후 조건(postconditions)라고 한다.
전제 조건의 오류는 사용자의 책임이다. 사용자들이 전제 조건을 어겨서 함수가 제대로 작동하지 않는다면 이는 사용자측에 버그가 있는 것이다. (함수에는 버그가 없다.)
하지만 만약 전제 조건은 알맞게 작성했는데 사후 조건이 이상하다면, 이것은 함수에 버그가 있는 것이다. 따라서 해당 함수의 전제 조건과 사후 조건을 명확하게 아는 것은 디버깅에 많은 도움을 준다.