우선 애플리케이션은 객체지향적이고 데이터베이스는 관계형 DB를 사용한다는 점에서 차이가 있다. 하지만 지금 시대는 객체를 관계형DB에서 관리한다는 점이 특징이다.
여기서 발생하는 문제점은 SQL 중심적으로 개발한다는 점이다. CRUD와 같은 무한 반복, 지루한 코드를 작성하면서 자바객체➡️SQL 코드로, SQL 코드➡️자바 객체로 변환하는 과정이 매우 많다는 점이다. 또, 자바 객체에서 필드 추가(예를 들면 tel)가 발생한다면, 내가 작성했던 SQL 코드를 모두 수정해야한다는 문제가 있었다. 이렇듯 SQL에 의존적인 개발을 피하기란 쉽지 않다.
객체 지향 프로그래밍은 추상화, 캡슐화, 정보은닉, 상속, 다형성 등 시스템 복잡성을 제어하는 다양한 장치를 제공한다. 객체를 저장하기 위해서 RDB, NoSQL, File, OODB 같은 다양한 저장소가 있지만 현실적인 대안은 관계형 데이터베이스다. 객체를 SQL로 변환해서 관계형 데이터베이스에 저장하는 것이 필요한데 이렇게 변환해주는 것이 개발자의 역할이기도 하다.
그렇다면 객체를 관계형 데이터베이스로 변환시켜야하는데, 이 둘의 차이는 무엇일까?(패러다임의 불일치 문제)
객체는 상속관계가 있지만, DB는 슈퍼타입과 서브타입이 있다. 따라서 자식 클래스로 객체를 저장할때에도 객체에 맞게 DB를 저장한다면 객체를 분해해서 부모와 자식을 모두 INSERT 쿼리를 날려야 할것이다. 또 자식 클래스의 객체를 조회할때도 각각 테이블에 조인 SQL을 생성해, 각자 객체 생성 후, 등등 여러 일들이 필요하다. 따라서 DB에서는 저장할 객체는 상속관계를 사용하지 않는다.
만약, 자바 컬렉션에 저장한다면 list.add(album), 자바 컬렉션에서 조회한다면 Album album = list.get(albumId)이 될 수도 있고 부모 타입으로 다형성을 활용한다면 Item item = list.get(albumId) 가 되어 더 편리하게 사용할 수 있다.
객체는 참조를 사용해 연관관계에 있는 객체를 가져올 수 있다. member.getTeam() 테이블은 외래키를 사용해서 같은 필드에 대해 조인해서 연관관계에 있는 테이블을 가져올 수 있다. JOIN ON M.TEAM_ID = T.TEAM_ID
그렇다면 객체를 테이블에 맞게 모델링하면,
class Member{
Long id; // MEMBER_ID PK 컬럼 이용
Long teamId; // TEAM_ID FK 외래키 컬럼 이용
String username; // USERNAME 컬럼 이용
}
class Team{
Long id; // TEAM_ID PK 컬럼 이용
String name; // NAME 컬럼 이용
}
테이블에 맞추었기 때문에, INSERT 쿼리는 편리하게 작성할 수 있다. 하지만 객체답게 모델링을 하면
class Member{
Long id; // MEMBER_ID PK 컬럼 이용
Team team; // **참조로 연관관계를 맺음**
String username; // USERNAME 컬럼 이용
}
class Team{
Long id; // TEAM_ID PK 컬럼 이용
String name; // NAME 컬럼 이용
}
TEAM_ID는 member.getTeam().getId()를 통해서만 가져올 수 있으므로 번거로워진다. INSERT INTO MEMBER(MEMBER_ID, TEAM_ID, USERNAME) VALUES ...
하지만 '상속' 내용처럼 자바 컬렉션으로 객체들을 관리하면 편리해진다.
list.add(member); Member member = list.get(memeberId);
객체는 자유롭게 객체 그래프로 탐색할 수 있어야 한다. Member라는 객체와 연관이 있는 객체가 Team, Order, OrderItem, Delivery, Item, Category가 있을때 이 객체에 대해 탐색이 가능해야 한다.
하지만 member.getTeam() 이나 member.getOrder().getDelivery() 일때 엔티티가 확실히 있어야 하지만 실제로 존재하는지 알 수 없는 문제가 있다. 따라서 상황에 따라서 동일한 회원 조회 메서드를 여러번 생성해야 한다. 그러나 모든 객체를 미리 로딩할 수 없는 문제가 있다.
Member member1 = memberDAO.getMember(memberId)
Member member2 = memberDAO.getMember(memberId)
member1 == member2 // 다르다
반면 자바 컬렉션에서 조회하면 같다는 결과가 나올 수 밖에 없다. 같은 멤버 객체를 조회한다면 서로 같아야 함에도 불구하고 다르다는 결과가 나와서 매핑 작업이 어렵다는 것을 알 수 있다.
Member member1 = list.get(memberId)
Member member2 = list.get(memberId)
member1 == member2 // 같다
객체답게 모델링을 하면 할수록 매핑작업만 늘어나게 된다. 객체를 자바 컬렉션에 저장하듯이 db에 저장할 수 없을까?라는 의문이 생긴다. 그래서 나온 것이 JPA-Java Persistence API이다.
JPA는 자바 진영의 ORM 기술 표준을 말한다. 여기서 ORM이란, Object-Relational mapping으로 객체와 관계형 db를 매핑해준다는 의미의 기술을 말한다. 객체는 객체대로 설계하고, 관계형 데이터베이스는 관계형 데이터베이스로 설계할 수 있도록 한다. ORM 프레임워크는 중간에서 매핑해주는 역할을 한다. 대중적인 언어는 대부분 ORM 기술이 존재한다.
JPA가 나오기까지, EJB라는 엔티티 빈(자바 표준)이 있었고 더 편리하게 사용하도록 하는 하이버네이트(오픈소스)가 나왔으며 이를 표준화한 JPA(자바표준)이 등장했다. JPA는 인터페이스의 모음인데, 이를 구현한 3가지 구현체로 하이버네이트, eclipseLink, DataNucleus가 있다. 우리는 특히 하이버네이트를 많이 쓰게 될 것이다.
JPA는 Java 어플리케이션과 JDBC API 사이에서 동작한다.

