ORM(Object-relational mapping)

아투·2026년 3월 30일

Database

목록 보기
4/6
post-thumbnail

ORM (Object-Relational Mapping)

  • 정의

    • ORM은 객체 지향 프로그래밍(OOP) 언어의 객체와 관계형 데이터베이스(RDB)의 데이터를 자동으로 연결해주는 기술이다.
    • 개발자가 SQL 쿼리를 직접 작성하는 대신, 프로그래밍 언어의 객체를 통해 데이터베이스의 데이터를 다룰 수 있게 해주는 추상화 계층의 역할을 수행한다.
  • 사용하는 이유

    • 객체 지향 모델과 관계형 모델 간의 불일치(Impedance Mismatch)를 해소하여 개발자가 비즈니스 로직에 더 집중할 수 있게 하기 위함이다.
    • 반복적인 SQL CRUD 작업을 줄여 생산성을 높이고, 데이터베이스 설계가 코드에 반영되는 구조를 통해 코드의 가독성과 유지보수성을 향상시키기 위해 사용한다.
    • 특정 데이터베이스 엔진에 종속되지 않는 코드를 작성함으로써 향후 데이터베이스 교체나 마이그레이션 시의 리스크를 줄일 수 있다.
  • 핵심 역할 및 특징

    • 데이터 매핑
      • 클래스는 데이터베이스의 테이블과, 클래스의 인스턴스는 테이블의 행(Row)과, 속성은 열(Column)과 각각 대응시킨다.
    • 영속성 관리
      • 응용 프로그램이 종료되어도 데이터가 사라지지 않도록 객체의 상태를 데이터베이스에 저장하고 관리하는 메커니즘을 제공한다.
    • 추상화된 쿼리 인터페이스
      • SQL 대신 각 언어의 고유한 문법이나 메서드 호출 방식을 통해 데이터를 조회하고 조작할 수 있는 환경을 제공한다.
    • 트랜잭션 및 캐싱 지원
      • 데이터베이스 트랜잭션을 코드 수준에서 관리하며, 동일한 데이터를 반복 조회할 때 성능 향상을 위해 1차 캐시 등의 기능을 지원한다.
  • 동작 원리

    1. 메타데이터 바인딩
    • 개발자가 어노테이션이나 설정 파일을 통해 정의한 객체와 테이블의 매핑 정보를 엔진이 읽어 들여 메모리에 로드한다.
    1. 쿼리 생성 및 실행
    • 개발자가 객체의 메서드(예: save, find)를 호출하면, ORM 엔진은 내부적인 방언(Dialect) 처리기를 통해 해당 데이터베이스 엔진에 최적화된 SQL 문을 동적으로 생성한다.
    1. 데이터 변환(Hydration)
    • 데이터베이스로부터 반환된 결과 셋(Result Set)을 다시 프로그래밍 언어의 객체 형태로 변환하여 응용 프로그램에 전달한다.
    1. 변경 감지(Dirty Checking)
    • 영속성 컨텍스트 내에서 관리되는 객체의 상태 변화를 감시하다가, 트랜잭션이 완료되는 시점에 변경된 부분만 추출하여 데이터베이스에 자동으로 반영한다.
  • 트레이드 오프

    • 생산성 vs 성능
      • 개발 속도는 비약적으로 향상되지만, 내부적으로 복잡한 SQL이 생성되거나 불필요한 연산이 추가되어 직접 쿼리를 작성하는 것보다 실행 성능이 다소 떨어질 수 있다.
    • 추상화 vs 제어권
      • 데이터베이스 작업을 단순화해주지만, 복잡한 통계 쿼리나 고도로 최적화가 필요한 대량 데이터 처리 시에는 ORM만으로 완벽하게 제어하기 어려운 한계가 있다.
    • 학습 곡선
      • 기본적인 사용법은 간단하나, 성능 최적화나 복잡한 관계 설정을 위해서는 ORM의 내부 동작 원리와 데이터베이스에 대한 깊은 이해가 동시에 요구된다.
  • 주의 사항

    • N+1 문제 발생
      • 연관 관계가 설정된 데이터를 조회할 때, 의도치 않게 수많은 추가 쿼리가 발생하는 현상이다. 이를 방지하기 위해 페치 조인(Fetch Join)이나 지연 로딩(Lazy Loading) 전략을 적절히 선택해야 한다.
    • 과도한 추상화 의존
      • ORM이 모든 것을 해결해 줄 것이라 믿고 데이터베이스 자체의 인덱스 설계나 실행 계획 확인을 소홀히 해서는 안 된다. 서비스 규모가 커질수록 데이터베이스 지식은 필수적이다.
    • 대량 데이터 처리의 비효율성
      • 수십만 건 이상의 데이터를 한 번에 수정하거나 삭제할 때는 객체 단위로 처리하는 ORM 방식보다 직접 SQL(Bulk Operation)을 사용하는 것이 훨씬 효율적이다.
    • 영속성 컨텍스트 이해
      • 객체의 생명 주기와 영속성 컨텍스트의 동작 방식을 정확히 이해하지 못하면 데이터가 실제 데이터베이스에 반영되지 않거나 의도치 않은 중복 저장이 발생할 수 있다.

