JPA Backgrounds

기존에 java 기반 웹 어플리케이션에서 DB와 연동하기 위해서는 우리는 MyBatis나 Spring의 JDBC template를 이용해서 SQL Mapper을 통해 SQL을 생성해서 DB로 부터 정보를 받아오고, 저장하는 방식으로 진행했다. 하지만 이런 방법은 반복되는 SQL문 생성으로 효율적이지 못하다.

그래서, 자바의 객체 지향형 프로그래밍의 성질을 이용한 ORM 개념 프레임워크 중에 하나인 JPA라는 것이 도입되게 된다.

Why JPA

SQL 기반 개발의 문제점

우선 JDBC 방식으로 DB를 이용했을 때 발생할 수 있는 문제점에 대해 알아보자.

jdbc_structure

자바 웹 어플리케이션 jdbc API를 통해 DB에 접근할 수 있다.

JDBC를 회원 관리 예제릍 통해 알아보자

Member Class

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

Member DAO

public class MemberDAO{
    public Member find(String memberId){};
    public void save(Member member){};
}

Member Search

SQL="SELECT MEMBER_ID, NAME FROM MEMBER M WHERE MEMBER_ID=?";

ResultSet rs=stmt.executeQuery(sql);

String memberID=rs.getString("MEMBER_ID");
STring name=rs.getString("NAME");

Member member=new Member();
member.setMemberId(memberId);
member.setName(name);

Member join

SQL="INSERT INTO MEMBER VALUES(?,?)";

ResultSet rs=stmt.executeQuery(sql);
pstmt.setString(1,member.getMemberId());
pstmt.setString(2,member.getName());
pstmt.executeUpdate(sql)

이런 식으로, 자바에서는 Member라는 객체 단위로 회원을 관리하지만, SQL는 오직 tuple 단위로 데이터를 조작할 수 있기 때문에 개발자는 중간에 객체-tuple 간에 변환작업을 수행해야한다.

만약 이런 상황에서 회원은 특정 Team에 속해있어야 한다는 요구조건이 추가되면 어떻게 해야될까?

Team Class

class Team{
    private String teamId;
    private String teamName;
}

Member Class

public class Member{
    private String memberId;
    private String name;
    private Team team;
}

MemberDAO

public class MemberDAO{
    public Member find(String memberId){};
    public void save(Member member){};
    public Member findWithTeam(String memberId){};
}

SQL

SELECT M.MEMBER_ID, M.NAME, T.TEAM_ID,T.TEAM_NAME
FROM MEMBER M, TEAM T
WHERE M.MEMBER.TEAM_ID=T.TEAM_ID

위와 같은 sql를 추가로 생성해서 대입해야한다.

MemberDAO를 사용하는 목적이 SQL을 직접적으로 드러내지 않고, DAO의 메소드 호출을 통해 DB의 접근을 쉽게 하는 것이 목적인데, 이렇게 되면 매번 요구사항이 변경될때 마다 SQL에 의존적인 개발을 할 수 밖에 없다.

JPA는 이런 문제를 어떻게 해결할까?

JPA를 이용하게 되면, 개발자가 직접 SQL를 작성하는 것이 아니라, JPA에서 제공해주는 API를 이용하게 된다.

가령 저장 기능을 예로 보면,

Member member=new Member("id1","hello")
jpa.persist(member);

기존에 SQL Insert 문을 저장기능을 구현한 것을 우리는 jpa.persist()라는 메소드 호출로 DB에 저장할 수 있다.

조회 기능

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

이런 식으로 jpa를 이용해서는 개발자가 직접 SQL를 활용하지 않고도 객체기반으로 개발을 진행할 수 있게 해준다.

패러다임 불일치

자바는 대표적인 객체 지향형 프로그래밍 언로, OOP에 관련된 여러 속성들을 활용한다. 추상화, 캡슐화, 정보은닉화, 상속, 다형성, 등 다양한 개념들이 사용되는데, 이를 직접적으로 RDBMS에서는 사용하지 않는 속성들인데, 이를 패러다임 불일치라고 한다.

그래서, 이 마저도 개발자가 직접 변환작업을 통해 DB에 저장해야 된다.

Inheritance

우선 테이블 상속 관계에 대해 알아보자.

자바에 만약 아래와 같은 Class 구조를 설계 했다면

object_inheritance

DB에서는 table을 아래와 같이 설계하게 된다.

