[자바/데이터베이스] 프로시저와 ORM
2026. 10. 5.
비즈니스 로직의 위치와 패러다임의 변화
백엔드 시스템 아키텍처를 설계할 때 핵심 쟁점 중 하나는 비즈니스 로직을 데이터베이스 계층에 둘 것인가, 애플리케이션 계층에 둘 것인가이다. 과거 모놀리식 환경과 상용 RDBMS 중심 환경에서는 데이터베이스의 저장 프로시저(stored procedure)를 통해 비즈니스 연산과 데이터 가공을 일괄 처리하는 패턴이 보편적이었다.
하지만 비즈니스 로직이 복잡해지고 비즈니스와 데이터를 분리하려는 방법이 표준으로 자리잡게 되면서 데이터베이스는 단순히 데이터를 저장하는 저장소로 역할이 분리될 필요성이 생겨나게 되었다. 또한 클라우드 네이티브 아키텍처, 마이크로서비스가 널리 도입되고 단위 테스트와 CI/CD 기반의 지속적 통합 환경으로 전환되면서 로직의 주도권은 애플리케이션 계층으로 이동하고 있다. 자바 진영에서는 SQL 중심의 SQL 매퍼(mapper)(예: MyBatis)와 객체 중심의 ORM(예: JPA/Hibernate + QueryDSL)이 데이터 접근 계층(data access layer)의 두 축을 형성하며 발전해 왔다. 이 두 프레임워크를 영속성 프레임워크(persistence framework)라고 한다. SQL 매퍼와 ORM은 매핑 대상과 SQL 생성 주체, 상태 관리 등의 측면에서 여러 차이가 있다.
저장 프로시저 (Stored Procedure)
저장 프로시저(이하 프로시저)는 RDBMS 서버 내부에 컴파일되어 저장되는 일련의 절차형 SQL(PL/SQL, T-SQL, PL/pgSQL 등) 코드 블록을 말한다.
프로시저는 애플리케이션 서버와 데이터베이스 서버 간에 오가는 다수의 쿼리 요청과 응답을 단 한 번의 원격 호출로 묶어 처리함으로써 네트워크 왕복 비용(round-trip)을 최소화한다는 강점이 있다. 또한 데이터베이스 엔진 내부 메모리 영역에서 대량의 데이터를 직접 집계하고 가공한 뒤 최종 결과만을 반환하므로 I/O 대역폭을 크게 절약할 수 있다. 컴파일 및 파싱을 거친 실행 계획(execution plan) 역시 프로시저 캐시에 보관되어 반복 실행 시 파싱 오버헤드가 줄어든다. 다만 현대적인 RDBMS 환경에서는 파라미터화된 일반 PreparedStatement 역시 유사하게 플랜 캐싱을 지원하고 있다.
반면 프로시저는 시스템 아키텍처 관점에서 컴퓨팅 리소스 확장의 불균형 문제를 야기한다. 상태를 갖지 않는(stateless) 애플리케이션 서버는 구조적으로 컨테이너 기반 수평 확장(scale-out)이 저비용으로 용이하다. 그러나 RDBMS는 트랜잭션 정합성(ACID)과 복제 제약으로 인해 수평 확장이 매우 복잡하고 비용이 크다. 프로시저에 복잡한 연산 로직을 집중시키면 고가의 데이터베이스 서버 CPU가 연산 병목 지점이 된다.
또한 격리된 단위 테스트(isolated unit testing)가 불가능하여 실제 DB 인스턴스와 스키마 상태에 강하게 종속되므로 테스트 용이성과 지속적 배포(CI/CD) 측면에서도 뚜렷한 한계를 보인다. 형상 관리와 코드 리뷰, 롤백, 릴리스 파이프라인의 자동화가 애플리케이션 코드에 비해 복잡하며, Oracle(PL/SQL), MS-SQL(T-SQL), PostgreSQL(PL/pgSQL) 간 문법과 내장 함수 정의가 상이하여 특정 벤더에 강하게 종속(vendor lock-in)되는 문제가 발생한다.
SQL 매퍼
SQL 매퍼(SQL mapper)란 SQL을 자바 객체로 매핑하는 도구이다. 개발자가 명시적으로 작성한 SQL 문(SQL statement)을 애플리케이션의 POJO 객체로 매핑해주는 영속성 프레임워크이다. SQL 매퍼는 관계형 데이터베이스의 테이블을 객체로 직접 연결하는 것이 아니라 개발자가 정의한 SQL 쿼리의 입력 파라미터(Parameter)와 쿼리 실행 결과(ResultSet)를 객체의 필드로 매핑해 주는 역할을 수행한다. 대표적인 구현체로는 MyBatis가 있다.
SQL 매퍼는 SQL 중심의 명시적 제어를 수행한다. 개발자가 XML 파일 또는 인터페이스 어노테이션(annotation)에 순수 SQL을 직접 작성하므로 복잡한 조인, 힌트, 전용 함수 정의 등 SQL에 대한 직접적인 완전한 제어가 가능하다. 프레임워크는 SQL을 내부적으로 생성하지 않으며 작성된 쿼리를 그대로 실행하기만 한다. 실행 시 입력 파라미터와 결과를 자바 객체에 매핑한다.
SQL 매퍼는 입출력 데이터 바인딩 처리를 자동화한다. 기본적으로 #{property} 문법을 통해 JDBC PreparedStatement의 바인드 변수로 파라미터를 처리한다. 이를 통해 SQL 인젝션을 원천 차단하고 RDBMS의 실행 계획 재사용을 보장한다. 단, ${}는 문자열을 직접 치환하므로 식별자 동적 처리 외에는 지양해야 한다.
SQL 매퍼는 또한 동적 쿼리(dynamic SQL)를 지원한다. 동적 쿼리란 런타임 파라미터에 따라 조건적으로 쿼리문을 서로 다르게 선언하는 것을 말한다. <if>, <choose>, <where>, <foreach> 등의 XML 태그를 통해 런타임 파라미터 조건에 따라 SQL 구문을 유연하게 선언적으로 구성할 수 있다.
SQL 매퍼 사용 시 고려해야 할 트레이드오프는 테이블 스키마 변경 시 수정 비용이 크다는 점이다. 테이블의 컬럼이나 관계가 변경되면 관련된 SQL XML, 파라미터 매핑, DTO 클래스를 일일이 수동으로 수정해야 한다. 또한 단순 조회나 단건 저장 로직조차 모든 SQL과 매핑 구문을 직접 작성해야 하므로 보일러플레이트 코드가 증가하게 된다. XML 내에 작성된 SQL 오류나 파라미터 오타는 컴파일 시점에 검증되지 않아 런타임에 쿼리가 실제로 호출되기 전까지 확인하기 어렵다는 한계도 존재한다.
ORM
ORM(object-relational mapping)은 객체 지향 프로그래밍 언어의 객체 모델과 관계형 데이터베이스의 관계형 모델 간의 패러다임 불일치(paradigm mismatch)를 해결하기 위한 기술 표준이다. 객체 지향 프로그래밍 언어의 객체(object)와 관계형 데이터베이스의 테이블(relation) 사이에는 캡슐화(encapsulation), 상속(inheritance), 연관관계(association), 동일성(identity) 등 패러다임 상의 본질적인 차이가 존재하는데, ORM 프레임워크는 이러한 불일치를 해결해 준다. SQL 매퍼와 달리 ORM은 테이블 자체를 객체로 변환하는 역할을 수행하며, SQL 작성 자체를 개발자 대신 프레임워크가 전담한다.
ORM의 핵심 기반인 영속성 컨텍스트(persistence context)란 엔티티의 생명주기를 관리하는 개념이다. 1차 캐시를 통해 동일 트랜잭션 내에서 동일 식별자(@Id)에 대한 메모리 주소 동일성(==)을 보장하며 불필요한 반복 조회를 제거한다. 또한 엔티티 등록, 수정, 삭제 쿼리를 내부 쓰기 지연 SQL 저장소에 모아두었다가 트랜잭션 커밋 시점(flush())에 일괄 실행하여 네트워크 배치를 최적화한다. 엔티티의 최초 조회 스냅샷과 커밋 시점 상태를 비교하여 변경된 필드에 대해 자동으로 UPDATE 쿼리를 생성하는 변경 감지(dirty checking)를 지원하므로, 개발자가 명시적인 업데이트 메서드를 호출하지 않아도 변경 사항이 영속화된다.
문자열 기반으로 작성되어 컴파일 시점에 문법 오류를 검출하지 못하는 JPA 표준 JPQL의 한계는 QueryDSL을 통해 보완된다. QueryDSL은 APT(annotation processing tool)를 통해 엔티티 기반의 Q-Class 메타모델을 생성하며, 이를 바탕으로 자바 코드로 타입 안전(type-safe)하게 쿼리를 작성할 수 있게 한다. 문법 오타나 타입 불일치를 컴파일 시점에 즉시 검출할 수 있고, BooleanBuilder나 BooleanExpression을 통해 복잡한 동적 검색 조건도 재사용 가능한 자바 메서드로 모듈화할 수 있다.
ORM 프레임워크는 비즈니스 로직을 객체의 메서드와 서비스 계층에 응집시켜 객체 지향적 설계와 단위 테스트를 용이하게 만들어 준다. 또한 하이버네이트(Hibernate)의 DB 방언(dialect) 추상화 덕분에 벤더 종속적인 SQL 문법 차이가 제거되어 데이터베이스 교체나 서로 다른 데이터베이스 환경 대응에 유리하다. 스프링 데이터 JPA의 인터페이스 기반 리포지토리 패턴과 결합하면 반복적인 기본 CRUD 쿼리 작성을 대폭 줄여 개발 생산성을 극대화할 수 있다.
이러한 장점에도 불구하고 ORM은 즉시 로딩(eager loading)과 지연 로딩(lazy loading), 영속 상태 전이(cascade), N+1 쿼리 문제, OSIV(open session in view) 생명주기 등 복잡한 내부 메커니즘에 대한 깊은 이해를 요구하므로 학습 곡선이 높다. 또한 수십만 건 이상의 대량 데이터 삽입/갱신 시 1차 캐시 엔티티 누적으로 인한 힙 메모리 부하와 변경 감지 오버헤드가 발생하므로 주기적인 flush() / clear(), 무상태 세션(StatelessSession), JDBC 배치, 또는 스프링 배치와의 연계가 필수적이다. 표준 JPQL의 추상화 계층으로 인해 데이터베이스 고유의 인덱스 힌트나 세밀한 윈도우 함수 활용에도 일정한 표현력 제약이 따른다.
비교
| 비교 항목 | 저장 프로시저 (Stored Procedure) | 자바 ORM (JPA + QueryDSL) | SQL Mapper (MyBatis) |
|---|---|---|---|
| 비즈니스 로직 위치 | 데이터베이스 서버 내부 (PL/SQL 등) | 애플리케이션 도메인 모델 및 서비스 계층 | 애플리케이션 서비스 계층 (SQL은 매퍼 XML에 분리) |
| 타입 안정성 | DB 컴파일 시점에 프로시저 단위 검증 | 자바 컴파일 시점 검증 (Q클래스 메타모델 기반) | 런타임 실행 시점 검증 (XML 문자열 파싱 기반) |
| 컴퓨팅 확장성 | 취약 (DB 서버 CPU 및 라이선스 비용 집중) | 우수 (무상태 애플리케이션 계층 수평 확장) | 우수 (무상태 애플리케이션 계층 수평 확장) |
| 단위 테스트 / CI | 복잡 (실제 DB 인스턴스 필수, 테스트 격리 비용 높음) | 매우 용이 (순수 POJO 단위 테스트, Testcontainers 격리 검증) | 보통 (매퍼 레이어 검증을 위한 테스트 DB 연결 필요) |
| 복잡한 쿼리 최적화 | 강력 (DB 내부 메모리 집계 및 절차적 제어 직결) | 보통 (객체 그래프 탐색에 최적화, 복잡한 통계 시 벌크/네이티브 쿼리 필요) | 강력 (작성자가 SQL 튜닝 및 인덱스 힌트 100% 제어) |
| 유지보수 / 변경 비용 | 높음 (스키마 변경 시 영향도 추적 및 버전 롤백 어려움) | 낮음 (엔티티 수정 시 컴파일 에러로 호출부 즉시 감지) | 보통~높음 (컬럼 변경 시 SQL XML과 ResultMap 수동 동기화 필요) |
| DB 종속성 | 매우 높음 (특정 DBMS 전용 절차형 언어) | 매우 낮음 (Hibernate Dialect 추상화 계층 제공) | 높음 (작성한 SQL이 DBMS별 문법에 직접 종속) |
아키텍처 설계 전략
대규모 트래픽을 처리하는 현대 백엔드 시스템에서는 시스템의 특성에 맞춘 역할 분담과 하이브리드 접근이 권장된다.
사용자 인증, 주문 생성, 결제 승인, 상태 변경과 같이 비즈니스 규칙이 복잡하고 잦은 변경이 발생하는 코어 도메인 모델은 JPA를 통해 객체 지향적으로 모델링한다. 검색 필터나 다중 정렬 등 복잡한 동적 조건이 필요한 조회 로직은 QueryDSL을 결합해 컴파일 시점의 타입 안정성과 조건식 재사용성을 확보한다.
반면 백오피스 대용량 통계 화면, 월말 정산, 다차원 리포트 등 JPA 엔티티 영속화가 불필요하고 정교한 SQL 튜닝(조인 순서 강제, 윈도우 함수 등)이 필수적인 영역은 SQL 매퍼를 사용한다. 스프링 환경에서는 동일한 DataSource를 공유하며 단일 트랜잭션 매니저를 공통으로 묶어 사용하는 아키텍처가 널리 활용된다.
프로시저의 경우 시스템 전반의 비즈니스 로직을 프로시저에 두는 것은 지양해야 한다. 다만 수백만 건의 대용량 데이터 마이그레이션 배치나 데이터베이스 엔진 내부 메모리 처리가 효율적인 것이 정량적으로 입증된 배치성 케이스의 경우 네트워크 전송 비용이 극단적인 병목이 되기 때문에 프로시저를 제한적으로 활용하는 것이 바람직하다.
참고
- Martin Fowler, Patterns of Enterprise Application Architecture (Addison-Wesley) - Data Mapper and Domain Logic Patterns.
- Martin Fowler, Evolutionary Database Design (https://martinfowler.com/articles/evodb.html).
- Jakarta Persistence Specification (Jakarta EE) (https://jakarta.ee/specifications/persistence/).
- Hibernate ORM Documentation (https://hibernate.org/orm/documentation/).
- MyBatis 3 Documentation (https://mybatis.org/mybatis-3/).
- QueryDSL Official Documentation (http://querydsl.com/).