JPA

[JPA] 패러다임 불일치

제리 . 2020. 11. 9. 16:49

애플리케이션은 발전하면서 점점 복잡성이 커진다. 지속 가능한 애플리케이션을 개발하는 일은 끊임없이 증가하는 복잡성과 의 싸움이다. 복잡성을 제어하지 못하면 유지보수하기 어려운 애플리케이션이 된다.

 

객체지향 프로그래밍의 경우 추상화, 캡슐화, 정보은닉, 상속, 다형성 등 시스템의 복잡성을 제어할 수 있는 방법이 존재하기 때문에 많은 애플리케이션이 객체지향 언어로 개발한다.

 

비즈니스 요구사항을 정의한 도메인 모델을 객체로 모델링하면 객체가 지니는 장점을 활용할 수 있다. 하지만 문제는 어떻게 객체를 저장해야할지다. 예를 들어 유저에 대한 정보를 담는 인스턴스를 생성했다면 메모리가 아닌 영구적인 저장소에 저장해야한다. 단순히 객체의 속성을 모두꺼내와 데이터베이스에 저장할 수도있지만, 만약 객체가 상속을 받거나 다른 객체를 참조하게된다면 쉽지않을 것이다. 자바의 경우 직렬화를 제공해 객체자체를 파일로 저장할 수 있도록 해준다. 파일은 역직렬화를 통해 다시 객체로 만들 수 있다. 다만, 파일로 생성된 데이터를 검색한다는 자체가 어렵다. 현실적으로는 데이터베이스에 저장해야한다.

 

데이터베이스는 데이터 중심으로 구조화되어있다. 객체의 상속, 다형성 같은 개념이없다. 그렇다보니 객체와 데이터베이스가 지향하는 점이 다르다. 이것을 객체와 데이터베이스의 패러다임 불일치라고 한다. 자바 언어는 객체지향으로 이뤄져있고 데이터베이스는 데이터 중심으로 구조화되어있기때문에 패러다임 불일치 문제를 개발자가 해결해야한다. 이 과정에서 시간과 코드를 소비하는 문제가 있다.

 

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

 

1. 상속

 

 

https://ultrakain.gitbooks.io/jpa/content/chapter1/chapter1.2.html

객체는 위와같이 상속의 기능을 지니지만 테이블은 상속의 기능이 없다. 그나마 데이터베이스 모델링에서 이야기하는 슈퍼타입 서브타입 관계를 사용하면 객체 상속과 유사한 형태로 테이블을 설계할 수는 있다. 

 

https://ultrakain.gitbooks.io/jpa/content/chapter1/chapter1.2.html

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 ...

 

JDBC API를 이용해서 코드를 작성하려면 부모 객체에서 부모 데이터만 꺼내 ITEM용 SQL을 작성하고 자식 객체에서는 자식 데이터만 꺼네 ALBUM용 SQL을 작성해야한다. 작성해야할 코드도 많고 자식타입에따라 DTYPE도 추가로 저장해야한다. 

 

조회의 경우도 만만치않다. ALBUM을 조회하려면 ITEM테이블과 ALBUM테이블을 조인하고 결과로 ALBUM객체를 생성해야한다. 이런 과정이 패러다임 불일치를 해결하기위해 개발자해 소모하는 비용이다.

 

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

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

Album albun = list.get(albumId);

 

JPA에서 상속은?

JPA에서 패러다임 불일치 문제를 개발자 대신해준다. 개발자는 자바 컬렉션에 저장하듯이 JPA에게 객체를 저장하면 된다.

 

먼저, presist()메소드를 사용해서 객체를 저장한다.

jpa.persist(album);

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

INSERT INTO ITEM ...

INSERT INTO ALBUM ...

 

조회의 경우 find()를 사용해서 객체를 조회하면 된다.

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

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

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

JDBC API와 비교해보면 부모 관계를 표현하기 위해 개발자가 쿼리를 작성하고 코드를 추가로 작성해야하는 부분이 사라졌다.

 

2. 연관관계

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

위와 같이 Member와 Team이 연관관계를 지니는 상황을 고려해보자.

 

 Member객체는 Member.team필드에 Team객체의 참조를 보관해서 Team객체와 관계를 맺는다. 이 참조 필드에 접근해 Member와 연관된 Team을 조회할 수 있다. 반면 MEMBER테이블은 MEMBER_ID 외래 키 컬럼을 사용해서 TEAM과 관계를 맺는다.  이 외래키를 사용해 TEAM테이블과 조인하면 MEMBER테이블과 연관된 TEAM테이블을 조회할 수 있다. 여기서 발생하는 문제는 객체의 참조 방향이다. 객체 연관관계의 경우 member.getTeam으로 참조가능하지만 반대로 Team.getMember()는 불가능하다. 반면 테이블은 어느쪽에서든 join을 사용할 수 있다.

 

