JPA를 왜 사용해야 하는가?

김민우·2022년 8월 27일
0

JPA

목록 보기
1/10

우리가 무언가를 공부하기에 앞서서 왜 공부를 해야하는지, 이게 정확히 어떤 것을 하는지를 알아야 한다.

이러한 관점에서 우리는 왜 JPA를 사용해야 하는지 알아보자.

현대적인 어플리케이션을 개발한다 하면 대부분은 OOP 언어를 사용한다.
데이터를 저장하기 위해 관계형 데이터베이스(이하 RDB)를 사용한다. (DB는 거의 99% RDB를 사용한다고 보면 된다.)
지금 시대는 객체를 관계형 DB에 저장해서 관리해야 한다. (언어는 OOP를 쓰고 DB는 RDB를 쓰므로) 그러기 위해선 객체를 RDB에 저장하기 위해 SQL로 바꿔야한다.

따라서, 우리는 RDB가 알아들을 수 있는 SQL을 계속 작성해야한다.
이렇게 되면 SQL 중심적인 개발을 할 수 밖에 없고 이는 많은 문제점들을 야기한다.


SQL 중심적인 개발의 문제점

객체를 RDB에 저장하기 위해 SQL을 계속 작성하다보면 뭔가 반복적인 코드만 작성하는 느낌이 든다. 이러한 반복적인 코드를 작성하는 것도 문제지만 중간에 간단한 로직을 수정(새로운 필드를 추가)한기 위해선 지금까지 작성해둔 모든 쿼리를 수정해야하는 대참사가 발생한다.

또한, OOP와 RDB가 지향하는 목표(패러다임)이 완전히 다르다.
RDB는 데이터를 잘 정규화해서 보관에 집중하는 반면, OOP는 필드, 메서드를 하나로 묶어 캡슐화하여 사용하는 것에 집중한다. 이는 뒤에서 자세히 알아보록하자.

물론 객체는 관계형 DB외에 다른 저장소에 저장할 수 있다. 하지만 현실적인 대안은 RDB이다.

  • 앞서 언급했듯 우리는 객체를 RDB에 저장하기 위해선 객체를 SQL로 변환해야 한다.

실제로 이렇게 개발을 해보면 SQL 변환 과정에 시간을 많이 소모하게 된다.


OOP와 RDB의 차이

OOP와 RDB의 차이는 크게 4가지로 구분할 수 있다.

  1. 상속
  2. 연관관계
  3. 데이터 타입
  4. 데이터 식별 방법

하나씩 알아보자.

상속

  • 객체는 당연히 상속이 가능하나 RDB는 상속이 불가능하다.
    (그나마 유사한게 Table의 슈퍼타입 서브타입 관계가 상속과 비슷하지만, 상속이라 볼 수 없다.)

이와 같은 상황에서 Album객체 저장을 생각해보자.
객체를 분해하고 Album 객체와 부모 객체인 Item 객체 2개에 대한 INSERT QUERY를 둘 다 작성해야 한다.

별거 아닌데? 라는 생각이 들 수도 있다. 조회의 경우를 생각해보자.

  1. 각각의 테이블에 따른 조인 SQL 작성...
  2. 각각의 객체 생성...
  3. 상상만 해도 복잡
  4. 더 이상의 설명은 생략한다.

그래서 DB에 저장할 객체에는 상속 관계 안쓴다.
근데 문뜩 이런 생각이 든다. 자바 컬렉션에 저장하면 어떨까?? 이런 복잡한 과정이 매우 간단해질 것이다.

연관관계

  • 객체는 참조를 사용한다. (객체.getXXX())
    • 그림에서 Member -> Team (o)
    • 그림에서 Team -> Team (x)
  • 테이블은 외래 키 사용 (fk, pk를 join)
    • Member -> Team (o)
    • Team -> Member (o)

일단 이런 차이가 있다는 것만 알아두자.

보통은 객체를 테이블에 맞춰서 모델링한다. 이러면 매핑은 간단하하나 객체 지향스럽다 할 수 없다. 객체 지향으로 모델링을 한다면 참조(.)로 연관관계를 맺어야 하니간

그리고 이렇게 하면 pk와 fk를 참조를 통해 작성하여 매핑해주면 되지않을까?
아니다. 조회에서 문제가 생기며 매우 번거로워진다.

이 또한 자바 컬렉션에 넣는다면 어떨까? 이 모델링은 매우 괜찮다.
(즉, 객체 설계를 객체 지향적으로 설계 후 자바 컬렉션에서 CRUD 자유롭게 사용)

객체 그래프 탐색

