ORM 구현 중 최대 난관은 SQL 결과가 JSON이고 플랫하다는 것입니다. 하지만 매핑은 객체에 해야 합니다. 객체는 중첩이 될 수도 있고 복잡한 구조를 가지고 있습니다. 따라서 컬럼 별칭 전략에 따라 적절하게 별칭을 만들어내야 하는 것이 최대의 난관입니다. 이렇게 해야 나중에 역직렬화할 때 규칙에 따라 올바른 객체를 조합하여 만들 수 있습니다.
ORM에서 채택한 방식인 TypeORM의 언더스코어 경로 방식의 경우, 구현해보니 직관적이진 않아 구현 단계에서 매우 많은 버그가 발생하고 있습니다.
이렇게 가면 유지 보수가 힘들어질 것 같단 생각이 들어서 다른 ORM의 전략은 어떠한가 살펴보기로 하였습니다.
ORM들이 생성하는 SQL RAW 쿼리를 분석해보면, 각각 다른 컬럼 별칭 전략을 사용합니다.
- 컬럼 별칭 전략:
- 점 표기법 (Sequelize): Posts.Comments.id
- 언더스코어 경로 (TypeORM): user_posts_comments_id
- 숫자 식별자 (Hibernate): id1_0_0_
- 테이블 별칭과 컬럼명 결합 (SQLAlchemy): comments_1_id
- 로딩 전략:
- 단일 대형 조인 쿼리 (Hibernate, SQLAlchemy, Entity Framework)
- 여러 개별 쿼리 (Django, Prisma)
Entity Framework
Entity Framework 같은 경우,
SELECT
[u].[Id], [u].[Name], [u].[Email],
[p].[Id], [p].[Title], [p].[Content], [p].[UserId],
[c].[Id], [c].[Content], [c].[CreatedAt], [c].[PostId]
FROM
[Users] AS [u]
LEFT JOIN
[Posts] AS [p] ON [u].[Id] = [p].[UserId]
LEFT JOIN
[Comments] AS [c] ON [p].[Id] = [c].[PostId]
ORDER BY
[u].[Id], [p].[Id], [c].[Id]
각 쿼리의 결과 셋의 어떤 인덱스가 어떤 엔티티의 어떤 속성에 매핑되는지, 이미 알고 있기 때문에, 첫 번째 Id는 User.Id, 네 번째 Id는 Post.Id, 여덟 번째 Id는 Comment.Id로 매핑이 가능합니다. 하지만 자바스크립트 (노드)의 경우, 결과 값을 JSON으로 반환하기 때문에 이러한 규칙을 사용하기에는 제한이 있습니다.
하이버네이트
하이버네이트 같은 경우에는 다음과 같습니다.
SELECT
u.id AS id1_0_0_,
p.id AS id1_1_1_,
c.id AS id1_2_2_,
u.name AS name2_0_0_,
u.email AS email3_0_0_,
p.title AS title2_1_1_,
p.content AS content3_1_1_,
p.user_id AS user_id4_1_1_,
c.content AS content2_2_2_,
c.created_at AS created_3_2_2_,
c.post_id AS post_id4_2_2_
FROM users u
LEFT OUTER JOIN posts p ON u.id = p.user_id
LEFT OUTER JOIN comments c ON p.id = c.post_id
이 별칭 패턴은 [속성명][속성순서][테이블인덱스][엔티티인덱스]_의 형태로 구성됩니다:
- 속성명: 원래 컬럼명(id, name 등)
- 속성순서: 엔티티 내에서의 속성 순서(1, 2, 3...)
- 테이블인덱스: 쿼리에서 테이블의 인덱스(0, 1, 2...)
- 엔티티인덱스: 조인된 결과 내에서 엔티티의 위치(0, 1, 2...)
예를 들어 id1_0_0_는 다음을 의미합니다:
- id: 속성명은 "id"
- 1: 엔티티의 첫 번째 속성
- 0: 쿼리의 첫 번째 테이블(users)
- 0: 결과 집합의 첫 번째 엔티티 위치
모든 메타데이터를 알고 있는 Entity Framework와 달리, 하이버네이트는 별칭에 이러한 식별자를 넣습니다.
다만 구현이 복잡할 것 같단 생각이 듭니다.
시퀄라이즈
가장 직관적이고 쉬워보이는 것은 시퀄라이즈 방식의 표기법입니다.
SELECT
`User`.`id`,
`User`.`name`,
`User`.`email`,
`Posts`.`id` AS `Posts.id`,
`Posts`.`title` AS `Posts.title`,
`Posts`.`content` AS `Posts.content`,
`Posts->Comments`.`id` AS `Posts.Comments.id`,
`Posts->Comments`.`content` AS `Posts.Comments.content`,
`Posts->Comments`.`createdAt` AS `Posts.Comments.createdAt`
FROM
`users` AS `User`
LEFT OUTER JOIN
`posts` AS `Posts` ON `User`.`id` = `Posts`.`userId`
LEFT OUTER JOIN
`comments` AS `Posts->Comments` ON `Posts`.`id` = `Posts->Comments`.`postId`
이 경우, comments의 별칭을 Posts->Comments로 지정하고 있는데 매우 지능적으로 보여지고 직관적입니다.
ORM 구현 중 최대 난관은 SQL 결과가 JSON이고 플랫하다는 것입니다. 하지만 매핑은 객체에 해야 합니다. 객체는 중첩이 될 수도 있고 복잡한 구조를 가지고 있습니다. 따라서 컬럼 별칭 전략에 따라 적절하게 별칭을 만들어내야 하는 것이 최대의 난관입니다. 이렇게 해야 나중에 역직렬화할 때 규칙에 따라 올바른 객체를 조합하여 만들 수 있습니다.
ORM에서 채택한 방식인 TypeORM의 언더스코어 경로 방식의 경우, 구현해보니 직관적이진 않아 구현 단계에서 매우 많은 버그가 발생하고 있습니다.
이렇게 가면 유지 보수가 힘들어질 것 같단 생각이 들어서 다른 ORM의 전략은 어떠한가 살펴보기로 하였습니다.
ORM들이 생성하는 SQL RAW 쿼리를 분석해보면, 각각 다른 컬럼 별칭 전략을 사용합니다.
Entity Framework
Entity Framework 같은 경우,
각 쿼리의 결과 셋의 어떤 인덱스가 어떤 엔티티의 어떤 속성에 매핑되는지, 이미 알고 있기 때문에, 첫 번째 Id는 User.Id, 네 번째 Id는 Post.Id, 여덟 번째 Id는 Comment.Id로 매핑이 가능합니다. 하지만 자바스크립트 (노드)의 경우, 결과 값을 JSON으로 반환하기 때문에 이러한 규칙을 사용하기에는 제한이 있습니다.
하이버네이트
하이버네이트 같은 경우에는 다음과 같습니다.
이 별칭 패턴은 [속성명][속성순서][테이블인덱스][엔티티인덱스]_의 형태로 구성됩니다:
예를 들어 id1_0_0_는 다음을 의미합니다:
모든 메타데이터를 알고 있는 Entity Framework와 달리, 하이버네이트는 별칭에 이러한 식별자를 넣습니다.
다만 구현이 복잡할 것 같단 생각이 듭니다.
시퀄라이즈
가장 직관적이고 쉬워보이는 것은 시퀄라이즈 방식의 표기법입니다.
이 경우, comments의 별칭을 Posts->Comments로 지정하고 있는데 매우 지능적으로 보여지고 직관적입니다.