모래블로그

자바 ORM 표준 JPA 프로그래밍 정리(1) - JPA 소개 본문

독서 및 정리/자바 ORM 표준 JPA 프로그래밍

자바 ORM 표준 JPA 프로그래밍 정리(1) - JPA 소개

별모래 2023. 12. 19. 00:33
728x90

 

JPA는 자바 진영의 ORM 기술 표준이다.

 

1. SQL을 직접 다룰 때 발생하는 문제점

자바로 개발하는 애플리케이션은 대부분 관계형 데이터베이스를 데이터 저장소로 사용한다.

데이터베이스에 데이터를 관리하려면 SQL을 사용해야 한다. 자바로 작성한 애플리케이션은 JDBC API를 사용해서 SQL을 데이터베이스에 전달한다.

JDBC API와 SQL

 

1) 반복, 반복 그리고 반복

SQL을 직접 다룰 떄의 문제점을 알아보기 위해 자바와 관계형 데이터베이스를 사용해서 회원 관리 기능을 개발해보자.

(회원 테이블은 이미 있다고 가정, 회원을 CRUD 하는 기능 개발해보자)

 

회원 객체

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

 

 

회원용 DAO

public class MemberDAO {
	public Member find(String memberId) {...}
}

 

 

이제 MemberDAO의 find() 메소드를 완성해서 회원을 조회하는 기능을 개발해보자.

보통은 다음 순서로 개발을 진행할 것이다.

 

1. 회원 조회용 SQL을 작성

SELECT MEMBER_ID,NAME FROM MEMBER M WHERE MEMBER_ID = ?

 

2. JDBC API를 사용해서 SQL을 실행

ResultSet rs = stmt.executeQuery(sql);

 

3. 조회 결과를 Member 객체로 매핑 

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

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

 

 

회원 등록 기능을 만들어보자.

public class MemberDAO {
    public Member find(String memberId) {...}
    public void save(Member member) {...} //추가
}

 

 

1. 회원 등록용 SQL을 작성

String sql = "INSERT INTO MEMBER (MEMBER_ID, NAME) VALUES (?,?)";

 

2. 회원 객체 값을 꺼내서 등록 SQL에 전달

pstmt.setString(1, member.getMemberId());
pstmt.setString(2, member.getName());

 

3. JDBC API를 사용해서 SQL을 실행

pstmt.executeUpdate(sql);

 

회원 수정하고 삭제하는 기능도 SQL 작성하고 JDBC API를 사용하는 비슷한 일을 반복해야 할 것이다.

 

 

 회원 객체를 데이터베이스가 아닌 자바 컬렉션에 보관한다면 ?

=> list.add(member); 

 이 한 줄로 객체를 저장할 수 있다.

 

하지만, 데이터베이스는 객체 구조와는 다른 데이터 중심 구조를 가지므로 객체를 데이터베이스에 직접 저장하거나 조회할 수는 없다. 따라서 개발자가 객체지향 애플리케이션과 데이터베이스 중간에서 SQL과 JDBC API를 사용해서 변환 작업을 직접 해주어야 한다. 

문제  => 객체를 DB에 CRUD 하려면 너무 많은 SQL과 JDBC API를 코드로 작성해야 한다.

 


 

2) SQL에 의존적인 개발

회원 객체 관리하는 MemberDAO 완성하고 애플리케이션 나머지 기능도 개발을 완료한 상황.

근데, 갑자기 회원의 연락처도 함꼐 저장해달라는 요구사항이 추가되었다.

 

등록 코드 변경

public class Member {
    private String memberId;
    private String name;
    private String tel; // 추가
}

 

연락처를 저장할 수 있도록 INSERT SQL을 수정

String sql = "INSERT INTO MEMBER (MEMBER_ID, NAME, TEL) VALUES (?,?,?)";

 

회원 객체의 연락처 값을 꺼내서 등록 SQL에 전달

pstmt.setString(3, member.getTel());

 

