
들어가며
백엔드 개발을 하다 보면 자연스럽게 두 가지 세계를 오가게 됩니다. 하나는 클래스와 인스턴스로 이루어진 객체지향의 세계, 다른 하나는 테이블과 row로 이루어진 관계형 데이터베이스의 세계입니다. 이 두 세계는 데이터를 표현하는 방식이 근본적으로 다르기 때문에, 둘을 연결하려는 순간부터 크고 작은 마찰이 생깁니다. 이 글에서는 그 마찰이 왜 생기는지, 그리고 오늘날 우리가 어떻게 그 간극을 메우고 있는지 정리해 보겠습니다.
객체와 테이블, 애초에 다른 존재
객체는 데이터(속성)와 그 데이터를 다루는 동작(메서드)을 하나로 묶은 단위입니다. 반면 관계형 데이터베이스의 테이블은 행과 열로 구성된 순수한 데이터 저장 구조이며, 동작이라는 개념이 없습니다.
public class Member {
private Long id;
private String name;
private Team team;
public void changeTeam(Team newTeam) {
this.team = newTeam;
}
}
CREATE TABLE MEMBER (
ID BIGINT PRIMARY KEY,
NAME VARCHAR(255),
TEAM_ID BIGINT REFERENCES TEAM(ID)
);
겉보기에는 비슷해 보이지만, Member 객체는 changeTeam()이라는 행위를 스스로 가지고 있는 반면 MEMBER 테이블은 그저 값을 저장할 뿐입니다. 이 차이에서부터 여러 구조적 불일치가 시작됩니다.
임피던스 불일치가 발생하는 지점들
1. 상속
객체는 상속 구조를 자연스럽게 표현합니다.
abstract class Item {
Long id;
String name;
int price;
}
class Book extends Item {
String author;
String isbn;
}
class Album extends Item {
String artist;
}
하지만 테이블에는 상속이라는 개념이 없습니다. 이를 표현하려면 개발자가 직접 전략을 선택해야 합니다.
-- 단일 테이블 전략
CREATE TABLE ITEM (
ID BIGINT PRIMARY KEY,
DTYPE VARCHAR(31),
NAME VARCHAR(255),
PRICE INT,
AUTHOR VARCHAR(255),
ARTIST VARCHAR(255)
);
2. 연관관계의 방향
객체는 참조를 통해 단방향으로 다른 객체를 탐색합니다.
Team team = member.getTeam(); // Member → Team, 단방향
반대로 테이블은 외래키 하나로 양방향 조회가 모두 가능합니다. 방향이라는 개념 자체가 테이블에는 존재하지 않습니다.
SELECT * FROM MEMBER WHERE TEAM_ID = 1; -- Team → Member 조회도 가능
이 차이 때문에 객체를 테이블에 맞추다 보면 실제로는 필요 없는 방향의 참조까지 양방향으로 만들게 되는 경우가 많습니다.
3. 그래프 탐색의 단절
객체는 참조를 계속 따라가며 자유롭게 탐색할 수 있습니다.
member.getTeam().getCompany().getAddress();
하지만 데이터베이스에서 가져온 객체는 처음 실행한 SQL의 범위 안에서만 탐색이 가능합니다. `SELECT * FROM MEMBER`로 조회한 객체는 `team`이 채워져 있다는 보장이 없습니다.
4. 동일성과 동등성
같은 row를 두 번 조회해도 매번 다른 객체 인스턴스가 반환될 수 있습니다.
Member m1 = memberRepository.findById(1L);
Member m2 = memberRepository.findById(1L);
m1 == m2 // 순수 JDBC라면 false일 수 있음
객체지향에서는 동일한 식별자를 가진 두 객체가 `==` 비교에서 다르게 취급될 수 있다는 점이 직관과 어긋납니다.
ORM으로 간극을 메우기
JPA나 Hibernate 같은 ORM(Object-Relational Mapping)* 은 위에서 살펴본 불일치를 상당 부분 자동으로 처리해 줍니다.
@Entity
public class Member {
@Id @GeneratedValue
private Long id;
private String name;
@ManyToOne
@JoinColumn(name = "TEAM_ID")
private Team team;
}
Member member = new Member();
member.setName("Aaron");
member.setTeam(team);
entityManager.persist(member); // INSERT 쿼리를 직접 작성하지 않음
개발자는 SQL을 직접 작성하는 대신 객체를 중심으로 코드를 작성하고, ORM이 내부적으로 이를 SQL로 변환해 실행합니다. 동일성 문제도 영속성 컨텍스트**가 1차 캐시를 통해 같은 트랜잭션 안에서는 동일한 객체를 반환하도록 보장해 줍니다.
Member m1 = em.find(Member.class, 1L);
Member m2 = em.find(Member.class, 1L);
m1 == m2 // true, 같은 영속성 컨텍스트 안에서는 동일 객체 보장
마치며
임피던스 불일치는 없앨 수 없는 근본적인 구조 차이입니다. ORM은 이 불일치를 완전히 없애는 것이 아니라, 반복적이고 기계적인 매핑 작업을 대신 처리해 줌으로써 개발자가 객체지향 설계에 더 집중할 수 있게 도와주는 도구입니다. 다만 지연 로딩, N+1 문제처럼 ORM이 만들어내는 또 다른 종류의 트레이드오프도 함께 이해하고 있어야 실무에서 제대로 활용할 수 있습니다.
- *ORM(Object-Relational Mapping): 객체와 관계형 데이터베이스의 데이터를 자동으로 매핑해 주는 기술 또는 프레임워크
- **영속성 컨텍스트(Persistence Context): JPA가 엔티티를 영구 저장하는 환경으로, 엔티티를 관리하며 1차 캐시, 동일성 보장, 쓰기 지연 등의 기능을 제공하는 논리적 개념