table

Class

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;
}

그래서 이를 DB에 저장하려고 하면

-- Album 객체 저장
INSERT INTO ITEM ...
INSERT INTO ALBUM ...

다음과 같이 두번의 SQL문을 작성해야 핸다. 부모 객체에서는 부모객체정보만을 가져와, ITEM table에 넣고, 자식 객체에서는 자식 정보만을 가져와 자식 관련 table에 넣는 작업을 해야한다.

만약 JPA를 이용한다면?

위와 같이 두번의 sql를 통해 구현한 저장 기능이 아래의 persist 메소드를 통해 간편하게 이루어지며,

Album album=new Item(...)
jpa.persist(album)

조회 기능 또한, 아래의 find 메소드를 통해 쉽게 구현할 수 있다.

String albumId="";
Album album=jpa.find(Album.class,albumId);

Association

자바에서는 객체 안에 인스턴스가 있으면, 해당 인스턴스들은 참조값을 이용해서 연관된 정보를 저장하게 된다. 이를 DB에서는 Foreign Key을 통한 외래키를 사용해서 다른 테이블과의 관계를 맺는다.

아래와 같은 연관관계를 지니는 Class와 이를 위한 Table이 있다고 하자

association

Member Class

// 참조를 사용하는 객체 모델
class Member {
    String id;
    Team team;          // 참조로 연관관계를 맺는다.
    String username;

    Team getTeam() {
        return team;
    }
}

class Team {
    Long id;
    String name;
}

class에서는 외부 정보인 Team를 참조 형태로 저장하지만, DB에서는 이를 외래키로 저장하게 된다. 따라서, 이에 대한 변환작업을 개발자가 직접 해줘야한다.

아래와 같이 객체 형태인 Team을 외래키인 teamId으로 변환을 해서 SQL을 작성해야한다.

Team -> member.getTeam().getId()

만약 JPA를 이용한다면?

다음의 persist, find 메소드 2개만을 이용해서 간단히 저장 및 조회 기능을 구현할 수 있다. JPA 내부에서 위의 작업들을 효율적으로 처리한다.

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

//회원 조회
Member member = jpa.find(Member.class, memberId);
Team team = member.getTeam();

위와 같이 회원에 소속된 팀을 찾을때, 참조값을 이용해서 회원을 찾게 되는데, 이를 객체 그래프 탐색이라고 한다.

Team team=member.getTeam();

아래와 같은 객체 연관구조가 있다고 하자

association2

// 회원 조회 비지니스 로직
class MemberService {
    public void process() {

        Member member = memberDAO.find(memberId);
        member.getTeam();                   // member->team 
        member.getOrder().getDelivery();    // ???
    }
}

위의 member.getTeam() 위의 예제에서 봤듯 성공적으로 수행된다. 하지만, member.getOrder나, member.getDelivery같은 경우는 해당 코드만을 통해서는 탐색이 가능한지 여부를 판단할 수 없다. 탐색 가능 여부를 확인하기 위해서는 memberDAO를 봐야하고 직접 SQL문을 살펴 봐야 알 수 있다. 또한, 그 객체를 이용하고자 하는 시점에 데이터가 없으면 안되므로 DB에서 만약 이것을 하고자 한다면

다음과 같이 여러 형태의 메소드를 만들어놔야한다.

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

만약 JPA를 이용한다면

JPA에서는 객체를 사용하는 시점에 맞게 적절하게 SQL를 생성해서 DB에 접근한다. 그래서, 실제 사용하는 시점까지 실제 데이터베이스 조회를 미루는 지연로딩 을 진행한다.

Transparent Entity

class Member {
    private Order order;

    public Order getOrder() {
        return order;
    }
}

// 처음 조회 시점에 SELECT MEMBER SQL
Member member = jpa.find(Member.class, memberId);

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

이와 같이 JPA를 실제 객체를 사용하는 시점에 맞춰서 SQL를 생성해서 실행하는 지연로딩이라는 것을 통해 자유로운 객체 그래프 탐색을 지원한다.

Comparison

자바에서는 객체 단위 비교를 위해 == 와 equals를 제공하는데, ==는 인스턴스 주소 값을 비교하는 것이고, equals는 인스턴스 내부의 값을 비교하는 것이다.

DB에서는 오직 Primary key를 통한 행 단위 비교만 존재한다.

