1. SQL과 JDBC를 직접 사용하던 시절의 고통
- 요약
- 초기 SQL과 JDBC API 작성하는데 오래걸림
- 요구사항이 추가되면 Entity와 Dao의 SQL을 동시에 수정해야하는 일이 비일비재함.
- 즉, 비지니스 로직의 엔티티와 데이터 접근 계층간에 아주 강한 의존관계가 존재함.
처음에 프로젝트를 시작할 땐, 모든 쿼리를 손으로 작성하고 JDBC API를 일일이 호출해야 했다.
회원 하나를 조회하려고 해도, Connection
객체 열고, PreparedStatement
에 파라미터 바인딩하고,
ResultSet
에서 칼럼 값을 꺼내와서 다시 자바 객체로 매핑해줘야 했다.
그뿐만 아니라, 여러 테이블을 조인해야 하는 상황이면 SQL문이 엄청나게 길어지고 복잡해졌다.
가장 괴로운 점은, 나중에 요구사항이 바뀌어서 Member
에 새로운 필드를 추가하거나,
혹은 Team
엔티티와의 연관 구조가 바뀐다면, DAO에 있는 SQL과 매핑 로직을 모두 수정해야 했었다.
(옛날 DAO 예시)
public class MemberDAO {
public Member find(String memberId) {
String sql = "SELECT MEMBER_ID, USERNAME, TEAM_ID FROM MEMBER WHERE MEMBER_ID = ?";
try(Connection con = DriverManager.getConnection("jdbc:mysql://...", "root", "root");
PreparedStatement pstmt = con.prepareStatement(sql)) {
pstmt.setString(1, memberId);
try(ResultSet rs = pstmt.executeQuery()) {
if (rs.next()) {
Member member = new Member();
member.setMemberId(rs.getString("MEMBER_ID"));
member.setUsername(rs.getString("USERNAME"));
member.setTeamId(rs.getLong("TEAM_ID"));
return member;
}
}
} catch (Exception e) {
e.printStackTrace();
}
return null;
}
public Delivery getDelivery(Long orderId) {
// 비슷한 방식으로 SQL 호출 -> Delivery 객체 매핑
// ...
return null;
}
}
2. MyBatis, Spring JdbcTemplate로 조금 나아졌지만...
- 요약
- 개발이 진행될수록 객체지향적인 특성(상속, 연관관계, 다형성 등)을 적극 활용하게 됨.
- 예:
Member
와Team
이 일대다 관계로 연결,Team
과Order
, 또Order
와Delivery
가 연쇄적으로 연결. - 옛날 방식에서는 DAO를 계속 호출하면서 ID를 넘겨주는 식으로, 로직이 복잡해지고 비효율적임.
- 한 번에 조인 SQL(
findUntilDelivery
)을 만들어보려고 해도, - 수많은 메서드와 조인 범위 때문에 유지보수가 어려워짐.
MyBatis나 Spring JdbcTemplate를 쓰면서 JDBC 반복 코드를 줄일 수 있었다.
SQL 매퍼를 통해 SQL과 객체 매핑을 어느 정도 자동화했기 때문이다.
하지만 여전히 SQL이 필요하고, 엔티티 변경 시 SQL 수정도 꼭 따라다녔다.
비지니스 로직의 엔티티와 데이터 접근 계층간에 아주 강한 의존관계는 해결하지 못한 것이다.
3. 복잡해지는 객체 모델링, 그리고 JPA의 등장
- 요약
- 개발이 진행될수록 객체지향적인 특성(상속, 연관관계, 다형성 등)을 적극 활용하게 됨.
- 예:
Member
와Team
이 일대다 관계로 연결,Team
과Order
, 또Order
와Delivery
가 연쇄적으로 연결. - 옛날 방식에서는 DAO를 계속 호출하면서 ID를 넘겨주는 식으로, 로직이 복잡해지고 비효율적임.
- 한 번에 조인 SQL(
findUntilDelivery
)을 만들어보려고 해도, 수많은 메서드와 조인 범위 때문에 메서드 폭발.
프로젝트가 커지다 보면, 객체지향적으로 상속, 연관관계, 다형성 등을 마음껏 활용하고 싶어진다.
객체지향은 추상화, 캡슐화, 정보은닉, 상속, 다형성등으로 시스템의 복잡성을 제어한다.
하지만 RDB에는 그런 개념이 없다. 객체와 RDB는 서로 지향하는 목적이 다르기 때문이다.
그 차이를 패러다임의 불일치라고 한다. 패러다임의 불일치 문제는 SQL매퍼가 해결하지 못하는 영역이었다.
불일치 문제가 발생할 때마다 개발자들이 수동으로 그 문제를 해결했다.
객체지향의 객체 그래프 탐색 특성은 SQL과 매우 큰 불일치를 야기했다.
다음의 예시를 보자.Member
가 Team
에 속해 있고, 그 Team
이 또 다른 엔티티와 엮여서, 눈덩이처럼 불어나 있다.
이를 과거에는 다음처럼 줄줄이 호출하여 해결했다.
MemberDAO memberDAO = new MemberDAO();
TeamDAO teamDAO = new TeamDAO();
OrderDAO orderDAO = new OrderDAO();
DeliveryDAO deliveryDAO = new DeliveryDAO();
Member member = memberDAO.find(memberId);
Team team = teamDAO.find(member.getTeamId());
Order order = orderDAO.find(team.getOrderId());
Delivery delivery = deliveryDAO.find(order.getDeliveryId());
이런 식으로, 하나의 Member에 대해 무려 4번의 SELECT를 날린다.
그만큼 DB 서버와 네트워크 요청을 4번이나 왕복하고, 논리적 가치가 없는 매핑 코드는 점점 길어진다.
그러면 한번에 조인 SQL을 만들어서 한번의 DAO에 갔다온다면..?findUntilDelivery
같은 메서드를 새로 만들어서 내부적으로 조인을 때려보자.
하지만 비즈니스 요구사항에 따라 객체의 수는 무수히 늘어나고,
이 객체들이 참조를 통해 복잡한 객체 그래프를 형성하게 될 것이다. 그러다가는 메서드 폭발 문제가 발생할 것이다.
조인 범위가 다를 때마다 새로운 메서드를 만들고,
복잡한 조인 SQL까지 새로 짜고 매핑 규칙까지 설정해야 하니, 얼마나 힘들지 상상하기 힘들다.
그리고, 그렇게 메서드를 만들었다가는 DB에 컬럼 하나 추가하면
(모든 조인 SQL + 모든 매핑 규칙 수정 + 모든 메서드의 매핑 로직 수정) 과 같은 종합 선물 세트가 올 것이다.
코드를 뜯어본 개발자들은 비명을 질렀을 것이다.
더 큰 문제는, DAO의 메서드가 늘어나면서 memberDAO.find????()
와 같은 메서드가 실제로 Team
객체 정보를
제대로 가져오는지, 아니면 Order
나 Delivery
에 해당하는 SQL을 포함하는지 파악하기 어려워진다는 점이다.
메서드를 사용할 때마다 DAO를 까보지 않으면 안심할 수 없게 될 것이다.
단순히 findMemberTeamOrderDelivery
와 같은 이름으로 해결할 문제가 아닌 것이다.
이 문제가 발생한 원인은 RDB에서 객체지향적인 데이터를 제공해줄 수 없었기 때문이다.
원인은 다시 말해 객체지향과 RDB사이의 패러다임의 불일치이다.
객체지향에서 복잡성을 제어하는 여러 방법들이 RDB에서는 존재하지 않기 때문에 생기는 문제이고,
RDB는 객체지향과 서로 지향하는 목적이 다르기 때문에 생기는 문제이다.
그리고, 또 이런 문제 말고도 아직 언급하지 못한 다른 문제 또한 존재한다.(상속 관련 문제)
이런 복잡한 상황을 해결할 수 있는 방법으로 나온 것이 바로 ORM 프레임워크이다.
4. ORM이 해주는 일: 객체와 RDB 사이의 패러다임 불일치 해결
- 요약
- 순수 JDBC → SQL 매퍼로 발전했지만, 이 방식이 Entity와 DAO 사이의 의존관계 문제를 완전히 해결하지는 않음.
- ORM은 객체와 RDB가 지닌 서로 다른 패러다임의 불일치(상속, 연관관계, 다형성 vs 테이블 구조)를 중간에서 해결
- 대표적인 ORM 구현체로 Hibernate, 그리고 자바 표준 API인 JPA가 있음.
- ORM이 주는 핵심 장점:
- 방대한 CRUD SQL을 직접 작성할 필요 감소
- 자동화된 객체 매핑
- 데이터베이스 교체 시 큰 수정 없이 대응 가능
- 트랜잭션 범위 내 동일성(동일 인스턴스) 보장
순수 JDBC → XML 기반 SQL 매퍼로 발전을 했다. 반복적인 JDBC 코드는 줄어들고, 객체 매핑도 좀 더 간편해졌다.
하지만, 엔티티가 바뀌면 SQL도 바뀌어야 하고, 엔티티끼리 관계가 얽힐 때마다 DAO가 복잡해지는 문제는 여전했다.
이걸 획기적으로 개선해주는 게 바로 ORM(Object Relational Mapping)이었다.
ORM은 매핑 설정 뿐만 아니라 다양한 설정으로 상속, 연관관계, 다형성을 통해 객체지향적으로 DBMS와 소통하게 한다.
예를 들어 JPA를 쓰면 아주 단순한 코드만으로도 DB와 소통할 수 있다.
Member member = em.find(Member.class, memberId); member.setUsername("NewName");
SQL을 쓰지 않고 객체에 setUsername()
만 해도, 트랜잭션 커밋 시점에 알아서 DB가 업데이트된다.
DBMS가 MySQL에서 Oracle로 바뀌더라도, JPA 설정만 약간 손보면 애플리케이션 코드를 거의 수정하지 않아도 된다.
무엇보다 지연 로딩(Lazy Loading) 덕분에 객체를 탐색할 때 필요한 시점에 쿼리가 나가고, 같은 트랜잭션 내에선 동일 엔티티는 동일 인스턴스로 관리되니, 개발자 입장에서는 “진짜 객체”와 대화하듯 코드를 짤 수 있다.
결국 JPA가 제공하는 이런 기능들은, “데이터 중심”이 아닌 “객체 중심”의 설계를 지향하게 만들고, 개발 효율과 유지보수성을 높인다.
5. 불일치 1 : 상속
객체지향과 RDBMS의 상속과 관련된 불일치를 보자. RDB는 상속개념이 없다. 그나마 가장 유사한 형태로 구현을 하면 슈퍼타입 서브타입 관계를 사용하는 것이다. 이 불일치 문제도 ORM이 없었다면 다양한 쿼리를 짜야 했을 것이다.
SQL
-- 부모 테이블 (ITEM)
-----------------------------------
| ITEM_ID | NAME | PRICE | DTYPE |
-----------------------------------
| 1 | 앨범A | 10000 | ALBUM |
| 2 | 영화B | 15000 | MOVIE |
| 3 | 책C | 20000 | BOOK |
-----------------------------------
-- 자식 테이블들
-- ALBUM 테이블
---------------------------
| ITEM_ID | ARTIST |
---------------------------
| 1 | 방탄 |
---------------------------
-- MOVIE 테이블
----------------------------------
| ITEM_ID | DIRECTOR | ACTOR |
----------------------------------
| 2 | 김감독 | 주인공 |
----------------------------------
-- BOOK 테이블
----------------------------
| ITEM_ID | AUTHOR | ISBN |
----------------------------
| 3 | 작가님 | 1234 |
----------------------------
ITEM
테이블에는 공통 속성(ID
, NAME
, PRICE
등)을 저장하고,
각 서브 테이블(ALBUM
, MOVIE
, BOOK
)에는 해당 타입에만 필요한 칼럼을 둔다.
JPA
@Entity
@Inheritance(strategy = InheritanceType.JOINED)
@DiscriminatorColumn(name = "DTYPE")
public class Item {
@Id @GeneratedValue
private Long id;
private String name;
private int price;
// ... 공통 속성
}
@Entity
@DiscriminatorValue("ALBUM")
public class Album extends Item {
private String artist;
// ...
}
@Entity
@DiscriminatorValue("MOVIE")
public class Movie extends Item {
private String director;
private String actor;
// ...
}
@Entity
@DiscriminatorValue("BOOK")
public class Book extends Item {
private String author;
private String isbn;
// ...
}
이렇게 하면, Album
엔티티를 persist()
할 때 ITEM
과 ALBUM
두 테이블에 INSERT가 나간다.
조회 시에도 두 테이블을 조인해서 데이터를 가져온다.
6. 불일치 2 : 연관관계 매핑
객체지향 세계에선 Member
가 Team
객체를 직접 참조한다.
SQL
public class Member {
private Long id;
private String username;
private Team team; // Team 객체 자체를 가지고 있음
// ...
}
객체지향에서는 member가 Team을 가지고 있지만, SQL에서는 team_id만 가지고 있다.
객체지향 세계와 다르게 테이블 세계에선, MEMBER
테이블이 TEAM_ID
라는 외래키를 가지고 있고, 실제로는 조인을 통해 TEAM
테이블과 연결된다.
JPA
@Entity
public class Member {
@Id @GeneratedValue
private Long id;
private String username;
@ManyToOne(fetch = FetchType.LAZY)
@JoinColumn(name = "TEAM_ID")
private Team team;
// ...
}
@Entity
public class Team {
@Id @GeneratedValue
private Long id;
private String name;
@OneToMany(mappedBy = "team")
private List<Member> members = new ArrayList<>();
// ...
}
JPA는 Member
가 team
필드를 갖고 있다고 선언만 해주면 알아서 매핑을 해준다. (DB 쿼리를 자동으로 생성)
7. 불일치 3 : 객체 그래프 탐색
JPA를 쓰면, 가령 Member
-> Order
-> Delivery
로 쭉 연결되어 있다고 할 때,
Member member = em.find(Member.class, memberId);
Order order = member.getOrder();
Delivery delivery = order.getDelivery();
이렇게 코드 한 줄씩 이동하는 것만으로 DB 조회가 자연스럽게 이뤄진다(필요한 시점에 쿼리).
옛날엔 DAO를 쉼 없이 호출하거나, 혹은 복잡한 조인 SQL을 미리 짜야 했는데,
이젠 “진짜 객체 그래프를 순회”하는 느낌이 드는 것이다.
8. 불일치 4 : 비교 - 동일성(Identity) vs 동등성(Equality)
- RDB는 row(행)를 식별자로 구분(기본키)하지만, 객체는
==
(동일성)과equals
(동등성) 개념이 있다. - JPA는 같은 트랜잭션 안에선 같은 키로 조회된 엔티티는 동일 인스턴스로 관리해준다.
Member m1 = em.find(Member.class, 100L);
Member m2 = em.find(Member.class, 100L);
System.out.println(m1 == m2); // true (동일 인스턴스)
그런데 JDBC로 직접 코드를 짜면, 매번 new Member()
가 생기므로 ==
비교는 실패한다.
9. 참고 자료
- 김영한, 자바 ORM 표준 JPA 프로그래밍
'백엔드' 카테고리의 다른 글
[Spring JPA] 2. JPA 시작 (김영한 JPA 교재) (0) | 2025.03.07 |
---|---|
JPA Entity(JPA 공식문서 정리 - 1) (1) | 2025.03.03 |
MySQL 쿼리 최적화: 테이블 스캔, 인덱스 스캔, 옵티마이저, 인덱스 힌트, EXPLAIN ANALYZE (0) | 2025.02.25 |
Included Columns(SQL-server) (0) | 2025.02.21 |
인덱스 조각화, 실행계획, 인덱스 scripts (0) | 2025.02.21 |