연락처를 DB에 저장하려고 SQL과 JDBC API를 수정, 그리고 연락처가 잘 저장되는지 테스트하고 DB에 연락처 데이터가 저장된 것도 확인했다.

 

 

조회 코드 변경

 

다음으로 회원 조회 화면을 수정해서 연락처 필드가 출력되도록 했지만, 모든 연락처의 값이 null로 출력된다.

생각해보니 조회 SQL에 연락처 컬럼을 추가하지 않았다.

 

회원 조회용 SQL을 수정

SELECT MEMBER_ID, NAME, TEL FROM MEMBER WHERE MEMBER_ID = ?

 

또한 연락처의 조회 결과를 Member 객체에 추가로 매핑

...
String tel = rs.getString("TEL");
member.setTel(tel); // 추가
...

 

이제 화면에 연락처 값이 출력된다.

 

 

수정 코드 변경

 

기능이 잘 동작한다고 생각했는데 이번엔 연락처가 수정되지 않는 버그를 발견.

자바 코드에는 MemberDAO.update(member) 메소드에 수정할 회원 정보와 연락처를 잘 전달했지만 MemberDAO를 열어서 UPDATE SQL을 확인해보니 TEL 컬럼을 추가하지 않아서 연락처가 수정되지 않는 문제였다.

 만약 회원 객체를 DB가 아닌 자바 컬렉션에 보관했다면 필드를 추가한다고 해서 이렇게 많은 코드를 수정할 필요는 없을 것이다.

list.add(member); // 등록
Member member = list.get(xxx); // 조회
member.setTel("xxx"); // 수정

 

 

연관된 객체

 

회원은 어떤 한 팀에 필수로 소속되어야 한다는 요구사항이 추가되었다.

Member 객체에 team 필드가 추가되었다.

회원 정보를 화면에 출력할 때 연관된 팀 이름도 함께 출력하는 기능을 추가해보자.

 

class Member {
    private String memberId;
    private String name;
    private String tel;
    priate Team team; // 추가
    ...
}

// 추가된 팀
class Team {
    ...
    private String teamName;
    ...
}

 

다음 코드를 추가해서 화면에 팀의 이름을 출력했다.

이름 : member.getName();
소속 팀 : member.getTeam().getTeamName(); // 추가

 

코드를 실행해보니 member.getTeam()의 값이 항상 null 이다.

회원과 연관된 팀이 없어서 그럴 것이라 생각하고 DB를 확인해보니 모든 회원이 팀에 소속되어 있다.

문제를 찾다가 MemberDAO에 findWithTeam()이라는 새로운 메소드가 추가된 것을 확인

public class MemberDAO {
    public Member find(String memberId) { ... }
    public Member findWithTeam(String memberId) { ... }

 

 

MemberDAO 코드를 열어서 확인해보니 회원을 출력할 때 사용하는 find() 메소드는 회원만 조회하는 다음 SQL을 그대로 유지했다.

 

SELECT MEMBER_ID, NAME, TEL FROM MEMBER M

 

또한 새로운 findWithTeam() 메소드는 다음 SQL로 회원과 연관된 팀을 함께 조회했다.

SELECT M.MEMBER_ID, M.NAME, M.TEL, T.TEAM_ID, T.TEAM_NAME
FROM MEMBER M
JOIN TEAM T
	ON M.TEAM_ID = T.TEAM_ID

 

결국 DAO를 열어서 SQL을 확인하고 나서야 원인을 알 수 있었고, 회원 조회 코드를 MemberDAO.find() 에서 MemberDAO.findWithTeam()으로 변경해서 문제를 해결했다.

 


SQL과 문제점을 정리해보자

Member 객체가 연관된 Team 객체를 사용할 수 있을지 없을지는 전적으로 사용하는 SQL에 달려 있다. 이런 방식의 가장 큰 문제는 데이터 접근 계층을 사용해서 SQL을 숨겨도 어쩔 수 없이 DAO를 열어서 어떤 SQL이 실행되는지 확인해야 한다는 점이다.

