저는 원래 이 기사를 러시아어로 썼습니다. 그러니 원어민이시라면 이 링크를 통해 읽어보실 수 있습니다.
지난 1년여 동안 JOOQ가 Hibernate에 대한 현대적이고 우수한 대안임을 시사하는 기사와 강연을 접했습니다. 인수에는 일반적으로 다음이 포함됩니다.
- JOOQ를 사용하면 컴파일 타임에 모든 것을 확인할 수 있지만 Hibernate는 그렇지 않습니다!
- Hibernate는 항상 최적이 아닌 이상한 쿼리를 생성하지만 JOOQ를 사용하면 모든 것이 투명해집니다!
- Hibernate 엔터티는 변경 가능하며 이는 좋지 않습니다. JOOQ에서는 모든 엔터티를 불변으로 만들 수 있습니다(안녕하세요, 함수형 프로그래밍)!
- JOOQ는 주석에 어떤 "마법"도 사용하지 않습니다!
저는 JOOQ를 훌륭한 라이브러리(특히 Hibernate와 같은 프레임워크가 아닌 라이브러리)라고 생각한다는 점을 미리 말씀드립니다. 이는 작업에 탁월합니다. 정적으로 입력된 방식으로 SQL을 사용하여 컴파일 타임에 대부분의 오류를 포착합니다.
그러나 Hibernate의 시대는 지났고 이제 JOOQ를 사용하여 모든 것을 작성해야 한다는 주장을 들으면 관계형 데이터베이스의 시대는 끝났고 이제는 NoSQL만 사용해야 한다는 것처럼 들립니다. 재미있을 것 같나요? 그런데 얼마 전까지만 해도 이런 논의가 꽤 심각했습니다.
문제는 이 두 도구가 해결하는 핵심 문제에 대한 오해에 있다고 생각합니다. 이 글에서는 이러한 질문을 명확히 하고자 합니다. 우리는 다음을 탐구할 것입니다:
- 트랜잭션 스크립트란 무엇인가요?
- 도메인 모델 패턴이란 무엇인가요?
- Hibernate와 JOOQ는 구체적으로 어떤 문제를 해결하나요?
- 왜 하나는 다른 하나를 대체하지 못하고, 어떻게 공존할 수 있나요?
거래 스크립트
데이터베이스를 사용하는 가장 간단하고 직관적인 방법은 트랜잭션 스크립트 패턴입니다. 간단히 말해서 모든 비즈니스 로직을 단일 트랜잭션으로 결합된 SQL 명령 집합으로 구성합니다. 일반적으로 클래스의 각 메소드는 비즈니스 작업을 나타내며 하나의 트랜잭션으로 제한됩니다.
발표자가 자신의 강연을 컨퍼런스에 제출할 수 있는 애플리케이션을 개발한다고 가정해 보겠습니다(단순화를 위해 강연 제목만 녹음하겠습니다). 트랜잭션 스크립트 패턴에 따라 강연을 제출하는 방법은 다음과 같습니다(SQL용 JDBI 사용).
@Service @RequiredArgsConstructor public class TalkService { private final Jdbi jdbi; public TalkSubmittedResult submitTalk(Long speakerId, String title) { var talkId = jdbi.inTransaction(handle -> { // Count the number of accepted talks by the speaker var acceptedTalksCount = handle.select("SELECT count(*) FROM talk WHERE speaker_id = :id AND status = 'ACCEPTED'") .bind("id", speakerId) .mapTo(Long.class) .one(); // Check if the speaker is experienced var experienced = acceptedTalksCount >= 10; // Determine the maximum allowable number of submitted talks var maxSubmittedTalksCount = experienced ? 5 : 3; var submittedTalksCount = handle.select("SELECT count(*) FROM talk WHERE speaker_id = :id AND status = 'SUBMITTED'") .bind("id", speakerId) .mapTo(Long.class) .one(); // If the maximum number of submitted talks is exceeded, throw an exception if (submittedTalksCount >= maxSubmittedTalksCount) { throw new CannotSubmitTalkException("Submitted talks count is maximum: " + maxSubmittedTalksCount); } return handle.createUpdate( "INSERT INTO talk (speaker_id, status, title) " + "VALUES (:id, 'SUBMITTED', :title)" ).bind("id", speakerId) .bind("title", title) .executeAndReturnGeneratedKeys("id") .mapTo(Long.class) .one(); }); return new TalkSubmittedResult(talkId); } }
이 코드에서는:
- 발표자가 이미 제출한 강연 수를 계산합니다.
- 최대 제출 가능 개수를 초과했는지 확인합니다.
- 모든 것이 정상이면 SUBMITTED 상태로 새 토크를 만듭니다.
여기에는 잠재적인 경쟁 조건이 있지만 단순화를 위해 이에 대해서는 다루지 않겠습니다.
이 접근 방식의 장점:
- 실행되는 SQL은 간단하고 예측 가능합니다. 필요한 경우 성능 향상을 위해 쉽게 조정할 수 있습니다.
- 데이터베이스에서 필요한 데이터만 가져옵니다.
- JOOQ를 사용하면 이 코드를 정적 타이핑으로 더욱 간단하고 간결하게 작성할 수 있습니다!
단점:
- 단위 테스트만으로는 비즈니스 로직을 테스트하는 것이 불가능합니다. 통합 테스트(그리고 그 중 상당수)가 필요합니다.
- 도메인이 복잡한 경우 이 접근 방식은 스파게티 코드로 빠르게 이어질 수 있습니다.
- 코드 중복의 위험이 있으며, 이는 시스템이 발전함에 따라 예상치 못한 버그로 이어질 수 있습니다.
이 접근 방식은 서비스에 시간이 지나도 더 복잡해질 것으로 예상되지 않는 매우 간단한 로직이 있는 경우 유효하고 합리적입니다. 그러나 도메인은 더 큰 경우가 많습니다. 그러므로 대안이 필요합니다.
도메인 모델
도메인 모델 패턴의 아이디어는 더 이상 비즈니스 로직을 SQL 명령에 직접 연결하지 않는다는 것입니다. 대신 동작을 설명하고 도메인 엔터티에 대한 데이터를 저장하는 도메인 객체(Java, 클래스의 맥락에서)를 생성합니다.
이 기사에서는 빈혈 모델과 부유한 모델의 차이점에 대해 논의하지 않습니다. 관심이 있으시면 해당 주제에 대해 자세히 글을 작성해 두었습니다.
비즈니스 시나리오(서비스)는 이러한 개체만 사용해야 하며 특정 데이터베이스 쿼리에 얽매이지 않아야 합니다.
물론 실제로는 성능 요구 사항을 충족하기 위해 도메인 개체와 직접적인 데이터베이스 쿼리와의 상호 작용이 혼합되어 있을 수 있습니다. 여기서는 캡슐화와 격리를 위반하지 않는 도메인 모델 구현에 대한 고전적인 접근 방식에 대해 논의하고 있습니다.
예를 들어 앞에서 언급한 것처럼 Speaker 및 Talk 엔터티에 대해 이야기하는 경우 도메인 개체는 다음과 같습니다.
@Service @RequiredArgsConstructor public class TalkService { private final Jdbi jdbi; public TalkSubmittedResult submitTalk(Long speakerId, String title) { var talkId = jdbi.inTransaction(handle -> { // Count the number of accepted talks by the speaker var acceptedTalksCount = handle.select("SELECT count(*) FROM talk WHERE speaker_id = :id AND status = 'ACCEPTED'") .bind("id", speakerId) .mapTo(Long.class) .one(); // Check if the speaker is experienced var experienced = acceptedTalksCount >= 10; // Determine the maximum allowable number of submitted talks var maxSubmittedTalksCount = experienced ? 5 : 3; var submittedTalksCount = handle.select("SELECT count(*) FROM talk WHERE speaker_id = :id AND status = 'SUBMITTED'") .bind("id", speakerId) .mapTo(Long.class) .one(); // If the maximum number of submitted talks is exceeded, throw an exception if (submittedTalksCount >= maxSubmittedTalksCount) { throw new CannotSubmitTalkException("Submitted talks count is maximum: " + maxSubmittedTalksCount); } return handle.createUpdate( "INSERT INTO talk (speaker_id, status, title) " + "VALUES (:id, 'SUBMITTED', :title)" ).bind("id", speakerId) .bind("title", title) .executeAndReturnGeneratedKeys("id") .mapTo(Long.class) .one(); }); return new TalkSubmittedResult(talkId); } }
여기서 Speaker 클래스에는 강연 제출을 위한 비즈니스 로직이 포함되어 있습니다. 데이터베이스 상호 작용이 추상화되어 도메인 모델이 비즈니스 규칙에 집중할 수 있습니다.
이 저장소 인터페이스를 가정하면:
@AllArgsConstructor public class Speaker { private Long id; private String firstName; private String lastName; private List<talk> talks; public Talk submitTalk(String title) { boolean experienced = countTalksByStatus(Status.ACCEPTED) >= 10; int maxSubmittedTalksCount = experienced ? 3 : 5; if (countTalksByStatus(Status.SUBMITTED) >= maxSubmittedTalksCount) { throw new CannotSubmitTalkException( "Submitted talks count is maximum: " + maxSubmittedTalksCount); } Talk talk = Talk.newTalk(this, Status.SUBMITTED, title); talks.add(talk); return talk; } private long countTalksByStatus(Talk.Status status) { return talks.stream().filter(t -> t.getStatus().equals(status)).count(); } } @AllArgsConstructor public class Talk { private Long id; private Speaker speaker; private Status status; private String title; private int talkNumber; void setStatus(Function<status status> fnStatus) { this.status = fnStatus.apply(this.status); } public enum Status { SUBMITTED, ACCEPTED, REJECTED } } </status></talk>
그러면 다음과 같이 SpeakerService를 구현할 수 있습니다.
public interface SpeakerRepository { Speaker findById(Long id); void save(Speaker speaker); }
도메인 모델의 장점:
- 도메인 개체는 구현 세부 정보(예: 데이터베이스)와 완전히 분리됩니다. 이를 통해 일반 단위 테스트를 통해 쉽게 테스트할 수 있습니다.
- 비즈니스 로직은 도메인 개체 내에 중앙 집중화되어 있습니다. 이는 트랜잭션 스크립트 접근 방식과 달리 애플리케이션 전체에 논리가 확산되는 위험을 크게 줄여줍니다.
- 원하는 경우 도메인 개체를 완전히 불변으로 만들 수 있으므로 작업 시 안전성이 향상됩니다(우발적인 수정에 대한 걱정 없이 어떤 메서드에든 전달할 수 있음).
- 도메인 개체의 필드를 값 개체로 대체할 수 있습니다. 이렇게 하면 가독성이 향상될 뿐만 아니라 할당 시 필드의 유효성이 보장됩니다(잘못된 콘텐츠가 포함된 값 개체를 생성할 수 없음).
간단히 말하면 장점이 많습니다. 그러나 한 가지 중요한 과제가 있습니다. 흥미롭게도 도메인 모델 패턴을 자주 홍보하는 도메인 기반 디자인(Domain-Driven Design)에 관한 책에서는 이 문제가 전혀 언급되지 않거나 간략하게만 다루어집니다.
문제는 도메인 개체를 데이터베이스에 저장한 다음 다시 읽는 방법입니다. 즉, 저장소를 어떻게 구현하나요?
요즘 답은 뻔하다. Hibernate(또는 더 나은 Spring Data JPA)를 사용하여 문제를 해결하세요. 하지만 우리가 ORM 프레임워크가 발명되지 않은 세상에 있다고 상상해 봅시다. 이 문제를 어떻게 해결할까요?
수동 매핑
SpeakerRepository를 구현하기 위해 JDBI도 사용합니다.
@Service @RequiredArgsConstructor public class TalkService { private final Jdbi jdbi; public TalkSubmittedResult submitTalk(Long speakerId, String title) { var talkId = jdbi.inTransaction(handle -> { // Count the number of accepted talks by the speaker var acceptedTalksCount = handle.select("SELECT count(*) FROM talk WHERE speaker_id = :id AND status = 'ACCEPTED'") .bind("id", speakerId) .mapTo(Long.class) .one(); // Check if the speaker is experienced var experienced = acceptedTalksCount >= 10; // Determine the maximum allowable number of submitted talks var maxSubmittedTalksCount = experienced ? 5 : 3; var submittedTalksCount = handle.select("SELECT count(*) FROM talk WHERE speaker_id = :id AND status = 'SUBMITTED'") .bind("id", speakerId) .mapTo(Long.class) .one(); // If the maximum number of submitted talks is exceeded, throw an exception if (submittedTalksCount >= maxSubmittedTalksCount) { throw new CannotSubmitTalkException("Submitted talks count is maximum: " + maxSubmittedTalksCount); } return handle.createUpdate( "INSERT INTO talk (speaker_id, status, title) " + "VALUES (:id, 'SUBMITTED', :title)" ).bind("id", speakerId) .bind("title", title) .executeAndReturnGeneratedKeys("id") .mapTo(Long.class) .one(); }); return new TalkSubmittedResult(talkId); } }
접근 방법은 간단합니다. 각 저장소에 대해 JOOQ 또는 JDBI와 같은 SQL 라이브러리를 사용하여 데이터베이스와 작동하는 별도의 구현을 작성합니다.
첫눈에(아마도 두 번째에도) 이 솔루션은 꽤 좋아 보일 수 있습니다. 다음을 고려하십시오:
- 트랜잭션 스크립트 접근 방식과 마찬가지로 코드는 매우 투명하게 유지됩니다.
- 통합 테스트를 통해서만 비즈니스 로직을 테스트하는 데 더 이상 문제가 없습니다. 이는 저장소 구현(및 일부 E2E 시나리오)에만 필요합니다.
- 매핑 코드가 바로 눈앞에 있습니다. Hibernate 마법은 포함되지 않습니다. 버그를 발견하셨나요? 알맞은 줄을 찾아서 수정해주세요.
최대 절전 모드의 필요성
다음과 같은 시나리오를 접할 수 있는 현실 세계에서는 상황이 훨씬 더 흥미로워집니다.
- 도메인 객체는 상속을 지원해야 할 수도 있습니다.
- 필드 그룹을 별도의 값 개체로 결합할 수 있습니다(JPA/Hibernate에 포함됨).
- 성능 향상(지연 로딩)을 위해 일부 필드는 도메인 개체를 가져올 때마다 로드되지 않고 액세스할 때만 로드하면 됩니다.
- 객체 간에 복잡한 관계가 있을 수 있습니다(일대다, 다대다 등).
- 다른 필드는 거의 변경되지 않고 네트워크(DynamicUpdate 주석)를 통해 전송할 필요가 없기 때문에 UPDATE 문에 변경된 필드만 포함해야 합니다.
또한 비즈니스 로직과 도메인 개체가 발전함에 따라 매핑 코드를 유지 관리해야 합니다.
이러한 각 사항을 스스로 처리하려고 하면 결국에는 (놀랍게도!) Hibernate와 유사한 프레임워크를 작성하거나 훨씬 더 간단한 버전을 작성하게 될 것입니다.
JOOQ와 Hibernate의 목표
JOOQ는 SQL 쿼리 작성 시 정적 타이핑 부족 문제를 해결합니다. 이는 컴파일 단계에서 오류 수를 줄이는 데 도움이 됩니다. 데이터베이스 스키마에서 직접 코드를 생성하면 스키마 업데이트 시 코드를 수정해야 하는 위치가 즉시 표시됩니다(컴파일되지 않음).
Hibernate는 도메인 개체를 관계형 데이터베이스에 매핑하거나 그 반대로 매핑하는 문제(데이터베이스에서 데이터를 읽고 이를 도메인 개체에 매핑하는 문제)를 해결합니다.
그러므로 Hibernate가 더 나쁘다거나 JOOQ가 더 낫다고 주장하는 것은 말이 되지 않습니다. 이러한 도구는 다양한 목적으로 설계되었습니다. 귀하의 애플리케이션이 트랜잭션 스크립트 패러다임을 기반으로 구축된 경우 JOOQ는 의심할 여지 없이 이상적인 선택입니다. 그러나 도메인 모델 패턴을 사용하고 최대 절전 모드를 피하려면 사용자 정의 저장소 구현에서 수동 매핑의 즐거움을 처리해야 합니다. 물론, 고용주가 또 다른 Hibernate 킬러를 구축하는 데 비용을 지불한다면 의문의 여지가 없습니다. 그러나 대부분의 사람들은 객체-데이터베이스 매핑을 위한 인프라 코드가 아닌 비즈니스 로직에 집중할 것을 기대합니다.
그런데 저는 Hibernate와 JOOQ의 조합이 CQRS에 잘 맞는다고 생각합니다. CREATE/UPDATE/DELETE 작업과 같은 명령을 실행하는 애플리케이션(또는 그 논리적 부분)이 있습니다. 이것이 Hibernate가 완벽하게 적합한 곳입니다. 반면에 데이터를 읽는 쿼리 서비스가 있습니다. 여기서 JOOQ는 훌륭합니다. Hibernate보다 복잡한 쿼리를 작성하고 최적화하는 것이 훨씬 쉽습니다.
JOOQ의 DAO는 어떻습니까?
사실이에요. JOOQ를 사용하면 데이터베이스에서 엔터티를 가져오기 위한 표준 쿼리가 포함된 DAO를 생성할 수 있습니다. 여러분의 메서드를 사용하여 이러한 DAO를 확장할 수도 있습니다. 또한 JOOQ는 Hibernate와 유사한 setter를 사용하여 채울 수 있고 DAO의 삽입 또는 업데이트 메서드에 전달될 수 있는 엔터티를 생성합니다. 스프링데이터 같지 않나요?
간단한 경우에는 실제로 작동할 수 있습니다. 그러나 저장소를 수동으로 구현하는 것과 크게 다르지 않습니다. 문제는 비슷합니다.
- 엔티티에는 어떤 관계도 없습니다. ManyToOne, OneToMany가 없습니다. 데이터베이스 열만 있으면 비즈니스 논리 작성이 훨씬 더 어려워집니다.
- 엔티티는 개별적으로 생성됩니다. 상속 계층 구조로 구성할 수 없습니다.
- 엔터티가 DAO와 함께 생성된다는 사실은 원하는 대로 수정할 수 없다는 것을 의미합니다. 예를 들어, 필드를 값 개체로 바꾸거나, 다른 엔터티에 관계를 추가하거나, 필드를 Embeddable로 그룹화하는 것은 엔터티를 재생성하면 변경 사항을 덮어쓰게 되므로 불가능합니다. 예, 엔터티를 약간 다르게 생성하도록 생성기를 구성할 수 있지만 사용자 정의 옵션은 제한되어 있습니다(코드를 직접 작성하는 것만큼 편리하지도 않습니다).
따라서 복잡한 도메인 모델을 구축하려면 수동으로 구축해야 합니다. Hibernate가 없으면 매핑에 대한 책임은 전적으로 당신에게 있습니다. 물론, JOOQ를 사용하는 것이 JDBI보다 더 즐겁지만 그 과정은 여전히 노동 집약적입니다.
JOOQ의 창시자인 Lukas Eder도 자신의 블로그에서 DAO가 반드시 사용을 권장하기 때문이 아니라 인기 있는 패턴이기 때문에 라이브러리에 추가되었다고 언급했습니다.
결론
글을 읽어주셔서 감사합니다. 나는 Hibernate의 열렬한 팬이고 그것이 훌륭한 프레임워크라고 생각합니다. 하지만 일부 사람들은 JOOQ를 더 편리하게 생각할 수도 있다는 점을 이해합니다. 내 기사의 주요 요점은 Hibernate와 JOOQ가 라이벌이 아니라는 것입니다. 이러한 도구는 가치를 제공한다면 동일한 제품 내에서도 공존할 수 있습니다.
콘텐츠에 대한 의견이나 피드백이 있으면 기꺼이 논의해 드리겠습니다. 생산적인 하루 보내세요!
자원
- JDBI
- 거래 스크립트
- 도메인 모델
- 내 기사 – Spring Boot 및 Hibernate를 사용한 풍부한 도메인 모델
- 저장소 패턴
- 값 개체
- JPA 임베디드
- JPA 동적 업데이트
- CQRS
- Lukas Eder: DAO로 갈 것인가, 말 것인가
위 내용은 JOOQ는 Hibernate를 대체하지 않습니다. 그들은 다양한 문제를 해결합니다의 상세 내용입니다. 자세한 내용은 PHP 중국어 웹사이트의 기타 관련 기사를 참조하세요!

이 기사에서는 Java 프로젝트 관리, 구축 자동화 및 종속성 해상도에 Maven 및 Gradle을 사용하여 접근 방식과 최적화 전략을 비교합니다.

이 기사에서는 Maven 및 Gradle과 같은 도구를 사용하여 적절한 버전 및 종속성 관리로 사용자 정의 Java 라이브러리 (JAR Files)를 작성하고 사용하는 것에 대해 설명합니다.

이 기사는 카페인 및 구아바 캐시를 사용하여 자바에서 다단계 캐싱을 구현하여 응용 프로그램 성능을 향상시키는 것에 대해 설명합니다. 구성 및 퇴거 정책 관리 Best Pra와 함께 설정, 통합 및 성능 이점을 다룹니다.

이 기사는 캐싱 및 게으른 하중과 같은 고급 기능을 사용하여 객체 관계 매핑에 JPA를 사용하는 것에 대해 설명합니다. 잠재적 인 함정을 강조하면서 성능을 최적화하기위한 설정, 엔티티 매핑 및 모범 사례를 다룹니다. [159 문자]

Java의 클래스 로딩에는 부트 스트랩, 확장 및 응용 프로그램 클래스 로더가있는 계층 적 시스템을 사용하여 클래스로드, 링크 및 초기화 클래스가 포함됩니다. 학부모 위임 모델은 핵심 클래스가 먼저로드되어 사용자 정의 클래스 LOA에 영향을 미치도록합니다.


핫 AI 도구

Undresser.AI Undress
사실적인 누드 사진을 만들기 위한 AI 기반 앱

AI Clothes Remover
사진에서 옷을 제거하는 온라인 AI 도구입니다.

Undress AI Tool
무료로 이미지를 벗다

Clothoff.io
AI 옷 제거제

AI Hentai Generator
AI Hentai를 무료로 생성하십시오.

인기 기사

뜨거운 도구

안전한 시험 브라우저
안전한 시험 브라우저는 온라인 시험을 안전하게 치르기 위한 보안 브라우저 환경입니다. 이 소프트웨어는 모든 컴퓨터를 안전한 워크스테이션으로 바꿔줍니다. 이는 모든 유틸리티에 대한 액세스를 제어하고 학생들이 승인되지 않은 리소스를 사용하는 것을 방지합니다.

스튜디오 13.0.1 보내기
강력한 PHP 통합 개발 환경

맨티스BT
Mantis는 제품 결함 추적을 돕기 위해 설계된 배포하기 쉬운 웹 기반 결함 추적 도구입니다. PHP, MySQL 및 웹 서버가 필요합니다. 데모 및 호스팅 서비스를 확인해 보세요.

VSCode Windows 64비트 다운로드
Microsoft에서 출시한 강력한 무료 IDE 편집기

WebStorm Mac 버전
유용한 JavaScript 개발 도구
