[JPA] 1장 - SQL을 직접 다룰 때 발생하는 문제점
1장 - SQL을 직접 다룰 때 발생하는 문제점
자바 ORM 표준 JPA 프로그래밍 1장을 요약한 내용 입니다.
SQL을 직접 다룰 때 발생하는 문제점
SQL
을 직접 다룰 때의 문제점을 알아보기 위해 자바
와 관계형 데이터베이스
를 사용해서 회원 관리 기능을 개발해보자.
먼저 회원을 조회하는 기능을 개발한다.
- 회원 조회용 SQL을 작성한다.
SELECT MEMBER_ID, NAME FROM MEMBER M WHERE MEMBER_ID = ?
- JDBC API를 사용해서 SQL을 실행한다.
ResultSet rs = stmt.executeQuery(sql);
- 조회 결과를 Member 객체로 실행한다.
String memberId = rs.getString("MEMBER_ID");String name = rs.getString("NAME");Member member = new Member();member.setMemberId(memberId);member.setName(name);...
이번엔 회원 등록 기능을 개발한다.
- 회원 등록용 SQL을 작성한다.
String sql = "INSERT INTO MEMBER(MEMBER_ID, NAME) VALUES(?,?)";
- 회원 객체의 값을 꺼내서 등록 SQL에 전달한다.
pstmt.setString(1, member.getMemberId());
pstmt.setString(2, member.getName());
- JDBC API를 사용해서 SQL을 실행한다.
pstmt.executeUpdate(sql);
회원 객체를 데이터베이스가 아닌 자바 컬렉션
에 보관한다면 어떨까? 컬렉션은 다음 한 줄로 객체를 저장할 수 있다.
list.add(member);
따라서 개발자가 객체지향 애플리케이션
과 데이터베이스
중간에서 SQL
과 JDBC API
를 사용해서 변환
작업을 직접 해주어야 한다.
문제는 객체를 데이터베이스에 CRUD
하려면 너무 많은 SQL
과 JDBC API
를 코드로 작성해야 한다는 점이다. 데이터 접근 계층(DAO)
을 개발하는 일은 이렇듯 지루함과 반복의 연속이다.
SQL에 의존적인 개발
회원 객체를 관리하는 MemberDAO
를 완성하고 애플리케이션의 나머지 기능도 개발을 완료했다. 그런데 갑자기 회원의 연락처도 함께 저장해 달라는 요구사항
이 추가되었다.
등록 코드 변경
- 회원 클래스에 연락처 필드 추가
public class Member {
private String memberId;
private String name;
private String tel;
// 추가 ...
}
- 등록 SQL을 수정
String sql = "INSERT INTO MEMBER(MEMBER_ID, NAME, TEL) VALUES(?,?,?)";
- 회원 객체의 연락처 값을 꺼내서 등록 SQL에 전달한다.
pstmt.setString(3, member.getTel());
조회 코드 변경
다음으로 회원 조회 화면에서 모든 연락처의 값이 null
로 출력되었다. 생각해보니 조회 SQL
에 연락처 컬럼을 추가하지 않았다.
- 조회용 SQL을 수정
SELECT MEMBER_ID, NAME, TEL FROM MEMBER WHERE MEMBER_ID = ?
- 연락처의 조회 결과를 Member 객체에 추가로 매핑한다.
String tel = rs.getString("TEL");member.setTel(tel);
연관된 객체
회원은 어떤 한 팀에 필수로 소속되어야 한다는 요구사항이 추가되었다. Member
객체에 team
필드가 추가되어 있었다. 그러나 코드를 실행해보니 member.getTeam()
의 값이 항상 null
이 나오고 있었다.
memberDAO
코드를 열어보니 회원을 출력할 때 사용하는 find()
메소드는 회원만 조회하는 SQL
을 그대로 유지하였다. 그래서 findWithTeam()
메서드를 추가하였다.
- 연관 팀 정보를 가져오는 SQL 추가
SELECT M.MEMBER_ID, M.NAME, M.TEL, T.TEAM_ID, T.TEAM_NAME
FROM MEMBER MJOIN TEAM T
ON M.TEAM_ID = T.TEAM_ID
위의 사례에서 보듯이 Member
객체가 연관된 Team
객체를 사용할 수 있을지 없을지는 전적으로 사용하는 SQL
에 달려 있다. 이런 방식의 가장 큰 문제는 데이터 접근 계층
을 사용해서 SQL
을 숨겨도 어쩔 수 없이 DAO
를 열어서 어떤 SQL
이 실행 되는지 확인해야 한다는 점이다.
이렇듯 SQL
에 모든 것을 의존하는 상황에서는 개발자들이 엔티티
를 신뢰하고 사용할 수 없다. 대신에 DAO
를 열어서 어떤 SQL
이 실행되고 어떤 객체 들이 함께 조회 되는지 일일이 확인해야 한다.
앞의 문제점을 요약하면 다음과 같다.
- 진정한 의미의 계층 분할이 어렵다.
- 엔티티를 신뢰할 수 없다.
- SQL에 의존적인 개발을 피하기 어렵다.
JPA와 문제 해결
JPA
를 사용하면 객체를 데이터베이스에 저장하고 관리할 때, 개발자가 직접 SQL
을 작성하는 것이 아니라 JPA
가 제공하는 API
를 사용하면 된다. 그러면 JPA
가 개발자 대신에 적절한 SQL
을 생성해서 데이터베이스에 전달한다.
이전에 구현하였던 애플리케이션의 기능을 JPA를 사용해서 구현 해보자.
- 조회 기능
String memberId = "helloId";
Member member = jpa.find(Member.class, memberId); // 조회
- 수정 기능
Member member = jpa.find(Member.class, memberId);
member.setName("이름변경"); // 수정
- 연관된 객체 조회
Member member = jpa.find(Member.class, memberId);
Team team = member.getTeam(); // 연관된 객체 조회
패러다임의 불일치
비즈니스 요구사항을 정의한 도메인 모델도 객체로 모델링
하면 객체지향 언어가 가진 장점
들을 활용할 수 있다. 문제는 이렇게 정의한 도메인 모델
을 저장할 때 발생한다.
객체의 기능
은 클래스에 정의되어 있으므로 객체인스턴스
의상태
인 속성만 저장 했다가 필요할 때 불러 와서 복구하면 된다.관계형 데이터베이스
는 데이터 중심으로 구조화되어 있고,집합적인 사고
를 요구한다. 그리고 객체 지향에서 이야기하는추상화
,상속
,다형성
같은 개념이 없다.
객체와 관계형 데이터베이스는 지향하는 목적
이 서로 다르므로 둘의 기능
과 표현 방법
도 다르다. 이것을 객체와 관계형 데이터베이스의 패러다임 불일치 문제
라 한다. 따라서 객체 구조
를 테이블 구조
에 저장하는 데는 한계가 있다.
문제는 이런 객체와 관계형 데이터베이스 사이의 패러다임 불일치 문제를 해결하는 데 너무 많은 시간
과 코드
를 소비
하는 데 있다.
상속
객체는 상속 이라는 기능을 가지고 있지만 테이블은 상속 이라는 기능이 없다.
jpa1
JDBC를 이용한 상속 구현
JDBC API를 사용해서 상속 관계의 객체를 저장 하려면 부모 객체
에서 부모 데이터
만 꺼내서 INSERT SQL을 작성하고 자식 객체
에서 자식 데이터
만 꺼내서 INSERT SQL을 별도로 작성해야 한다.
INSERT INFO ITEM ...INSERT INTO ALBUM ...
JPA를 이용한 상속 구현
JPA는 상속과 관련된 패러다임의 불일치 문제를 개발자 대신 해결해준다. 개발자는 마치 자바 컬렉션
에 객체를 저장 하듯이 JPA
에게 객체를 저장하면 된다.
jpa.persist(album);
연관관계
객체는 참조를 사용해서 다른 객체와 연관관계를 가지고 참조
에 접근
해서 연관된 객체를 조회한다. 반면에 테이블은 외래 키
를 사용해서 다른 테이블과 연관관계를 가지고 조인
을 사용해서 연관된 테이블을 조회한다.
- 테이블에 맞춘 객체 모델
class Member {
String id; // MEMBER_ID 컬럼 사용
Long temId; // TEAM_ID FK 컬럼 사용
String username; // USERNAME 컬럼 사용
}
class Team {
Long id; // TEAM_ID PK 사용
String name; // NAME 컬럼 사용
}
그런데 여기서 TEAM_ID
외래 키의 값을 그대로 보관하는 teamId
필드에는 문제가 있다. 객체는 연관된 객체의 참조
를 보관해야 다음처럼 참조를 통해 연관된 객체를 찾을 수 있어야 하는데 하지 못하게 된다.
Team team = member.getTeam();
이런 방식을 따르면 좋은 객체 모델링은 기대하기 어렵고 결국 객체 지향의 특징을 잃어버리게 된다.
엔티티를 직접 참조하는 것과 간접 참조하는 것에 대한 장단점이 무엇이 있을까?
- 직접 참조
- 장점 : 연관된 데이터를
한번에
추출할 수 있다. - 단점 : 연관된 데이터에 대한 수정이 발생할 경우
영향의 범위
가 커질 수 있다.
- 장점 : 연관된 데이터를
- 간접 참조
- 장점 : 복잡도를 낮출 수 있고,
응집도
를 높이고결합도
를 낮출 수 있다. - 단점 : 연관된 데이터를 한번에 추출 하려면 구현해야 하는
로직
이 복잡하다.
- 장점 : 복잡도를 낮출 수 있고,
- 참조를 사용하는 객체 모델
class Member {
String id; // MEMBER_ID 컬럼 사용
Long temId; // TEAM_ID FK 컬럼 사용
Team team; // 참조로 연관관계를 맺는다.
}
class Team {
Long id; // TEAM_ID PK 사용
String name; // NAME 컬럼 사용
}
JDBC를 이용한 연관 관계 구현
만약 JDBC로 저장하는 로직을 만들기 위해서는 team 필드를 TEAM_ID 외래 키
값으로 변환해야 한다.
member.getId(); // MEMBER_ID PK에 저장
member.getTeam().getId(); // TEAM_ID FK에 저장
member.getUsername(); // USERNAME 컬럼에 저장
또는 조회하는 로직에도 객체를 생성하고 연관관계를 설정해서 반환하는 로직이 필요하다.
public Member find(String memberId) {
// SQL 실행 ...
Member member = new Member();
... // 데이터베이스에서 조회한 회원 관련 정보를 모두 입력
Team team = new Team();
... // 데이터베이스에서 조회한 팀 관련 정보를 모두 입력
// 회원과 팀 관계 설정
member.setTeam(team);
return member;
}
JPA와 연관관계 구현
개발자는 회원
과 팀
의 관계를 설정하고 회원 객체
를 저장하면 된다. JPA
는 team
의 참조를 외래 키로 변환해서 적절한 INSERT SQL을 데이터베이스에 전달한다.
member.setTeam(team);
jpa.persist(member);
객체를 조회할 때 외래 키를 참조로 변환하는 일도 JPA가 처리 해준다.
Member member = jpa.find(Member.class, memberId);
Team team = member.getTeam();
객체 그래프 탐색
SQL
을 직접 다루면 처음 실행하는 SQL
에 따라 객체 그래프를 어디까지 탐색할 수 있는지 정해진다. 이것은 객체지향 개발자에겐 너무 큰 제약이다. 왜냐하면 비즈니스 로직에 따라 사용하는 객체 그래프가 다른데 언제 끊어질지 모를 객체 그래프
를 함부로 탐색할 수는 없기 때문이다.
결국, 어디까지 객체 그래프 탐색이 가능한지 알아보려면 데이터 접근 계층인 DAO
를 열어서 SQL
을 직접 확인해야 한다.
class MemberService {
...
public void process() {
Member member = memberDAO.find(memberId);
member.getTeam();
// member->team 객체 그래프 탐색이 가능한가?
member.getOrder().getDelivery();
// ???
}
}
JPA와 객체 그래프 탐색
JPA를 사용하면 연관된 객체를 신뢰하고 마음껏 조회할 수 있다. 이 기능은 실제 객체를 사용하는 기점까지 데이터베이스 조회를 미룬다고 해서 지연 로딩
이라 한다.
// 처음 조회 시점에 SELECT MEMBER SQL
Member member = jpa.find(Member.class, memberId);
Order order = member.getOrder();
order.getOrderDate(); // Order를 사용하는 시점에 SELECT ORDER SQL
여기서 마지막 줄의 order.getOrderDate()
같이 실제 Order
객체를 사용하는 시점에 JPA는 데이터베이스에서 ORDER
테이블을 조회한다.
비교
데이터베이스는 기본 키의 값으로 각 로우(row)를 구분한다. 반면에 객체는 동일성(indentity)
비교와 동등성(equality)
비교라는 두 가지 비교 방법이 있다.
- 동일성 비교는 == 비교다. 객체 인스턴스의 주소 값을 비교한다.
- 동등성 비교는 equals() 메소드를 사용해서 객체 내부의 값을 비교한다.
JDBC로 구현한 비교
class MemberDAO {
public Member getMember(String memberId) {
String sql = "SELECT * FROM MEMBER WHERE MEMBER_ID + ?";
...
// JDBC API, SQL 실행
return new Member(...);
}
public void equals(){
String memberId = "100";
Member member1 = memberDAO.getMember(memberId);
Member member2 = memberDAO.getMember(memberId);
member1 == member2; // false
}
}
데이터베이스의 같은 로우를 조회했지만 객체의 동일성 비교에는 실패한다.
JPA로 구현한 비교
JPA는 같은 트랜잭션일 때 같은 객체가 조회되는 것을 보장한다. 그러므로 다음 코드에서 member1과 member2는 동일성 비교에 성공한다.
String memberId = "100";
Member member1 = jpa.find(Member.class,memberId);
Member member2 = jpa.find(Member.class,memberId);
member1 == member2; // true
정리
객체 모델
과 관계형 데이터베이스 모델
은 지향하는 패러다임
이 서로 다르다. 더 어려운 문제는 객체 지향 애플리케이션 답게 정교한 객체 모델링을 할수록 패러다임의 불일치 문제가 더 커진다는 점이다. 이는 결국 객체 모델링은 힘을 잃고 점점 데이터 중심의 모델
로 변해간다. JPA는 패러다임의 불일치 문제를 해결 해주고 정교한 객체 모델링 유지하게 도와준다.
데이터 주도 설계와 도메인 주도 설계의 장단점은 무엇일까?
JPA란 무엇인가?
JPA(Java Persistence API)
는 자바 진영의 ORM 기술 표준이다.
그렇다면 ORM이란 무엇일까?
ORM(Object-Relational Mapping)
은 이름 그대로 객체와 관계형 데이터베이스를 매핑
한다는 뜻이다. ORM 프레임워크는 객체와 테이블을 매핑 해서 패러다임의 불일치 문제를 개발자 대신 해결해준다.
따라서 객체 측면에서는 정교한 객체 모델링을 할 수 있고 관계형 데이터베이스는 데이터베이스에 맞도록 모델링하면 된다. 그리고 둘을 어떻게 매핑 해야 하는지 매핑 방법만 ORM 프레임워크에게 알려주면 된다.
왜 JPA를 사용해야 하는가?
-
생산성
이전에
DAO
에서 작업하던 지루하고 반복적인 일은JPA
가 대신 처리 해준다. 이런 기능들을 사용하면 데이터베이스 설계 중심의 패러다임을객체 설계 중심
으로 역전시킬 수 있다. -
유지보수
이전엔
엔티티
필드 하나만 수정해도 관련된DAO
로직의SQL
문을 모두 변경해야 했다. 반면에JPA
는 대신 처리해주므로 필드를 추가하거나 삭제해도 수정해야 할 코드가 줄어든다. -
패러다임의 불일치 해결
-
성능
JPA는 애플리케이션과 데이터베이스 사이에 동작하여
최적화
관점에서 시도해 볼 수 있는 것들이 많다. 예를 들어 동일한 조건으로 조회 했을 경우엔 SELECT SQL을 한 번만 데이터베이스에 전달하고 두 번째 조회한 회원 객체는재사용
할 수 있다. -
데이터 접근 추상화와 벤더 독립성
관계형 데이터베이스는 같은 기능도 벤더 마다 사용법이 다른 경우가 많다. 단적인 예로
페이징 처리
는 데이터베이스마다 달라서 사용법을 각각 배워야 한다. 결국 애플리케이션은 데이터베이스에종속
되어 변경 하기는 매우 어렵다.*JPA
는 추상화된 데이터 접근 계층을 제공해서 애플리케이션이 특정 데이터베이스 기술에 종속되지 않도록 할 수 있다.**
ORM에 대한 궁금증과 오해
-
JPA를 사용하면 성능이 느리지 않나요?
JPA
는 다양한 성능최적화
기능을 제공해서 잘 이해하고 사용하면SQL
을 직접 사용할 때보다 더 좋은 성능을 낼 수도 있다. 하지만 JPA를 잘 이해하지 못하고 사용하면N+1
같은 문제로 인해 심각한 성능 저하가 발생할 수 있다.JPA N+1 문제란 무엇인가? 해결 방법은 또한 무엇인가?
-
통계 쿼리처럼 매우 복잡한 SQL은 어떻게 하나요?
JPA는 통계 쿼리 같이 복잡한 쿼리보다는
실시간 처리용 쿼리
에 더 최적화되어 있다. 따라서 통계용은네이티브 SQL
을 사용하거나마이바티스
나 스프링의jdbcTemplate
같은 SQL 매퍼 형태의 프레임워크를 혼용하는 것도 좋은 방법이다.