패러다임 불일치 (Object-Relational Impedance Mismatch)

  • 정의

    • 패러다임 불일치란 객체 지향 프로그래밍(OOP) 언어의 구조와 관계형 데이터베이스(RDBMS)의 구조적/논리적 차이로 인해 발생하는 부조화를 의미한다.
    • 개발자가 비즈니스 로직을 객체 모델로 설계하더라도, 데이터를 저장할 때는 관계형 모델인 테이블 구조에 맞춰 변환해야 하는 과정에서 발생하는 간극을 뜻한다.
  • 발생 이유

    • 객체 지향 언어는 추상화, 상속, 다형성 등의 개념을 통해 시스템의 복잡성을 관리하는 데 최적화되어 있는 반면, RDBMS는 데이터의 정규화와 무결성을 보장하며 데이터를 표(Table) 형태로 관리하는 데 특화되어 있기 때문이다.
    • 두 모델이 데이터를 바라보는 관점과 다루는 방식이 근본적으로 다르기 때문에, 이를 연결하는 과정에서 코드의 복잡성이 증가하고 개발 생산성이 저하되는 문제가 발생한다.
  • 핵심 역할 및 특징

    • 밀도(Granularity) 차이
      • 객체 모델은 성명, 주소 등 다양한 하위 객체를 정의할 수 있지만, RDBMS는 제한된 데이터 타입만 지원하므로 테이블을 쪼개거나 여러 컬럼으로 풀어서 저장해야 한다.
    • 상속(Inheritance)의 부재
      • 객체 지향에는 상속 계층 구조가 존재하지만, RDBMS에는 상속 개념이 없다. 이를 구현하려면 조인 전략, 단일 테이블 전략 등을 선택해야 한다.
    • 식별성(Identity) 문제
      • 객체는 메모리 주소(참조값)나 equals()로 동일성을 비교하지만, RDBMS는 기본키(Primary Key)를 통해서만 행을 식별한다.
    • 연관관계(Associations)
      • 객체는 참조(Reference)를 사용하여 한 방향으로 관계를 맺지만, RDBMS는 외래키(Foreign Key)를 사용하며 조인을 통해 양방향 조회가 가능하다.
    • 객체 그래프 탐색(Navigation)
      • 객체는 참조를 타고 자유롭게 연관 객체를 조회할 수 있으나, RDBMS는 SQL을 통해 필요한 데이터를 미리 정의하여 조인해야 하므로 탐색 범위가 SQL 질의에 의해 결정된다.
  • 동작 원리 (ORM을 통한 해결 과정)

    1. 메타데이터 매핑
    • XML이나 어노테이션(@Entity, @Table 등)을 통해 객체의 필드와 데이터베이스 테이블의 컬럼 간 연결 정보를 설정한다.
    1. SQL 자동 생성 및 실행
    • 개발자가 객체를 저장하거나 조회하는 메서드를 호출하면, 프레임워크가 매핑 정보를 바탕으로 적절한 INSERT, SELECT(JOIN 포함) 쿼리를 자동으로 생성하여 DB에 전달한다.
    1. 영속성 컨텍스트(Persistence Context) 관리
    • DB에서 가져온 로우 데이터를 객체로 변환하여 보관하며, 객체의 상태 변화를 감지(Dirty Checking)하여 변경된 사항만 DB에 반영한다.
    1. 지연 로딩(Lazy Loading) 지원
    • 객체 그래프 탐색 시 실제 데이터가 필요한 시점까지 쿼리 실행을 미룸으로써 패러다임 불일치로 인한 불필요한 데이터 로드 문제를 해결한다.
  • 트레이드 오프

    • 개발 생산성 vs 시스템 성능
      • ORM을 사용하면 반복적인 CRUD SQL 작성이 줄어 생산성이 비약적으로 상승하지만, 복잡한 쿼리의 경우 직접 최적화한 SQL보다 성능이 떨어질 수 있다.
    • 추상화 vs 제어권
      • 데이터베이스 처리를 추상화하여 비즈니스 로직에만 집중할 수 있게 해주지만, 특정 DB 전용 기능이나 세밀한 쿼리 튜닝에 대한 제어권은 낮아진다.
    • 학습 곡선 vs 유지보수성
      • 패러다임 불일치를 해결하기 위한 도구(JPA/Hibernate 등)는 배우기 어렵지만, 한 번 적용하면 객체 중심의 코드를 유지할 수 있어 장기적인 유지보수성이 향상된다.
  • 주의 사항

    • N+1 문제 발생
      • 연관된 엔티티를 조회할 때 예상치 못한 수많은 추가 쿼리가 발생하는 N+1 문제를 방지하기 위해 Fetch Join이나 Batch Size 설정을 반드시 고려해야 한다.
    • 성능 최적화의 한계
      • 통계성 대용량 쿼리나 아주 복잡한 동적 쿼리는 ORM만으로 해결하기 어려우므로 QueryDSL이나 MyBatis, Native SQL을 혼용하여 보완해야 한다.
    • 객체와 DB 설계의 균형
      • 객체 모델을 너무 복잡하게 설계하면 DB 매핑이 어려워지고, 반대로 DB 중심으로만 설계하면 객체 지향의 장점을 잃게 되므로 두 모델 사이의 적절한 설계 타협이 필요하다.
    • 트랜잭션 및 영속성 이해
      • 영속성 컨텍스트의 생명주기와 트랜잭션 범위를 정확히 이해하지 못하면 데이터가 DB에 반영되지 않거나 준영속 상태의 객체에서 오류가 발생할 수 있다.

