0. 들어가기 전
이전에 대부분의 프로젝트에서 JPA를 사용해서 SQL을 작성하지 않고
추상화된 메소드를 사용하여 영속화 계층에 접근하고, DB에 SQL을 반영했습니다.
이번에 개인적으로 JPA를 사용할 때 내부 동작 원리를 이해하지 않고 추상화된 메소드를 사용하기만 한다는 것을 깨닫게 되었습니다.
따라서, JPA에서 추상화된 메소드들이 어떻게 내부적으로 동작해서 쿼리가 생성되는지 알아보도록 하겠습니다.
더 나아가서, Spring Data JPA은 어떻게 JPA를 추상화했는지도 살펴보도록 하겠습니다.
이번 포스팅은 JPA의 기본 개념, 동작들을 이미 안 상태에서 원리를 알아보는 것이므로
기본적인 JPA의 개념이나 동작 설명들은 생략하고 가도록 하겠습니다!
1. JPA의 기본 동작 원리
먼저, JPA가 어떻게 객체와 테이블을 맵핑해서 쿼리하는지 원리를 살펴보겠습니다.
기본적으로 아래 링크인 JPA의 공식 문서인 JSR-338과 JAVA EE 8의 공식문서를 참고했습니다!
https://jcp.org/aboutJava/communityprocess/mrel/jsr338/index.html
https://javaee.github.io/tutorial/persistence-intro.html
※ EntityManager / Persistence Context
JPA의 동작 원리를 알기 위해서는, EntityManager, Persistence Context부터 시작해서 내부로 파고 들어가야 합니다.
JAVA EE 8 공식문서에서는 'Managing Entities' 챕터에서
EntityManager와 Persistence Context를 다음과 같이 소개하고 있습니다.
Entities are managed by the entity manager.
Each EntityManager instance is associated with a persistence context.
- persistence context: a set of managed entity instances that exist in a particular data store.
The EntityManager interface defines the methods that are used to interact with the prsistence context.
- JPA의 엔티티들은 EntityManager에 의해 관리된다.
- EntityManager 인스턴스는 'persistence context'와 연결된다.
- .persistence context : managed 엔티티 인스턴스들의 모음
- EntityManager는 persistence context와 상호작용하는 메소드를 정의한다.
이를 요약해보면 다음과 같습니다.
- EntityManager는 엔티티 인스턴스들의 모음인 persistence context와 연결되어 있다.
- EntityManager는 persistence context와 상호작용하여 엔티티들을 관리한다.
결국 JPA를 사용할 때는 EntityManager를 사용해서 Persistence Context와의 상호작용을 통해
엔티티들을 관리하고 쿼리 메소드들을 호출하여 쿼리가 실행되는 것임을 알 수 있습니다.
그렇다면, JPA를 사용해서 요청이 들어왔을 때 어떻게 쿼리를 생성해서 동작하는지에 관한 동작 원리를 알아보기 위해서는
EntityManager와 Persistence Context를 중심으로 다음과 같은 챕터로 나눠서 정리할 수 있습니다.
- 1. EntityManager가 어떻게 생성되고 주입되는지?
- 2. EntityManager를 통해 쿼리 메소드들을 호출할 때 어떤 동작으로 쿼리가 실행되는지?
위의 챕터로 글을 이어가보도록 하겠습니다.
1-1. EntityManager는 어떻게 생성될까?
JSR-338에서는 EntityManager의 생성에 대해 다음과 같이 소개하고 있습니다.
The entity manager for a persistence context is obtained from an entity manager factory.
- EntityManger는 EntityManagerFactory에서 얻을 수 있다.
따라서, EntityManger는 EntityManagerFactory에서 가져오는 것을 알 수 있습니다.
그렇다면, EntityManagerFactory는 어떻게 생성될까요?
EntityManagerFactory는 Persistence Unit의 정보들을 바탕으로 생성됩니다.
JSR-338에서는 Persistence Unit에 대해 다음과 같이 설명합니다.
A persistence unit is a logical grouping that includes:
- An entity manager factory and its entity managers, together with their configuration information.
- Persistence Unit은 설정 정보와 Entity Manager Factory를 포함한다.
기본적으로 Persistence Unit의 설정 정보는 다음과 같이 xml 파일로 정의합니다.
<persistence xmlns="http://xmlns.jcp.org/xml/ns/persistence" version="2.1">
<persistence-unit name="myPersistenceUnit">
<class>com.example.MyEntity</class>
<properties>
<property name="javax.persistence.jdbc.url" value="jdbc:h2:mem:testdb"/>
<property name="javax.persistence.jdbc.user" value="sa"/>
<property name="javax.persistence.jdbc.password" value=""/>
<property name="javax.persistence.jdbc.driver" value="org.h2.Driver"/>
<property name="hibernate.dialect" value="org.hibernate.dialect.H2Dialect"/>
</properties>
</persistence-unit>
</persistence>
그래서 해당 정보를 바탕으로 Persistence Unit을 정의하고, 해당 정보를 바탕으로 EntityManagerFactory가 생성되는 것입니다.
해당 정보들은 DB 커넥션 생성에 필요한 DataSoruce 생성에 사용됩니다.
따라서 이후에 EntityManager를 사용해서 쿼리를 생성하고, DB에 반영할 때 해당 정보로 커넥션을 맺고 쿼리를 반영하게 됩니다.
※ Spring Boot를 사용한 EntityManagerFactory 생성
현재는 대부분 Spring을 사용하면 Spring Boot의 Auto-Configuration 기능을 활용해서 DB 정보들을 정의하고 읽어옵니다.
spring:
datasource:
url: jdbc:h2:mem:testdb
username: sa
password:
jpa:
properties:
hibernate:
dialect: org.hibernate.dialect.H2Dialect
이런 식으로 DB 정보를 정의하면, Spring 내부에서 DB 정보로 DataSource를 생성하고 EntityManageFactory를 생성합니다.
1-2. EntityManager는 어떻게 주입될까?
앞서 EntityManager의 생성은 EntityManagerFactory에서 이루어지는 것을 확인했습니다.
그렇다면, EntityManager는 어떻게 주입해서 사용할 수 있을까요?
순수 JPA를 사용해봤다면 많이 알 수 있듯이, 다음과 같이 주입해서 사용할 수 있습니다.
@PersistenceContext
private EntityManager entityManager;
위와 같이 @PersistenceContext가 붙은 필드는 EntityManagerFactory에서 생성한 EntityManager를 주입해줍니다.
※ EntityManager 인스턴스의 Scope
이때 EntityManager의 주입이 여러 곳이라면 모두 같은 EntityManager가 주입되어서 사용될까요?
EntityManager는 모두 같은 인스턴스를 사용하지 않고, 트랜잭션 별로 인스턴스가 주입되어서 사용됩니다.
이는 JSR-338의 7.6.2 챕터인 'Container-managed Transaction-scoped Persistence Context'을 보면 알 수 있습니다.
The persistence context type for the entity manager is defaulted or defined as PersistenceContextType.TRANSACTION
- Persistence Context Type은 기본적으로 TRANSACTION 타입으로 정의된다.
따라서, Transaction별로 Persistence Context가 생성되고 EntityManger가 주입되는 것입니다.
1-3. EntityManager는 어떻게 쿼리를 생성하고 실행할까?
앞서 EntityManger의 생성과 주입을 알아봤으니,
이제 생성된 EntityManger를 사용해서 어떻게 객체를 기반으로 테이블 기반의 쿼리가 생성되고 실행되는지 알아봅시다.
JPA에서 사용하는 쿼리 언어는 JPQL(Java Persistence Query Language)입니다.
JPQL이 무엇인지, 어떤 문법으로 작성하는지 등 JPQL에 관한 기본 개념은 안다고 가정하고 넘어가도록 하겠습니다.
대신, JPQL로 SQL이 어떻게 생성되고 실행되는지에 대해 초점을 맞춰서 알아보겠습니다.
(구현체는 많이 사용하는 Hibernate 기준으로 알아봤습니다.)
JPQL로 SQL을 어떻게 생성할까?
해당 부분은 JPQL을 SQL로 변환하는 과정을 통해 생성하게 됩니다.
관련해서 많은 클래스들을 살펴봤지만, 구체적으로 변환하는 과정을 찾지는 못해서 추상적으로 언급만 하고 넘어가겠습니다.
JPA에서는 기본적으로 EntityManager의 createQuery() 메소드를 통해 JPQL로 SQL 쿼리를 생성합니다.
해당 createQuery() 메소드를 호출할 때, SQL로 변환하는 실행 계획(QueryPlan)이 생성되어 저장됩니다.
그리고 마지막에 결과를 조회하는 getResultList(), getSingleResult() 또는 실행하는 executeUpdate()에서
저장한 실행 계획 정보를 바탕으로 JPQL을 SQL로 변환하여 실행하게 됩니다.
JPQL로 SQL을 어떻게 실행할까?
JPA를 사용하지 않고 데이터를 영속화할 때,
기본적으로 JDBC(Java DataBase Connectivity) API를 사용해서 애플리케이션에서 SQL문을 실행합니다.
JPA가 등장한 배경은, 잘 사용하는 JDBC의 SQL 실행 API가 불편해서 등장한 것이 아니라
'Java의 객체와 DB의 테이블 사이의 패러다임 불일치를 극복하기 위해' 등장한 것입니다.
해당 부분은 엔티티를 정의하는 부분과 연관관계를 설정하는 부분, 쿼리를 객체를 사용해서 작성하는 JPQL을 통해 실현하고 있습니다.
따라서, 생성된 SQL문을 단순히 DB에 실행하는 로직 자체는 JDBC의 API를 호출하여 SQL문을 실행합니다.
이러한 상황에서 우리가 집중해야할 것은, 'JPA의 API가 JDBC의 어떤 API에 대응되는지'에 초점을 맞춰서 봐야합니다.
따라서, JPA의 최종 쿼리 실행 API인 다음 2가지가 JDBC의 어떤 API에 대응되는지 살펴보겠습니다.
- getResultList(), getSingleResult()
- executeUpdate()
결과적으로, 다음과 같이 대응됩니다.
- getResultList(), getSingleResult() - executeQuery() (PreparedStatement)
- executeUpdate() - executeUpdate()(PreparedStatement)
1-4. 요약
JPA의 동작 원리를 알아보기 위해서는 EntityManager를 중심으로 다음과 같은 챕터로 나눠서 정리할 수 있었습니다.
- 1. EntityManager가 어떻게 생성되고 주입되는지?
- 2. EntityManager를 통해 쿼리 메소드들을 호출할 때 어떤 동작으로 쿼리가 실행되는지?
먼저, EntityManager의 생성 및 주입은 설정 정보를 통해 EntityManagerFactory가 생성되고
트랜잭션별로 EntityManagerFactory에서 EntityManager 인스턴스를 생성하여 주입하는 것을 알 수 있었습니다.
그리고 EntityManager의 쿼리 생성 및 실행은 다음과 같이 요약할 수 있습니다.
- createQuery() : JPQL을 파라미터로 받고, SQL 변환 정보가 담긴 실행 계획을 생성
- getResultList(), getSingleResult(), executeUpdate() : 실행 계획을 기반으로 JPQL을 SQL로 변환 후 JDBC의 쿼리 실행 API를 호출하여 DB에 반영
2. Spring Data JPA의 기본 동작 원리
현재 Spring을 사용한다면, 대부분 JPA 대신 Spring Data JPA를 사용할 것입니다.
Spring Data JPA는 위에서 살펴본 JPA의 내부 동작을 추상화하여 더욱 간편하게 사용함과 동시에
JPQL 작성을 네이밍 규칙을 통해 자동으로 해주고 있습니다.
앞서 JPA의 기본 동작 원리를 살펴볼 때 다음과 같은 2가지 챕터로 나누어서 설명을 했었습니다.
- 1. EntityManager가 어떻게 생성되고 주입되는지?
- 2. EntityManager를 통해 쿼리 메소드들을 호출할 때 어떤 동작으로 쿼리가 실행되는지?
Spring Data JPA의 동작 원리는 결국 JPA의 위의 동작 원리를 추상화한 것이므로,
'해당 JPA의 기본 원리들을 어떻게 추상화했는지'에 초점을 맞춰서 진행해보겠습니다.
- 1. EntityManager 생성, 주입 과정을 어떻게 추상화했는지?
- 2. EntityManager의 쿼리 생성 과정을 어떻게 추상화했는지?
Spring Data JPA의 공식문서에서는 Spring Data JPA의 효과를 다음과 같이 언급하고 있습니다.
- Spring Data JPA aims to significantly improve the implementation of data access layers
- you write your repository interfaces using any number of techniques, Spring will wire it up for you automatically.
- You can even use custom finders or query by example and Spring will write the query for you!
- JPA의 목적은 Data Access 계층의 구현을 개선하는 것
- Repository Interface를 사용하면 Spring이 자동으로 이를 주입해준다.
- 심지어, 커스텀 Finder나 Query를 사용하면 Spring이 쿼리를 알아서 작성해준다.
이때, 해당 효과는 다음과 같이 맵핑할 수 있습니다.
- 1. EntityManager 생성, 주입 과정을 어떻게 추상화했는지? -> Repository Interface 사용
- 2. EntityManager의 쿼리 생성 과정을 어떻게 추상화했는지? -> 커스텀 Finder나 Query를 사용하면 Spring이 쿼리를 알아서 작성
해당 부분에 초점을 맞춰서 알아보도록 하겠습니다!
2-1. EntityManager 생성, 주입 과정을 어떻게 추상화했을까?
앞서 JPA에서 EntityManager를 생성하고 주입하는 과정은 다음과 같이 진행됐었습니다.
* 생성
1. 설정 정보를 바탕으로 DataSource 생성
2. DataSoruce를 기반으로 EntityManagerFactory를 생성
3. EntityManagerFactory에서 EntityManager 인스턴스 생성
* 주입
@PersistenceContext
private EntityManager entityManager;
Spring Data JPA도 EntityManager를 생성하는 것까지는 JPA와 동일합니다.
Spring Data JPA는 Entity Manager를 추상화하여 사용하기 때문에, Entity Manager를 주입받고 사용하는 부분이 다릅니다.
예를 들면, Member의 nickname을 통해 Member를 조회하는 EntityManger의 동작은 다음과 같이 추상화됩니다.
* JPA
@PersistneceContext
private EntityManager em;
em.createQuery("select m from Member as m where m.id = :id", Member.class)
.setParameter("id", 1L);
* Spring Data JPA
memberRepository.findById(1L);
Spring Data JPA를 사용할 때 EntityManager를 주입 받는 부분은 추상화되어 사용하는 쪽에서는 찾아볼 수 없습니다.
이 과정에서 JPA를 사용할 때 매번 EntityManager를 주입받아야 했던 불편함이 사라지는 것이죠!
Spring Data JPA에서 해당 과정을 추상화한 핵심 인터페이스는 Repository 인터페이스입니다.
Spring Data JPA의 Core Concepts 챕터에서도 다음과 같이 설명하고 있습니다.
The central interface in the Spring Data repository abstraction is `Repository`
Repository Interface
Spring Data JPA에서는 Repository Interface를 다음과 같이 소개합니다.
This interface acts primarily as a marker interface to capture the types to work with and to help you to discover interfaces that extend this one.
- 작업할 데이터의 유형을 포착하고 이를 확장하는 인터페이스를 찾게 도와주는 'marker interface'
- marker interface : 인터페이스 내부에 상수도, 메소드도 없는 인터페이스
- 해당 인터페이스는 객체의 타입과 관련한 정보들을 제공
- 따라서, 컴파일러와 JVM은 해당 마커 인터페이스를 통해 객체에 대한 타입 정보를 얻을 수 있다!
The `CrudRepository` and `ListCrudRepository` interfaces provide sophisticated CRUD functionality for the entity class that is being managed.
- Repository Interface를 상속하는 CrudRepository, ListCrudRepository Interface는 기본적인 CRUD 기능을 제공한다.
따라서, 보통 Spring Data JPA를 사용할 때는 Repository 인터페이스를 상속한 CrudRepository, ListCrudRepository를 상속받아서
다음과 같이 인터페이스를 정의해서 사용합니다.
(보통 CrudRepository, ListCrudRepository를 상속하여 부가 기능을 추가한 JpaRepository를 사용합니다.)
public interface MemberRepository extends JpaRepository<Member, Long> {
...
}
이렇게 JpaRepository를 상속한 커스텀 인터페이스를 생성하면,
해당 인터페이스를 구현한 구현체는 자동으로 컴포넌트 스캔 대상이 됩니다.
그렇다면, 해당 인터페이스의 구현체를 매번 생성해줘야 할까요?
이렇게 되면 클래스 수도 많아지고 번거로워지므로 Spring Data JPA에서는 런타임 시 동적으로 구현체를 프록시로 생성해줍니다.
실제로, 주입된 MemberRepository 인터페이스 객체를 getClass()를 사용해보면 프록시 객체임을 확인할 수 있습니다.
결론적으로 말하면 JpaRepository를 상속한 인터페이스를 생성하기만 하면, 해당 인터페이스를 구현한 구현체가 빈으로 등록됩니다!
그렇다면, 여기서 EntityManager는 어디서 주입되어서 CRUD 같은 동작에 사용되는 걸까요?
바로 JpaRepository의 구현체인 SimpleJpaRepository에서 EntityManager를 주입받고 내부 동작에 사용합니다.
앞서 MemberRepository의 구현체가 프록시 객체로 생성될 때,
JpaRepository 인터페이스를 구현한 SimpleJpaRepository 또한 JpaRepository의 구현체로 등록되어 사용됩니다.
SimpleJpaRepository의 내부를 살펴보면, 다음과 같습니다.
(EntityManager를 제외한 정보들은 생략했습니다!)
public class SimpleJpaRepository<T, ID> implements JpaRepositoryImplementation<T, ID> {
private final EntityManager entityManager;
public SimpleJpaRepository(..., EntityManager entityManager) {
...
this.entityManager = entityManager;
...
}
@Transactional
public <S extends T> S save(S entity) {
...
this.entityManager.persist(entity);
return entity;
}
}
위의 SimpleJpaRepository에서는 생성자 주입을 통해 EntityManager를 주입받습니다.
그리고 save()와 같은 내부 동작 호출 시에 주입받은 EntityManager를 사용하여 동작하는 것을 알 수 있습니다!
2-2. EntityManager의 쿼리 생성, 실행 과정을 어떻게 추상화했을까?
Spring Data JPA를 사용하여 쿼리를 DB에 반영하는 동작은 다음과 같이 2가지로 추상화하여 사용할 수 있습니다.
- 메소드 이름 자체에서 쿼리를 도출
- 수동으로 정의된 쿼리를 사용
이 부분은 Spring Data JPA의 공식 문서 Defining Query Methods 챕터에서도 다음과 같이 언급하고 있습니다.
The repository proxy has two ways to derive a store-specific query from the method name:
- By deriving the query from the method name directly.
- By using a manually defined query.
- Repository 프록시는 메소드 이름에서 쿼리를 도출하는 2가지 방법이 존재합니다.
- 메소드 이름 자체에서 쿼리를 도출하는 방법
- 수동으로 정의된 쿼리를 사용하는 방법
* 메소드 이름 자체에서 쿼리 도출
memberRepository.findById(1L);
* 수동으로 정의된 쿼리를 사용
public interface RouteRepository extends JpaRepository<Route, Long> {
@Query(
"select r from Route r " +
"where r.member.id = :memberId and r.date >= :startDate and r.date <= :endDate " +
"order by r.date asc"
)
List<Route> findByMemberIdAndPeriod(final Long memberId, final LocalDate startDate, final LocalDate endDate);
}
2번째 수동으로 쿼리를 정의하는 방법은 Query 어노테이션을 사용해서 JPQL을 작성하여 생성합니다.
이는 JPA에서 JPQL을 사용해서 쿼리를 생성, 실행하는 부분과 동일하기 때문에 넘어가도록 하겠습니다.
첫 번째 방법인 findById() 같은 메소드 이름에서 쿼리를 도출하는 내부 구현은 어떻게 되는지 알아봅시다.
Spring Data JPA에서는 메소드 이름에서 쿼리 생성에 대해 다음과 같이 언급하고 있습니다.
The query builder mechanism built into the Spring Data repository infrastructure is useful for building constraining queries over entities of the repository.
- Spring Data JPA에 내장된 쿼리 빌더 매커니즘은 쿼리를 생성하는 데에 유용하다.
이를 보면, 메소드 이름으로부터 빌더 패턴의 느낌으로 쿼리를 생성한다는 것을 알 수 있습니다.
또한, Spring Data JPA의 공식문서에서는 간단한 예제를 통해 메소드 이름으로부터 쿼리가 생성되는 느낌을 설명합니다.
interface PersonRepository extends Repository<Person, Long> {
List<Person> findByEmailAddressAndLastname(EmailAddress emailAddress, String lastname);
...
}
Parsing query method names is divided into subject and predicate.
The first part (find…By, exists…By) defines the subject of the query, the second part forms the predicate.
- 메소드 이름에서 쿼리를 파싱할 때 subject와 predicate로 나누어서 파싱한다.
- subject(주제) : 어떤 동작을 수행할지에 대한 부분 (findBy - 조회, existsBy - 데이터 존재 확인, ...)
- predicate(조건) : 쿼리에서 필터링할 조건에 대한 부분 (EmailAddress, Lastname을 조건으로 사용)
- findBy, existsBy와 같은 첫 부분은 subject로 정의되어 사용
- 그 다음 부분은 predicate로 정의되어 사용
또한, 위의 예제와 같이 predicate 부분을 And, Or과 같은 단어로 연결하여 빌더처럼 조건을 체이닝할 수 있습니다.
결과적으로, Spring Data JPA는 내부적으로 메소드 이름에서 내용을 분석하는 것을 알 수 있습니다.
그 과정에서 subject를 통해 행위(select, exists, ...)들을 추출하고 predicate를 통해 조건들을 추출해서 쿼리를 생성하는 것을 알 수 있습니다.
* Spring Data JPA 메소드 정의
List<Person> findByEmailAddressAndLastname(EmailAddress emailAddress, String lastname);
* 쿼리 분석 후 생성되는 실제 SQL
SELECT *
FROM person
WHERE email_address = ? AND lastname = ?
2-3. 요약
Spring Data JPA의 내부 동작 원리를 알아보기 위한 챕터는 다음과 같이 설정했었습니다.
- 1. EntityManager 생성, 주입 과정을 어떻게 추상화했는지?
- 2. EntityManager의 쿼리 생성 과정을 어떻게 추상화했는지?
1. EntityManager 생성, 주입 과정을 어떻게 추상화했는지?
먼저 Spring Data JPA의 EntityManager 생성 과정은 JPA와 동일하게 DataSource를 통해 EntityManagerFactory 생성 후
EntityManagerFactory에서 EntityManager를 생성하는 과정으로 동작했었습니다.
EntityManager의 주입은 Spring Data JPA의 JpaRepository를 상속한 커스텀 인터페이스를 생성하기만 하면
JpaRepository를 구현한 SimpleJpaRepository가 구현체로 등록되고, 내부에서 EntityManager를 생성자 주입으로 주입되는 과정으로 동작하는 것을 알 수 있습니다.
따라서 사용하는 쪽에서는 사용 시마다@PersistenceContext로 EntityManager를 주입받지 않고,
JpaRepository를 상속한 커스텀 인터페이스를 생성하기만 하면 이를 주입받아서 영속화 계층에 접근할 수 있었습니다.
2. EntityManager의 쿼리 생성 과정을 어떻게 추상화했는지?
Spring Data JPA의 EntityManger 쿼리 생성 과정은 메소드 이름을 분석하여 내장된 쿼리 빌더 매커니즘을 통해 쿼리가 생성되는 것을 알 수 있었습니다.
쿼리 분석 시 subject와 predicate로 나누어서 행위와 조건들을 추출하여 SQL을 생성하는 과정이 있었습니다.
따라서 사용하는 쪽에서는 EntityManager의 쿼리 생성, 실행 메소드를 호출하지 않고
JpaRepository를 상속한 커스텀 인터페이스의 메소드 이름 정의를 통해 간단하게 쿼리를 생성하고 실행할 수 있었습니다.
Reference
https://jcp.org/aboutJava/communityprocess/mrel/jsr338/index.html
https://javaee.github.io/tutorial/persistence-intro.html
https://docs.spring.io/spring-data/jpa/reference/index.html
'Spring > JPA' 카테고리의 다른 글
테스트 시 @ElementCollection 테이블 동적으로 가져오기(테스트 데이터 초기화) (0) | 2024.05.09 |
---|---|
[JPA] JPA N+1 문제 및 근본적인 원인에 대한 개인적인 고찰 (6) | 2024.04.05 |
[JPA] JPA 1:N 관계에서 연관관계 주인을 1 대신 N에 두는 이유 (4) | 2023.08.06 |
[JPA] @JoinColumn 파헤치기 (feat. JPA 연관관계별 사용) (6) | 2023.07.09 |