 Member나 Team 처럼 비즈니스 요구사항을 모델링한 객체를 엔티티라 하는데, 지금처럼 SQL에 모든 것을 의존하는 상황에서는 개발자들이 엔티티를 신뢰하고 사용할 수 없다. 대신에 DAO를 열어서 어떤 SQL이 실행되고 어떤 객체들이 함께 조회되는지 일일이 확인해야 한다. 이것은 진정한 의미의 계층 분할이 아니다. 물리적으로는 SQL와 JDBC API를 데이터 접근 계층에 숨기는 데 성공했을지 몰라도 논리적으로는 엔티티와 아주 강한 의존관계를 가지고 있다. 

이런 강한 의존관계 때문에 회원을 조회할 때는 물론이고 회원 객체에 필드를 하나 추가할 때도 DAO의 CRUD 코드와 SQL의 대부분을 변경해야 하는 문제가 발생한다.

 

문제점 요약

  • 진정한 의미의 계층 분할이 어렵다
  • 엔티티를 신뢰할 수 없다
  • SQL에 의존적인 개발을 피하기 어렵다

 

3)  JPA와 문제 해결

JPA를 사용하면 객체를 데이터베이스에 저장하고 관리할 때, 개발자가 직접 SQL을 작성하는 것이 아니라 JPA가 제공하는 API를 사용하면 된다. 그러면 JPA가 개발자 대신 적절한 SQL을 생성해서 데이터베이스에 전달한다.

 

JPA가 제공하는 CRUD API

 

1. 저장 기능

jpa.persist(member); // 저장

 

persist() 메소드는 객체를 데이터베이스에 저장한다. 이 메소드를 호출하면 JPA가 객체와 매핑정보(어떤 객체를 어떤 테이블에 관리할지 정의한 정보)를 보고 적절한 INSERT SQL을 생성해서 데이터베이스에 전달한다.

 

2. 조회 기능

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

 

find() 메소드는 객체 하나를 데이터베이스에서 조회한다. JPA는 객체와 매핑 정보를 보고 적절한 SELECT SQL을 생성해서 데이터베이스에 전달하고 그 결과로 Member 객체를 생성해서 반환한다.

 

3. 수정 기능

Member member = jpa.find(Member.class, memberId);
member.setName("이름변경") // 수정

 

JPA는 별도로 수정 메소드를 제공하지 않는다. 대신 객체를 조회해서 값을 변경만 하면 트랜잭션을 커밋할 때 데이터베이스에 적절한 UPDATE SQL이 전달된다.

 

4. 연관된 객체 조회

Member member = jpa.find(Member.class, memberId);
Team team = member.getName(); //연관된 객체 조회

 

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

 


 

2. 패러다임의 불일치

객체는 속성(필드)과 기능(메소드)을 가진다. 객체의 기능은 클래스에 정의되어 있으므로 객체 인스턴스의 상태인 속성만 저장했다가 필요할 때 불러와서 복구하면 된다. 객체가 단순하면 객체의 모든 속성 값을 꺼내서 파일이나 DB에 저장하면 되지만, 부모 객체를 상속받았거나 다른 객체를 참조하고 있으면 객체의 상태를 저장하기가 쉽지 않다. 예를 들어 회원 객체를 저장해야하는데 회원 객체가 팀 객체를 참조하고 있다면, 회원 객체를 저장할 때 팀 객체도 함께 저장해야 한다.

 자바는 이런 문제까지 고려해서 객체를 파일로 저장하는 직렬화 기능과 저장된 파일을 객체로 복구하는 역 직렬화 기능을 지원하지만, 이 방법은 직렬화된 객체를 검색하기 어렵다는 문제가 있으므로 현실성이 없다. 현실적인 대안은 관계형 DB에 객체를 저장하는 것인데, 관계형 DB는 데이터 중심으로 구조화되어 있고, 집합적인 사고를 요구한다. 그리고 객체지향에서 이야기하는 추상화, 상속, 다형성 같은 개념이 없다.

 객체와 관계형 DB는 지향하는 목적이 서로 달라 둘의 기능과 표현 방법도 다르다. 이것을 객체와 관계형 데이터베이스의 패러다임 불일치 문제라고 한다. 따라서 객체 구조를 테이블 구조에 저장하는 데는 한계가 있다.

 

