[JPA 기본편] 1. JPA 소개

HJ·2024년 2월 21일
0

JPA 기본편

목록 보기
1/10
post-thumbnail
post-custom-banner

김영한 님의 자바 ORM 표준 JPA 프로그래밍 - 기본편 강의를 보고 작성한 내용입니다.


1. SQL 중심 개발의 문제점

객체 지향 프로그래밍은 추상화, 캡슐화, 정보은닉, 상속, 다형성 등 시스템의 복잡성을 제어할 수 있는 다양한 장치들을 제공하는데 RDB 와의 차이점은 크게 상속, 연관관계, 데이터 타입, 데이터 식별 방법으로 볼 수 있습니다.

1-1. 상속

객체의 상속 관계를 DB 에 저장할 수는 없고 슈퍼타입, 서브타입을 사용해서 해결해야 하는데 이를 사용하는 방법은 복잡합니다.

Album 을 저장한다고 했을 때 객체를 분해하여 Item 테이블에 필요한 데이터, Album 테이블에 필요한 데이터를 가져옵니다. 그 다음 Item 테이블과 Album 테이블에 각각 Insert 쿼리를 날려주어야 합니다.

조회할 때는 Item 과 Album 테이블을 조인하는 SQL 을 작성하고, 각각의 객체를 생성해서 데이터를 넣는 등 복잡한 과정을 거치게 됩니다.


만약 DB 가 아닌 자바 컬렉션인 리스트에 저장한다고 가정했을 때 list.add(album) 을 사용하면 됩니다. 조회하는 것도 아래처럼 get() 을 이용해 꺼낼 수 있으며, 다형성을 활용할 수도 있습니다.

Album album = list.get(albumId);
Item item = list.get(albumId);

1-2. 연관관계

다른 곳에 있는 데이터가 필요할 때 객체는 참조를 사용하지만 테이블은 참조가 없기 때문에 외래키를 사용해서 조인을 합니다.

그래서 Member 와 Team 객체를 테이블에 저장하기 위해서는 위처럼 객체를 테이블에 맞추어 모델링 해야합니다.

class Member {
    String id;
    Long teamId;    // TEAM_ID FK 컬럼을 사용
    String username;
}

class Team {
    Long id;    // PK : TEAM_ID
    String name;
}

객체를 테이블에 맞추어 모델링하면 Member 가 Team 에 대한 참조를 가지는 것이 아닌 Team 의 Id 를 가지고 있게 됩니다. 이렇게 구현해야 아래 그림처럼 SQL 을 작성할 때 필요한 정보들을 꺼내서 사용할 수 있습니다.


이렇게 테이블에 맞추어 생성하면 객체다운 모델링이 이루어지지 않습니다. 왜냐하면 객체는 참조를 통해 연관관계를 맺기 때문입니다. 아래는 참조를 이용하여 객체다운 모델링을 한 예시입니다.

class Member {
    String id;
    Team team;  // 참조로 연관관계를 맺는다
    String username;
}

class Team {
    Long id;    // PK : TEAM_ID
    String name;
}

이전과는 다르게 Team 이라는 참조를 가진 것을 확인할 수 있습니다. 그래서 필요할 때 member.team 혹은 member.getTeam() 을 통해 바로 Team 을 꺼낼 수 있게 됩니다.

하지만 이렇게 했을 때 DB 에 Insert 하기 번거로워지는데 Member 가 가진 정보를 저장할 때 객체는 Team 에 대한 FK 를 가지고 있는 것이 아닌 참조를 가지고 있기 때문에 member.getTeam().getId() 를 사용해서 TEAM_ID 를 꺼내야 합니다.


참조를 가진 객체로 모델링 한 상태에서 DB 에서 Member 와 Team 이 연관관계가 있는 상태로 조회하고 싶을 때 아래처럼 해야합니다.

SELECT 
    M.*, T.*
FROM MEMBER M
JOIN TEAM T 
    ON M.TEAM_ID = T.TEAM_ID 
public Member find(String memberId) {
    //SQL 실행 ...

    Member member = new Member();
    //데이터베이스에서 조회한 회원 관련 정보를 모두 입력

    Team team = new Team();
    //데이터베이스에서 조회한 팀 관련 정보를 모두 입력

    //회원과 팀 관계 설정
    member.setTeam(team);
    return member;
}

우선 Member 와 Team 을 조인을 통해 둘 다 조회해야 합니다. 그리고 Member 객체와 Team 객체를 만들어 조회된 정보를 입력한 후에 Member 와 Team 의 연관관계를 세팅해야 합니다.


만약 자바 컬렉션에서 관리를 한다고 가정했을 때 list.add(member) 를 하면 Member 를 저장할 수 있습니다.

Member member = list.get(memberId);
Team team = member.getTeam();

Member 가 필요할 때는 list 에서 바로 꺼내서 사용할 수 있고, Team 이 필요한 경우 member 에서 참조를 통해 Team 을 꺼낼 수 있습니다.


1-3. 객체 그래프 탐색

