패러다임 불일치란?
애플리케이션은 발전하면서 그 내부의 복잡성도 점점 커진다.
지속 가능한 애플리케이션을 개발하는 일은 끊임없이 증가하는 복잡성과의 싸움이다. 복잡성을 제어하지 못하면 결국 유지보수하기 어려운 애플리케이션이 된다.
객체지향 프로그래밍은 추상화, 캡슐화, 정보은닉, 상속, 다형성 등 시스템의 복잡성을 제어할 수 있는 다양한 장치들을 제공한다.
비즈니스 요구사항을 정의한 도메인 모델도 객체로 모델링하면 객체지향 언어가 가진 장점들을 활용할 수 있다.
문제는 이렇게 정의한 도메인 모델을 저장할 때 발생한다.
객체는 속성(필드)과 기능(메서드)을 가진다. 객체의 기능은 클래스의 정의되어 있으므로 객체 인스턴스의 상태인 속성만 저장했다가 필요할 때 불러와서 복구하면 된다. 객체가 단순하면 객체의 모든 속성 값을 꺼내서 파일이나 데이터페이스에 저장하면 되지만, 부모객체를 상속받았거나, 다른 객체를 참조하고 있다면 객체의 상태를 저장하기는 쉽지 않다.
자바는 이런 객체를 파일로 저장하기 위해 직열화와 역직열화를 지원한다. 하지만 이 방법은 직열화된 객체를 검색하기 어렵다는 문제가 있으므로 현실성이 없다.
현실적인 대안은 관계형 데이터베이스에 객체를 저장하는 것인데, 관계형 데이터베이스는 데이터 중심으로 구조화되어 있고, 집합적인 사고를 요구한다. 그리고 객체지향에서 이야기하는 추상화, 상속, 다형성 같은 개념이 없다.
객체와 관계형 데이터베이스는 지향하는 목적이 서로 다르므로 둘의 기능과 표현 방법도 다르다.
이것을 객체와 관계형 데이터베이스의 패러다임 불일치 문제라 한다.
지금부터 패러다임 불일치로 인해 발생하는 문제를 구체적으로 살펴보자. 그리고 JPA를 통한 해결책도 함께 알아보자.
상속
문제점
객체는 상속이라는 기능을 가지고 있지만 테이블은 상속이라는 기능이 없다.
그나마 데이터베이스 모델링에서 이야기하는 슈퍼타입 서브타입 관계를 사용하면 객체 상속과 가장 유사한 형태로 테이블을 설계할 수 있다.
위 그림에서 ITEM 테이블의 DTYPE
컬럼을 사용해서 어떤 자식 테이블과 관계가 있는지 정의했다.
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용 INSERT SQL을 작성하고 자식 데이터만 꺼내서 ALBUM용 INSERT SQL을 작성해야 하는데, 작성해야 할 코드량이 만만치 않다. 그리고 자식 타입에 따라서 DTYPE
도 저장해야 한다.
조회하는 것도 쉬운 일은 아니다. 예를 들어 Album을 조회한다면 ITEM과 ALBUM 테이블을 조인해서 조회한 다음 그 결과로 Album 객체를 생성해야 한다.
이런 과정이 모두 패러다임의 불일치를 해결하려고 소모하는 비용이다.
JPA의 상속
JPA는 상속과 관련된 패러다임의 불일치 문제를 개발자 대신 해결해 준다.
개발자는 마치 자바 컬렉션에 객체를 저장하듯이 JPA에게 객체를 저장하면 된다.
JPA를 사용해서 Item을 상속한 Album 객체를 저장해 보자.
객체를 저장할 땐 persist()
메서드를 사용해서 저장하면 된다.
jpa.persist(album);
JPA는 다음 SQL을 실행해서 객체를 ITEM, ALBUM 두 테이블에 나누어 저장한다.
INSERT INTO ITEM ...
INSERT INTO ALBUM ...
다음으로 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
연관관계
객체는 참조를 사용해서 다른 객체와 연관관계를 가지고 참조에 접근해서 연관된 객체를 조회한다.
반면에 테이블은 외래 키를 사용해서 다른 테이블과 연관관계를 가지고 조인을 사용해서 연관된 테이블을 조회한다.
참조를 사용하는 객체와 외래 키를 사용하는 관계형 데이터베이스 사이의 패러다임 불일치는 객체지향 모델링을 거의 포기하게 만들 정도로 극복하기 어렵다.
Member 객체는 Member.team
필드에 Team 객체의 참조를 보관해서 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 컬럼 사용
}
이렇게 객체를 테이블에 맞추어 모델링하면 객체를 테이블에 저장하거나 조회할 때는 편하다.
그런데 여기서 TEAM_ID
외래 키의 값을 그대로 보관하는 teamId
필드에는 문제가 있다.
관계형 데이터베이스에는 조인이라는 기능이 있으므로 외래 키의 값을 그대로 보관해도 된다. 하지만 객체는 연관된 객체의 참조를 보관해야 연관된 객체를 찾을 수 있다. 특정 회원이 소속된 팀을 조회하는 가장 객체지향적인 방법은 이처럼 참조를 사용하는 것이다.
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_ID
외래 키로 연관관계를 맺기 때문인데
개발자는 객체의 참조와 테이블의 왜래 키의 반환 역할을 해야 한다.
만약 객체지향 모델로 데이터를 저장하기 위해서는 아래와 같이 참조 객체를 키값으로 변환해서 사용해야 한다.
member.getId() //MEMBER_ID PK저장
member.getTeam.getId() //TEAM_ID FK저장
member.getUserName() //USERNAME 컬럼에 저장
조회할 때는 TEAM_ID 외래 키 값을 Member 객체의 team 참조로 변환해서 객체에 보관해야 한다.
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();
객체 그래프 탐색
객체에서 회원이 소속된 팀을 조회할 때는 다음처럼 참조를 사용해서 연관된 팀을 찾으면 되는데, 이것을 객체 그래프 탐색이라 한다.
Team team = member.getTeam();
객체 연관관계가 아래와 같이 설계되어 있다고 가정해 보자.
다음은 객체 그래프를 탐색하는 코드다.
member.getOrder().getOrderItem()...//자유로운 객체 그래프 탐색
SQL을 직접 다루면 처음 실행하는 SQL에 따라 객체 그래프를 어디까지 탐색할 수 있는지 정해진다.
이것은 객체지향 개발자에겐 너무 큰 제약이다.
왜냐하면 비즈니스 로직에 따라 사용하는 객체 그래프가 다른데 언제 끊어질지 모를 객체 그래프를 함부로 탐색할 수는 없기 때문이다.
class MemberService {
...
public void process() {
Member member = memberDAO.find(MemberId);
member.getTeam(); //member-> team 객체 그래프 탐색이 가능한가?
member.getOrder().getDelivery(); //???이게 가능할지 확신할 수 있는가?
}
}
member 객체를 조회했지만, 이 객체와 연관된 Team, Orderm Delivery 방향으로 객체 그래프를 탐색할 수 있을지 없을지는 이 코드만 보고는 전혀 예측할 수 없다.
그렇다고 member와 연관된 모든 객체 그래프를 데이터베이스에서 조회해서 애플리케이션 메모리에 올려두는 것은 현실성이 없다. 결국 MemberDAO에 회원을 조회하는 메서드를 상황에 따라 여러 벌 만들어서 사용해야 한다.
memberDAO.getMember();
memberDAO.getMemberWithTeam();
memberDAO.getMemberWithOrderWithDelivery();
JPA와 객체그래프 탐색
JPA는 연관된 객체를 사용하는 시점에 적절한 SELECT SQL을 실행한다. 따라서 JPA를 사용하면 연관된 객체를 신뢰하고 마음껏 조회할 수 있다. 이 기능은 실제 객체를 사용하는 시점까지 데이터베이스 조회를 미룬다고 해서 지연 로딩이라 한다.
JPA는 지연 로딩을 투명하게 처리한다.
class Member {
private Order order;
public Order getOrder() {
return order;
}
}
Member 객체에는 JPA와 관련된 어떤 코드도 직접 사용하지 않는다.
//처음 조회 시점에 SELECT MEMBER SQL
Member member = jpa.find(Member.class, memberId);
Order order = memeber.getOrder();
order.getOrderDate(); //Order를 사용하는 시점에 SELECT ORDER SQL
Member를 사용할 때마다 Order를 함께 사용하면, 이렇게 한 테이블씩 조회하는 것보다는 Member를 조회하는 시점에 SQL 조인을 사용해서 Member와 Order를 함께 조회하는 것이 효과적이다.
JPA는 연관된 객체를 즉시 함께 조회할지 아니면 실제 사용되는 시점에 지연해서 조회할지를 간단한 설정으로 정의할 수 있다.
비교
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
기본 키 값이 같은 회원 객체를 두 번 조회했다. 그런데 둘을 동일성(==) 비교하면 false
가 반환된다.
왜냐하면 member1
과 member2
는 같은 데이터베이스 로우에서 조회했지만, 객체 측면에서 볼 때 둘은 다른 인스턴스기 때문이다.
따라서 데이터베이스의 같은 로우를 조회했지만 객체의 동일성 비교에는 실패한다. 만약 객체를 컬렉션에 보관했다면 다음과 같이 동일성 비교에 성공했을 것이다.
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는 패러다임의 불일치 문제를 해결해 주고 정교한 객체 모델링을 유지하게 도와준다.
참고 자료:
https://product.kyobobook.co.kr/detail/S000000935744
'백엔드 > JPA' 카테고리의 다른 글
[JPA] flush(), detach(), clear(), close(), merge() (0) | 2023.05.02 |
---|---|
[JPA] 엔티티 조회, 등록, 수정, 삭제 (0) | 2023.05.02 |
[JPA] 영속성 컨텍스트(persistence context)란? (0) | 2023.05.02 |
[JPA] JPA란? (0) | 2023.05.01 |
[JPA] SQL Mapper 와 ORM (1) | 2023.05.01 |