패러다임 불일치로 인해 발생하는 문제를 살펴보자.

 

1) 상속

 객체는 상속이라는 기능을 가지고 있지만 테이블은 상속이라는 기능이 없다. (일부 db는 상속 기능을 지원하지만 객체의 상속과는 약간 다르다)

객체 상속 모델

 

 

그나마 데이터베이스 모델링에서 이야기하는 슈퍼타입 서브타입 관계를 사용하면 객체 상속과 가장 유사한 형태로 테이블을 설계할 수 있다.

 ITEM 테이블의 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 ...

 

Movie 객체도 마찬가지다

INSERT INTO ITEM ...
INSERT INTO MOVIE ...

 

JDBC API를 사용해서 이 코드를 완성하려면 부모 객체에서 부모 데이터만 꺼내서 ITEM 용 INSERT SQL을 작성하고 자식 객체에서 자식 데이터만 꺼내서 ALBUM용 INSERT SQL을 작성해야 하는데, 작성해야 할 코드량이 만만치 않다. 그리고 자식 타입에 따라 DTYPE도 저장해야 한다.

 조회하는 것도 예를 들어 Album을 조회한다면 ITEM과 ALBUM 테이블을 조인해서 조회한 다음 그 결과로 Album 객체를 생성해야 하므로 쉽지 않다.

 이런 과정이 모두 패러다임의 불일치를 해결하려고 소모하는 비용이다. 만약 해당 객체들을 자바 컬렉션에 보관한다면 다음 같이 부모 자식이나 타입에 대한 고민 없이 해당 컬렉션을 그냥 사용하면 된다.

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

Album album = list.get(albumId);

 

JPA와 상속

JPA는 상속과 관련된 패러다임의 불일치 문제를 개발자 대신 해결해준다. 개발자는 자바 컬렉션에 객체를 저장하듯 JPA에게 객체를 저장하면 된다.

Item을 상속한 Album 객체를 저장해보자.

jpa.persist(album);

 

JPA는 다음 SQL을 실행해서 객체를 ITEM, ALBUM 두 테이블에 나누어 저장한다.

INSERT INTO ITEM ...
INSERT INTO ALBUM ...

 

다음으로 Album 객체를 조회해보자

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

 

JPA는 ITEM 과 ALBUM 두 테이블을 조인해서 필요한 데이터를 조회하고 그 결과를 반환한다.

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

 

 

2) 연관관계

객체참조를 사용해서 다른 객체와 연관관계를 가지고 참조에 접근해서 연관된 객체를 조회한다.

반면에 테이블외래 키를 사용해서 다른 테이블과 연관관계를 가지고 조인을 사용해서 연관된 테이블을 조회한다.

 

참조를 사용하는 객체와 외래 키를 사용하는 관계형 데이터베이스 사이의 패러다임 불일치는 객체지향 모델링을 거의 포기하게 만들 정도로 극복하기 어렵다.

연관관계

 

Member 객체는 Member.team 필드에 Team 객체의 참조를 보관해서 Team 객체와 관계를 맺는다. 따라서 이 참조 필드에 접근하면 Member와 연관된 Team을 조회할 수 있다.

class Member {
    Team team;
    ...
    Team getTeam() {
        return team;
    }
}

class Team {
    ...
}

member.getTeam(); // member -> team 접근

 

Member 테이블은 MEMBER.TEAM_ID 외래 키 컬럼을 사용해서 TEAM 테이블과 관계를 맺는다. 이 외래키를 사용해서 MEMBER 테이블과 TEAM 테이블을 조인하면 MEMBER 테이블과 연관된 TEAM 테이블을 조회할 수 있다.

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

 

