About VATOS
home
Company
home

techlog_12

← Tech Insights

내부 로직: 시스템 안에 사는 코드가 가장 옮기기 어렵다

프로시저·함수·트리거 등 DB 내부 코드 이관의 난점과 접근 방법을 정리합니다.

데이터베이스 안에는 데이터가 아닌 코드도 함께 살고 있습니다. 프로시저, 함수, 패키지, 트리거, 뷰, 머티리얼라이즈드 뷰, 그리고 잡 스케줄러입니다.

이 코드는 보이지 않습니다. 애플리케이션 화면이나 API 응답에 드러나지 않고, DDL을 추출하면 다른 객체에 비해 분량이 가장 큽니다. 그리고 어느 DB에서든 가장 깊이 박힌 메타데이터입니다. 데이터 타입과 저장구조를 모두 정확히 옮겨도, 내부 로직이 정확히 옮겨지지 않으면 시스템의 행동이 달라집니다. 사용자는 이 변화를 "기능이 사라졌다", "결과가 미묘하게 다르다"는 형태로 마주합니다.

내부 로직 이관이 가장 어려운 이유는 세 가지입니다. 첫째, 코드의 양이 가장 많고 방언이 가장 깊습니다. PL/SQL, PL/pgSQL, MySQL stored routine은 표면 문법은 비슷해 보여도 의미와 동작이 다른 곳에서 갈립니다. 둘째, DB 외부에서는 보이지 않습니다. 애플리케이션 코드 리뷰만으로는 잡히지 않는 로직이 DB 안에 누적되어 있습니다. 셋째, 객체 카테고리 자체가 다른 경우가 많습니다. Oracle의 패키지는 다른 DB에 직접 대응이 없고, 머티리얼라이즈드 뷰의 동작 모델은 DB마다 근본적으로 다릅니다.

이 글은 양방향, 즉 Oracle의 풍부한 PL/SQL 자산을 오픈소스로 옮기는 방향과 오픈소스의 단순한 내부 로직을 Oracle로 옮기는 방향 모두를 다룹니다.


세 DB의 내부 로직 모델 — 같은 객체 이름, 다른 깊이

내부 로직 영역은 두 축에서 차이가 납니다. 하나는 객체 카테고리 자체의 비대칭이고, 다른 하나는 언어와 런타임 특성의 비대칭입니다. 표를 둘로 나누어 봅니다.

객체 카테고리

객체OraclePostgreSQLMySQL
프로시저PROCEDUREPROCEDURE (11+)PROCEDURE
함수FUNCTIONFUNCTIONFUNCTION (DETERMINISTIC/SQL DATA 명시)
패키지PACKAGE / PACKAGE BODY(없음; SCHEMA + 함수 prefix로 우회)(없음)
트리거 단위FOR EACH ROW / STATEMENT, COMPOUNDFOR EACH ROW / STATEMENT (trigger function 별도)FOR EACH ROW만
INSTEAD OF 트리거지원 (뷰)지원 (뷰)미지원
뷰 옵션WITH READ ONLY, WITH CHECK OPTIONWITH CHECK OPTION, INSTEAD OF 트리거WITH CHECK OPTION, ALGORITHM
머티리얼라이즈드 뷰MV + MV Log (Fast Refresh, ON COMMIT/DEMAND, Query Rewrite)MV (전체 재생성 / CONCURRENTLY)네이티브 부재
Job / SchedulerDBMS_SCHEDULER, DBMS_JOB(legacy)pg_cron, pgAgent 등 외부 확장Event Scheduler (native)

언어·런타임 특성

특성OraclePostgreSQLMySQL
표준 PL 언어PL/SQLPL/pgSQL (+ PL/Python, PL/V8, PL/Perl 등)stored routine (SQL/PSM 기반)
자율 트랜잭션PRAGMA AUTONOMOUS_TRANSACTION(없음; PROCEDURE 11+ 내 COMMIT은 다른 의미)(없음)
컬럼/행 타입 참조%TYPE, %ROWTYPE%TYPE, %ROWTYPE (PL/pgSQL)(없음)
컬렉션 타입Index-By Table, VARRAY, Nested TableARRAY, 복합 타입(없음; JSON/문자열로 우회)
예외 모델명명 예외, PRAGMA EXCEPTION_INIT, SQLCODE/SQLERRMSQLSTATE 기반 EXCEPTION WHENDECLARE ... HANDLER, SIGNAL SQLSTATE
함수 오버로딩지원지원미지원

