ORM(Object-Relational Mapping)이란?
ORM은 객체 지향 프로그래밍 언어와 관계형 데이터베이스 사이의 호환되지 않는 데이터를 변환하는 프로그래밍 기술이다.
간단히 말해서, ORM은 데이터베이스의 테이블을 객체로, 그리고 그 객체의 인스턴스를 테이블의 레코드에 매핑해 준다.
이를 통해 개발자가 SQL 쿼리를 직접 작성하지 않고도 데이터베이스 작업을 수행할 수 있다.
먼저 ORM의 장단점을 알아 보자.
주요 장점
1. 코드 재사용성: 데이터베이스 작업을 위한 코드를 재사용할 수 있다.
2. 유지보수성: SQL 쿼리가 아닌, 클래스와 메서드를 통해 데이터베이스를 다루므로 코드의 유지보수가 쉽다.
3. 데이터베이스 독립성: 대부분의 ORM 도구는 다양한 종류의 데이터베이스 시스템을 지원한다.
주요 단점
1. 성능: 복잡한 쿼리의 경우, ORM이 생성하는 SQL이 최적화되지 않을 가능성이 있어 성능 문제가 발생할 수 있다.
2. 학습곡선: ORM 자체를 사용하는 것이 복잡하거나 다양한 기능을 제대로 활용하기 위해선 추가적인 학습이 필요하다.
ORM을 사용하지 않고도 데이터베이스 작업은 가능하다.
SQL 쿼리를 직접 작성하거나, 다른 데이터 저장 방법을 사용할 수도 있다.
그러나 많은 현대 웹 애플리케이션 프레임워크에서는 ORM을 사용하고 있으며, 특히 복잡한 비즈니스 로직과 다수의 데이터베이스 테이블이 관여하는 경우 ORM을 사용하면 많은 이점이 있다.
따라서 ORM을 "꼭" 사용해야 하는 것은 아니지만, 프로젝트의 필요성, 팀의 능력, 애플리케이션의 복잡성 등을 고려하여 결정하는 것이 좋다.
JAVA로 한정하자면...
대표적으로
- Hibernate: 자바 환경에서 가장 널리 사용되는 ORM 프레임워크 중 하나다. JPA(Java Persistence API)의 구현체 중 하나로도 알려져 있다.
- JPA (Java Persistence API): 자바 EE 스펙의 일부로, 여러 ORM 솔루션에 공통 인터페이스를 제공한다. Hibernate, EclipseLink 등이 JPA를 구현하고 있다.
- EclipseLink: JPA의 또 다른 구현체로, Oracle에서 관리하고 있다.
- JDO (Java Data Objects): 자바 객체를 데이터베이스에 저장하기 위한 표준이다. 현재는 거의 사용되지 않는다.
가 있다.
ORM의 큰 그림은 파악했으니 이제
JPA(Java Persistence API)는 Java EE(Java Platform, Enterprise Edition)과 Java SE(Java Platform, Standard Edition) 애플리케이션에서 관계형 데이터베이스의 데이터를 객체지향적으로 관리하기 위한 Java API다.
이 API는 ORM을 구현하기 위한 표준 인터페이스를 제공한다.
JPA는 여러 ORM 프레임워크가 공통적으로 사용할 수 있는 API 규격을 정의하며, 그 구현은 각 ORM 프레임워크에서 담당한다(이것이 인터페이스!).
Hibernate, EclipseLink, OpenJPA 등이 JPA를 구현한 대표적인 라이브러리다.
주요 특징
- 객체-테이블 매핑: JPA는 애너테이션(@Entity, @Table, @Column 등)을 이용해 Java 객체와 데이터베이스 테이블을 매핑한다.
- JPQL (Java Persistence Query Language): SQL과 유사한 문법을 가진 쿼리 언어로, 객체지향적인 쿼리를 가능하게 한다.
- 캐시 지원: 일반적으로 JPA는 1차 캐시를 내장하고 있으며, 이를 통해 반복된 데이터베이스 액세스를 최적화한다.
- 트랜잭션 관리: ACID (원자성, 일관성, 고립성, 지속성) 특성을 지원한다.
- Lazy Loading과 Eager Loading: 연관된 엔터티를 언제 불러올지에 대한 설정이 가능하다.
주요 구성요소
- EntityManager: 엔터티 객체의 생명주기를 관리한다.
- Entity: 데이터베이스의 레코드에 해당하는 Java 객체다.
- EntityTransaction: 데이터베이스의 트랜잭션을 관리한다.
- Persistence Context: 엔터티를 영구 저장하는 환경을 의미한다.
사용 예시
// 엔터티 클래스
@Entity
public class Member {
@Id
private Long id;
private String name;
// Getter, Setter 등
}
// 엔터티 매니저를 통한 데이터베이스 접근
EntityManagerFactory emf = Persistence.createEntityManagerFactory("persistence-unit");
EntityManager em = emf.createEntityManager();
// 트랜잭션 시작
EntityTransaction tx = em.getTransaction();
tx.begin();
// 새 멤버 저장
Member member = new Member();
member.setId(1L);
member.setName("홍길동");
em.persist(member);
// 트랜잭션 커밋
tx.commit();
- Entity class 정의하고
- EntityManagerFactory 생성하고
- EntityManager를 생성한다.
- DB 작업을 하기 전에 트랜잭션을 시작한다.
- Member Entity instance를 생성한다.
- member에 값을 설정한다.
- 다 했으면 EntityManager의 persist 메서드를 사용해 DB에 저장한다.
자꾸 persist, persistence, 영속성 등의 표현이 등장한다.
나는 이걸 자바 객체와 DB '상태'를 동일하게 유지하는 것이라 이해하고 있다.
em.persist(member)를 통해 member와 DB 상태가 persist하게 유지되었는가?
그럼 커밋해야지.
tx.commit();
DB 작업이 성공적으로 완료되면 트랜잭션을 커밋한다.
실제로는 이 시점에서 DB에 변경이 반영된다.
먼저 개념(?)적으로 작업을 완료하고 > 물리(?)적으로도 작업을 완료한다.
하이버네이트에 대해 막 자세히 설명하고 싶진 않다.
왜냐면, 하이버네이트는 JPA 인터페이스의 '구현체'이기 때문이다.
그리 큰 차이가 있을리 없다.
하지만 소소하게 다른 점들이 있다.
그 부분을 좀 언급하겠다.
JPA VS 하이버네이트
JPA는...
- 표준 스펙: JPA는 Java EE(이제는 Jakarta EE)의 일부로, ORM을 위한 표준 Java 스펙이다.
JPA 자체는 구현이 아니라 인터페이스와 애너테이션, 쿼리 언어 등을 정의한다.- 구현체 의존성 없음: JPA 스펙을 준수하는 여러 구현체가 있고, 이를 변경하는 것이 상대적으로 쉽다(예: 하이버네이트에서 EclipseLink로).
- 제한된 기능: JPA는 표준 스펙이므로, 특정 데이터베이스에 최적화된 작업이나 고급 기능을 사용하려면 구현체에 의존적인 코드를 작성해야 할 수 있다.
하이버네이트는...
- JPA의 구현체: 하이버네이트는 JPA 스펙을 구현한 라이브러리 중 하나다. 따라서, JPA의 모든 기능을 하이버네이트에서 사용할 수 있다.
- 추가 기능: 하이버네이트는 JPA의 표준 스펙 이외에도 다양한 추가 기능을 제공합니다 (예: Lazy Initialization, Fetch Profiles, Caching 등).
내가 알기로 JPA는 1차 캐싱 가능, 하이버네이트는 2차 캐싱까지 가능하다.- 성숙함: 하이버네이트는 오랜 역사와 넓은 사용자 베이스를 가지고 있어, 매우 안정적이고 다양한 문제 상황에 대한 해결책을 제공한다.
핵심 차이점
- 표준 vs 구현체: JPA는 Java의 ORM에 대한 표준 스펙이며, 하이버네이트는 그 표준 스펙의 하나의 구현체다.
- 기능 범위: 하이버네이트는 JPA보다 더 다양한 기능과 설정 옵션을 제공합니다(인터페이스 구현체니까!).
- 유연성: JPA를 사용하면 다른 구현체로 쉽게 전환할 수 있다. 하이버네이트는 특정 기능이 필요한 경우 더 유용할 수 있다.
- 쿼리 언어: 둘 다 JPQL(Java Persistence Query Language)을 지원한다.
하이버네이트는 추가로 HQL(Hibernate Query Language)도 지원한다.- 커뮤니티와 지원: 하이버네이트는 더 오래되고 큰 커뮤니티를 가지고 있지만, JPA는 Java EE의 일부로서 역시 꾸준한 지원과 업데이트가 이루어지고 있다.
아이고, 힘들어라.
양이 너무 많다.
많이 하려고 하지 않는데도, 줄일 수가 없다.
꼭 언급해야 하는 부분이라 생각하기 때문이다.
빨리 하고 로제 떡볶이 먹어야지.
참고로 난 오늘 엔티티에 대해서 가장 할 말이 많다...
엔티티는 ORM에서 중요한 개념 중 하나로, 데이터베이스의 테이블과 매핑되는 클래스를 의미한다.
즉, 하나의 엔티티 클래스 인스턴스는 데이터베이스의 하나의 레코드(row)와 일치한다.
엔티티는 보통 JPA, 하이버네이트 등의 ORM 프레임워크에서 사용되며, 해당 프레임워크의 애너테이션 또는 설정을 통해 선언된다.
엔티티의 주요 특징
- 식별성 (Identity): 각 엔티티는 고유한 식별자(ID)를 가진다. 이 식별자는 데이터베이스의 Primary Key와 매핑된다.
- 지속성 (Persistence): 엔티티 객체는 데이터베이스에 영구적으로 저장될 수 있다.
- 상태 정보: 엔티티는 비즈니스 로직에 사용되는 상태 정보를 가질 수 있다. 이 정보는 데이터베이스의 레코드와 동기화된다.
- 연관관계: 다른 엔티티와의 관계(One-to-One, One-to-Many, Many-to-One, Many-to-Many 등)를 가질 수 있다.
- 데이터베이스 독립성: 엔티티 클래스는 특정 데이터베이스에 종속되지 않으며, 다른 데이터베이스로 쉽게 마이그레이션할 수 있다.
사용 시 주의사항
- 엔티티 클래스는 반드시 기본 생성자를 포함해야 합니다.
- 필드 이름은 데이터베이스의 컬럼 이름과 일치하거나 매핑 정보를 통해 매칭되어야 한다.
- 엔티티는 가능하면 '단순한' 상태 정보만을 가지고, 비즈니스 로직은 서비스 레이어에서 처리하는 것이 좋다.
자... 진짜 중요한 거 나간다(사실 내가 궁금한 거...).
ORM 사용 시에 Entity를 써야 되나, DAO를 써야 되나, DTO를 써야 되나?
섞어서도 쓰던데 어떻게 쓰는 게 맞나?
보통
Entity는 ORM을 통한 데이터베이스 매핑을 위해 필요하다.
DAO는 데이터베이스 연산을 추상화하기 위해 사용된다.
DTO는 계층간 데이터 전달을 위해 사용된다.
라고 말한다.
그런데 이렇게 말하면 못 알아 듣겠다고...
그러니 이렇게 정리해 보자.
Entity
- 목적: 데이터베이스의 테이블을 Java 객체로 표현하기 위해 사용된다.
- 언제 사용해야 하나: 데이터베이스와의 CRUD 작업이 필요한 경우.
- 특징: Entity 객체는 데이터베이스 테이블의 각 행을 나타낸다. ORM 프레임워크는 이 Entity 객체를 이용해서 데이터베이스와 자동으로 매핑을 수행한다.
DAO (Data Access Object)
- 목적: 데이터베이스 CRUD 연산 로직을 캡슐화한다.
- 언제 사용해야 하나: 실제 데이터베이스 작업을 추상화하고 캡슐화할 필요가 있는 경우.
- 특징: DAO 내에서 Entity 객체를 생성, 조회, 업데이트, 삭제할 때 사용된다.
DTO (Data Transfer Object)
- 목적: 계층간 데이터 전송을 담당한다.
- 언제 사용해야 하나: 예를 들어, Presentation Layer와 Business Layer 사이, 혹은 다른 시스템과의 통신에서 사용된다.
- 특징: 단순히 데이터를 담는 역할을 하며 로직을 포함하지 않는다.
섞어서 사용하는 방법
1. DAO에서 Entity 사용: DAO 메서드 내부에서는 Entity 객체를 사용하여 데이터베이스 연산을 수행한다.
2. DTO로 데이터 전송: 클라이언트와 서버 간에 데이터를 주고받을 때는 DTO를 사용한다. DTO는 Entity와 유사할 수 있으나, 분리하여 사용하는 것이 좋다.
3. Entity와 DTO 변환: 일반적으로 서비스 레이어에서 Entity 객체를 DTO 객체로 변환하거나 그 반대의 작업을 수행한다. 이 작업은 Assembler 패턴, Mapper 등을 사용하여 수행할 수 있다.
- 클라이언트로부터 데이터를 받아와 데이터베이스에 저장할 때: DTO -> Entity -> DAO
- 데이터베이스에서 데이터를 읽어와 클라이언트에 전달할 때: DAO -> Entity -> DTO
이렇게 세 가지를 적절히 혼합하여 사용하면 각 계층과 관심사가 잘 분리되고, 유지보수 및 테스트가 용이해진다고는 하는데...
이건 좀 괴랄한 거 같다.
나는 Entity를 두고 DTO를 통해 DB 작업하는 걸 본 정도다...
이거는 알아서 하자(?)! ㅠㅠ
영속성 컨텍스트(Persistence Context)는 ORM 프레임워크에서 중요한 역할을 하는 개념이다.
일반적으로 JPA나 하이버네이트 같은 ORM 프레임워크에서 주로 사용된다.
영속성 컨텍스트는 데이터베이스와 애플리케이션 사이에서 객체를 캐시하고 관리하는 논리적인 작업 영역이다.
주요 특징과 역할
- 1차 캐시: 영속성 컨텍스트는 조회한 Entity 객체를 내부 캐시에 저장한다. 이로 인해 같은 트랜잭션 내에서는 다시 데이터베이스에 접근하지 않아도 된다.
- 동일성 보장: 같은 영속성 컨텍스트에서 조회한 Entity는 동일성(identity)이 보장된다. 즉, 같은 데이터베이스 레코드에 대한 Entity 객체는 항상 같은 자바 객체를 반환한다.
- 트랜잭션을 지원하는 쓰기 지연 (Write-Behind): 변경 감지(Dirty Checking)를 통해 Entity가 수정되면, 이 변경을 즉각적으로 데이터베이스에 반영하지 않고 트랜잭션이 커밋되는 시점에 반영한다.
- 지연 로딩 (Lazy Loading)과 즉시 로딩 (Eager Loading): 연관된 Entity를 어떻게 로딩할지 영속성 컨텍스트가 관리한다.
- 캐스케이딩: 하나의 Entity를 저장하거나 삭제할 때 연관된 Entity까지 자동으로 저장, 삭제를 수행할 수 있다.
- 생명주기 관리: Entity는 영속성 컨텍스트에 의해 생명주기 상태(Transient, Persistent, Detached, Removed)가 관리됩니다.
일시적 상태에서 - 영속적 상태가 되었다가 - 분리되고 - 삭제된다.
JPA 환경에서의 작동 방식 예시다.
// 엔터티 매니저를 통해 영속성 컨텍스트에 접근
EntityManager em = entityManagerFactory.createEntityManager();
EntityTransaction tx = em.getTransaction();
tx.begin(); // 트랜잭션 시작
// 객체 저장: 영속성 컨텍스트에 Entity 객체를 저장
Member member = new Member();
member.setId(1L);
member.setName("홍길동");
em.persist(member);
// 객체 조회: 영속성 컨텍스트에서 먼저 찾고, 없을 경우 데이터베이스에서 조회
Member foundMember = em.find(Member.class, 1L);
// 객체 수정: 영속 상태의 객체를 변경하면 영속성 컨텍스트가 알아서 변경 감지하여 DB에 반영
foundMember.setName("임꺽정");
tx.commit(); // 트랜잭션 커밋: 영속성 컨텍스트의 변경 내용을 데이터베이스에 반영
영속성 컨텍스트는 이처럼 Entity의 생명주기를 관리하면서 데이터베이스와의 작업을 최적화하고, 일관성을 유지하는 등의 역할을 수행한다.
마지막!
스프링 데이터 JPA는 스프링 프레임워크에서 제공하는 데이터 접근 레이어의 모듈 중 하나다.
JPA를 기반으로 하며, 데이터베이스의 CRUD(Create, Read, Update, Delete) 작업을 더 편리하고 효율적으로 처리할 수 있도록 도와준다.
주요 특징
- Repository 인터페이스: 개발자는 단순히 인터페이스를 작성하고, 몇 가지 어노테이션을 추가하는 것만으로 CRUD 연산을 처리할 수 있다.
public interface MemberRepository extends JpaRepository<Member, Long> { List<Member> findByName(String name); }
- 쿼리 메서드: 메서드 이름만으로 JPA 쿼리를 생성하며, 복잡한 쿼리도 메서드 이름을 통해 자동으로 생성할 수 있다.
List<Member> findByEmailAndStatus(String email, Status status);
- 페이징과 정렬: Pageable 인터페이스를 매개변수로 받아 자동으로 페이징과 정렬을 처리한다.
Page<Member> findAll(Pageable pageable);
- 트랜잭션 관리: @Transactional 어노테이션을 통해 선언적으로 트랜잭션을 관리할 수 있다.
- JPA Auditing: @CreatedBy, @LastModifiedBy, @CreatedDate, @LastModifiedDate 등을 통해 엔터티가 생성되거나 변경되는 시점을 자동으로 관리할 수 있다.
- 커스텀 쿼리: @Query 어노테이션을 이용해 JPQL(HQL), Native Query, QueryDSL 등을 활용해 복잡한 쿼리를 작성할 수 있다.
@Query("SELECT m FROM Member m WHERE m.name = :name") List<Member> findByName(@Param("name") String name);
장점
- 데이터베이스 연산을 위한 대부분의 코드가 자동으로 생성되므로, 개발자는 비즈니스 로직에 더 집중할 수 있다.
- 스프링 프레임워크와 잘 통합되어 있어, 다른 스프링 모듈과 쉽게 연동할 수 있다.
- 데이터 접근 레이어를 추상화하여, 다양한 데이터 소스나 다양한 데이터베이스 기술에 쉽게 대응할 수 있다.
@Service
public class MemberService {
@Autowired
private MemberRepository memberRepository;
@Transactional
public Member createMember(Member member) {
return memberRepository.save(member);
}
public List<Member> findAll() {
return memberRepository.findAll();
}
public Optional<Member> findById(Long id) {
return memberRepository.findById(id);
}
}
끝!
이 글은 골든래빗 《스프링 부트 3 백엔드 개발자 되기 - 자바 편》의 5장 써머리입니다.