조금 어려운 문제도 있는데, 객체는 참조가 있는 방향으로만 조회할 수 있다. 

방금 예에서 member.getTeam()은 가능하지만 team.getMember()는 참조가 없으므로 불가능하다.

반면, 테이블은 외래키 하나로 MEMBER JOIN TEAM도 가능하지만 TEAM JOIN MEMBER도 가능하다.

 

객체를 테이블에 맞추어 모델링

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

class Team {
    Long id; // TEAM_ID PK 사용
    String name; // NAME 컬럼 사용
}

 

MEMBER 테이블의 컬럼을 그대로 가져와서 Member 클래스를 만들었다. 이렇게 객체를 테이블에 맞추어 모델링하면 객체를 테이블에 저장하거나 조회할 때는 편리하다. 그런데 여기서 TEAM_ID 외래 키의 값을 그대로 보관하는 teamId 필드에는 문제가 있다. 관계형 데이터베이스에는 조인이라는 기능이 있으므로 외래 키의 값을 그대로 보관해도 되지만 객체는 연관된 객체의 참조를 보관해야 다음처럼 참조를 통해 연관된 객체를 찾을 수 있다.

Team team = member.getTeam();

 

특정 회원이 소속된 팀을 조회하는 가장 객체지향 적인 방법은 이처럼 참조를 사용하는 것이다.

Member.teamId 필드처럼 TEAM_ID 외래 키까지 관계형 데이터베이스가 사용하는 방식에 맞추면 Member 객체와 연관된 Team 객체를 참조를 통해서 조회할 수 없다. 이런 방식을 따르면 좋은 객체 모델링은 기대하기 어렵고 객체지향의 특징을 잃어버리게 된다.

 

객체지향 모델링

객체는 참조를 통해서 관계를 맺으므로 참조를 사용하도록 모델링해야 한다.

class Member {
    String id; // Member_ID 컬럼 사용
    Team team; // 참조로 연관관계를 맺는다
    String username; // USERNAME 컬럼 사용
    
    Team getTeam() {
        retrun team;
    }
}

class Team {
    Long id; // TEAM_ID PK 사용
    String name; // NAME 컬럼 사용
}

 

Member.team 필드를 보면 외래 키의 값을 그대로 보관하는 것이 아닌 연관된 Team의 참조를 보관한다. 이제 회원과 연관된 팀을 조회할 수 있다.

Team team = member.getTeam();

 

그런데 이처럼 객체지향 모델링을 사용하면 객체를 테이블에 저장하거나 조회하기가 쉽지 않다.

Member 객체는 team 필드로 연관관계를 맺고, MEMBER 테이블은 TEAM_ID 외래 키로 연관관계를 맺기 때문인데, 객체 모델은 외래키가 필요 없고 단지 참조가 있으면 된다. 반면 테이블은 참조가 필요 없고 외래 키만 있으면 된다.

=> 결국, 개발자가 중간에서 변환 역할을 해야 함

 

 

저장

객체를 DB에 저장하려면 team 필드를 TEAM_ID 외래키 값으로 변환해야 한다.

외래 키 값을 찾아서 INSERT SQL을 만들어야 하는데 MEMBER 테이블에 저장해야 할 TEAM_ID 외래 키는 TEAM 테이블의 기본 키이므로 member.getTeam().getId()로 구할 수 있다.

member.getId(); 	  //MEMBER_ID PK에 저장
member.getTeam().getId(); // TEAM_ID FK에 저장
member.getUsername(); 	  // USERNAME 컬럼에 저장

 

 

조회

조회할 떄는 TEAM_ID 외래키 값을 Member 객체의 team 참조로 변환해서 객체에 보관해야 한다.

먼저 다음 SQL과 같이 MEMBER와 TEAM을 조회하자.

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

 