두 표가 함께 보여주는 핵심은 객체와 언어 양쪽에서 비대칭이라는 점입니다. 객체 측에서는 패키지, 자율 트랜잭션 객체화, MV Fast Refresh — Oracle만 가지는 것이 분명합니다. 반대로 PostgreSQL의 다중 PL 언어와 EVENT TRIGGER는 Oracle에 직접 대응이 없습니다. 언어 측에서는 예외 모델·컬렉션 타입·오버로딩 같은 기본 빌딩 블록부터 다릅니다. 이름만 같은 객체를 1:1로 옮기겠다는 전제는 첫 번째 함정입니다.


프로시저, 함수, 그리고 Oracle 패키지

세 DB 모두 PROCEDURE와 FUNCTION을 가집니다. 다만 도입 시점과 표현 방식에 차이가 있습니다.

PostgreSQL의 PROCEDURE는 11부터 정식 도입되었습니다. 그 이전에는 FUNCTION으로 절차적 로직을 모두 표현했고, 반환값이 필요 없으면 RETURNS void로 흉내 냈습니다. 따라서 10 이하 환경에서 올라온 PostgreSQL은 PROCEDURE가 있어야 할 자리에 VOID FUNCTION이 있는 경우가 많고, 이관 시점에 PROCEDURE로 재정의할지 결정해야 합니다. 11+ PROCEDURE의 가장 중요한 특징은 프로시저 안에서 COMMIT/ROLLBACK 호출이 가능하다는 점입니다. 다만 PostgreSQL PROCEDURE의 트랜잭션 제어는 CALL 실행 컨텍스트와 외부 트랜잭션 블록 여부에 제약을 받으므로, Oracle 자율 트랜잭션처럼 자유롭게 사용할 수는 없습니다.

MySQL의 FUNCTION은 DML 제약이 있습니다. 함수 정의 시 DETERMINISTIC, READS SQL DATA, MODIFIES SQL DATA, NO SQL을 명시해야 하고, 복제 환경에서 비결정적 함수는 제약을 받습니다. Oracle 함수에서 자유롭게 쓰던 패턴을 MySQL로 옮길 때는 이 속성 명시를 빠뜨리지 말아야 합니다.

Oracle 패키지는 다른 DB에 직접 대응이 없습니다. 패키지는 단순히 여러 프로시저·함수를 묶는 그릇이 아니라, 패키지 변수(세션 단위 전역 상태), 초기화 코드, 오버로딩, 공개·비공개 분리 같은 모듈 단위 기능을 함께 제공합니다. 이관 시 다음 매핑이 일반적입니다.

  • 패키지의 묶음 의미 → PostgreSQL의 SCHEMA로 묶거나, 함수 prefix 컨벤션으로 흉내(예: pkg_orders_create_order)
  • 패키지 변수(세션 단위 전역 상태) → PostgreSQL은 GUC 사용자 변수(SET LOCAL custom.var = ...), 임시 테이블, 또는 pg_temp 스키마 활용. MySQL은 세션 변수(@var) 또는 임시 테이블로 우회
  • 초기화 블록 → 패키지가 처음 호출될 때 실행되던 로직은 명시적 초기화 함수로 분리하고 애플리케이션이나 첫 호출 지점에서 호출
  • 함수 오버로딩 → PostgreSQL은 함수 오버로딩 지원, MySQL은 미지원이므로 이름을 분기시켜야 함
  • 공개·비공개 분리 → PostgreSQL은 SCHEMA 권한으로 부분 통제, MySQL은 명확한 대응 없음

자율 트랜잭션은 또 다른 큰 함정입니다. Oracle은 PRAGMA AUTONOMOUS_TRANSACTION으로 부모 트랜잭션과 독립된 별도 트랜잭션을 만들 수 있습니다. 감사 로그를 부모 트랜잭션이 롤백되어도 남기는 용도로 자주 쓰입니다. PostgreSQL과 MySQL에는 자율 트랜잭션이 없습니다. 앞서 언급한 PostgreSQL 11+ PROCEDURE 안의 COMMIT/ROLLBACK은 자율 트랜잭션과 다릅니다(트랜잭션을 분리하는 것이 아니라 현재 트랜잭션을 끊는 것). 우회 방법은 dblink로 자기 자신에게 별도 세션을 열어 INSERT하거나, pg_background 같은 확장을 쓰거나, 별도 큐/외부 시스템으로 분리하는 것입니다. 감사 로그가 자율 트랜잭션에 의존하던 시스템이라면 이관 시 아키텍처 재설계가 필요합니다.