만약 MemberDAO를 통해서 멤버 객체를 저장하고 싶다면 JPA의 persist를 사용하면 편리하게 저장할 수 있다. 기존에 INSERT 쿼리를 작성하고 패러다임이 불일치했던 내용들을 JPA에서 대신 해준다.

엔티티 조회도 마찬가지로 JPA에서 쿼리를 대신 생성해주고 JDBC API를 사용하며 패러다임 불일치를 해결해준다.
SQL 중심적인 개발에서 객체 중심적으로 개발할 수 있고 생산성 향상, 유지보수 증가, 패러다임 불일치를 해결해주며 성능상 이점 있고 표준화된 ORM을 사용할 수 있기 때문이다. 마지막은 데이터 접근 추상화와 벤더 독립성때문이다.
crud 쿼리를 작성하는 비효율적인 생산과정이 줄어들고 빠르고 쉽게 작업을 할 수 있다.
저장 : jpa.persist(member)
조회 : Member member = jpa.find(memberId)
수정 : member.setName("변경이름")
삭제 : jpa.remove(member)
Member 클래스가 있을때 하나의 필드가 추가된다면 sql을 전부 수정해야하는 문제가 있었다. 하지만 JPA에서는 필드만 추가하면 SQL은 모두 JPA에서 처리해줄 수 있어서 유지보수가 향상된다.
(1) JPA와 상속
상속관계에 있는 부모와 자식 클래스가 있을때 객체는 [상속], 데이터베이스는 [슈퍼타입-서브타입]으로 해결해야 한다. 하지만 JPA를 사용한다면 개발자는 jpa.persist(album) 만 작성하고 JPA가 알아서 INSERT 쿼리를 포함해서 알아서 해결해준다.
(2) JPA와 조회
조회하려면 연관관계에 있는 객체들을 조인해서 어렵고 힘들게 조회해야했다면 개발자는 JPA로 jpa.find(Album.class, albumId)만 작성하고 JPA가 알아서 조인을 하는 등 불일치를 해결해준다.
(3) JPA와 연관관계, 객체 그래프 탐색
연관관계를 저장할때는 개발자는 JPA를 사용해서 아래처럼 작성해주기만 하면 된다. member.setTeam(team), jpa.persist(member) 이렇게 저장 후 객체 그래프 탐색에서 쉽게 Member member = jpa.find(Member.class, memberId), Team team = member.getTeam()으로 탐색이 가능하다.
(4) 신뢰가능한 엔티티와 계층
이전에는 find로 찾은 Member 객체에서 그래프 탐색시 신뢰할 수 없다는 문제가 있었는데, member.getTeam(), member.getOrder().getDelivery() 처럼 자유롭게 탐색이 가능하다.
(5) 객체 비교
Member member1 = jpa.find(Member.class, memberId)
Member member2 = jpa.find(Member.class, memberId)
member1 == member2 // 같다
동일한 트랜잭션에서 조회한 엔티티는 같음을 보장하기 때문에 위처럼 같다는 결론이 난다.
같은 트랜잭션 안에서 같은 엔티티를 반환한다. 따라서 최초 엔티티 조회시 SQL 쿼리문이 발생하지만, 이후 같은 엔티티 조회시 JPA가 들고있는 메모리 상에서 엔티티를 반환하게 되어 캐시를 활용하므로 쿼리가 발생하지 않는다.
트랜잭션 커밋전까지 INSERT SQL을 모으다가 JDBC BATCH SQL 기능을 통해 한번에 SQL을 전송할 수 있다. 한번에 sql을 보내므로 네트워크 통신비용이 줄어든다.
transaction.begin(); // 트랜잭션 시작
em.persist(memberA)
em.persist(memberB)
em.persist(memberC)
// 여기까지 insert sql을 db에 보내지 않고 있다가
// 커밋하는 순간 db에 insert 쿼리를 모아서 보낸다.
transaction.commit();
지연로딩은 객체가 실제로 사용될때 그 때 로딩되는 것을 말하고, 즉시로딩은 JOIN SQL로 한번에 연관된 객체까지 미리 조회해오는 것을 말한다.
Member member = memberDAO.find(memberId)
//(지연로딩)select * from Member where...
//(즉시로딩)select M.*, T.* from Member JOIN Team ...
Team team = member.getTeam();
String teamName = team.getName();
//(지연로딩)select * from Team where...
개발할때는 지연로딩으로 모두 설정해놓고 필요시에 나중에 즉시로딩으로 세팅하는 것을 권장한다.