객체를 테이블에 맞추어 모델링하는 경우는 아래와 같다.

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

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

객체를 테이블에 맟추어 모델링하면 객체를 테이블에 저장하거나 조회할때는 편한다. 그런데 여기서 teamId필드에 문제가 있다. 관계형 데이터베이스는 조인기능이 있어 외래 키의 값을 그대로 보관해도 된다. 하지만 객체는 연관된 객체의 참조를 보관해야 다음 처럼 참조를 통해 연관된 객체를 찾을 수 있다.

Team team = member.getTeam();

객체지향적으로 참조하는 방법을 위와 같은 것이다.

 

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

 

객체지향 모델링은 아래와 같다.

객체는 참조를 통해 관계를 맺는다.

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

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

Member.team은 외래키를 그대로 보관하지않고 객체로 보관한다. 이제 member와 연관된 팀을 조회할 수 있다.

Team team = member.getTeam();

그런데 객체 모델을 사용하면 테이블에 저장하거나 조회하기 쉽지않다. 객체 모델을 Team을 객체로 데이터베이스는 Team을 TEAM_ID로 저장하기 때문이다. 이런 차이가 있어 개발자가 중간에서 변환 역할을 해야한다.

 

만약 객체지향 모델로 데이터를 저장하기 위해서는 아래와 같이 참조 객체를 키값으로 변환해서 사용해야한다.

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

외래 키의 값을 찾아서 INSERT SQL문을 작성해야한다.

 

조회의 경우는 아래와 같다.

public Member find(String memberId) {

  //SQL실행
  ...
  Member member = new Member();
  ...
  //데이터베이스에서 조회한 회원 관련 정보를 모두 입력
  Team team = new Team();
  //데이터베이스에서 조회한 팀 관련 정보 모두 입력
  
  //회원과 팀 관계 설정
  member.setTeam(team);
  return 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();

만약, member의 스키마가 변한다고 생각해보자. JPA를 사용하지 않는다면 추가되는 컬럼을 member.getxx()을 사용해서 추가해주거나 변경해야하는 작업이 생길 것 이다. 

 

지금까지는 SQL을 직접 다루어도 열심히 코드만 작성하면 극복할 수 있는 문제들이었다. 하지만 연관관계와 관련된 극복하기 어려운 패러다임 불일치 문제도 있다.

 

3. 객체 그래프 탐색

객체에서 회원이 소속된 팀을 조회할 때 다음처럼 참조해서 사용하면 연관된 팀을 찾을 수 있다. 이것을 객체 그래프 탐색이라고 한다. 아래와 같이 설계되었다고 해보자.

 

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-> team 객체 그래프 탐색이 가능한가?
    member.getOrder().getDelivery(); //???이게 가능할지 확신할 수 있는가?

위의 코드만 가지고는 객체그래프를 어디까지 탐색할지 알 수없다. 전적으로 SQL문에 달려있다. 그렇다고 연관된 객체를 모두 조회해서 메모리에 올리는 것 또한 현실성이 없다. 결국 매번 MemberDAO에는 상황에따라 메서드를 여러개 만들어 두어야한다.

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

 

JPA에서 객체그래프 탐색은?

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와 관련된 어떤 코드도 직접 사용하지 않는다.

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

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

Member를 사용할 떄마다 Order를 함께 사용한다면 이렇게 한 테이블씩 조회하는 것보다 동시에 조회하는게 효과적이다. JPA는 연관된 객체를 즉시 함께 조회할지 아니면 실제 사용시점에 조회할지 간단한 설정으로 정의할 수 있다.

 

4. 비교

데이터베이스는 기본 키의 값으로 각 로우를 구분한다. 반면, 객체는 동일성 비교과 동등성 비교라는 두가지 방법이있다.

동일성 비교는 ==비교다. 객체의 인스턴스 주소 값을 비교한다. 동등성 비교는 equals() 메소드를 사용해 객체 내부의 값을 비교한다. 따라서 테이블 로우와 객체를 구분하는 방법에는 차이가 있다.

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

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

 

JPA에서 비교는?

JPA는 같은 트랙잭션일 때 같은 객체가 조회되는 것을 보장한다. 

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

member1 == member2; // true

 

정리하자면 객체 모델과 데이터베이스 모델은 지향하는 패러다임이 다르고 이것을 극복하기위해 개발자는 많은 시간과 코드를 소비한다. 객체 지향에 가깝게 모델링 할 수록 패러다임 불일치의 문제는 더욱 커진다. 결국 애플리케이션은 점점 데이터 중심 모델로 변해간다. JPA는 이런 문제를 해결 할 수있게 도와준다.