예외 처리 모델도 세 DB가 모두 다릅니다. Oracle은 명명 예외(NO_DATA_FOUND, DUP_VAL_ON_INDEX 등)와 사용자 정의 예외, PRAGMA EXCEPTION_INIT(특정 ORA 코드를 사용자 예외명에 바인딩), 그리고 SQLCODE/SQLERRM을 통한 진단 정보 접근까지 풍부한 예외 의미론을 제공합니다. PostgreSQL은 SQLSTATE 기반 EXCEPTION WHEN 블록을 사용하며, WHEN unique_violation 같은 조건명 또는 WHEN SQLSTATE '23505' 코드로 분기합니다. Oracle 명명 예외와 SQLSTATE 사이의 일대일 매핑이 모두 자명하지는 않아 변환 시 의미가 미묘하게 어긋날 수 있습니다. MySQL은 모델 자체가 다른데, DECLARE ... HANDLER FOR SQLSTATE(또는 FOR SQLEXCEPTION/NOT FOUND) 로 핸들러를 사전 등록하고, SIGNAL SQLSTATE로 예외를 명시적으로 발생시킵니다. 핸들러 등록 방식이라 Oracle/PostgreSQL의 EXCEPTION 블록과는 코드 구조 자체가 달라집니다.

Oracle → PostgreSQL 변환에서 가장 자주 죽는 분기SELECT INTO + EXCEPTION WHEN NO_DATA_FOUND 패턴입니다. Oracle의 SELECT INTO는 0건이면 NO_DATA_FOUND, 2건 이상이면 TOO_MANY_ROWS를 자동으로 던집니다. 반면 PL/pgSQL의 SELECT INTOSTRICT를 명시하지 않으면 0건이어도 예외를 던지지 않고 변수에 NULL을 담으며, 여러 건이면 첫 행을 조용히 가져옵니다. 그래서 Oracle의 EXCEPTION WHEN NO_DATA_FOUND 분기를 그대로 옮기면 예외가 아예 발화하지 않아 분기 전체가 죽은 코드가 됩니다. PL/pgSQL에서 같은 의미를 보존하려면 SELECT INTO STRICT를 명시해야 하고, 이렇게 해야 0건은 no_data_found, 다건은 too_many_rows 예외가 발생합니다. 명명 예외 매핑 수준의 작업이 아니라 트리거 조건 자체가 달라지는 함정이라, 변환 시점에 SELECT INTO가 등장하는 모든 지점을 전수 점검해야 합니다. 이 글 전체가 경고하는 "동작은 하되 의미가 달라진다"의 대표 사례입니다.

Oracle → 오픈소스 방향에서 명명 예외와 SQLSTATE/HANDLER 사이 번역은 위 사례 외에도 의미가 완전히 일치하지 않는 경우가 자주 나오므로, 변환 후 예외 분기 단위 테스트가 필수입니다.

%TYPE%ROWTYPE 같은 타입 참조는 PostgreSQL PL/pgSQL이 동일 문법으로 지원합니다. MySQL은 지원하지 않으므로, 컬럼 타입 변경에 코드가 자동 적응하던 패턴이 사라집니다. 이관 후 컬럼 타입 변경 시점마다 PROCEDURE/FUNCTION을 함께 수정해야 한다는 운영 부담을 미리 인지해야 합니다.

컬렉션 타입(PL/SQL Index-By Table, VARRAY, Nested Table)은 PostgreSQL의 ARRAY 또는 복합 타입으로 대체 가능하지만 의미가 완전히 일치하지는 않습니다. MySQL은 JSON 컬럼이나 문자열 직렬화로 우회해야 하므로 변환 비용이 가장 큽니다.