SQL 결과로 아래처럼 객체를 생성하고 연관관계를 설정해서 반환하면 된다.

public Member find(String memberId) {
    // SQL 실행
    ...
    Member member = new Member();
    ...
    
    // 데이터베이스에서 조회한 회원 관련 정보를 모두 입력
    Team team = new Team();
    ...
    
    // 데이터베이스에서 조회한 팀 관련 정보를 모두 입력
    
    // 회원과 팀 관계 설정
    member.setTeam(team);
    retrun member;
}

 

이런 과정들은 모두 패러다임 불일치를 해결하려고 소모하는 비용이고,

만약 자바 컬렉션에 회원 객체를 저장한다면 이런 비용이 전혀 들지 않는다.

 

 

JPA와 연관관계

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

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

 

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

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

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

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

 

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

 

연관관계와 관련해서 극복하기 어려운 패러다임 불일치 문제를 알아보자.

 

3) 객체 그래프 탐색

객체에서 회원이 소속된 팀을 조회할 때는 다음처럼 참조를 사용해서 연관된 팀을 찾으면 되는데, 이것을 객체 그래프 탐색이라 한다.

Team team = member.getTeam();

 

 

객체 연관관계가 이와 같이 설계되어 있다고 가정해보자.

 

다음은 객체 그래프를 탐색하는 코드다.

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

 

객체는 마음껏 객체 그래프를 탐색할 수 있어야 한다. 그런데 맘껏 객체 그래프를 탐색할 수 있을까?

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

 

예를 들어, MemberDAO 에서 member 객체를 조회할 때 이런 SQL을 실행해서 회원과 팀에 대한 데이터만 조회했다면 member.getTeam() 은 성공하지만 다음처럼 다른 객체 그래프는 데이터가 없으므로 탐색할 수 없다.

member.getOrder(); // null

 

SQL을 직접 다루면 처음 실행하는 SQL에 따라 객체 그래프를 어디까지 탐색할 수 있는지 정해진다.

이것은 객체지향 개발자에겐 너무 큰 제약이다. 왜냐하면 비즈니스 로직에 따라 사용하는 객체 그래프가 다른데 언제 끊어질지 모를 객체 그래프를 탐색할 수는 없기 때문이다.

// 회원 조회 비즈니스 로직
class MemberService {
    ...
    public void process() {
        Member member = memberDAO.find(memberId);
        member.getTeam(); // member -> team 객체 그래프 탐색이 가능한가?
        member.getOrder().getDelivery(); // ??
    }
}

 

MemberService는 memberDAO를 통해 member 객체를 조회했지만, 이 객체와 연관된 Team, Order, Delivery 방향으로 객체 그래프를 탐색할 수 있을지 없을지는 이 코드만 보고 전혀 예측할 수 없다.

결국, 어디까지 객체 그래프 탐색이 가능한지 알아보려면 데이터 접근 계층인 DAO를 열어서 SQL을 직접 확인해야 한다.

=> 엔티티가 SQL에 논리적으로 종속되어서 발생하는 문제다.

 그렇다고 member와 연관된 모든 객체 그래프를 데이터베이스에서 조회해서 애플리케이션 메모리에 올려두는 것은 현실성이 없다. 결국 MemberDAO에 회원을 조회하는 메소드를 상황에 따라 여러 벌 만들어서 사용해야 한다.

memberDAO.getMember(); // Member만 조회
memberDAO.getMemberWithTeam(); // Member와 Team 조회
memberDAO.getMemberWithOrderWithDelivery // Member와 Order와 Delivery 조회

 

객체 그래프를 신뢰하고 사용할 수 있으면 이런 문제를 어느 정도 해소할 수 있다.

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

 

JPA와 객체 그래프 탐색

JPA를 사용하면 객체 그래프를 마음껏 탐색할 수 있다.

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

 

 JPA는 연관된 객체를 사용하는 시점에 적절한 SELECT SQL을 실행한다. 따라서 JPA를 사용하면 연관된 객체를 신뢰하고 마음껏 조회할 수 있다. 이 기능은 실제 객체를 사용하는 시점까지 데이터베이스 조회를 미룬다고 해서 지연 로딩이라 한다.

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

 

