객체지향 프로그래밍은 추상화, 캡슐화, 정보은닉, 상속, 다형성 등 시스템의 복잡성을 제어할 수 있는 다양한 장치들을 제공한다.
객체 중심형 사고방식은 객체를 어딘가에 저장해야 한다.
현실적 대안은 주로 관계형 DB에 저장하는 것. 이 방식에는, 필연적으로 SQL 변환의 과정이 요구된다.
객체의 상속 개념이 관계형 DB에는 없음. 그나마 슈퍼타입, 서브타입 관계가 유사하지만 같은 개념은 아니다.
만약 Album을 db에 저장하려고 한다면, 다음과 같은 절차를 거쳐야 한다.
1. Album 객체 분해 (쪼갠 자료를 두개의 테이블에 두개의 쿼리로 저장)
2. INSERT INTO ITEM ...
3. INSERT INTO ALBUM ...
만약 Album을 조회하려고 한다면,
1. 각각의 테이블에 따른 조인 SQL 작성 ...
2. 각각의 객체 생성...
3. 상상만 해도 복잡
4. 더 이상의 설명은 생략한다.
5. 그래서 DB에 저장할 객체에는 상속 관계 안쓴다.
만약 자바 컬렉션에 저장한다면, 코드 한줄로 조회, 저장이 모두 끝난다.
list.add(album);
Album album = list.get(albumId);
// 부모 타입으로 조회 후 다형성 활용
Item item = list.get(albumId);
중간에 SQL로 바꾸는 역할을 수행하느라 로직이 번잡해지게 되는 것.
member.getTeam()
JOIN ON M.TEAM_ID = T.TEAM_ID
이 문제를 해결하기 위해, 객체를 테이블에 맞춰 모델링하는 경우가 많다.
class Member {
String id;
Long teamId; // TEAM_ID FK 컬럼에 사용되는 내용
String username;
}
class Team {
Long id;
String name;
}
// SQL문
INSERT INTO MEMBER(MEMBER_ID, TEAM_ID, USERNAME) VALUES ...
이런식으로 객체를 맞춰두면, SQL INSERT문에 사용하기 편함. 하지만 객체의 연간관계 자체는 참조가 들어가는 (Team 객체를 내부적으로 가지고 있는) 상태가 더 적절하다.
만약 객체 모델링을 조회하려고 한다면,
SELECT M.*, T.*
FROM member M
JOIN TEAM T ON M.TEAM_ID = T.TEAM_ID
public Member find(String memberId) {
// SQL 실행..
Member member = new Member();
// 데이터베이스에서 조회한 회원 관련 정보를 모두 입력
Team team = new Team();
// 데이터베이스에서 조회한 팀 관련 정보를 모두 입력
// 회원과 팀 관계 설정
member.setTeam(team);
return member;
}
그러나 이와 달리, 객체 모델링을 자바 컬렉션에서 관리하는 모습은 훨씬 간단하게 이루어진다.
list.add(member);
Member member = list.get(memberId);
Team team = member.getTeam();
객체는 자유롭게 객체 그래프를 탐색할 수 있어야 한다.
그러나 SQL을 사용하는 순간, 처음 실행하는 SQL에 따라 탐색 범위가 결정된다.
SELECT M.*, T.*
FROM member M
JOIN TEAM T ON M.TEAM_ID = T.TEAM_ID
member.getTeam(); // OK
memger.getOrder(); // null
SQL로 Member랑 Team만 조회한 상황이면, Order는 객체에서 조회할 수 없다. (sql에서 조회한 적 없으므로) 이는 곧 엔티티에 대한 신뢰의 문제와 연결된다.
class MemberService {
...
public void process() {
Member member = memberDAO.find(memberId);
member.getTeam(); //???
member.getOrder().getDelivery(); //???
}
}
물리적으로는 MemberService
와 MemberRepository
가 분리되어있다. (파일 두개가 분리되어 있다.) 물리적으로는 계층 구조를 짰으나, 우리는 이 Service의 process
를 신뢰할 수가 없다. SQL에서 team정보와 order정보를 함께 불러오지 않았다면, 두 정보는 null이다. 즉, 서비스 로직을 작성하려면 해당 DAO코드를 다 확인하여 어떤 SQL이 실행되어 어떤 데이터를 담았는지 눈으로 확인해야만 코드를 작성할 수 있다. 즉, 물리적으로는 계층이 분리되어 있으나 논리적으로는 계층이 분리되어있지 않은 상황이다.
그럼 그냥 특정 객체를 조회할때마다 연관관계가 있는 모든 객체를 다 가져오면 되는게 아닐까? 그럴 수 없다. 엄청나게 많은 조인이 발생할 것이고, 한 번 데이터를 조회할때마다 메모리가 폭주할 것이다. 따라서, 상황에 따라 동일한 회원 조회 메서드를 여러벌 생성해야 한다.
memberDAO.getMember(); // Member만 조회
memberDAO.getMemberWithTeam(); // Member와 Team 조회
memberDAO.getMemberWithOrderWithDelivery(); // Member, Order, Delivery
즉, 진정한 의미의 계층 분할이 어렵다.
String memberId = "100";
Member member1 = memberDAO.getMember(memberId);
Member member2 = memberDAO.getMember(memberId);
member1 == member2; // 다르다.
class MemberDAO {
public Member getMember(String memberId) {
String sql ="SELECT * FROM MEBER WHERE MEMBER_ID = ?";
...
// JDBC API, SQL 실행
return new Member(...);
}
}
두개의 인스턴스가 다른 인스턴스이므로, member1과 member2는 다른 인스턴스이다.
그러나 자바 컬렉션에서 같은 key를 가진 두 객체를 조회하면, 두 객체는 같다.
String memberId = "100";
Member member1 = list.get(memberId);
Member member2 = list.get(memberId);
member1 == member2; //같다.
MemberDAO에서 객체를 보내주면 JPA에서 사진의 작업을 진행한다. 중간의 SQL와 객체 변환 작업을 모두 JPA에서 수행하므로, 우리는 마치 자바 컬렉션을 사용하는 것 처럼 사용 가능하다. 무엇보다도, 패러다임 불일치의 문제를 해결해준다!
예전에 Java 진영에 EJB 엔티티 빈이라는 ORM 기술이 있었다. 그러나 이 기술이 너무 복잡해 개빈 킹이라는 개발자가 직접 하이버네이트라는 오픈 소스를 개발했다. (이것이 EJB 엔티티빈 보다 훨신 좋았다.) 그래서 자바 진영에서 하이버네이트를 기반으로 JPA 표준을 만들었다.
저장: jpa.persist(member)
조회: Member member = jpa.find(memberId)
수정: member.setName("변경할 이름")
삭제: jpa.remove(member)
필드만 추가하면 JPA가 처리.
jpa.persist(album);
INSERT INTO TEAM...
INSERT INTO ALBUM...
Album album = jpa.find(Album.class, albumId);
SELECT I.*, A.*
FROM ITEM I
JOIN ALBUM A ON I.ITEM_ID = A.ITEM_ID
member.setTeam(team);
jpa.persist(member);
member랑 team을 입력하면, jpa가 알아서 연관관계 고래해 인서트 쿼리를 만들어준다. (member 테이블에 알아서 team의 외래키 값이 들어가있다.)
Member memeber = jpa.find(Member.class, memberId);
Team team = member.getTeam();
Member만 조회해도 Team의 값이 같이 나온다.
class MemberService {
...
public void process() {
Member member = memberDAO.find(memberId);
member.getTeam(); // 자유로운 객체 그래프 탐색
member.getOrder().getDelivery();
}
}
자유롭게 객체를 검색할 수 있고, 해당 엔티티 역시 (SQL문을 확인하지 않고도) 신뢰할 수 있다. 계층 분리의 문제도 해결된다!
String memberId = "100";
Member member1 = memberDAO.getMember(memberId);
Member member2 = memberDAO.getMember(memberId);
member1 == member2; // 같다.
JPA의 기본 컨셉이 Java Collection. 조회한 PK가 같은 경우, JPA는 같은 객체 인스턴스를 반환한다. (단, Java Collection에서는 모든 경우에 두 인스턴스가 동일하지만, JPA는 동일한 트랜젝션에서만 조회한 엔티티의 동일성을 보장한다.)
JPA는 애플리케이션과 DB사이에 하나의 계층이 존재하는 것. 항상 둘 사이에 계층이 존재하면 2가지 기능을 할 수 있다: 캐시, 버퍼링 라이트. 이를 통해 성능 최적화가 가능한데, JPA 역시 이를 통해 성능 최적화가 가능하다.
String member Id = "100";
Member m1 = jpa.find(Member.class, memberId); //SQL
Member m2 = jpa.find(Member.class, memberId); //캐시
println(m1 == m2) //true
이런 경우, SQL이 1번만 실행한다.
첫 번째 요청에서는 SQL이 날아가고, 두 번째 요청에서 같은 키를 찾는 경우 캐시가 적용된다.
transaction.begin(); // [트랜잭션] 시작
em.persist(memberA);
em.persist(memberB);
em.persist(memberC);
// 여기까지 INSERT SQL을 데이터베이스에 보내지 않는다.
//커밋하는 순간 데이터베이스에 INSERT SQL을 모아서 보낸다.
transaction.commit(); // [트랜잭션] 커밋
버퍼링 라이트 버퍼로 SQL을 모아 한번에 전송한다. 트랜잭션을 커밋할 때까지 JPA는 기본적으로 INSERT SQL을 모아둔다. 이는 JDBC 배치 SQL이란 기능을 활용하는 것인데, JPA에 A,B,C 멤버를 모아두었다가 커밋 발생 시 네트워크 통신 한 번으로 모아둔 데이터를 전송한다. 이를 통해 성능 최적화를 이룰 수 있다. 옵션을 켜서 적용 가능하다.
언급했듯 과거 JDBC 배치 SQL기능을 활용하는 것인데, 이 로직이 매우 복잡하다. 이를 중간에서 다 자동으로 해주는 것.
transaction.begin(); // [트랜잭션] 시작
changeMember(memberA);
deleteMenber(memberB);
비즈니스_로직_수행(); // 비즈니스 로직 수행 동안 DB 로우 락이 걸리지 않는다.
// 커밋하는 순간 데이터베이스에 UPDATE, DELETE SQL을 보낸다.
transaction.commit(); // [트랜잭션] 커밋
먼저 MEMBER정보를 가져오는데, 이 당시에는 Team 객체에 관한 정보가 없다.
실제로 Team 정보가 호출될 때, Team을 조회하는 SQL문을 사용해 Team정보를 가져온다. (Team 정보를 사용하지 않는 경우, 불필요하게 Team 정보를 가져오지 않을 수 있다.) 단, 쿼리를 두번 실행해야 한다.
코드 상에서 Team 정보 또한 요구된다는 것을 알 수 있다. 이런 경우 즉시 로딩 설정이 걸려있으면, Member정보를 가져오는 경우 항상 Team정보를 불러온다. (원래는 이런 작업을 모두 개발자가 수행해야 한다.)
ORM은 객체지향, 관계형 DB를 모두 잘 알아야 제대로 사용할 수 있다. 따라서 관계형 DB, SQL에 대해서도 깊이있게 학습해야 한다.
spring-boot-starter-data-jpa
라이브러리를 사용해 간단히 설정 가능하다.
//JPA, 스프링 데이터 JPA 추가
implementation 'org.springframework.boot:spring-boot-starter-data-jpa'
//JdbcTemplate 추가
//implementation 'org.springframework.boot:spring-boot-starter-jdbc'
추가하는 의존에서 제거되는 외존도 포함하고 있어서 제거해도 괜찮다.
dependencies {
implementation 'org.springframework.boot:spring-boot-starter-thymeleaf'
implementation 'org.springframework.boot:spring-boot-starter-web'
//JdbcTemplate 추가
//implementation 'org.springframework.boot:spring-boot-starter-jdbc'
//MyBatis 추가
implementation 'org.mybatis.spring.boot:mybatis-spring-boot-starter:2.2.0'
//JPA, 스프링 데이터 JPA 추가
implementation 'org.springframework.boot:spring-boot-starter-data-jpa'
//H2 데이터베이스 추가
runtimeOnly 'com.h2database:h2'
compileOnly 'org.projectlombok:lombok'
annotationProcessor 'org.projectlombok:lombok'
testImplementation 'org.springframework.boot:spring-boot-starter-test'
//테스트에서 lombok 사용
testCompileOnly 'org.projectlombok:lombok'
testAnnotationProcessor 'org.projectlombok:lombok'
}
다음과 같은 라이브러리가 추가된다.
hibernate-core
: JPA 구현체인 하이버네이트 라이브러리jakarta.persistence-api
: JPA 인터페이스spring-data-jpa
: 스프링 데이터 JPA 라이브러리#JPA log
logging.level.org.hibernate.SQL=DEBUG
logging.level.org.hibernate.type.descriptor.sql.BasicBinder=TRACE
#JPA log
logging.level.org.hibernate.SQL=DEBUG
logging.level.org.hibernate.type.descriptor.sql.BasicBinder=TRACE
org.hibernate.SQL=DEBUG
: 하이버네이트가 생성하고 실행하는 SQL 확인 가능org.hibernate.type.descriptor.sql.BasicBinder=TRACE
: SQL에 바인딩 되는 파라미터 확인 가능spring.jpa.show-sql=true
: 이 설정은 콘솔 System.out
으로 SQL이 출력된다. 따라서 사용 권장하지 않음.스프링 부트 3.0
스프링부트 3.0 이상 사용시 하이버네이트 6 버전이 사용되는데, 로그 설정 방식이 다르다.
#JPA log logging.level.org.hibernate.SQL=DEBUG logging.level.org.hibernate.orm.jdbc.bind=TRACE
package hello.itemservice.domain;
import lombok.Data;
import javax.persistence.*;
@Data
@Entity
//@Table(name = "item")
// 테이블명. 객체와 테이블명이 같은 경우 생략 가능.
public class Item {
@Id
@GeneratedValue(strategy = GenerationType.AUTO)
private Long id;
@Column(name = "item_name", length = 10)
private String itemName;
private Integer price;
private Integer quantity;
public Item() {
}
public Item(String itemName, Integer price, Integer quantity) {
this.itemName = itemName;
this.price = price;
this.quantity = quantity;
}
}
@Entity
: JPA가 사용하는 객체라는 의미. 이 애노테이션이 있어야 JPA가 인식할 수 있다. 해당 애노테이션이 붙은 객체를 JPA에서는 엔티티라 한다.@Id
: 테이블의 PK와 해당 필드를 매핑한다.@GeneratedValue(strategy = GenerationType.IDENTITY)
: PK 생성 값을 데이터베이스에서 생성하는 IDENTITY
방식을 사용한다. 예) MySQL auto increment@Column
: 객체의 필드를 테이블의 컬럼과 매핑한다.name = "item_name"
: 객체는 itemName
이지만 테이블의 컬럼은 item_name
이므로 이렇게 매핑했다.length = 10
: JPA의 매핑 정보로 DDL(create taable
)도 생성할 수 있는데, 그때 컬럼의 길이 값으로 활용된다.(varchar 10
)@Column
을 생략할 경우 필드의 이름을 테이블 컬럼 이름으로 사용한다. 참고로 지금처럼 스프링 부트와 통합해서 사용하면 필드 이름을 테이블 컬럼 명으로 변경할 때 객체 필드의 카멜 케이스를 테이블 컬럼의 언더스코어로 자동으로 변환해준다.itemName
-> item_name
, 따라서 위 예제의 @Column(name = "item_name")
를 생략해도 된다.JPA는 public 또는 protected의 기본 생성자가 필수이다. 기본 생성자를 꼭 넣어주자. 이 생성자가 들어가야만 프록시 기술을 사용하기 편하다.
public item() {}
package hello.itemservice.repository.jpa;
import hello.itemservice.domain.Item;
import hello.itemservice.repository.ItemRepository;
import hello.itemservice.repository.ItemSearchCond;
import hello.itemservice.repository.ItemUpdateDto;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Repository;
import org.springframework.util.StringUtils;
import javax.persistence.EntityManager;
import javax.persistence.TypedQuery;
import javax.transaction.Transactional;
import java.util.List;
import java.util.Optional;
@Slf4j
@Repository
@Transactional
public clas JpaItemRepository implements ItemRepository {
private final EntityManager em;
pulic JpaItemRepository(EntityManager em) {
this.em = em;
}
@Override
public Item save(Item item) {
em.persist(item);
return item;
}
@Override
public void update(Long itemId, ItemUpdateDto updateParam) {
Item findItem = em.find(Item.class, itemId);
findItem.setItemName(updateParam.getItemName());
findItem.setPrice(updateParam.getPrice());
findItem.setQuantity(updateParam.getQuantity());
}
@Override
public Optional<Item> findById(Long id) {
Item item = em.find(Item.class, id);
return Optoinal.ofNullable(item);
}
@Override
public List<Item> findAll(ItemSearchCond cond) {
String jpql = "select i from Item i";
Integer maxPrice = cond.getMaxPrice();
String itemName = cond.getItemName();
if (StringUtils.hasText(itemName) || maxPrice != null) {
jpql += " where";
}
boolean andFlag = false;
if (StringUtils.hasText(itemName)) {
jpql += " i.itemName like concat('%',:itemName,'%')";
andFlag = true;
}
if (maxPrice != null) {
if (andFlag) {
jpql += " and";
}
jpql += " i.price <= :maxPrice";
}
log.info("jpql={}", jpql);
TypedQuery<Item> query = em.createQuery(jpql, Item.class);
if (StringUtils.hasText(itemName)) {
query.setParameter("itemName", itemName);
}
if (maxPrice != null) {
query.setParameter("maxPrice", maxPrice);
}
List<Item> result = em.createQuery(jpql, Item.class)
.getResultList();
return result;
}
}
private final EntityManager em;
EntityManager
)라는 것을 주입받은 것을 확인할 수 있다. JPA의 모든 동작은 엔티티 매니저를 통해서 이루어진다. 엔티티 매니저는 내부에 데이터소스를 가지고 있고, 데이터베이스에 접근할 수 있다. @Transactional
: JPA의 모든 데이터 변경(등록, 수정, 삭제)은 트랜잭션 안에서 이루어져야 한다. 조회는 트랜잭션이 없어도 가능하다. 변경의 경우 일반적으로 서비스 계층에서 트랜잭션을 시작하기 때문에 문제가 없다. 하지만 이번 예제에서는 복잡한 비즈니스 로직이 없어서 서비스 계층에서 트랜잭션을 걸지 않았다. JPA에서는 데이터 변경 시 트랜잭션이 필수다. 따라서 리포지토리에 트랜잭션을 걸어주었다. 다시한번 강조하지만 일반적으로는 비지니스 로직을 시작하는 서비스 계층에 트랜잭션을 걸어주는 것이 맞다.참고
JPA를 설정하려면
EntityManagerFactory
, JPA 트랜잭션 매니저(JpaTransactionManager
), 데이터 소스 등등 다양한 설정을 해야한다. 스프링 부트는 이 과정을 모두 자동화해준다.main()
메서드 부터 시작해서 JPA를 처음부터 어떻게 설정하는지는 JPA 기본편을 참고하자. 그리고 스프링 부트의 자동 설정은JpaBaseConfiguration
을 참고하자.
package hello.itemservice.config;
import hello.itemservice.repository.ItemRepository;
import hello.itemservice.repository.jpa.JpaItemRepository;
import hello.itemservice.service.ItemService;
import hello.itemservice.service.ItemServiceV1;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import javax.persistence.EntityManager;
@Configuration
public class JpaConfig {
private final EntityManager em;
public JpaConfig(EntityManager em) {
this.em = em;
}
@Bean
public ItemService itemService() {
return new ItemServiceV1(itemRepository());
}
@Bean
public ItemRepository itemRepository() {
return new JpaItemRepository(em);
}
}
@Import(JpaConfig.class)
@SpringBootApplication(scanBasePackages = "hello.itemservice.web")
public class ItemServiceApplication {}
public Item save(Item item) {
em.persist(item);
return item;
}
em.persist(item)
: JPA에서 객체를 테이블에 저장할 때에는 엔티티 매니저가 제공하는 persist()
메서드를 사용하면 된다.insert into item (id, item_name, price, quantity) values (null, ?, ?, ?)
또는
insert into item (id, item_name, price, quantity) values (default, ?, ?, ?)
또는
insert into item (item_name, price, quantity) values (?, ?, ?)
id
에 값이 빠져있는 것을 확인할 수 있다. PK 키 생성 전략을 IDENTITY
로 사용했기 때문에 JPA가 이런 쿼리를 만들어서 실행한 것이다. 물론 쿼리 실행 이후에 Item
객체의 id
필드에 데이터베이스가 생성한 PK값이 들어가게 된다.(JPA가 INSERT SQL 실행 이후에 생성된 ID 결과를 받아서 넣어준다.)@Entity
public class Item {
@Id @GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
}
public void update(Long itemId, ItemUpdateDto updateParam) {
Item findItem = em.find(Item.class, itemId);
findItem.setItemName(updateParam.getItemName());
findItem.setPrice(updateParam.getPrice());
findItem.setQuantity(updateParam.getQuantity());
}
update item set item_name=?, price=?, quantity=?, where id=?
🤔 메서드에서 commit과 같은 전송 단계를 호출하지 않고 어떻게 UPDATE SQL이 실행되는 걸까?
em.update()
같은 메서드를 전혀 호출하지 않았다. 그런데 어떻게 UPDATE SQL이 실행되는 것일까?@Commit
을 붙이면 확인할 수 있다. public Optional<Item> findById(Long id) {
Item item = em.find(Item.class, id);
return Optional.ofNullable(item);
}
find()
를 사용하고 조회 타입과, PK 값을 주면 된다. 그러면 JPA가 다음과 같은 조회 SQL을 만들어서 실행하고, 결과를 객체로 바로 반환해준다.select
item0_.id as id1_0_0_,
item0_.item_name as item_nam2_0_0_,
item0_.price as price3_0_0_,
item0_.quantity as quantity4_0_0_
from item item0_
where item0_.id=?
JPA에서 단순히 PK를 기준으로 조회하는 것이 아닌, 여러 데이터를 복잡한 조건으로 데이터를 조회하려면 어떻게 하면 될까?
식별자를 기반으로 하나의 데이터를 조회하는 것이 아니라, 여러 조건으로 복잡하게 쿼리를 써야 하는 경우를 위해 JPA는 SQL과 거의 유사한 JPQL이라는 것을 제공하고 있다. (객체 쿼리 언어)
@Override
public List<Item> findAll(ItemSearchCond cond) {
String jpql = "select i from Item i";
Integer maxPrice = cond.getMaxPrice();
String itemName = cond.getItemName();
if (StringUtils.hasText(itemName) || maxPrice != null) {
jpql += " where";
}
boolean andFlag = false;
if (StringUtils.hasText(itemName)) {
jpql += " i.itemName like concat('%',:itemName,'%')";
andFlag = true;
}
if (maxPrice != null) {
if (andFlag) {
jpql += " and";
}
jpql += " i.price <= :maxPrice";
}
log.info("jpql={}", jpql);
TypedQuery<Item> query = em.createQuery(jpql, Item.class);
if (StringUtils.hasText(itemName)) {
query.setParameter("itemName", itemName);
}
if (maxPrice != null) {
query.setParameter("maxPrice", maxPrice);
}
List<Item> result = em.createQuery(jpql, Item.class)
.getResultList();
return result;
}
from
다음에 Item
엔티티 객체 이름이 들어간다. 엔티티 객체와 속성의 대소문자는 구분해야 한다."select i from Item i"
결과적으로 JPQL을 실행하면 그 안에 포함된 엔티티 객체의 매핑 정보를 활용하여 SQL을 만들게 된다.
select i from Item i
where i.itemName like concat('%',:itemName,'%')
and i.price <= :maxPrice
select
item0_.id as id1_0_,
item0_.item_name as item_nam2_0_,
item0_.price as price3_0_,
item0_.quantity as quantity4_0_
from item item0_
where (item0_.item_name like ('%'||?||'%'))
and item0_.price<=?
:
)를 통해 입력한다.where price <= :maxPrice
query.setParameter("maxPrice", maxPrice)
동적 쿼리 문제
JPA를 사용해도 동적 쿼리 문제가 남아있다. 동적 쿼리는 뒤에서 설명하는 Querydsl이라는 기술을 활용하면 매우 깔끔하게 사용할 수 있다. 실무에서는 동적 쿼리 문제 때문에, JPA 사용할 때 Querydsl도 함께 선택하게 된다.
@Repository
@Transactional
public class JpaItemRepository implements ItemRepository {
private final EntityManager em;
public JpaItemRepository(EntityManager em) {
this.em = em;
}
@Override
public Item save(Item item) {
em.persist(item);
return item;
}
}
EntityManager
는 순수한 JPA 기술이고, 스프링과는 관계가 없다. 따라서 엔티티 매니저는 예외가 발생하면 JPA 관련 예외를 발생시킨다.PersistenceException
과 그 하위 예외를 발생시킨다.IllegalStateException
, IllegalArgumentException
을 발생시킬 수 있다.DataAccessException
)로 어떻게 변환할 수 있을까?@Repository
에 있다.실제로
@Repository
,@Transactional
애노테이션에 주석을 달면 예외에 AOP가 적용되는 것을 확인할 수 있다.
@Repository
가 붙은 클래스는 컴포넌트 스캔의 대상이 된다.@Repository
가 붙은 클래스는 예외 변환 AOP의 적용 대상이 된다.PersistenceExceptionTranslator
)를 등록한다.결과적으로 리포지토리에 @Repository
애노테이션만 있으면 스프링이 예외 변환을 처리하는 AOP를 만들어준다.
참고
스프링부트는
PersistenceExceptionTranslationPostProcessor
를 자동으로 등록하는데, 여기에서@Repsoitory
를 AOP 프록시로 만드는 어드바이저가 등록된다.
참고
복잡한 과정을 거쳐서 실제 예외를 변환하는데, 실제 JPA 예외를 변환하는 코드는
EntityManagerFactoryUtils.convertJpaAccessExceptionIfPossible()
이다.