ORM
- ORM (Object-Relational Mapping)
- 애플리케이션 Class와 RDB(Relational DataBase)의 테이블을 매핑 (연결)
- 어플리케이션의 객체를 RDB 테이블에 자동으로 영속화
- 객체는 객체대로 설계하고, 관계형 데이터베이스는 관계형 데이터베이스대로 설계한다.
- ORM 프레임워크가 중간에서 매핑해 준다.
- 대중적인 언어에는 대부분 ORM 기술이 존재한다.
ORM 장점
- SQL문이 아닌 Method를 통해 DB를 조작할 수 있어, 개발자는 객체 모델을 이용하여 비즈니스 로직을 구성하는데만 집중할 수 있음
- 내부적으로는 쿼리를 생성하여 DB를 조작함
- 하지만 개발자가 이를 신경 쓰지 않아도 됨
- Query와 같이 필요한 선언문, 할당 등의 부수적인 코드가 줄어들어 각종 객체에 대한 코드를 별도로 작성하여 코드의 가독성을 높임
- 객체지향적인 코드 작성이 가능함. 오직 객체지향적 접근만 고려하면 되기때문에 생산성 증가
- 매핑하는 정보가 Class로 명시 되었기 때문에 ERD를 보는 의존도를 낮출 수 있고 유지보수 및 리팩토링에 유리
- 기존 방식에서 MySQL 데이터베이스를 사용하다가 ORACLE로 변환한다고 가정해보면, 새로 쿼리를 짜야하는 경우가 생김. 이런 경우에 ORM을 사용한다면 쿼리를 수정할 필요가 없음
ORM 단점
- 프로젝트의 규모가 크고 복잡하여 설계가 잘못된 경우, 속도 저하 및 일관성을 무너뜨리는 문제점이 생길 수 있음
- 복잡하고 무거운 Query는 속도를 위해 별도의 튜닝이 필요하기 때문에 결국 SQL문을 써야할 수도 있음
ORM과 SQL Mapper와의 차이점
1. SQL Mapper
- myBatis, jdbcTemplate
- SQL을 명시하여 직접 DB 조작
- 필드를 매핑시키는 것이 목적
- 자바 클래스와 sql을 매핑
2. ORM
- JPA, Hibernate
- 자바 클래스와 DB 테이블을 매핑
- 객체간의 관계를 바탕으로 SQL 자동 생성
- RDB의 관계를 Object 에 반영하는 것이 목적
JPA
- JPA(Java Persistence API)
- JPA는 자바 진영에서 ORM(Object-Relational Mapping) 기술 표준으로 사용되는 인터페이스의 모음
- JPA를 구현한 대표적인 오픈소스로는 Hibernate가 있음
- 자바 어플리케이션에서 관계형 데이터베이스를 사용하는 방식을 정의한 인터페이스
- JPA는 API의 규격 인터페이스일 뿐 (라이브러리나 프레임워크가 아님)
- JPA 2.1 표준 명세를 구현한 구현체 : Hibernate, EclipseLink, DataNucleus, OpenJPA, TopLink Essentials 등이 있다. (ORM 프레임워크)
- JPA 는 애플리케이션과 JDBC 사이에서 동작하여 개발자가 JPA 를 사용하면, JPA 내부에서 JDBC API 를 사용하여 SQL 을 호출, DB 와 통신한다.
왜 JPA를 사용해야 할까?
1. 생산성
- JPA를 사용하면 자바 컬렉션에 저장하듯이 JPA에게 저장할 객체를 전달하면 된다.
- 지루하고 반복적인 코드를 개발자가 직접 작성하지 않아도 되며, DDL문도 자동으로 생성해주기 때문에 데이터베이스 설계 중심을 객체 설계 중심으로 변경할 수 있다.
2. 유지보수
- 필드를 하나만 추가해도 관련된 SQL과 JDBC 코드를 전부 수행해야 했지만 JPA는 이를 대신 처리해주기 때문에 개발자가 유지보수해야하는 코드가 줄어든다.
- SQL을 직접 작성하는 것이 아닌, 객체를 사용하여 동작 -> 재사용성 up
- 기존 SQL 방식 : 테이블 필드 변경시 모든 SQL을 수정해야 한다.
- JPA 방식 : Entity 객체에 필드만 추가하면 된다.
3. 패러다임의 불일치 해결
- JPA는 연관된 객체를 사용하는 시점에 SQL을 전달할 수 있고, 같은 트랜잭션 내에서 조회할 때 동일성도 보장하기 때문에 다양한 패러다임의 불일치를 해결한다.
4. 성능
- 애플리케이션과 데이터베이스 사이에서 성능 최적화 기회를 제공한다.
- 같은 트랜잭션안에서는 같은 엔티티를 반환하기 때문에 데이터 베이스와의 통신 횟수를 줄일 수 있다.
- 트랜잭션을 commit하기 전까지 메모리에 쌓고 한번에 SQL을 전송한다.
5. 데이터 접근 추상화와 벤더 독립성
- RDB는 같은 기능이라도 벤더마다 사용법이 다르기 때문에 처음 선택한 데이터베이스에 종속되고 변경이 어렵다. JPA는 애플리케이션과 데이터베이스 사이에서 추상화된 데이터 접근을 제공하기 때문에 종속이 되지 않도록한다.
- 만약 DB가 변경되더라도 JPA에게 알려주면 간단하게 변경이 가능하다.
JPA와 객체 그래프 탐색
신뢰할 수 있는 엔티티, 계층
class MemberService {
...
public void process(){
// 기존 방식처럼 직접 구현한 DAO에서 객체를 가져온 경우
Member member1 = memberDAO.find(memberId);
member1.getTeam(); // 엔티티를 신뢰할 수 없음
member1.getOrder().getDelivery();
// JPA를 통해서 객체를 가져온 경우
Member member2 = jpa.find(Member.class, memberId);
member2.getTeam(); // 자유로운 객체 그래프 탐색
member2.getOrder().getDelivery();
}
}
- 내가 아닌 다른 개발자가 직접 구현한 DAO에서 가져오는 경우
- DAO에서 직접 어떤 쿼리를 날렸는지 확인하지 않는 이상, 그래프 형태의 관련된 객체들을 모두 잘 가져왔는지 알 수 가 없다.
- 즉, 반환한 엔티티를 신뢰하고 사용할 수 없다.
- JPA를 통해서 가져오는 경우
- 객체 그래프를 완전히 자유롭게 탐색할 수 있게 된다.
- 동일한 트랜잭션에서 조회한 엔티티는 같음을 보장한다.
JPA의 성능 최적화 기능
1. 1차 캐시와 동일성(identity) 보장 - 캐싱 기능
- 같은 트랜잭션 안에서는 같은 엔티티를 반환 - 조회 성능 향상
- 동일 조건인 경우 쿼리를 2번 실행하지 않고, 캐시에서 읽어들임
- String memberId = "100"; Member member1 = jpa.find(Member.class, memberId); // DB에서 가져옴 Member member2 = jpa.find(Member.class, memberId); // 1차 캐시에서 가져옴 member1 == member2; // 같다
2. 트랜잭션을 지원하는 쓰기 지연 (transactional write-behind) - 버퍼링 기능
(1) INSERT
// 1. 트랜잭션을 커밋할 때까지 INSERT 쿼리를 수행하지 않음
transaction.begin(); // 트랜잭션 시작
em.persist(memberA);
em.persist(memberB);
em.persist(memberC);
// 2. 커밋하는 순간 데이터베이스에 INSERT 쿼리를 일괄 처리한다.
// JDBC BATCH SQL 기능을 사용해서 한번에 SQL 전송
transaction.commit(); // 트랜잭션 커밋
(2) UPDATE
// 1. UPDATE, DELETE로 인한 로우 락(ROW LOCK) 시간 최소화
transaction.begin(); // 트랜잭션 시작
changeMember(memberA);
deleteMember(memberB);
비즈니스_로직_수행(); // 비즈니스 로직 수행하는 동안 DB 로우 락이 걸리지 않는다.
// 커밋하는 순간 데이터베이스에 UPDATE, DELETE 쿼리를 보내고 바로 커밋
transaction.commit(); // 트랜잭션 커밋
3. 지연 로딩(Lazy Loading)
(1) 지연 로딩
객체가 실제로 사용될 때 로딩하는 전략
(2) 즉시 로딩
JOIN 쿼리로 한번에 연관된 객체까지 미리 조회하는 전략
어플리케이션 개발시 기본적으로 지연 로딩을 설정한 후 필요한 경우에 즉시 로딩으로 옵션을 변경하는 것을 추천한다.
상속 관계
- JAVA에서는 부모클래스와 자식클래스의 관계 즉, 상속관계가 존재하는데 데이터베이스에서는 이러한 객체의 상속관계를 지원하지 않는다. 이런 상속관계를 JPA는 아래와 같은 방식으로 해결하였다.
- 위의 구조에서 만약 Album 클래스를 저장한다고 가정해보자.
- // Album 객체저장 jpa.persist(album);
- 그러면 JPA는 위의 코드를 아래의 쿼리로 변환해서 실행한다.
- INSERT INTO ITEM (ID, NAME, PRICE) ..... INSERT INTO ALBUM (ARTIST) .....
- 위처럼 저장하면 당연히 조회할때도 두 테이블을 엮어서 가져올 것이다. 조회하는 JAVA코드와 변환되는 쿼리를 보도록하자.
- // JAVA 코드 String albumId = "id100"; Album album = jpa.find(Album.class, albumId); // 변환된 쿼리 SELECT I.*, A.* FROM ITEM I JOIN ALBUM A ON I.ITEM_ID = A.ITEM_ID
연관 관계
- 위와 같이 상속관계에 대한 접근도 제공해주는데 객체지향에는 연관관계라는 것도 있다.
- 코드로 따지면 Class에서 또 다른 Class Type을 필드 변수로 가지고 있는것이다.
- 객체관계와 이를 테이블 구조로 나타낸 아래의 그림을 보도록하자.
- 위의 그림은 Member 클래스가 Team 타입의 team 필드 변수를 가지고 있는 형태인데, 코드로 나타내면 아래와 같다.
- class Member { String id; Team team; String username; } class Team { Long id; String name; }
- 그렇다면 Team 객체를 참조하는 필드를 가지고 있는 Member 객체는 어떻게 저장할까?
- Member member = new Member(); member.setId("100"); member.setUsername("dbjh"); Team team = new Team(); team.setName("dev_team"); member.setTeam(team); jpa.persist(member);
- 위처럼 Member 객체의 team 필드에 Team 객체를 set하고 Member 객체를 DB에 저장하게 되면 JPA는 아래와 같은 코드를 데이터베이스에게 실행하라고 할것이다.
- INSERT INTO MEMBER (ID, TEAM_ID, USERNAME) .... INSERT INTO TEAM (ID, NAME) ....
- 이렇게 저장 후 Member 객체만 조회하면, Team 객체 정보도 가져와서 Member 객체의 team필드에 주입해주기 때문에 아래와 같이 사용할 수 있다.
- // JAVA 코드 Member member = jpa.find(Member.class, memberId); Team team = member.getTeam(); // 변환된 쿼리 SELECT M.*, T.* FROM MEMBER M JOIN TEAM T ON M.TEAM_ID = T.TEAM_ID
- 위와 같은 구조들이 더 복잡해진다고 해도 JPA는 이를 모두 지원해주기 때문에 문제없이 사용할 수 있다.
JPA의 저장 및 조회는 아래와 같은 구조로 실행
**저장** : jpa.persist(member)
**조회** : Member member = jpa.find(memberId)
**수정** : member.setName("홍길동")
**삭제** : jpa.remove(member)
[저장]
MemberDAO에서 객체를 저장하고 싶을 때 개발자는 JPA에 Member 객체를 넘기면 JPA는 아래와 같이 동작한다.
- Member 엔티티를 분석한다.
- INSERT SQL을 생성한다.
- JDBC API를 사용하여 SQL을 DB 에 날린다.
[조회]
Member 객체를 조회하고 싶을 때 개발자는 member의 pk 값을 JPA에 넘긴다.
- 엔티티의 매핑 정보를 바탕으로 적절한 SELECT SQL을 생성한다.
- JDBC API를 사용하여 SQL을 DB에 날린다.
- DB로 부터 결과를 받아온다.
- 결과(ResultSet)를 객체에 매핑한다.
쿼리를 JPA가 만들어 주기 때문에 Object와 RDB간의 패러다임 불일치를 해결 할 수 있다.
[수정]
- JPA는 데이터 수정시, 매핑된 객체(테이블 데이터)를 조회해서 값을 변경 후 커밋하면 DB 서버에 UPDATE 문을 전송하여 UPDATE를 실행