프로젝트 개발하면서 @Transient Annotation과 관련하여 고생한 경험을 바탕으로, 깨달은 점을 정리해보려고 합니다.
@Transient
Specifies that the property or field is not persistent. It is used to annotate a property or field of an entity class, mapped superclass, or embeddable class.
https://docs.oracle.com/javaee/7/api/javax/persistence/Transient.html
Oracle에서 제공하는 Java Persisence의 @Transient 항목을 보면, 다음과 같이 설명하고 있습니다.
영속적이지 않은 프로퍼티나 필드를 명시한다. 엔티티, 매핑된 슈퍼클래스, 임베더블 클래스의 프로퍼티나 필드에 적용될 수 있다.
@Transient가 유용한 이유는, JPA 규격을 이용한 Entity 객체가 곧 DB에 저장되는 정보 형태이면서, 비즈니스 로직의 중심이 되는 도메인 객체이기 때문입니다.
간단한 예제여서 잘 전달이 안될 수 있지만, 다음 예제를 통해서 이해해보면 좋겠습니다.
@Entity
public class Order {
@Id
private Long id;
private String customerName;
@OneToMany(mappedBy = "order")
// 주문 항목 리스트 (데이터베이스에 저장됨)
private List<OrderItem> items;
// 총 가격 (계산에만 사용되고 데이터베이스에는 저장되지 않음)
@Transient
private Double totalPrice;
public Order() {}
public Order(Long id, String customerName, List<OrderItem> items) {
this.id = id;
this.customerName = customerName;
this.items = items;
this.totalPrice = calculateTotalPrice();
}
// 총 가격을 계산하는 메서드
private Double calculateTotalPrice() {
return items.stream()
.mapToDouble(OrderItem::getPrice)
.sum();
}
}
위의 예제에서, Order 엔티티의 id, customerName은 테이블에 저장되는 항목입니다.
그러나 totalPrice는 비즈니스 로직에서는 필요할 수 있지만, 데이터베이스에 저장될 필요는 없는 정보입니다.
Order 객체는 @Entity를 달고 관리되는 객체이므로 이 클래스의 프로퍼티들은 DB의 column에 대응됩니다.
그러나 어떤 프로퍼티들은 DB에서 관리될 필요가 없습니다.
이런 경우에 영속적으로 관리되지 않고, @Transient를 사용하게 됩니다.
만약에 MyBatis처럼 DAO 객체를 두고, DB와의 연계를 담당하는 책임을 다른 객체에 넘긴다면 @Transient는 불필요할 것입니다.
그러나 JPA에서는 도메인 객체가 곧 DB에 저장되는 형태를 나타내는 책임을 갖습니다.
동시에 DB에 저장되는 객체가 비즈니스 로직 또한 가지고 있습니다.
이것이 근본적으로 @Transient가 필요한 이유입니다.
@Transient와 @Transient?
그런데 @Transient를 사용했는데, 테이블이 예상한 대로 생성되지 않고 비즈니스 로직을 실행하면서도 Exception이 자주 발생했습니다.
근본적으로 이런 문제가 발생한 이유는 @Transient가 두 개 존재하는데, 둘은 비슷하면서도 다른 동작을 하기 때문입니다.
사실 위에서 설명한 @Transient는 Java Persistence API의 @Transient입니다.
jakarta.persistence.Transient
그런데 Spring Data 규격의 @Transient가 있습니다.
org.springframework.data.annotation.Transient
Marks a field to be transient for the mapping framework. Thus the property will not be persisted and not further inspected by the mapping framework.
공식 문서에 따르면, mapping framework를 위해서 영속되지 않을 프로퍼티를 설정한다고 써져 있는데요.
사실 이 문서만으로는 설명되지 않는 점이 많습니다.
왜냐면, 위에서 제공한 Order 예제 코드에서 Spring Data 규격의 @Transient를 사용하면 필드가 영속되기 때문입니다. (!)
Spring Data 규격의 @Transient를 사용하면 이 Annotation은 적절한 역할을 하지 못하고 무시되고, @Entity annotation이 달린 클래스의 필드는 @Column이 달린 것으로 취급되기 때문에 그렇습니다.
왜 여기에 적힌 대로 동작하지 않는 것일까요?
Spring Data와 JPA의 관계
일반적으로 Spring Data를 이용해서 JPA를 자주 사용하기 때문에 (Spring Data JPA 지원) 크게 생각하지 않는 부분이지만, Spring Data와 JPA는 전혀 별개의 프로젝트입니다.
Spring Data를 이용해서 JPA를 편리하게 이용할 수 있도록 지원할 뿐, JPA 자체는 순수 자바 프로젝트로도 사용할 수 있습니다.
저는 이전에 Dataverse라고 하는, Java EE + JPA 기반의 오픈소스 프로젝트도 본 적이 있었는데도 이 부분을 잊어버리고 있었습니다.
https://github.com/IQSS/dataverse
궁금하신 분들은 Java EE + JPA로 코딩하면 어떤 형태가 되는지 위의 레포를 참고해주세요.
그러니까 Spring Data와 JPA는 전혀 별개의 프로젝트이고, 다만 Spring 유저들을 위해서 Spring Data 프로젝트에서 JPA 사용을 지원하고 있는 것입니다.
JPA는 Java 코드와 테이블 기반의 RDBMS의 조합을 위한 프로젝트입니다. 사상 자체가 테이블 기반의 RDB를 Java 코드에서 네이티브하게 핸들링하겠다는 설계 사상을 가지고 태어났습니다.
반면에 Spring Data 프로젝트는 스프링 유저들이 Database를 네이티브하게 핸들링하겠다는 설계 사상을 가지고 태어났습니다.
이 둘의 차이가 무엇일까요? Spring Data는 일반적인 모든 Database를 핸들링하는 걸 지원하지만 JPA는 MySQL, MariaDB, PostgreSQL 같은 RDBMS만을 고려했다는 것입니다.
실제로 Spring Data 하위 프로젝트들을 보면 Spring Data MongoDB 등, 다른 DBMS에 대해서는 JPA가 아닌 다른 하위 프로젝트로 관리되고 있습니다.
도메인 객체를 다루는 코드도 조금 달라져야 하는데요.
Order 객체 예시를 이어가보겠습니다.
import org.springframework.data.annotation.Id;
import org.springframework.data.mongodb.core.mapping.Document;
import org.springframework.data.mongodb.core.mapping.DBRef;
import java.util.List;
@Document(collection = "orders") // MongoDB의 컬렉션 이름을 지정
public class Order {
@Id
private String id; // MongoDB에서 id는 String 타입이 될 수 있음
private String customerName;
// 주문 항목 리스트 (다른 문서 참조 가능)
@DBRef
private List<OrderItem> items;
// 총 가격 (계산 필드, MongoDB에 저장되지 않음)
private transient Double totalPrice;
public Order() {}
public Order(String customerName, List<OrderItem> items) {
this.customerName = customerName;
this.items = items;
this.totalPrice = calculateTotalPrice();
}
// 총 가격을 계산하는 메서드
private Double calculateTotalPrice() {
return items.stream()
.mapToDouble(OrderItem::calculateTotalPrice)
.sum();
}
// Getter와 Setter
public String getId() {
return id;
}
public void setId(String id) {
this.id = id;
}
public String getCustomerName() {
return customerName;
}
public void setCustomerName(String customerName) {
this.customerName = customerName;
}
public List<OrderItem> getItems() {
return items;
}
public void setItems(List<OrderItem> items) {
this.items = items;
this.totalPrice = calculateTotalPrice(); // 항목이 변경되면 총 가격을 재계산
}
public Double getTotalPrice() {
return totalPrice;
}
}
@Entity와 같은 annotation은 JPA 규격이기 때문에 사용하지 않는 것을 보실 수 있습니다.
결론적으로, JPA는 RDBMS와의 통합을 위한 프로젝트고 Spring Data는 Database 전반과 통합을 위한 프로젝트입니다.
그리고 JPA의 @Transient는 RDBMS의 테이블에서 영속성이 제외되는 기능을 제공하고, Spring Data의 @Transient는 NoSQL 기반의 Database에서 영속성이 제외되는 기능을 제공합니다.
그러므로 RDBMS와 JPA를 조합해서 사용하실 때는 Spring Data 규격의 @Transient를 사용하지 않도록 조심해야 합니다.
테이블에 원하지 않은 컬럼을 생성시킬 수 있기 때문입니다.
@Entity + JPA 규격 @Transient, @Document + Spring Data 규격의 @Transient가 각각 맞는 조합입니다.
Spring Data에서 제공해주는 기능들이 기존의 다른 프로젝트 (JPA와 같은)와 좋은 호환을 보여주기 때문에 자주 잊어버리곤 합니다.
그러나 둘은 엄연히 다른 프로젝트이고, 이런 @Transient와 같은 굉장히 애매한 기능 차이를 보이기도 합니다.
특히 공식 문서를 봐도 이런 부분이 잘 명시되어 있지 않아서 실수하기 쉬운 부분입니다.
그러므로 사용할 때 있어서 조심해야겠습니다.
'개발 - Coding > Java & Spring' 카테고리의 다른 글
[Java] Optional.of는 왜 있을까? (1) | 2024.10.10 |
---|---|
[JPA] @OneToOne 관계와 UNIQUE 제약조건 (0) | 2024.04.08 |
[JPA] 컴파일시에는 발견할 수 없는 @Enumerated와 @Embeddable 오류 (1) | 2023.06.01 |
[JPA] Spring Boot JPA Query Creation (0) | 2022.07.06 |
[Spring Boot] 스프링 @Configuration 모듈화하기 (0) | 2022.06.29 |