위처럼 연관관계가 있다고 했을 때 객체는 참조를 통해 자유롭게 객체 그래프를 탐색할 수 있어야 합니다. Member 에서 Member.getOrder(), Order.getOrderItem() 처럼 사용해서 계속 따라갈 수 있어야 합니다.

하지만 DB 에 객체를 보관하다보면 내가 처음에 어떤 SQL 을 실행해서 객체를 만들었는지에 따라 탐색 범위가 결정되게 됩니다.

SELECT 
    M.*, T.*
FROM MEMBER M
JOIN TEAM T
    ON M.TEAM_ID = T.TEAM_ID

예를 들어, 위의 SQL 로 조회한다고 했을 때 Member 와 Team 에 대해서 조회할 수 있습니다. 연관관계에 의해 member.getTeam() 을 하면 Member 와 Team 을 둘 다 조회했기 때문에 가능합니다.

하지만 member.getOrder() 를 하면 null 이 됩니다. 왜냐하면 Member 와 Team 에 대한 select 를 했기 때문에 Order 는 DB 에서 조회되지 않았기 때문입니다.

계층형 아키텍처에서는 다음 계층을 믿고 사용할 수 있어야 하는데 이렇게 되면 엔티티에 대한 신뢰 문제가 발생하게 됩니다.

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

예를 들어, memberRepository 를 통해 memberId 로 Member 를 조회합니다. 이때 여기서 member.getTeam() 이나 member.getOrder().getDelivery() 을 할 수 있을지 확신할 수 없습니다.

왜냐하면 SELECT 를 통해 어떤 데이터를 조회했는지 모르기 때문입니다. 그래서 이를 확신할 수 없어 memberRepository 의 find() 내부를 살펴보아야 합니다.

객체 그래프를 통해 자유롭게 탐색할 수 있게 하려면 모든 객체를 미리 로딩해야 하는데 그렇게 되면 상황에 따라 동일한 조회 메서드를 아래처럼 여러 개 생성해야 합니다.

memberDAO.getMember();  //Member만 조회

memberDAO.getMemberWithTeam();//Member와 Team 조회

//Member,Order,Delivery
memberDAO.getMemberWithOrderWithDelivery();

1-4. 비교하기

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(...);
    }
}

만약 memberId 가 100 이라고 했을 때 이를 두 번 조회하면 두 개의 Member 객체가 나오게 됩니다. 이를 == 으로 비교하면 서로 다르다고 출력됩니다.

왜냐하면 Member 를 조회할 때 SQL 문을 작성해 조회하고, 그 결과로 새로운 인스턴스를 반환하기 때문입니다.


String memberId = "100";
Member member1 = list.get(memberId);
Member member2 = list.get(memberId);

member1 == member2; //같다.

하지만 위처럼 컬렉션을 이용한다면 member 객체를 두 번 조회하고 비교해도 동일한 인스턴스가 나오기 때문에 둘이 동일하다고 판단합니다.


1-5. 정리

참조와 연관관계를 사용하면서 객체답게 모델링 할 수록 매핑 작업만 늘어나게 됩니다. 지금까지 함께 살펴보았듯이 자바 컬렉션에 저장하면 편하게 할 수 있습니다. 객체를 자바 컬렉션에 저장하듯이 DB 에 저장한다면 굉장히 편리할 것입니다. 이때 사용하는 것이 JPA( Java Persistence API ) 입니다.




2. JPA 소개

JPA 는 Java Persistence API 의 약자로, 자바 진영의 ORM 기술 표준입니다.

ORM 은 Object-relational mapping 의 약자로 객체 관계 매핑을 의미하는데 객체는 객체대로 설계하고, RDB 는 RDB 대로 설계하면 ORM 프레임워크가 중간에서 매핑을 해줍니다.

2-1. JPA 동작 위치

JAVA 애플리케이션에서 DB 랑 통신을 하려면 JDBC API 를 사용해야 합니다. JPA 는 애플리케이션과 JDBC 사이에서 동작하며, 개발자가 직접 사용해야 했던 JDBC API 를 JPA 가 대신 사용해줍니다.


2-2. JPA 동작

2-2-1. 저장

Member 객체를 저장한다고 가정했을 때 Member 객체를 MemberDAO 에 넘기고, MemberDAO 가 JPA 에게 Member 엔티티를 넘겨주면 JPA 가 Member 엔티티를 분석해서 Insert SQL 을 만들어줍니다. 그리고 JDBC API 를 사용해서 DB 에 Insert 쿼리를 날리게 됩니다.


2-2-2. 조회

조회할 때도 JPA 에게 memberId 를 넘겨주면 JPA 가 Member 객체를 분석해서 select 쿼리를 만들고, JDBC API 를 사용해 조회하고, ResultSet 을 매핑까지 해줍니다. 결과적으로 Entity Object 를 만들어서 우리에게 반환해줍니다.


2-3. JPA 사용 시 장점

2-3-1. 생산성 향상

SQL 중심적인 개발에서 객체 중심으로 개발할 수 있으며, CRUD 를 구현할 때 아래처럼 사용하면 됩니다.

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


2-3-2. 유지보수

기존에는 필드가 추가되면 관련된 모든 SQL 을 수정했어야 했습니다. 하지만 JPA 를 사용하면 직접 SQL 을 작성하지 않아도 되기 때문에 필드가 추가되어도 개발자가 유지보수 하지 않아도 됩니다.