영속성 컨텍스트 (Persistence Context)

  • 정의

    • 영속성 컨텍스트는 "엔티티를 영구 저장하는 환경"이라는 뜻으로, JPA(Java Persistence API)가 관리하는 엔티티 객체들의 집합체이자 메모리 저장소이다.
    • 애플리케이션과 데이터베이스 사이에서 객체를 관리하는 논리적인 영역이며, 엔티티 매니저(Entity Manager)를 통해 접근하고 소통할 수 있다.
  • 사용하는 이유

    • 데이터베이스와의 직접적인 통신 횟수를 줄여 성능 최적화를 도모하고, 객체 지향적인 프로그래밍 모델을 유지하기 위함이다.
    • 객체의 동일성을 보장하고, 데이터 변경 시 SQL을 직접 작성하지 않아도 자동으로 DB에 반영되는 메커니즘을 제공하여 개발 생산성을 높이기 위해 사용한다.
  • 핵심 역할 및 특징

    • 1차 캐시
      • 영속 상태의 엔티티를 내부에 저장한다. 조회 시 DB보다 먼저 캐시를 확인하여 성능을 향상시킨다.
    • 동일성(Identity) 보장
      • 동일한 트랜잭션 내에서 같은 식별자(@Id)를 가진 엔티티를 조회할 경우, 항상 같은 인스턴스임을 보장한다. (== 비교 시 true)
    • 트랜잭션을 지원하는 쓰기 지연 (Transactional Write-behind)
      • 엔티티를 저장하거나 수정해도 즉시 DB에 쿼리를 보내지 않고, 내부 쿼리 저장소에 모아두었다가 트랜잭션 커밋 시점에 한꺼번에 실행한다.
    • 변경 감지 (Dirty Checking)
      • 엔티티의 상태 변화를 자동으로 감지한다. 트랜잭션 커밋 시점에 스냅샷과 비교하여 변경 사항이 있으면 UPDATE SQL을 생성해 DB에 반영한다.
    • 지연 로딩 (Lazy Loading)
      • 연관된 객체를 실제 사용하는 시점에 DB에서 조회할 수 있도록 지원하여 불필요한 데이터 로딩을 방지한다.
  • 동작 원리

    1. 엔티티 관리 시작 (Persist)
    • 엔티티 매니저를 통해 엔티티를 영속성 컨텍스트에 저장한다. 이때 엔티티는 1차 캐시에 등록되며 '영속(Managed)' 상태가 된다.
    1. 조회 및 캐싱
    • 조회 요청 시 먼저 1차 캐시에서 식별자로 엔티티를 찾는다. 캐시에 없으면 DB에서 조회 후 1차 캐시에 저장하고 영속 상태로 만든다.
    1. 변경 내용 추적
    • 영속성 컨텍스트는 엔티티를 처음 읽어온 시점의 상태를 스냅샷으로 찍어둔다. 애플리케이션에서 객체 값을 변경하면 컨텍스트가 이를 추적한다.
    1. 플러시 (Flush) 및 커밋
    • 트랜잭션 커밋 시 flush()가 호출된다. 변경 감지를 통해 생성된 SQL들이 DB로 전송되고, 마지막으로 DB 트랜잭션이 커밋되어 물리적으로 반영된다.
  • 트레이드 오프

    • 성능 최적화 vs 메모리 사용량
      • 1차 캐시와 쓰기 지연을 통해 네트워크 비용을 줄일 수 있지만, 대량의 데이터를 한 번에 관리할 경우 메모리 부하가 발생할 수 있다.
    • 개발 편의성 vs 추상화 비용
      • SQL을 직접 다루지 않아 생산성이 높으나, 영속성 컨텍스트의 동작 원리를 정확히 이해하지 못하면 의도치 않은 쿼리 실행이나 성능 저하(N+1 문제 등)를 겪을 수 있다.
  • 주의 사항

    • 트랜잭션 범위 준수
      • 영속성 컨텍스트는 대개 트랜잭션 범위와 생명주기를 같이 한다. 트랜잭션 밖에서 엔티티를 수정하면 변경 감지가 동작하지 않는 '준영속(Detached)' 상태가 되므로 주의해야 한다.
    • 대량 데이터 처리
      • 수만 건 이상의 데이터를 처리할 때는 1차 캐시에 너무 많은 객체가 쌓여 OutOfMemoryError가 발생할 수 있다. 주기적으로 clear() 또는 flush()를 호출하거나, Stateless Session 등을 고려해야 한다.
    • 변경 감지의 오버헤드
      • 모든 필드를 검사하여 변경 사항을 확인하므로, 엔티티의 필드가 너무 많거나 복잡한 경우 스냅샷 비교 과정에서 CPU 비용이 발생할 수 있다.
    • OSIV (Open Session In View) 설정
      • 뷰 계층까지 영속성 컨텍스트를 유지할지 여부에 따라 지연 로딩 가능 범위가 달라진다. 이는 커넥션 유지 시간과 밀접하여 애플리케이션 성능과 리소스 관리에 큰 영향을 미친다.

