일 | 월 | 화 | 수 | 목 | 금 | 토 |
---|---|---|---|---|---|---|
1 | 2 | |||||
3 | 4 | 5 | 6 | 7 | 8 | 9 |
10 | 11 | 12 | 13 | 14 | 15 | 16 |
17 | 18 | 19 | 20 | 21 | 22 | 23 |
24 | 25 | 26 | 27 | 28 | 29 | 30 |
- C++
- Fluent-bit
- While
- quicksort
- 자료형
- Sprint Security
- ansible
- redis
- JavaScript
- Spring Security
- IAC
- g1gc
- jvm
- zgc
- JPA
- Kotlin
- If
- Algorithm
- Class
- lambda
- SpringBoot Initializr
- Java
- programmers
- datatype
- MergeSort
- For
- datastructure
- 기초
- UserDetails
- 연산자
- Today
- Total
뭐라도 끄적이는 BLOG
RDA와 Object oriented 비교 본문
SQL 중심적 개발의 문제점
현대적인 애플리케이션을 개발할 때 대부분은 객체지향 언어를 사용한다. 그리고 우리는 데이터를 저장하기 위해 관계형 데이터베이스를 사용한다. NoSQL이 많긴 하더라도 주요 DB는 오라클이나 MySQL과 같은 관계형 데이터베이스를 더 많이 사용한다.
이렇다 보니 현재는 객체를 관계형 데이터베이스에 저장해야 되고, 저장한 데이터를 사용하는 것도 관계형 데이터베이스 테이블에서 객체로 불러와야 한다. 여기서 문제점이 발생한다. 관계형 데이터베이스는 SQL만 알아들을 수 있기 때문에 애플리케이션에선 SQL을 만들어서 보내주어야 한다. 애플리케이션은 객체지향적으로 한다고 노력을 하더라도 결국 코드는 SQL이 많아져 SQL중심적인 개발이 이루어지는 현상이 일어난다.
반복되는 지루한 코드
SQL과 객체지향으로 이루어진 코드는 INSERT, UPDATE, SELECT, DELETE의 CRUD를 작성한다. 이 과정에서 자바 객체를 SQL로 SQL을 자바 객체로 변경하는 일을 무한 반복한다. 테이블이 10개라면 10개 모두 CRUD 작업이 이루어지고 Join을 이용해야 한다면 더 많은 SQL을 생성해야 한다. 물론 Mybatis 등 이를 줄여주는 SQL Mapper 라이브러리가 있지만 비슷하게 코드가 반복되는 것을 막을 순 없다. 결국 개발자는 이를 매핑하는데 많은 시간을 보내게 되며 SQL에 의존적인 개발을 피하기 어렵다.
필드 추가
개발을 하다보면 중간에 갑작스럽게 필요해진 정보를 저장해야 될 때가 있다. 이럴 때 데이터베이스 테이블을 조정해야 하는데 이는 수많은 코드들을 한땀한땀 변경해 주어야 된다는 큰 문제를 야기한다.
패러다임의 불일치
객체지향에선 추상화, 캡슐화, 정보은닉, 상속, 다형성등 시스템의 복잡성을 제어할 수 있는 다양한 장치들을 제공한다. 이러한 객체 인스턴스는 생성한 후에 객체를 메모리가 아닌 어딘가에 영구 보관해야 한다. 보관을 위해 RDB, NoSQL, File 등 다양한 방법이 있지만 가장 많이 사용하는 것은 RDB이다.
관계형 데이터베이스와 객체지향이 각각 나오게 된 사상이 다르다. 관계형 DB는 데이터를 잘 정규화하여 보관하는 것이 목적이며, 객체는 필드와 메소드를 캡슐화하여 사용하는 것이 목적이다. 이러한 패러다임의 불일치 문제는 개발자가 중간에서 해결해야 한다. 문제는 이런 객체와 RDB사이의 패러다임 불일치 문제를 해결하는 데 너무 많은 시간과 코드를 소비하는 데 있다. 이러한 패러다임의 불일치로 인해 발생하는 문제를 살펴본다.
상속
객체는 상속관계라는 기능을 가지고 있다. RDB에서는 같진 않지만 이와 유사하게 Table간의 슈퍼타입, 서브타입 관계가 있긴 하다. 이를 이용해서 객체와 테이블을 잘 쪼개어 설계하더라도 여러 문제가 발생한다. 다음의 다이어그램을 보며 몇 가지 예시로 문제점을 살펴보자.
비슷하게 만든 객체와 데이터베이스 테이블을 만들어 Album 객체를 데이터베이스에 저장해보자. Album객체를 저장하기 위해서 이 객체를 분해한 뒤 다음 두 SQL을 생성해야 한다.
INSERT INTO ITEM ...
INSERT INTO ALBUM ...
이는 Movie나 Book 객체를 저장할 때도 마찬가지다.
조회하는 것도 쉬운 일은 아니다. Album을 조회한다면 ITEM과 ALBUM테이블을 조인해서 조회한 다음 그 결과로 Album객체를 생성해야 한다. 이러한 과정이 패러다임의 불일치를 해결하기 위한 비용이다. 만약 해당 객체들을 데이터베이스가 아닌 자바 컬렉션에 보관한다면 상속이나 타입에 대한 고민 없이 컬렉션을 사용하기만 하면 된다.
list.add(album);
list.add(movie);
Album album = list.get(albumId);
JPA는 이러한 문제를 개발자 대신 해결해 주어 마치 자바 컬렉션처럼 객체를 저장할 수 있도록 도와준다.
연관관계
객체는 참조를 사용해서 다른 객체와 연관관계에 접근할 수 있다. 테이블은 외래 키를 사용해서 다른 테이블과 연관관계를 조인으로 조회할 수 있다. 여기서 객체 연관관계와 RDB 연관관계의 다른 점을 확인해 볼 수 있다. 객체는 참조가 있는 방향으로만 조회할 수 있지만 테이블은 외래키를 사용하여 어느 테이블에서나 두 테이블 간의 조인이 가능하다. 이러한 차이점으로 발생되는 문제를 살펴보자.
객체를 테이블에 맞추어 모델링
class Member {
String id;
Long teamId;
String username;
}
class Team {
Long id;
String name;
}
이렇게 객체를 테이블에 맞추어 모델링하면 테이블에 저장하거나 조회할 때는 편리하다. 하지만 TEAM_ID외래 키의 값을 그대로 보관하는 teamId 필드에는 문제가 있다. 관계형 데이터베이스는 조인이라는 기능이 있으므로 외래 키의 값을 그대로 보관해야 한다. 하지만 객체는 연관된 객체의 참조를 보관해야 참조를 통해 연관된 객체를 찾을 수 있다. 이러한 방식을 따르면 좋은 객체 모델링은 기대하기 어렵고 결국 객체지향의 특징을 잃어버리게 된다.
객체지향 모델링
class Member {
String id;
Team team;
String username;
Team getTeam() {
return team;
}
}
class Team {
Long id;
String name;
}
Member class와 Team class는 참조를 통해서 관계를 맺는 것을 볼 수 있다. 이 처럼 객체지향 모델링을 사용하면 객체를 테이블에 저장하거나 조회하기가 쉽지 않다. 객체는 외래 키가 필요 없고 단지 참조만 있으면 되지만 테이블은 참조가 필요 없고 외래 키가 있어야한다. 결국 개발자가 중간에서 변환 역할을 해야 한다.
Member 객체 저장 시 team필드를 TEAM_ID외래 키 값으로 변환해서 저장해야한다. 그리고 조회할 때는 TEAM_ID 외래 키 값을 Member 객체의 team참조로 변환해서 객체에 보관해야 한다. 다음은 조회를 위한 SQL 예시이다.
SELECT M.*, T.*
FROM MEMBER M JOIN TEAM T
ON M.TEAM_ID = T.TEAM_ID
위 SQL의 결과로 Member와 Team을 조회한 뒤 객체를 생성하고 연관관계를 참조로 변환하여 반환하는 과정이다.
public Member find(String memberId) {
// run SQL
Member member = new Member();
...
Team team = new Team();
...
member.setTeam(team);
return member;
}
이러한 번거로운 과정을 거쳐야 사용할 수 있다. JPA에서 연관관계와 객체의 참조에대한 패러다임의 불일치 문제를 해결할 수 있다.
객체 그래프 탐색
객체는 회원이 소속된 팀을 조회할 때 참조를 사용해서 연관된 팀을 찾을 수 있다. 이를 객체 그래프 탐색이라 하며, 객체는 자유롭게 객체 그래프를 탐색할 수 있어야 한다. 하지만 SQL을 직접 다루면 처음 실행하는 SQL에 따라 객체 그래프를 어디까지 탐색할 수 있는지 정해진다. 이런점은 객체지향 개발에 있어 큰 제약입니다. 다음 코드로 그 문제를 살펴보자.
class Member {
String id;
Team team;
Order order;
String username;
Team getTeam() {
return team;
}
Order getOrder() {
return order;
}
}
class Team {
Long id;
String name;
}
class Order {
Long id;
String name;
}
위와 같은 객체들이 있다고 가정했을 때 다음과 같은 SQL을 실행하여 데이터를 가지고 온다.
SELECT M.*, T.*
FROM MEMBER M JOIN TEAM T
ON M.TEAM_ID = T.TEAM_ID
Member와 Team에 대한 데이터는 조회하였지만 Order에 대한 객체를 조회하지 않았기 때문에 다음과 같은 코드에서 문제가 발생한다.
class MemberService {
...
public void process() {
Member member = memberDAO.find(memberId);
member.getTeam();
member.getOrder().getId();
}
}
단순히 개발자는 MemberService 코드만으로 member에서 order객체를 참조할 수 있는지 전혀 예측할 수 없다. 결국 어디까지 그래프 탐색이 가능한지 알아보려면 데이터 접근 계층인 DAO를 열어 SQL을 직접 확인해야 한다. 이것은 엔티티가 SQL에 논리적으로 종속되었기 때문에 문제가 발생한다.
그렇다고 member와 연관된 모든 객체 그래프를 데이터베이스에서 조회하는 것은 현실성이 없다. 결국 MemberDAO에 회원을 조회하는 메소드를 상황에 따라 여러 벌 만들어서 사용해야 한다. JPA는 이러한 객체 그래프를 마음껏 탐색할 수 있도록 도와줄 수 있다.
비교하기
SQL에서 데이터를 가져와 비교하는 것과 Java 컬렉션에서 값을 꺼내어 비교하는 것은 다르다. 데이터베이스는 기본 키의 값으로 각 row를 구분하며, 객체는 동일성(identity) 비교와 동등성(equality) 비교라는 두 가지 비교방법이 있다.
동일성 비교는 `==` 즉 객체 인스턴스의 주소 값을 비교합니다.
동등성 비교는 `equals()` 메소드를 사용해서 객체 내부의 값을 비교합니다.
SQL로 데이터를 불러오면 서로 다른 객체에 저장하게 되어 참조값이 다르지만 컬렉션에서 꺼낸 값은 참조값이 같기 때문에 서로 같은 객체가 된다. 이러한 문제를 해결하기 위해 데이터베이스의 같은 로우를 조회할 때마다 같은 인스턴스를 반환하도록 구현하는 것은 쉽지 않다. 여기에 여러 트랜잭션이 동시에 실행되는 상황까지 고려한다면 문제는 더 어려워진다.
JPA에서는 같은 트랜잭션일 때 같은 객체가 조회되는 것을 보장해 줄 수 있다.
참고자료
'Java > JPA' 카테고리의 다른 글
JPA 소개 (0) | 2023.07.17 |
---|---|
JPA 06.02 - 다양한 연관관계 매핑 [ 1 : 1, N : M ] (0) | 2021.04.17 |
JPA 06.01 - 다양한 연관관계 매핑 [ N : 1, 1 : N ] (0) | 2021.04.17 |
JPA 05.02 - 양방향 연관관계와 연관관계의 주인 (0) | 2021.04.16 |
JPA 05.01 - 연관관계 매핑, 단방향 연관관계 (0) | 2021.04.16 |