Oracle → 오픈소스 방향에서 PL/SQL 이관 작업의 80%는 위 매핑의 반복입니다. 도구가 자동 변환해 주는 부분도 많지만, 패키지 변수와 자율 트랜잭션, 예외 모델은 수작업 재설계가 필수입니다. 자동 변환된 코드를 그대로 운영에 올리면 동작은 하되 의미가 달라져 있는 경우가 흔합니다.

오픈소스 → Oracle 방향에서는 다양한 PL 언어가 단일 PL/SQL로 수렴됩니다. PostgreSQL의 PL/Python으로 작성된 머신러닝 추론 함수가 있다면 Oracle 환경에서는 Java Stored Procedure 또는 외부화가 필요합니다. 반면 Oracle 쪽에서는 패키지로 묶을 기회가 생기므로, 흩어진 함수들을 패키지로 재구성하는 정리 작업의 시점이 됩니다.


트리거 — 같은 이름, 다른 모델

세 DB 모두 BEFORE/AFTER INSERT/UPDATE/DELETE 트리거를 가집니다. 그런데 모델이 다릅니다.

Oracle은 트리거 본문에 PL/SQL 블록을 직접 작성합니다. :NEW, :OLD로 행을 참조합니다. FOR EACH ROW와 FOR EACH STATEMENT를 모두 지원하고, 11g+ COMPOUND TRIGGER로 한 트리거 안에 BEFORE STATEMENT / BEFORE EACH ROW / AFTER EACH ROW / AFTER STATEMENT 시점을 모두 묶을 수 있습니다. 뷰에 INSTEAD OF 트리거를 걸 수 있고, mutating table 에러(ORA-04091)를 회피하는 패턴(자율 트랜잭션, COMPOUND TRIGGER의 행 컬렉션 패턴)이 자주 등장합니다.

PostgreSQL은 모델이 다릅니다. 트리거 함수를 먼저 만들고, 트리거가 그 함수를 호출하는 구조입니다. 트리거 함수는 RETURNS trigger이고, 콜론 없는 NEW/OLD를 사용합니다. FOR EACH ROW와 FOR EACH STATEMENT 모두 지원합니다. 같은 이벤트에 트리거가 여러 개면 이름 알파벳 순서로 실행됩니다. EVENT TRIGGER(DDL 이벤트에 대한 트리거)는 Oracle에 직접 대응이 없는 PostgreSQL 고유 기능입니다.

MySQLFOR EACH ROW만 지원합니다(FOR EACH STATEMENT 없음). 8.0부터는 같은 이벤트에 여러 트리거를 만들 수 있고 PRECEDES/FOLLOWS로 순서를 지정합니다(5.7까지는 같은 이벤트당 하나만 허용). INSTEAD OF 트리거 자체가 없어 뷰에 트리거를 걸 수 없습니다. NEW/OLD 참조는 PostgreSQL과 같이 콜론 없이 사용합니다.

Oracle → 오픈소스 방향의 주요 함정.

  • COMPOUND TRIGGER는 PostgreSQL/MySQL에 직접 대응이 없습니다. 시점별로 분리된 여러 트리거로 분해하거나, 시점 간 공유 상태를 어떻게 보존할지 재설계해야 합니다.
  • INSTEAD OF 트리거로 뷰를 업데이트 가능하게 만들던 패턴은 MySQL에서는 재현 불가능합니다. 뷰 자체를 업데이트 가능한 형태(단순 SELECT)로 바꾸거나, 애플리케이션이 직접 기반 테이블을 다루도록 변경해야 합니다.
  • Mutating table 회피 패턴(자율 트랜잭션, COMPOUND TRIGGER)은 오픈소스에서는 애초에 mutating table 제약이 다르므로 패턴 자체가 불필요해지는 경우도 있습니다. 그대로 옮기지 말고 필요성을 재검토해야 합니다.
  • :NEW/:OLD의 콜론 표기는 단순 치환이지만, 트리거가 OUT 파라미터처럼 사용되거나 PL/SQL 블록 안에서 복잡하게 얽혀 있으면 단순 치환으로는 안 됩니다.

오픈소스 → Oracle 방향에서는 PostgreSQL의 EVENT TRIGGER에 의존하던 DDL 감사 로직이 Oracle에서는 DDL 트리거나 Database Vault 같은 다른 메커니즘으로 옮겨져야 합니다. MySQL의 FOR EACH ROW 전제 코드를 Oracle로 옮길 때는 STATEMENT 단위 최적화 기회가 생기므로, 일괄 변환보다 패턴별 검토가 권장됩니다.