참고
객체 그래프 탐색 : 객체에 .을 통해 계속 다음 객체를 방문하는 것
예) Member.Order.OrderItem.Item.Category....

객체는 자유롭게 객체 그래프를 탐색할 수 있어야 한다.

그러나 RDB는 처음 실행하는 SQL에 따라 탐색 범위가 결정되어서 이렇게 할 수 없다.
(Member, TEAM 을 실행하면 중간에 있는 Order는 호출 불가능)
그렇다고 상황마다 필요한 객체를 꺼내올 수 있는 것도 아니다.

엔티티 신뢰 문제

앞선 상황같이 객체를 자유롭게 넘나들 수 없다면 엔티티 신뢰문제가 발생한다. 보통 레이아웃 아키텍쳐라는 그 다음 계층에 대해 신뢰를 하고 사용해야 하는데 논리적으로는 신뢰가 안된다. 다음 코드를 보자.

class MemberService {
	...
	public void process() {
		Member member = memberDAO.find(memberId);
		member.getTeam(); //???
		member.getOrder().getDelivery(); // ???
	}
}

객체 그래프를 보면 당연히 Member를 통해 Order를 호출 할 수 있다. 그러나, MemberTeam 객체만 조회한 상황에서는 SQL에서 Order을 탐색하지 않아 Member.getOrder() 을 하여 꺼낸 객체를 신뢰할 수 없다는 것이다.

이러한 상황은 모든 객체를 미리 로딩할 수는 없다는 치명적인 문제를 야기한다. 이를 해결하기 위해선 상황에 따라 동일한 회원 조회 메서드를 여러벌 생성하면 되겠지만 그것을 구현하는 것은 매우 복잡한 일이다.

memberDAO.getMember(); //Member만 조회 
memberDAO.getMemberWithTeam();//Member와 Team 조회
 
//Member,Order,Delivery
memberDAO.getMemberWithOrderWithDelivery();
  • 상황에 따른 조회 메소드를 생성하는 경우의 수는 매우 크다.

진정한 의미의 계층 분할이 어렵다.

String memberId = "100";
Member member1 = memberDAO.getMember(memberId);
Member member2 = memberDAO.getMember(memberId);

member1 == member2; // 다르다. 

class MemberDAO {
	public Member getMember(String memberId) {
		String sql = "SELECT * FROM MEMBER WHERE MEMBER_ID = ?";
		...
		//JDBC API, SQL 실행
		return new Member(...);
	}
}
  • 일반적인 SQL에서 동일한 pk를 사용하여 값을 조회하면 다른 참조가 나온다.
String memberId = "100";
Member member1 = list.get(memberId);
Member member2 = list.get(memberId);

member1 == member2; // 같다.
  • 자바 컬렉션에서 동일한 필드값을 통해 두 객체를 얻으면 두 객체의 참조는 당연히 같다고 나온다.

어떻게 해야하는가?

지금까지 알아본 결과 객체답게 모델링을 할수록 번잡한 매핑작업만 늘어나고 매우 힘들어진다. 근데 각 문제점 마다 공통적인 해결책이 있었다. 바로 객체를 자바 컬렉션에 저장하듯이 하는 방법이였다.

그러면 객체를 자바 컬렉션에 저장하듯이 DB에 저장할 수는 없을까? 자바 진영에서 그 고민의 결과가 JPA이다. 이제부터 JPA를 알아보자.


JPA

JPA는 Java Persistenc API의 약자로 자바 ORM 표준이다.

ORM은 Object-relational mapping(객체 관계 매핑)의 약자로 객체는 객체대로, 관계형 DB는 관계형 DB대로 설계를 하면 ORM 프레임워크가 중간에서 객체와 관계형 DB를 매핑하도록 해준다. 대중적인 언어에는 대부분 ORM 기술이 존재한다.

그동안 봤던 문제점들을 해결해주는 JPA는 얼마나 대단할까? 사실 엄청난 것은 아니다. JPA는 다음과 같이 단순히 애플리케이션과 JDBC 사이에서 동작한다.

지금부터 JPA를 통한 저장/조회 동작은 어떻게 작동하는 알아보자.

JPA 동작 - 저장

개발자가 직접 JDBC API를 사용하는 것이 아니라 JPA 명령을 하면 JPA가 JDBC API를 사용해서 SQL을 호출하고 DB에서 결과를 받는다.

JPA가 하는 일을 순차적으로 알아보자.

1. JPA에게 객체를 넘기면 JPA가 그 객체를 분석한다.
2. 그 후 적절한 INSERT QUERY를 생성한다.
3. JDBC API를 사용해서 INSERT QUERY를 DB로 보내고 결과를 받는다.