2-3-3. JPA 와 패러다임 불일치 해결

1.JPA와 상속
2.JPA와 연관관계
3.JPA와 객체 그래프 탐색
4.JPA와 비교하기

[ 상속 - 저장 ]

// 개발자
jpa.persist(album);

// JPA
INSERT INTO ITEM ...
INSERT INTO ALBUM ...

개발자가 persist() 를 호출하면서 객체를 넣어주면 JPA 가 필요한 INSERT 문을 생성해서 DB 에 쿼리를 날려주게 됩니다.

[ 상속 - 조회 ]

// 개발자
Album album = jpa.find(Album.class, albumId);

// JPA
SELECT 
    I.*, A.*
FROM ITEM I
JOIN ALBUM A 
    ON I.ITEM_ID = A.ITEM_ID

find() 에 객체 타입과 ID 를 넘겨주면 JPA 가 쿼리를 작성해서 조회를 해주게 되고, 개발자는 자바 컬렉션에서 가져오는 것처럼 조회할 수 있습니다.

[ 연관관계와 객체 그래프 탐색 ]

member.setTeam(team);
jpa.persist(member);

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

연관관계를 사용하려면 원래는 외래키를 넣고 했어야 하는데, JPA 에서는 참조를 사용할 수 있습니다.

member.setTeam() 으로 team 을 세팅한 후에 member 를 저장합니다. 그 후 find() 를 통해 member 를 꺼냈을 때 member.getTeam() 으로 team 객체를 사용할 수 있습니다.

[ 신뢰할 수 있는 엔티티와 동일 보장 ]

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


// 동일 보장
String memberId = "100";
Member member1 = jpa.find(Member.class, memberId);
Member member2 = jpa.find(Member.class, memberId);

member1 == member2;    //같다.

JPA 는 알아서 쿼리를 작성해주면서 이전에 SQL 의 조회범위에 따른 신뢰성 문제를 해결해줍니다. 그 결과 자유로운 객체 그래프 탐색이 가능해집니다.

또 JPA 는 동일한 트랜잭션에서 조회한 엔티티는 같음을 보장하기 때문에 find() 로 두 번 조회한 후에 == 로 비교해도 동일하다고 판단됩니다.




3. JPA 의 성능 최적화 기능

3-1. 1차 캐시와 동일성 보장

  1. 같은 트랜잭션 안에서는 같은 엔티티를 반환하기 때문에 약간의 조회 성능이 향상됩니다.

  2. DB Isolation Level 이 Read Commit 이어도 애플리케이션에서 Repeatable Read 보장합니다.

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

println(m1 == m2);  //true

첫 번째의 find() 에서는 SQL 쿼리가 DB 에 날라가게 되고, 조회 결과를 JPA 가 들고있게 됩니다. 또 find() 를 호출하면 JPA 가 SQL 을 날리지 않고 JPA 가 메모리 상에서 들고있는 결과를 반환해줍니다.

그래서 == 로 비교했을 때 두 객체가 동일하다고 판단됩니다. 단, 같은 트랜잭션 안에서만 성립합니다.


3-2. 트랜잭션을 지원하는 쓰기 지연

  1. 트랜잭션을 커밋할 때까지 INSERT SQL 을 모음

  2. JDBC BATCH SQL 기능을 사용해서 한번에 SQL 전송

transaction.begin();  // [트랜잭션] 시작

em.persist(memberA);
em.persist(memberB);
em.persist(memberC);
//-- 여기까지 INSERT SQL을 데이터베이스에 보내지 않는다.

// 커밋하는 순간 데이터베이스에 INSERT SQL을 모아서 보낸다.
transaction.commit(); // [트랜잭션] 커밋

트랜잭션이 시작되고 데이터를 버퍼에 계속 보관합니다. 트랜잭션이 커밋되면 버퍼에 저장된 것들에 대한 Insert 쿼리를 한 번에 날라가게 되고, 그 후에 트랜잭션이 커밋됩니다.


3-3. 지연 로딩

  • 지연 로딩 : 객체가 실제 사용될 때 로딩

  • 즉시 로딩 : JOIN SQL로 한번에 연관된 객체까지 미리 조회

만약 Member 와 Team 을 항상 함께 사용하는 경우라면 SQL 한 번에 Member 와 Team 을 조회하는 것이 DB 도 여러 번 갈 필요도 없고, 네트워크 통신도 줄이기 때문에 좋습니다.

하지만 Member 를 사용할 때 Team 을 함께 사용하지 않는 경우라면 Member 만 조회하는 것이 성능 상 조금 더 좋기 때문에 Member 만 가지고 오는 것이 좋습니다.

그래서 JPA 는 상황에 따라 사용할 수 있도록 지연로딩과 즉시로딩을 지원합니다. 지연로딩에서 member 를 사용할 때 Member 만 조회하고 Team 은 조회하지 않습니다. 그 후 Team 을 사용할 때 Team 에 대한 프록시 객체의 값을 채워주게 됩니다.

post-custom-banner

0개의 댓글