본문 바로가기
[ ORM ]/JPA

[ JPA ] JPA 소개

by 환이s 2024. 2. 15.


 

JPA?

실무에서 개발을 하다 보면 Mybatis를 활용해서 CRUD 기능을 직접적인 쿼리 작성으로 구현하는 방식을 많이 접할 것이다. 이렇듯 SQL에 의존적인 개발을 피하기 어렵다.

 

하지만 지금 시대는 객체를 관계형 데이터베이스에 저장하는 방식이고, SQL에 의존적인 개발을 하다 보면

모델링을 할수록 매핑 작업이 늘어나는 과정을 매일 겪을 것이다.

 

객체를 자바 컬렉션에 저장하듯이 DB에 저장할 수는 없을까?

 

이러한 고충을 해결할 수 있는 ORM 기술이 JPA(Java Persistence API) 자바 진영의 기술 표준이다.

 

 

 

 

ORM?

 

ORM이란 Object Relational Mapping의 약자로 객체와 관계형 데이터베이스의 데이터를 자동으로 매핑(연결)해주는 것을 말한다.

 

객체 지향 프로그래밍은 클래스를 사용하고, 관계형 데이터베이스는 테이블을 사용한다. 객체 모델과 관계형 모델 간에 불일치가 존재하는데, ORM을 통해 객체 간의 관계를 바탕으로 SQL을 자동으로 생성하여 이러한 불일치를 해결할 수 있다.

 

간단하게 정리해 보면 다음과 같다.

 

  • Object-relational mapping(객체 관계 매핑)
  • 객체는 객체대로 설계
  • 관계형 데이터베이스는 관계형 데이터베이스대로 설계
  • ORM 프레임워크가 중간에서 매핑
  • 대중적인 언어에는 대부분 ORM 기술이 존재

 

그렇다면 JPA를 왜 사용해야 하는가?

다양한 장점에 대해서 하나씩 알아보자.


생산성 - JPA와 CRUD

 

JPA는 애플리케이션과 JDBC 사이에서 동작한다. 

 

간단하게 알아보자면

 

• 저장: jpa.persist(member)

 

• 조회: Member member = jpa.find(memberId)

 

• 수정: member.setName(“변경할 이름”)

 

• 삭제: jpa.remove(member)

 

직접적인 쿼리 작성을 할 필요 없이 각 CRUD에 맞게 호출만 해주면 JPA가 알아서 기능에 맞게 호출을 해준다.


유지보수

 

글쓴이는 JPA 장점 중에서 유지 보수할 때 제일 큰 빛을 본다고 생각한다.

예시를 통해 알아보자.

 

public class Member {
 private String memberId;
 private String name;
 private String tel;
   ...
 }

 

간단한 모델 Member 클래스가 있다고 가정해 보자.

유지 보수 사항으로 필드 변경 요청이 들어왔다.

그렇다면 일일이 모든 SQL을 수정해야 하는 상황이 발생한다.

 

INSERT INTO MEMBER(MEMBER_ID, NAME, TEL) VALUES

 SELECT MEMBER_ID, NAME, TEL FROM MEMBER M
 
 UPDATE MEMBER SET … TEL = ?

 

예시처럼 필드 변경할 때 SQL이 적게 있으면 상관없다.

하지만 실무에서는 눈이 빠질 정도로 쿼리도 많고 필드 변경 시 수정해야 할 곳이 정말 많다.

 

여기서 JPA를 사용하면 필드만 추가하면 된다. 

기존 방식에서 SQL을 직접 수정해야 하는 번거로운 작업을 했다면

JPA는 SQL을 알아서 처리해 준다.


JPA와 패러다임의 불일치 해결

 

 

데이터베이스는 데이터 중심으로 구조화되어 있다.

객체의 상속, 다형성 같은 개념이 없다.

그렇다 보니 객체와 데이터베이스가 지향하는 점이 다르다.

 

이것을 객체와 데이터베이스의 패러다임 불일치라고 한다.

 

자바 언어는 객체지향으로 이뤄져 있고 데이터베이스는 데이터 중심으로 구조화되어 있기 때문에

패러다임 불일치 문제를 개발자가 해결해야 한다.

이 과정에서 시간과 코드를 소비하는 문제가 있다.

 

아래는 패러다임 불일치의 사례이다.

 

  • 상속

 

객체는 위와 같이 상속의 기능을 지니지만 테이블은 상속의 기능이 없다.

그나마 데이터베이스 모델링에서 이야기하는 슈퍼타입 서브타입 관계를 사용하면

객체 상속과 유사한 형태로 테이블을 설계할 수는 있다.

 

 

Item Table의 DTYPE 칼럼을 사용하면 어떤 자식 테이블과 관계가 있는지 정의할 수 있다.

예를 들어 DTYPE 값이 MOVIE라면 영화 테이블과 관계가 있는 것이다.

 

abstract class Item {
    Long id;
    String name;
    int price;
}

class Album extends Item {
    String artist;
}

