출제기준 세세항목: 프로시저 · 프로그램 디버깅 · 단위테스트 도구
쇼핑몰에서 주문 하나가 들어오면 데이터베이스에서는 생각보다 많은 일이 일어납니다. 재고를 확인하고, 재고를 줄이고, 주문 기록을 남기고, 적립금을 계산해 넣습니다. 이 SQL 문들을 애플리케이션이 하나씩 데이터베이스에 보내면 그때마다 요청과 응답이 네트워크를 오가야 합니다.
이 일련의 처리를 하나의 이름으로 묶어 데이터베이스 안에 저장해 두고, 애플리케이션은 이름만 불러 실행하게 할 수 있습니다. 이렇게 SQL 문과 변수, 조건문, 반복문, 예외 처리를 묶어 데이터를 조회·입력·수정·삭제하는 프로그램을 데이터 조작 프로시저라고 합니다. Oracle의 PL/SQL, SQL Server의 T-SQL처럼 DBMS마다 이런 절차형 SQL을 쓰는 문법을 제공합니다.
데이터 조작 프로시저는 데이터가 저장된 곳에서 바로 처리되므로, 데이터를 애플리케이션으로 잔뜩 가져와 가공한 뒤 다시 보내는 방식보다 효율적인 경우가 많습니다. 또 애플리케이션에 테이블 직접 접근 권한을 주지 않고 프로시저 실행 권한만 주면 데이터를 다루는 길을 좁힐 수 있어 보안에도 도움이 됩니다. 다만 업무 규칙이 데이터베이스 안에 흩어지면 관리가 어려워집니다. 또한 PL/SQL, T-SQL처럼 문법이 DBMS마다 달라 다른 DBMS로 옮기기 어렵고, 애플리케이션 코드보다 디버깅 도구가 부족해 문제를 찾기 까다로운 편입니다. 그래서 어떤 처리를 프로시저로 둘지는 신중히 정해야 합니다.
절차형 SQL로 만드는 대표적인 형태는 세 가지이며, '어떻게 실행되는가'와 '무엇을 돌려주는가'로 구분하면 쉽습니다. 저장 프로시저(Stored Procedure)는 사용자나 애플리케이션이 이름으로 호출해 실행합니다. 결과를 꼭 돌려줄 필요는 없고, 필요하면 출력용 매개변수로 여러 값을 내보낼 수 있습니다. 주문 처리처럼 여러 단계로 된 작업을 묶는 데 잘 맞습니다.
사용자 정의 함수는 프로시저와 비슷하게 호출해 쓰지만, 반드시 RETURN으로 값 하나를 돌려준다는 점이 다릅니다. 값을 돌려주기 때문에 SELECT 문 안에서 다른 열처럼 섞어 쓸 수 있습니다. '회원 등급별 할인율 구하기'처럼 계산 결과 하나가 필요한 곳에 어울립니다.
트리거(Trigger)는 사람이 직접 부르지 않습니다. 특정 테이블에 INSERT, UPDATE, DELETE 같은 이벤트가 일어나면 데이터베이스가 자동으로 실행합니다. 주문 테이블에 행이 추가될 때마다 변경 이력 테이블에 기록을 남기는 식으로, 놓치면 안 되는 후속 처리를 보장하는 데 씁니다. 다만 눈에 보이지 않게 실행되므로 너무 많이 쓰면 데이터가 왜 바뀌었는지 추적하기 어려워집니다.
프로시저는 보통 CREATE PROCEDURE 문으로 만들고(이미 있으면 바꾸도록 CREATE OR REPLACE를 쓰기도 합니다), EXECUTE나 CALL 같은 명령으로 실행하며, DROP PROCEDURE로 지웁니다. 프로시저 이름 뒤에는 매개변수를 적는데, 값을 받기만 하는 IN, 값을 내보내기만 하는 OUT, 받은 값을 바꿔 다시 내보내는 IN OUT(INOUT)으로 방향을 정합니다.
본문은 몇 개의 블록으로 나뉩니다. 처음에는 사용할 변수와 커서 등을 선언하는 부분(DECLARE)이 오고, BEGIN과 END 사이에 실제 처리를 적습니다. 그 안에서 IF, CASE, LOOP 같은 제어문으로 흐름을 정하고, SELECT·INSERT·UPDATE·DELETE 같은 SQL 문으로 데이터를 다룹니다. 오류가 났을 때 할 일은 예외 처리 부분(EXCEPTION)에 따로 모아 두고, 작업을 확정하거나 되돌리는 COMMIT과 ROLLBACK으로 트랜잭션을 마무리합니다.
여러 행을 하나씩 꺼내 처리해야 할 때는 커서(Cursor)를 씁니다. 커서는 SELECT 결과를 한 행씩 가리키는 손가락 같은 것으로, 열고(OPEN), 한 행씩 가져오고(FETCH), 다 쓰면 닫는(CLOSE) 순서로 사용합니다. 작성할 때는 예외 처리를 빠뜨리지 않는 것, 그리고 오류가 나면 반쯤 바뀐 데이터가 남지 않도록 트랜잭션 범위를 분명히 하는 것이 가장 중요합니다.
테스트와 디버깅은 자주 섞여 쓰이지만 역할이 다릅니다. 테스트는 '이 입력에서 결과가 틀린다'처럼 결함이 있다는 사실을 드러내는 활동이고, 디버깅은 그 결함이 코드의 어디에서 왜 생기는지 찾아내 고치는 활동입니다. 병원에 빗대면 검사가 테스트이고, 원인을 진단해 치료하는 일이 디버깅입니다.
디버깅은 대개 같은 흐름으로 진행됩니다. 먼저 문제가 일어나는 상황을 똑같이 다시 만들어(재현) 언제 생기는지 확인합니다. 다음으로 범위를 좁혀 가며 원인이 되는 위치를 찾고, 고친 뒤에는 원래 문제가 사라졌는지와 함께 다른 곳이 새로 깨지지 않았는지(회귀 테스트)까지 확인합니다. 재현이 안 되는 문제는 고쳤는지 확인할 방법도 없으므로, 재현 조건을 정확히 기록하는 것이 첫걸음입니다.
디버깅 도구를 쓰면 원인을 훨씬 빨리 찾을 수 있습니다. 원하는 줄에 중단점(Breakpoint)을 걸어 실행을 멈추고, 한 줄씩 실행(단계 실행)하면서 변수 값이 언제 이상해지는지 지켜볼(Watch) 수 있습니다. 어떤 함수들을 거쳐 지금 위치까지 왔는지는 호출 스택으로 확인합니다. 데이터베이스 프로시저는 개발 도구가 제공하는 디버거를 쓰거나, 중간 값을 화면에 출력하는 기능(Oracle의 DBMS_OUTPUT 등)과 오류 로그 테이블을 이용해 흐름을 따라가는 경우가 많습니다.
프로시저에서 자주 만나는 결함도 알아 두면 좋습니다. 조건에 맞는 행이 없거나 여러 개일 때의 예외를 처리하지 않은 경우, 반복문의 경계값이 하나 어긋난 경우, 값이 비어 있는(NULL) 경우를 고려하지 않아 비교나 계산 결과가 엉뚱해지는 경우가 대표적입니다.
단위 테스트는 프로그램을 이루는 가장 작은 단위, 즉 함수나 프로시저, 모듈 하나를 다른 부분과 떼어 놓고 검사하는 테스트입니다. 자동차를 조립하기 전에 브레이크 부품만 따로 시험대에 올려 확인하는 것과 같습니다. 부품 단계에서 결함을 잡으면 원인 범위가 좁아 고치기 쉽고 비용도 적게 듭니다. 단위 테스트에서는 코드 내부 구조를 보고 경로를 검사하는 화이트박스 테스트와, 입력과 출력만 보고 명세대로 동작하는지 확인하는 블랙박스 테스트를 함께 씁니다.
모듈 하나를 떼어 놓으면 주변이 비어 있다는 문제가 생깁니다. 내 모듈을 불러 줄 상위 모듈이 아직 없으면 그 대신 테스트용 입력을 넣고 호출해 주는 임시 코드를 만드는데, 이것을 테스트 드라이버(Driver)라고 합니다. 반대로 내 모듈이 불러 써야 하는 하위 모듈이 아직 없으면 정해진 값만 돌려주는 가짜 모듈을 끼워 두는데, 이것을 테스트 스텁(Stub)이라고 합니다. '위에서 불러 주는 쪽이 드라이버, 아래에서 대신 답해 주는 쪽이 스텁'으로 기억하면 됩니다.
좋은 단위 테스트는 몇 번을 돌려도 같은 결과가 나오고, 다른 테스트의 실행 순서나 결과에 영향을 받지 않으며, 사람이 지켜보지 않아도 자동으로 성공·실패를 판정합니다. 이를 위해 테스트 코드 안에 '이 입력이면 결과는 이 값이어야 한다'는 기대값 비교(단언, assertion)를 적어 둡니다.
단위 테스트를 사람이 매번 손으로 돌리면 금방 지치고 빠뜨리게 됩니다. 그래서 테스트 코드를 정해진 형식으로 작성해 두면 한꺼번에 실행하고 결과를 보고해 주는 도구, 즉 테스트 프레임워크를 씁니다. 가장 널리 알려진 것은 xUnit이라고 묶어 부르는 계열로, 언어마다 이름만 다르고 구조는 비슷합니다. Java용 JUnit, C++용 CppUnit, .NET용 NUnit이 대표적이며, 웹 페이지를 대상으로 하는 HttpUnit도 같은 계열에 속합니다.
단위 테스트 자동화를 넘어 테스트 전반을 돕는 도구들도 함께 출제됩니다. FitNesse는 웹 기반으로 테스트 케이스를 표 형태로 작성하고 실행할 수 있게 해 주어 개발자가 아닌 사람도 테스트 작성에 참여하기 쉽습니다. STAF는 서로 다른 환경에 흩어진 테스트를 서비스 호출 방식으로 자동화하는 프레임워크이고, NTAF는 FitNesse와 STAF의 장점을 합친 테스트 자동화 프레임워크입니다. Selenium은 여러 브라우저에서 웹 애플리케이션 동작을 자동으로 시험하며, watir는 Ruby 언어로 웹 애플리케이션을 테스트하는 도구입니다.
데이터베이스 프로시저도 이런 방식으로 테스트할 수 있습니다. PL/SQL용 utPLSQL처럼 프로시저 전용 단위 테스트 프레임워크를 쓰거나, 애플리케이션 쪽 xUnit 테스트에서 테스트용 데이터를 넣고 프로시저를 호출한 뒤 결과 테이블을 확인하는 방식으로 검사합니다. 어느 쪽이든 테스트 전에 데이터를 준비하고 끝난 뒤 원래대로 되돌려, 테스트끼리 서로 영향을 주지 않게 하는 것이 핵심입니다.
주문 한 건마다 애플리케이션이 재고 조회, 재고 차감, 주문 기록, 적립금 반영 SQL을 따로 보내 네트워크 왕복이 네 번 일어나고 있었습니다. 이 과정을 '주문_처리'라는 저장 프로시저로 묶고 중간에 하나라도 실패하면 ROLLBACK하도록 만들자, 왕복은 한 번으로 줄고 재고만 줄고 주문은 기록되지 않는 어중간한 상태도 사라졌습니다.
매달 마지막 날에만 정산 금액이 하루치 모자라는 결함이 신고되었습니다. 월말 날짜를 넣어 문제를 재현한 뒤 중단점을 걸고 반복문을 한 줄씩 실행해 보니, 반복 조건이 '마지막 날 이하'가 아니라 '마지막 날 미만'으로 적혀 있었습니다. 경계값이 하나 어긋난 결함으로, 조건을 고친 뒤 월초·월중·월말 날짜로 다시 테스트해 다른 날의 결과도 그대로인지 확인했습니다.
할인 계산 모듈을 먼저 완성했는데, 이 모듈이 불러야 하는 결제 모듈은 아직 개발 중입니다. 결제 모듈 자리에 항상 '승인'만 돌려주는 스텁을 끼우고, 여러 금액을 넣어 할인 계산 모듈을 불러 주는 드라이버를 JUnit 테스트로 작성했습니다. 덕분에 결제 모듈이 완성되기 전에도 할인 계산의 결함을 미리 찾아 고칠 수 있었습니다.