뷰와 머티리얼라이즈드 뷰

일반 뷰의 표준 옵션(WITH CHECK OPTION)은 세 DB 모두 지원합니다. 차이는 부가 옵션에 있습니다.

Oracle의 WITH READ ONLY는 PostgreSQL/MySQL에 직접 대응이 없습니다. PostgreSQL은 뷰에 INSTEAD OF 트리거로 거절 로직을 거나, GRANT에서 INSERT/UPDATE/DELETE를 빼는 방식으로 흉내 냅니다. MySQL은 뷰의 정의에 따라 자동으로 갱신 불가가 되는 경우(JOIN, GROUP BY, DISTINCT 등 포함 시)가 많아 자연스럽게 READ ONLY처럼 동작하지만, 명시적인 READ ONLY 옵션은 없습니다.

MySQL 뷰의 ALGORITHM 옵션(MERGE / TEMPTABLE / UNDEFINED)은 다른 DB에 직접 대응이 없는 MySQL 고유 옵티마이저 힌트입니다. MERGE는 뷰 정의를 메인 쿼리에 합쳐 실행하므로 성능에 유리하지만 모든 뷰가 MERGE 가능하지는 않습니다. TEMPTABLE은 뷰 결과를 임시 테이블에 저장하고 조회합니다. MySQL → 다른 DB 이관 시 이 힌트는 사라지고, 옵티마이저가 다시 판단하므로 실행 계획 검증이 필요합니다.

머티리얼라이즈드 뷰가 가장 큰 차이가 나는 영역입니다.

Oracle MV는 강력합니다. Fast Refresh(증분 갱신)를 위해 MV Log를 만들어 두면, 기반 테이블의 변경분만 MV에 반영합니다. ON COMMIT이면 트랜잭션 커밋과 동시에 자동 갱신되고, ON DEMAND면 명시적 호출 시점에 갱신됩니다. Query Rewrite를 켜면 옵티마이저가 사용자 쿼리를 자동으로 MV 조회로 바꿔주기까지 합니다.

PostgreSQL의 머티리얼라이즈드 뷰는 9.3에서 도입되었지만 모델이 단순합니다. REFRESH MATERIALIZED VIEW전체 재생성이고, CONCURRENTLY 옵션은 동시 조회를 허용할 뿐 갱신 비용 자체를 줄이지는 못합니다. Incremental refresh는 네이티브로 지원되지 않습니다. 일부 확장(IVM)이나 트리거 기반 수동 구현으로 흉내 낼 수 있지만, Oracle Fast Refresh의 자동성과는 거리가 있습니다.

MySQL은 머티리얼라이즈드 뷰가 네이티브에 없습니다. 테이블 + 트리거 + 배치로 직접 구현하거나, 외부 도구로 동기화해야 합니다.

Oracle → 오픈소스 방향에서 Fast Refresh + ON COMMIT MV를 쓰던 시스템은 가장 큰 재설계 대상이 됩니다. PostgreSQL로 가면 REFRESH 비용이 갱신 빈도에 비례해 누적되므로, 갱신 주기와 MV 크기를 다시 계산해야 합니다. 일부 MV는 일반 테이블 + 트리거 + 배치 갱신 패턴으로 다시 만드는 것이 효율적이고, 일부는 MV를 포기하고 그때그때 조회로 돌아가는 결정이 더 합리적인 경우도 있습니다. MySQL로 가는 경우는 MV 자체가 없으므로 전부 테이블 기반으로 재구성해야 합니다.

오픈소스 → Oracle 방향에서는 Oracle MV의 풍부한 옵션을 활용해 기존 배치 동기화 로직을 MV로 정리할 수 있는 기회가 됩니다. 다만 라이센스(Query Rewrite는 Enterprise) 확인이 필요합니다.


Job과 Scheduler

스케줄러는 운영 자동화의 핵심이지만, 세 DB의 모델이 가장 다릅니다.