class Movie extends Item {
    String director;
    String actor;
}

class Book extends Item {
    String author;
    String isbn;
}

 

만약 Album을 저장하려면 이 객체를 분해해서 다음 두 SQL을 만들어야 한다.

 

INSERT INTO ITEM ...

INSERT INTO ALBUM ...

 

JDBC API를 이용해서 코드를 작성하려면 부모 객체에서 부모 데이터만 꺼내

ITEM용 SQL을 작성하고 자식 객체에서는 자식 데이터만 꺼내 ALBUM용 SQL을 작성해야 한다.

 

작성해야 할 코드도 많고 자식 타입에 따라 DTYPE도 추가로 저장해야 한다.

이런 과정이 패러다임 불일치를 해결하기 위해 개발자에 소모하는 비용이다.

 

만약, 데이터가 자바 컬렉션에 보관된다면 부모, 자식이나 타입에 대한 고민 없이 컬렉션을 조회하면 된다.

 

list.add(album);
list.add(movie);

Album albun = list.get(albumId);

 

JPA는 패러다임 불일치 문제를 개발자 대신 처리해 준다.

개발자는 자바 컬렉션에 저장하듯이 JPA에게 객체를 저장하면 된다.

 


연관관계

 

객체는 참조를 사용해 다른 객체와 연관 관계를 가지고 참조에 접근해 연관된 객체를 조회한다. 반면, 테이블은 외래 키를 사용해 다른 테이블과 연관 관계를 가지고 JOIN을 사용해서 연관된 테이블을 조회한다.

 

아래와 같이 Member, Team이 연관 관계를 지니는 상황으로 알아보자.

 

 

Member 객체는 Member.team 필드에 Team 객체의 참조를 보관해서 Team 객체와 관계를 맺는다.

 

이 참조 필드에 접근해 Member와 연관된 Team을 조회할 수 있다.

반면 MEMBER 테이블은 MEMBER_ID 외래 키 칼럼을 사용해서 TEAM 테이블과 관계를 맺는다.

이 외래키를 사용해 TEAM 테이블과 JOIN 하면 MEMBER 테이블과 연관된 TEAM 테이블을 조회할 수 있다.

 

여기서 발생하는 문제는 객체의 참조 방향이다.

객체 연관 관계의 경우 member.getTeam으로 참조가 가능하지만

반대로 Team.getMember()는 불가능하다.

 

반면 테이블은 어느 쪽에서든 JOIN을 사용할 수 있다.

 

즉, 관계형 데이터베이스가 사용하는 방식에 맞추면

Member 객체와 연관된 Team 객체를 참조를 통해 조회할 수 없다.

 

기존 방식을 따르면 좋은 객체 모델링이 어렵고 객체지향의 특징을 잃게 된다.

 

JPA는 이러한 연관 관계와 관련된 패러다임 불일치를 해결해 준다.

 

member.setTeam(team); //회원과 팀 관계 설정
jpa.persist(member); //회원과 연관관계 함께 저장

 

개발자는 회원과 팀의 관계를 설정하고 저장하기만 하면 된다.

JPA는 team의 참조를 외래 키로 변환해 적절한 Insert SQL문을 데이터베이스로 전달한다.

 

객체를 조회할 때 외래 키를 참조로 변환하는 일도 JPA가 처리해 준다.

 

Member member = jpa.find(Member.class, memberId);
 Team team = member.getTeam();

 

간단하게 member의 스키마가 변한다고 생각해 보자.

JPA를 사용하지 않는다면 추가되는 컬럼을 member.getxx()을 사용해서 추가해 주거나

변경해야 하는 작업이 생길 것이다.

 

지금까지는 SQL을 직접 다루어도 열심히 코드만 작성하면 극복할 수 있는 문제들이었다.

하지만 연관관계와 관련된 극복하기 어려운 패러다임 불일치 문제도 있다.

 


객체 그래프 탐색(신뢰할 수 있는 엔티티, 계층)

 

객체에서 회원이 소속된 팀을 조회할 때

다음 예제처럼 참조해서 사용하면 연관된 팀을 찾을 수 있다.

 

 

이것을 객체 그래프 탐색이라고 한다.

 

Team team = member.getTeam();

member.getOrder().getOrderItem()...//자유로운 객체 그래프 탐색

 

객체는 마음껏 객체 그래프를 탐색할 수 있어야 한다.

그런데 데이터베이스에서는 객체를 조회할 때

member와 Team의 데이터만 조회했다면 member.getOrder()은 null이 된다.

 

SQL을 직접 다루면 처음 실행하는 SQL문에 따라 객체 그래프의 탐색이 한정된다.

객체 지향 개발자에게는 큰 제약이다.

 

비즈니스 로직에 따라 사용하는 객체 그래프가 다른데 언제 끊어질지 모를 객체 그래프를 함부로 탐색할 수 없다.

 

class MemberService {
    ...
    public void process() {
        Member member = memberDAO.find(memberId);
        member.getTeam(); //자유로운 객체 그래프 탐색
        member.getOrder().getDelivery();
    }
 }

 