JPA가 뭔가 대단한 기능을 하는 것은 아니다. 단순히 JDBC API를 통해서 DB와 통신을 한다.

중요한 것은 QUERY를 개발자가 아닌 JPA가 만들어주고, 패러다임 불일치를 해결해준다.

JPA 동작 - 조회

개발자는 JPA에 필드값(pk값)을 넘긴다. 그러면 JPA가 해당 필드값을 가진 멤버 객체를 보고 SELECT QUERY를 생성하고 JDBC API를 통해 DB로 보내고 결과를 받아 ResultSet을 매핑한다.

저장과 마찬가지로 중요한 것은 패러다임 불일치를 해결해준다는 것이다.

JPA 소개

JPA는 인터페이스의 모음이고 구현체로는 하이버네이트, EclipseLink, DataNucleus 이렇게 3가지가 있다. 이들은 JPA 2.1 표준 명세를 구현한 구현체이다.
참고로, 구현체로 8~90%는 하이버네이트를 사용한다.

JPA 버전

  • JPA 1.0(JSR 220) 2006년 : 초기 버전. 복합 키와 연관관계 기능이 부족
  • JPA 2.0(JSR 317) 2009년 : 대부분의 ORM 기능을 포함, JPA Criteria 추가
  • JPA 2.1(JSR 338) 2013년 : 스토어드 프로시저 접근, 컨버터(Converter), 엔티 티 그래프 기능이 추가

그냥 JPA 2.0 이후로는 왠만한 기능이 다 된다는 것만 알아두자.


왜 JPA를 사용해야 하는가?

이제까지 JPA의 간단한 소개와 기본적인 동작들을 알아봤다. 이제 JPA를 사용하면 얻는 이점들에 대해 알아보자.

  • SQL 중심적인 개발에서 객체 중심으로 개발
  • 생산성
  • 유지보수
  • 패러다임의 불일치 해결
  • 성능
  • 데이터 접근 추상화와 벤더 독립성
  • 표준

하나씩 알아보자.

생산성

JPA를 사용하면 CRUD가 매우 간단해진다. 코드가 다 만들어져 있다.

  • 저장 : jpa.persist(member)
  • 조회 : Member member = jpa.find(memberId)
  • 수정: member.setName(“변경할 이름”)
  • 삭제: jpa.remove(member)

이렇게만 하면 쿼리는 JPA가 알아서 쿼리를 만들어주고DB에서 꺼내고 이런 것들을 다 해준다.

이 중에서도 수정이 매우 혁신적이다. 단순히 값만 바꾸고 별도로 저장을 안해도 JPA가 알아서 수정을 해준다. 이는 마치 자바의 컬렉션과 비슷하다고 볼 수 있다. (아까 JPA가 지향했던 것을 생각해보자.)

유지 보수

기존에는 필드 변경시 이렇게 모든 SQL을 수정해야 했다.

JPA를 사용하면 DB에 컬럼이 추가되있다는 가정하에 SQL은 건들 필요 없이 객체 코드만 수정하면 된다.

객체와 RDB 패러다임(목표) 불일치 해결

앞서 알아본 패러다임 불일치 문제를 JPA가 어떻게 해결하는지 알아보자.

상속

  • 저장
    객체 저장시 JPA는 해당 객체의 부모객체에 대한 INSERT QUERY를 생성한다. 덕분에 개발자는 데이터 구조에 대한 고민없이 단순히 객체를 저장하기만 하면된다.
    예) Album 객체를 저장할 시 JPA가 부모 객체인 Item 객체에 대한 INSERT QUERY도 생성해준다.
  • 조회
    객체 조회시 JPA는 해당 객체의 부모 객체를 Join해서 데이터를 가져온다.
    예) Album 객체를 꺼내면 해당 Item 객체와 Join을 한 SELECT QUERY를 생성하여 데이터를 가져온다.

JPA와 연관관계, 객체 그래프 탐색

  • 마치 자바의 컬렉션에 저장하고 조회하듯이 사용가능하게 해준다.

신뢰할 수 있는 엔티티, 계층

class MemberService {
	...
	public void process() {
		Member member = memberDAO.find(memberId);
		member.getTeam(); // 자유로운 객체 그래프 탐색
		member.getOrder().getDelivery();
	}
}
  • JPA의 지연/로딩 기능을 사용하여 객체 그래프를 자유롭게 탐색할 수 있게 해주어 신뢰할 수 있다.
String memberId = "100";
Member member1 = jpa.find(Member.class, memberId);
Member member2 = jpa.find(Member.class, memberId);