아래 Member 객체를 보면 getOrder() 메소드의 구현부분에 JPA와 관련된 어떤 코드도 직접 사용하지 않는다.

// 투명한 엔티티
class Member {
    private Order order;
    
    public Order getOrder() {
        return order;
    }
}

 

아래는 지연 로딩을 사용하는 코드인데, 여기서 마지막 줄의 order.getOrderData() 같이 실제 Order 객체를 사용하는 시점에 JPA는 데이터베이스에서 ORDER 테이블을 조회한다.

//지연 로딩 사용

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

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

 

 Member를 사용할 때마다 Order를 함께 사용하면, 이렇게 한 테이블 씩 조회하는 것보다는 Member를 조회하는 시점에 SQL 조인을 사용해서 Member와 Order를 함께 조회하는 것이 효과적이다.

  JPA는 연관된 객체를 즉시 함께 조회할지 아니면 실제 사용되는 시점에 지연해서 조회할지 간단한 설정으로 정의할 수 있다. 만약 Member와 Order를 즉시 함께 조회하겠다고 설정하면 JPA는 Member를 조회할 때 다음 SQL을 실행해서 연관된 Order도 함께 조회한다.

SELECT M.*, O.*
FROM MEMBER M
JOIN ORDER O ON M.MEMBER_ID = O.MEMBER_ID

 

 

4) 비교

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

반면, 객체동일성(identity) 비교와 동등성(equality) 비교 가 있다.

동일성 비교 : == 비교. 객체 인스턴스의 주소 값을 비교
동등성 비교 : equals() 메소드를 사용해서 객체 내부의 값 비교

 

따라서 테이블의 로우를 구분하는 방법과 객체를 구분하는 방법에는 차이가 있다.

 

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 // 다르다

 

기본 키 값이 같은 회원 객체를 두번 조회했는데, 동일성(==)을 비교하면 false가 반환된다. member1과 member2는 같은 데이터베이스 로우에서 조회했지만 객체 측면에서 볼 때 둘은 다른 인스턴스기 때문이다.

(MemberDAO.getMember()를 호출할 때마다 new Member()로 인스턴스가 새로 생성된다.)

 따라서 데이터베이스의 같은 로우를 조회했지만 객체의 동일성 비교에는 실패한다. 만약 객체를 컬렉션에 보관했다면 동일성 비교에 성공했을 것이다.

 Member member1 = list.get(0);
 Member member2 = list.get(0);
 
 member1  == member2 // 같다

 

 이런 패러다임 불일치 문제를 해결하기 위해 데이터베이스의 같은 로우를 조회할 때마다 같은 인스턴스를 반환하도록 구현하는 것은 쉽지 않다. 여기에 여러 트랜잭션이 동시에 실행되는 상황까지 고려하면 문제는 더 어려워진다.

 

JPA와 비교

JPA는 같은 트랜잭션일 때 같은 객체가 조회되는 것을 보장한다. 그러므로 다음 코드에서 member1과 member2는 동일성 비교에 성공한다.

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

member1 == member2 // 같다

5) 정리

 객체모델과 관계형 데이터베이스 모델은 지향하는 패러다임이 서로 다르다.

 객체지향 애플리케이션 답게 정교한 객체 모델링을 할수록 패러다임 불일치 문제는 더 커지고, 소모하는 비용이 커진다. 결국, 객체 모델링은 힘을 잃고 점점 데이터 중심의 모델로 변해간다.

  이러한 문제를 해결하기 위한 결과물이 바로 JPA 다. JPA는 패러다임 불일치 문제를 해결해주고 정교한 객체 모델링을 유지하게 도와준다.

 

 

*자바 ORM 표준 JPA 프로그래밍 책을 읽고 정리한 내용들입니다.

728x90