위의 코드만 가지고는 객체 그래프를 어디까지 탐색할지 알 수 없다.

전적으로 SQL문에 달려있다.

 

그렇다고 연관된 객체를 모두 조회해서 메모리에 올리는 것 또한 현실성이 없다.

결국 매번 MemberDAO 클래스에는 상황에 따라 메서드를 여러 개 만들어 두어야 한다.

memberDAO.getMember();
memberDAO.getMemberWithTeam();
memberDAO.getMemberWithOrderWithDelivery();

 

 

JPA는 객체 그래프를 편하게 탐색할 수 있다.

 

member.getOrder().getOrderItem()...//자유로운 객체 그래프 탐색

 

JPA는 연관된 객체를 사용하는 시점에 적절한 Select SQL문을 실행한다.

따라서 JPA를 사용하면 연관된 객체를 신뢰하고 조회할 수 있다.

 

이 기능은 실제 객체를 사용할 때까지 조회를 미룬다고 하여 lazy loading(지연 로딩)이라고 한다.

JPA는 lazy loading(지연 로딩) 을 투명하게 처리한다.

 

class Member {
  private Order order;
  
  public Order getOrder() {
    return order;
  }
}

 

getOrder 메서드 구현 부분에는 JPA와 관련된 어떤 코드도 직접 사용하지 않는다.

 

Member member = jpa.find(Member.class, memberId);

Order order = memeber.getOrder();
order.getOrderDate(); //Order를 사용하는 시점에 SELECT ORDER SQL

 

Member를 사용할 때마다 Order를 함께 사용한다면

이렇게 한 테이블씩 조회하는 것보다 동시에 조회하는 게 효과적이다.

 

JPA는 연관된 객체를 즉시 함께 조회할지

아니면 실제 사용 시점에 조회할 지 간단한 설정으로 정의할 수 있다.


JPA와 비교하기

 

데이터베이스는 기본 키의 값으로 각 low를 구분한다.

반면, 객체는 동일성 비교와 동등성 비교라는 두 가지 방법이 있다.

 

동일성 비교는 (==) 비교다.

객체의 인스턴스 주소 값을 비교한다. 

동등성 비교는 equals() 메서드를 사용해 객체 내부의 값을 비교한다.

 

따라서 테이블 low와 객체를 구분하는 방법에는 차이가 있다.

class MemberDAO {

  public Member getMember(String memberId)  {
      String sql = "SELECT * FROM MEMBER WHERE MEMBER_ID = ? ";
      ...
      //JDBC API, SQL 실행
      return new Member(...);
      }
}

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

member1 == member2; // false

 

같은 id로 데이터를 조회했을 때 두 객체가 다르다.

같은 low에서 조회했지만 객체 측면에서 둘은 다른 인스턴스이다.

 

getMember를 호출할 때마다 새로운 인스턴스를 만들기 때문이다.

만약 객체를 컬렉션에 보관했다면 동일성 비교에 성공했을 것이다.

 

Member member1 = list.get(0);
Member member2 = list.get(1);

member1 == member2; // true

 

이런 패러다임 불일치를 해결하기 위해 같은 low를 조회할 때마다

같은 인스턴스를 반환하도록 구현하는 것은 쉽지 않다.

여기에 트랜잭션이 동시에 실행되는 상황까지 고려하면 문제는 더 어려워진다.

 

JPA는 1차 캐시와 동일성을 보장한다.즉, 같은 트랜잭션일 때 같은 객체가 조회되는 것을 보장한다.

 

String memberId = "100";
Member member1 = jpa.find(Member.class, memberId);
Member member2 = jpa.find(Member.class, memberId);

member1 == member2; // true

 

정리하자면

객체 모델과 데이터베이스 모델은 지향하는 패러다임이 다르고

이것을 극복하기 위해 개발자는 많은 시간과 코드를 소비한다.

 

객체 지향에 가깝고 모델링할수록 패러다임 불일치의 문제는 더욱 커진다.

결국 애플리케이션은 점점 데이터 중심 모델로 변해간다.JPA는 이런 문제를 해결할 수 있게 해 준다.


마치며

 

오늘은 JPA의 개념 및 장단점에 대해 알아봤습니다.초기에 공부했다가 시간이 지나 기억이 가물가물 하여 강의를 통해서 복습하고 있는데JPA에 대한 이해도가 점점 더 단단해지는 거 같습니다.

 

위 포스팅 글은 김영한님의 자바 ORM 표준 JPA 프로그래밍-기본편을 참고했습니다.

 

자바 ORM 표준 JPA 프로그래밍 - 기본편 강의 - 인프런

현업에서 실제로 JPA로 개발을 하고 있습니다. 그런 입장에서보면 지금 작성하고 있는 코드들이 어떻게 작동하는지 이해하는데 큰 도움을 주는 강의입니다. 다음은 제가 느낀 이 강의의 장점들

www.inflearn.com

728x90