객체 지향 프로그래밍의 도전 과제
객체 지향 프로그래밍의 영역에서는 추상화, 상속, 다형성이라는 3박자가 복잡한 시스템의 제어를 조율한다. 그러나 이러한 원칙을 데이터 저장으로 확장하면 수많은 과제가 발생한다. JDBC API 사용 시 불편한 점을 고려하여 그 복잡성을 자세히 살펴보자. 예를 들어 User
객체를 조회해보자.
// 저장
class UserJdbcTemplate {
public User findUserByUsername(String username) {
String sql = "SELECT * FROM users WHERE username = ?";
return jdbcTemplate.queryForObject(sql, new Object[]{username}, (resultSet, rowNum) ->
new User(
resultSet.getLong("id"),
resultSet.getString("username"),
resultSet.getString("email"),
)
);
}
}
여기서는 데이터베이스와 상호 작용하는 데 JDBC API를 사용하여 어느 정도 추상화 수준을 제공한다. 그러나 School
와 같은 새로운 요소를 도입하면 문제가 발생한다.
public User findUserByUsername(String username) {
String sql = "SELECT u.*, s.school_name FROM users u " +
"LEFT JOIN schools s ON u.school_id = s.id " +
"WHERE u.username = ?";
return jdbcTemplate.queryForObject(sql, new Object[]{username}, (resultSet, rowNum) ->
new UserWithSchool(
resultSet.getLong("id"),
resultSet.getString("username"),
resultSet.getString("email")
new School(
resultSet.getLong("school_id"),
resultSet.getString("school_name")
)
)
);
이를 위해서는 UserJdbcTemplate의 코드와 SQL쿼리가 어떻게 나가는지 확인해야 한다. 즉 비즈니스 요구사항을 모델링한 객체인 엔티티를 신뢰할 수 없게 된다. 또, 물리적으로는 추상화가 되어있지만 논리적으로는 강한 연관관계를 맺고 있기 때문에 요구사항이 바뀌면 SQL도 같이 바뀌어야 한다.
정리하면 다음과 같은 문제가 있다.
- 엔티티를 신뢰할 수 없다.
- SQL과 객체가 강한 의존관계를 갖고 있어 OCP원칙을 지키기 어렵다.
원인 분석
이러한 문제는 데이터 중심 데이터베이스와 객체 지향 프로그래밍의 원칙 사이의 불일치에서 비롯된다.
1. 상속
데이터베이스에는 상속 개념이 없어 객체 지향 모델과의 원활한 통합에 장애가 된다.
2. 연관관계
- 객체는 연관된 객체를 조회할 때 참조를 사용하지만 데이터베이스는 외래 키를 사용한다.
- 객체 참조의 단방향 특성은 외래 키의 양방향 특성과 충돌한다.
// 객체지향 기준
class User {
Long id;
String username;
School school;
}
class School {
Long id;
String schoolName;
}
// 데이터베이스 기준
class UserDB {
Long id;
String username;
Long schoolId; // Fk
}
class SchoolDB {
Long id;
String schoolName;
}
데이터베이스 쿼리를 이용해서는 schoolId를 통해 UserDB와 SchoolDB모두 조회가 가능하지만, 객체지향 개념에서는 UserDB만 School을 조회할 수 있다.
3. 모델링
모델링 할 때도 큰 차이가 있다. 데이터베이스는 기본적으로 컬럼 값을 기본으로 외래 키 관계를 맺는다.
- 데이터베이스 기준 모델링
class User { Long id; Long schoolId; }
- 객체 기준 모델링
class User { Long id; School school; }
이런 모델링 방식은 객체 그래프를 탐색할 때 큰 차이를 보인다. 예를 들어 User클래스에서 user.school을 통해 School
객체를 참조할 수 있다. SQL에서는 SQL 실행 시점에 참조할 객체들이 지정된다. 하지만 JPA에서는 지연로딩 이라는 개념을 통해서 원하는 시점에 select 조회를 함으로써 객체 조회가 되기 때문에 이러한 차이가 생긴다.
비교
데이터베이스에서는 같은 ID의 객체를 두 번 조회하면 이는 서로 다른 객체가 된다.
User user = getUser(1L);
User user2 = getUser(2L);
System.out.println(user == user2) // false
기본적으로 == 비교는 참조 주소를 기준으로 비교하기 때문이다. 데이터베이스적으로 같은 인스턴스를 반환하도록 하는 것은 쉽지 않다. JPA에서는 같은 트랜잭션일 때 같은 객체가 조회되는 것을 보장한다.
결론
관계형 데이터베이스의 사용은 데이터 중심이기 때문에 객체지향프로그래밍이 어려워진다. 그래서 다음과 같은 문제를 가진다.
- 추상화를 했음에도 불구하고 강한 의존관계가 생긴다.
- 비즈니스 로직을 모델링한 엔티티를 신뢰할 수 없다.
JPA에서는 지연로딩, 객체 그래프 탐색, 동일성 보장 등의 방법을 이용하여 이러한 문제점을 중간에서 해결해준다. 다음 포스팅에서는 JPA가 데이터베이스에 데이터를 저장할 때 겪는 문제점을 어떻게 해결하는지 자세히 알아보자.