N+1 문제 (N+1 Problem)

  • 정의

    • N+1 문제란 관계형 데이터베이스(RDBMS)와 객체 지향 프로그래밍 언어 사이의 ORM 기술을 사용할 때 발생하는 대표적인 성능 저하 현상이다.
    • 1번의 쿼리로 주 데이터를 조회했으나, 연관된 데이터를 사용하려는 시점에서 의도치 않게 N번의 추가 쿼리가 실행되어 총 N+1번의 쿼리가 발생하는 상황을 의미한다.
  • 발생하는 이유

    • ORM의 지연 로딩(Lazy Loading) 메커니즘과 데이터 조회 최적화 사이의 간극 때문에 발생한다.
    • ORM은 객체 그래프 탐색을 위해 필요한 시점에 데이터를 가져오려 하지만, 루프(Loop) 내에서 연관 객체에 접근할 경우 각 객체마다 별도의 SELECT 쿼리를 발행하게 되면서 데이터베이스 부하가 급증하게 된다.
  • 핵심 역할 및 특징

    • 성능 병목의 주범
      • 데이터 양이 적을 때는 체감되지 않으나, 데이터(N)가 늘어날수록 쿼리 실행 횟수가 기하급수적으로 증가하여 애플리케이션 응답 속도를 저하시킨다.
    • 추상화의 부작용
      • 개발자가 SQL을 직접 작성하지 않고 객체 단위로 데이터를 다루는 과정에서, 내부적으로 어떤 쿼리가 나가는지 인지하지 못할 때 빈번히 발생한다.
    • 연관 관계 종속성
      • 일대다(1:N) 또는 다대일(N:1) 관계에서 부모 엔티티를 조회한 후 자식 엔티티를 순회하며 참조할 때 주로 나타난다.
  • 동작 원리

    1. 부모 엔티티 조회
    • 클라이언트가 특정 조건으로 부모 엔티티 리스트를 조회하는 쿼리를 1회 실행한다.
    1. 프록시 객체 생성
    • 지연 로딩 설정 시, ORM은 자식 엔티티 자리에 실제 데이터 대신 가짜 객체(Proxy)를 채워둔다.
    1. 연관 데이터 접근
    • 비즈니스 로직(반복문 등) 내에서 조회된 부모 엔티티들의 자식 객체에 접근하여 실제 값을 요구한다.
    1. 추가 쿼리 실행(N회)
    • ORM은 각 부모 객체마다 대응하는 자식 데이터를 가져오기 위해, 부모의 수(N)만큼 추가적인 SELECT 쿼리를 데이터베이스에 개별적으로 전송한다.
  • 트레이드 오프

    • 지연 로딩(Lazy) vs 즉시 로딩(Eager)
      • 지연 로딩은 불필요한 데이터 조회를 방지하지만 N+1 문제를 야기할 수 있고, 즉시 로딩은 N+1을 피하려다 필요 없는 데이터까지 한꺼번에 가져와 메모리 낭비와 또 다른 형태의 N+1(JPQL 등에서)을 유발할 수 있다.
    • 개발 편의성 vs 튜닝 리소스
      • ORM의 기본 기능을 그대로 사용하면 개발 속도는 빠르나, 성능 최적화를 위해 Fetch Join이나 EntityGraph 같은 별도의 튜닝 로직을 작성해야 하는 비용이 발생한다.
  • 주의 사항

    • 페치 조인(Fetch Join) 활용
      • SQL의 JOIN 문을 사용하여 연관된 엔티티를 한 번에 가져오도록 명시하여 N+1 문제를 원천 차단해야 한다. 단, 페이징 처리가 필요한 경우 메모리 과부하가 올 수 있으므로 주의가 필요하다.
    • Batch Size 설정
      • hibernate.default_batch_fetch_size 설정을 통해 N번의 쿼리를 IN 절을 사용하여 설정한 크기만큼 묶어서 실행함으로써 쿼리 횟수를 획기적으로 줄여야 한다.
    • 데이터 모델링 검토
      • 무분별한 양방향 연관 관계나 깊은 객체 그래프 탐색이 필요한 설계인지 검토하고, 필요한 경우 DTO로 직접 조회하는 방식을 고려해야 한다.
    • 로그 모니터링
      • 개발 단계에서 실행되는 SQL 로그를 상시 확인하여, 단순 리스트 조회 시 예상보다 많은 쿼리가 수행되고 있지는 않은지 반드시 체크해야 한다.

0개의 댓글