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);

따라서 개발자가 객체지향 애플리케이션데이터베이스 중간에서 SQLJDBC API를 사용해서 변환 작업을 직접 해주어야 한다.

문제는 객체를 데이터베이스에 CRUD하려면 너무 많은 SQLJDBC 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(); // 연관된 객체 조회

패러다임의 불일치

비즈니스 요구사항을 정의한 도메인 모델도 객체로 모델링하면 객체지향 언어가 가진 장점들을 활용할 수 있다. 문제는 이렇게 정의한 도메인 모델을 저장할 때 발생한다.

  • 객체의 기능은 클래스에 정의되어 있으므로 객체 인스턴스상태인 속성만 저장 했다가 필요할 때 불러 와서 복구하면 된다.
  • 관계형 데이터베이스는 데이터 중심으로 구조화되어 있고, 집합적인 사고를 요구한다. 그리고 객체 지향에서 이야기하는 추상화, 상속, 다형성 같은 개념이 없다.

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

문제는 이런 객체와 관계형 데이터베이스 사이의 패러다임 불일치 문제를 해결하는 데 너무 많은 시간코드소비하는 데 있다.

상속

객체는 상속 이라는 기능을 가지고 있지만 테이블은 상속 이라는 기능이 없다.

https://user-images.githubusercontent.com/2491418/78592088-79ed5c80-787f-11ea-8174-8afcfcc8ff7a.png

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와 연관관계 구현

개발자는 회원의 관계를 설정하고 회원 객체를 저장하면 된다. JPAteam의 참조를 외래 키로 변환해서 적절한 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 매퍼 형태의 프레임워크를 혼용하는 것도 좋은 방법이다.