그래서 어떤 문제가 발생하는가를 보면

// MemberDAO 코드
class MemberDAO {

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

// 조회한 회원 비교하기
String memberId = "100";
Member member1 = memberDAO.getMember(memberId);
Member member2 = memberDAO.getMember(memberId);

member1 == member2;     //다르다.

다음과 같이 DB에서는 같다고 판별하는 2개의 member 값에 대해서, 자바는 다르다고 한다. (getMember내에서 매번 new Member() 호출을 통한 새로운 인스턴스를 만들기 때문이다.)

만약 JPA를 이용한다면

// member1과 member2는 동일성 비교에 성공
String memberId = "100";
Member member1 = jpa.find(Member.class, memberId);
Member member2 = jpa.find(Member.class, memberId);

member1 == member2;     //같다.

JPA에서는 같은 트랜잭션에 대해 동일한 ID에 대해 동일한 객체가 조회되도록 보장한다.

이처럼 자바와 DB가 가지는 패러다임 불일치 문제로 여러 추가적인 작업이 발생하게 되는데, JPA에서는 이를 내부적으로 구현하고 있기 때문에 개발자로서는 더욱 더 정교한 OOP를 수행할 수 있게 만든다.

JPA라는 정확히 무엇일까?

jpa_intro1

위 그림을 보면, jpa는 java와 jdbc 사이에서 동작한다.

JPA는 ORM 기반 프레임워크인데, ORM은 무엇일까? Object-Relational Model로 객체와 DB간에 매핑을 수행한다는 개념이다. 매번 객체를 tuple 형태로 가공하는 것이 아니라, 객체 단위로 DB에서 접근할 수 있도록 내부적으로 구현하고 있다. JPA는 내부적으로 패러다임 불일치를 해결하는 코드가 구현되어 있고, 사용자가 요청하는 작업에 맞게 적절한 SQL를 자동으로 생성한다.

가령, 저장 기능을 보면 아래와 같이 동작한다.

jpa_persist

조회 기능은 아래와 같이 동작한다.

jpa_find

개발자는 객체와 DB간에 매핑하는 방법만 알려주면, ORM이 알아서 객체-DB 간 매핑을 수행해서 객체지향형 DB 접근을 가능하도록 제공해준다.

JPA

whatisjpa

jpa란 어떻게 나오게 된걸까?

기존에 자바는 EJB 기반으로 작동하고, 이에 포함된 ORM 기술이 있었다. 하지만, 너무 복잡하고 특정 서버 내에서만 동작하는 제한적인 방식이었다. 하지만 여러 개발자들이 합심하여 하이버네이트라는 오픈 소스 ORM라는 것을 만들었고, 이 하이버네이트 기반으로 나온 것이 JPA다

JPA Characteristics

생산성

jpa에서는 자바가 컬렉션에 객체 단위로 저장하듯이 객체를 인자로 받아서 처리한다. 직접적인 SQL를 개발자가 설계할 필요가 없이 사용자는 객체 중심의 설계를 이어가면 된다.

ArrayList<Member> members=new ArrayList<>();
Member member=new Member(....);
members.add(member)

//위의 collection에 저장하는 것처럼 jpa도 객체 단위로 저장한다.
jpa.persist(member);

유지보수성

앞서 소개했던 예제에서 SQL에 의존적인 코드를 설계하면 어떻게 되는 지 다뤘었다. 요구조건의 변경이 발생하면 여러 코드를 추가/변경해야 됬다. 하지만, JPA 기반으로 설계 하게 되면 객체 수정을 통한 SQL 수정을 하지 않아도 되서 수정 요소가 현저히 줄어들게 된다.

패러다임 불일치

자바에서 가지는 다양한 객체 지향형 특징들을 직접 SQL로 설계할 때는 많은 변환 작업을 개발자가 직접 했어야 했는데, JPA에서는 이를 자동적으로 해결해준다.

벤더 독립성

데이터베이스의 접근 방식에 차이가 발생하게 되면, 기존에 SQL 의존적으로 설계했던 방식대로면, 모든 SQL에 대해 수정했어야 하지만, JPA를 통해 이러한 접근 방식에 차이가 발생하더라도 코드의 수정 없이 이용할 수 있다.

References

book: 자바 ORM 표준 JPA 프로그래밍 -김영한 저

book_link

태그: ,

카테고리:

업데이트:

댓글남기기