member1 == member2; // 같다.
  • JPA를 통해 동일한 트랜잭션에서 조회한 엔티티는 같음을 보장해준다. 이는 자바 컬렉션에서 객체를 조회하는 것과 같다.

JPA의 성능 최적화 기능

JPA와 같은 중간기능이 있으면 기존 방법에 비해 성능이 악화된다고 생각된다.
하지만 이를 통해 버퍼링과 캐싱 가능해지면서 성능 향상을 시킬 수 있다.

따라서, JPA를 정말 잘 활용을 하면 성능을 향상시킬 수 있다. 어떻게 향상시킬 수 있는지 알아보자.

1차 캐시와 동일성(identity) 보장

String memberId = "100";
Member m1 = jpa.find(Member.class, memberId); // SQL
Member m2 = jpa.find(Member.class, memberId); // 캐시

println(m1 == m2) // true
  • SQL을 1번만 실행한다.
  • 같은 엔티티를 반환하는 것을 통해 같은 객체를 계속 조회한다고 하면 새로운 객체를 생성하지 않으므로 기존에 생성한 것을 그대로 리턴한다. (캐싱)
  • 물론, JPA에서 캐시는 굉장히 짧은 시간(고객의 요청이 오고, 트랜잭션이 종료된 시점까지)에서만 유효하므로 실무에서 크게 의미는 없다.
  • DB Isolation Level이 Read Commit이어도 애플리케이션에서 Repeatable Read를 보장한다.

트랜잭션을 지원하는 쓰기 지연(transactional write-behind)

transaction.begin(); // [트랜잭션] 시작
 
em.persist(memberA);
em.persist(memberB);
em.persist(memberC);
//여기까지 INSERT SQL을 데이터베이스에 보내지 않는다.

//커밋하는 순간 데이터베이스에 INSERT SQL을 모아서 보낸다.
transaction.commit(); // [트랜잭션] 커밋
  • 트랜잭션을 커밋할 때 까지 INSERT QUERY를 모은 후 JDBC BATCH SQL 기능을 사용해서 한 번에 SQL을 전송 (버퍼링)
  • 커밋하는 순간 DB에 그 동안 생성해놨던 INSERT QUERY를 보낸다.

참고
DB에서 트랜잭션이 매우 중요하다. DB 입장에선 쿼리를 따로 보내든 한번에 보내든 커밋하기 전까지만 보내면 된다.
따라서, JPA는 생성한 쿼리를 별도의 저장소에 저장 후 커밋 메소드가 호출되면 한 번에 보낸다.


JPA의 지연 로딩(Lazy Loading)과 즉시 로딩

이는 JPA에서 제일 중요한 부분이라 별도로 분리했다.

지연 로딩

객체가 실제로 사용이 될 때 까지 로딩하는 기능이다.

실제 객체가 생성되는 시점에서 쿼리를 보내는 것이 아니라 값이 실제로 필요한 시점에서 쿼리를 보낸다.

참고
프록싱 기능을 사용하여 지연 로딩을 구현한다.

즉시 로딩

JOIN SQL로 한 번에 연관된 객체까지 미리 조회하는 기능이다.

Member 객체를 통해 Team 객체를 접근할 때마다 매번 JOIN SQL을 생성한다. 이러한 작업이 많이 일어나면 그때 마다 생성되는 것은 비효율적이다.

이러한 이유때문에 JPA에서는 Member 객체를 생성했을 때 한 번에 연관된 객체까지 미리 JOIN SQL을 만들어놓는 기능을 제공한다.

공통점

JPA는 이 옵션들은 별도의 설정을 통해 on/off가 가능하다.
만약 JPA가 없었다면 즉시 로딩이 구현안 된 상태에서 구현을 하려면 수 많은 코드들을 수정함은 물론 쿼리 또한 일일히 다 바꿔야한다.

JPA는 설정 하나로 이것을 매우 쉽게 해준다.

실제 개발 환경에서는 지연 로딩으로 작성 후 뭔가 최적화가 필요한 부분에만 즉시 로딩 옵션을 키면 된다. 매우 효율적인 개발이 된다.


결론

물론, JPA만 잘 안다고 해서 되는 것은 절대 아니다. OOP와 RDB 모두 잘 해야 한다는 것이다.

그 중 RDB가 더 중요하다고 보여진다.

객체지향 언어는 자바든 파이썬이든 유행하는 언어가 바뀔 가능성이 크고 어플리케이션이 변경될 가능성도 있다. 그러나 데이터 같은 분야는 변동 가능성이 매우 적다.

0개의 댓글