Oracle DBMS_SCHEDULER는 풍부한 기능을 제공합니다(10g 이후). 잡, 잡 클래스, 잡 체인(여러 잡을 의존 관계로 묶기), 윈도우(시간대), 리소스 매니저 통합까지 운영 거버넌스의 일부로 자리잡혀 있습니다. 이전 세대의 DBMS_JOB은 legacy이지만 여전히 운영되는 시스템이 있습니다.

PostgreSQL은 잡 스케줄러가 네이티브에 없습니다. 가장 흔한 선택은 pg_cron 확장(Citus가 개발, cron 문법으로 SQL 실행 예약)이고, pgAgent도 사용됩니다. 둘 다 외부 확장이라 설치·운영·모니터링의 거버넌스가 별도로 필요합니다. 또는 OS 레벨 cron이나 별도 워크플로우 도구(Airflow 등)로 옮기는 것이 더 자연스러운 경우도 많습니다.

MySQL Event Scheduler는 native입니다(5.1+). event_scheduler 시스템 변수로 활성화하고, AT 또는 EVERY로 일회성/반복 잡을 정의합니다. DBMS_SCHEDULER만큼 풍부하지는 않지만 단순 반복 잡에는 충분합니다.

Oracle → 오픈소스 방향에서 가장 큰 결정은 잡 스케줄링을 DB 안에 둘 것인가, 밖으로 뺄 것인가입니다. pg_cron으로 DB 안에 두면 Oracle과 유사한 운영 모델을 유지할 수 있지만, 잡 체인 같은 의존성은 직접 구현해야 합니다. Airflow 같은 외부 워크플로우 엔진으로 빼면 의존성·재시도·모니터링 전반이 개선되지만 운영팀의 추가 도구가 늘어납니다. DBMS_SCHEDULER 잡이 수십 개 이상인 시스템이라면 이관은 거버넌스 결정의 기회입니다.

오픈소스 → Oracle 방향에서는 흩어진 cron 잡과 Event Scheduler 잡을 DBMS_SCHEDULER 하나로 통합하는 기회가 됩니다. 단, 외부 시스템 호출이 포함된 잡(HTTP 요청, 파일 처리 등)은 Oracle 환경에서의 대응 방식(UTL_HTTP, 외부 작업)이 적절한지 검토가 필요합니다.


한 가지 더 — DB 안의 로직, 정말 DB에 있어야 하는가

마지막으로 짚어야 할 관점이 있습니다. 이관은 DB 안의 로직을 어디까지 유지할지 결정할 자연스러운 기회이기도 합니다.

Oracle에서 DB가 절차적 코드의 중심이었던 시스템은 흔합니다. PL/SQL과 패키지가 잘 정비된 환경에서는 비즈니스 로직을 DB 안에 두는 것이 합리적인 선택이었고, 옵티마이저와 결합된 성능 이점도 분명했습니다. 그런데 오픈소스 환경에서는 같은 모델이 항상 최선은 아닙니다. 오픈소스 DB의 PL 런타임은 일반적으로 Oracle PL/SQL만큼 정교하게 최적화되어 있지 않고, 디버깅·테스트·버전 관리 같은 개발 인프라도 애플리케이션 언어 생태계에 비해 빈약합니다. 무엇보다 다중 인스턴스·수평 확장·읽기 복제본 분리 시점에서 DB 내 로직은 결합도와 병목의 원인이 되기 쉽습니다.

애플리케이션 계층이 충분히 성숙해 있다면, 단순 CRUD 래퍼나 검증·계산·집계 함수는 애플리케이션으로 밀어 올리는 것이 장기적으로 운영 비용을 줄입니다. 모든 PL/SQL을 다시 짜라는 의미가 아니라, 변환의 시점에 '이 로직이 정말 DB 안에 있어야 하는가'를 함수 단위·트리거 단위로 다시 묻는 것을 권합니다. 이관 작업이 어차피 코드를 손대는 시점이므로, 옮기는 것과 빼는 것의 한계 비용 차이가 가장 작은 시점이기도 합니다.

이 질문 없이 변환을 끝내면, 새 환경의 특성에 맞지 않는 코드가 그대로 남아 운영 동안 지속적인 부담이 됩니다. 반대로 모든 로직을 무조건 애플리케이션으로 빼는 것도 답이 아닙니다. 트랜잭션 일관성이 중요한 무결성 로직, 대량 데이터를 다루며 네트워크 왕복이 치명적인 배치, DB 메타데이터에 의존하는 동적 SQL은 DB 안에 남는 편이 자연스럽습니다. 핵심은 일괄 결정이 아니라 개별 판단이고, 그 판단의 비용이 가장 낮은 시점이 이관이라는 점입니다.


결론

내부 로직 이관은 보통 시리즈 마지막에 언급되는 영역이지만, 실제 이관 프로젝트에서는 가장 먼저 영향 범위를 측정하고 가장 일찍 작업을 시작해야 하는 영역입니다. 코드의 양이 가장 많고, 변환 자동화율이 가장 낮으며, 검증에 가장 많은 인력이 필요하기 때문입니다.

방향에 관계없이 다섯 가지 질문으로 수렴됩니다.

첫째, 패키지·자율 트랜잭션·머티리얼라이즈드 뷰 Fast Refresh처럼 타겟 DB에 직접 대응이 없는 객체가 얼마나 있는가. 그리고 각각을 어떤 패턴으로 대체할 것인가.

둘째, 트리거의 시점·단위·뷰 대상 여부(FOR EACH ROW/STATEMENT, COMPOUND, INSTEAD OF)가 타겟 DB에서 어떻게 분해되는가. mutating table 회피 같은 Oracle 고유 패턴은 타겟에서 여전히 필요한가.

셋째, 예외 모델 차이(명명 예외 ↔ SQLSTATE ↔ HANDLER)와 SELECT INTO 동작 차이까지 변환 후 분기 의미가 일치하는지 단위 테스트로 검증되었는가.

넷째, 잡 스케줄링을 DB 안에 둘 것인가, 외부 워크플로우 엔진으로 뺄 것인가. 이 결정이 운영 거버넌스 전반에 어떤 영향을 주는가.

다섯째, DB 안에 있는 각 로직이 정말 DB 안에 있어야 하는가. 이관은 이 질문을 가장 낮은 비용으로 던질 수 있는 시점이다.

질문 다섯 개에 답이 나왔다면, 운영 절차 측면에서 두 가지를 더 챙겨야 합니다. 하나는 내부 로직 안에 숨어 있는 의존성의 전수 조사입니다. DBMS_xxx 패키지 호출, UTL_HTTP, DB Link, 데이터 사전 참조처럼 변환 도구가 매핑하지 못하는 부분은 사전 추출 후 타겟 DB 대응을 별도로 결정해야 합니다. 다른 하나는 이관 직후 내부 로직 객체가 실제로 정상 동작하는지 일괄 검증하는 것입니다. 다만 이 검증의 성격이 DB마다 다릅니다. Oracle은 PROCEDURE/FUNCTION/TRIGGER/PACKAGE가 의존성이 깨지면 INVALID 상태로 데이터 사전에 영속 기록되므로, utlrp.sql로 의존성 순서에 따라 일괄 재컴파일해 INVALID를 해소하는 것이 표준 절차입니다. 반면 PostgreSQL에는 Oracle 같은 INVALID 객체 개념 자체가 없습니다. 함수는 소스로 저장되고 세션 최초 호출 시점에 지연 컴파일되며, 깨진 의존성은 영속 플래그가 아니라 실행 시점 에러로 드러납니다. 따라서 PostgreSQL에서는 재컴파일이 아니라 모든 함수를 실제로 호출(또는 테스트 실행)해 검증해야 하고, plpgsql_check 같은 확장으로 정적 점검을 보강할 수 있습니다. 어느 쪽이든 PL/SQL 비중이 큰 이관에서는 사실상 필수 절차이며, 이 단계를 건너뛰면 깨진 객체가 첫 호출 시점에 줄줄이 드러납니다.

이 질문과 절차가 모두 마무리되기 전에 코드 변환 도구를 돌리기 시작하면, 변환 결과의 80%는 동작하지만 나머지 20%가 운영 안정화 기간 내내 사고로 돌아옵니다. 그리고 그 20%는 보통 가장 오래된 코드, 가장 적게 문서화된 코드, 가장 자주 호출되는 코드에 분포합니다.

다음 글에서는 통계 정보와 옵티마이저 — 인덱스 편(11)에서 다룬 "옵티마이저는 DB마다 다르게 생각한다"의 입력이 되는 메타데이터 — 를 살펴봅니다.

← 목록으로 돌아가기