ORM(Object Relatinal Mapping)이란 객체는 객체대로, 관계형 데이터베이스는 관계형 데이터베이스대로 설계를 하고 둘 사이의 관계를 중간에서 매핑시켜 관계형 데이터베이스를 객체지향적으로 사용하게 해주는 기술이다. JPA는 자바 애플리케이션과 JDBC 사이에서 JDBC API를 이용해 DB에 SQL 전달하여 동작하는 자바 ORM 기술 표준 인터페이스로 대표적인 구현체로는 하이버네이트 JPA가 있다.

 

먼저 JPA를 사용하려면 EntityManagerFactory, EntityManager, 영속성 컨텍스트, 트랜잭션에 대해 알아야 한다.

 

EntityManagerFactory는 웹서버가 올라오는 시점에 DB당 하나만 생성되어 애플리케이션 전체에서 공유되며 멀티 스레드 환경에서 사용할 수 있으며 createEntityManager()를 통해 EntityManager를 생성할 수 있다.

엔티티 매니저트랜잭션을 수행하고 엔티티, SQL을 관리하며 커넥션을 통해 DB에 접근하는 등 핵심 동작을 담당하는 중요한 역할을 하며 트랜잭션에 대해 생각해보면 당연히 스레드 간에 공유는 불가능하며 트랜잭션 수행 후에 반드시 소멸(close) 시켜야한다.

 

 

[DB 접근 기술1] 트랜젝션(Transaction) 기초

트랜젝션 데이터를 저장할 때 단순 파일이 아닌 데이터베이스에 저장하는 가장 큰 이유는 데이터베이스가 트랜젝션이라는 개념을 지원하기 때문이다. 트랜젝션은 데이터베이스에서 하나의 거

treecode.tistory.com

 

 

// Persistence에서 하나의 EntityManagerFactory를 생성해서 애플리케이션 전체에서 공유
EntityManagerFactory emf = Persistence.createEntityManagerFactory("hello");

// EntityManagerFactory에서 EntityManager 생성, 쓰레드간에 공유할 수 없고 사용하고 버려야 한다.
EntityManager em = emf.createEntityManager();

// JPA의 모든 데이터 변경은 트랜잭션 안에서 실행
EntityTransaction tx = em.getTransaction();

tx.begin();

try {
	Member member = new Member();
	member.setId(1L);
	member.setName("Kim");
	
	//save
	em.persist(member);
	//read
	Member findMember = em.find(Member.class, 1L);
	//update
	findMember.setName("Lee");
	//delete
	em.remove();

	tx.commit();
} catch (Exception e) {
    // 문제가 생기면 롤백
	tx.rollback();
} finally {
	// 나중에 생성된 순서대로 close
	em.close();	
}
emf.close();

 

 

영속성 컨텍스트

영속성 컨텍스트는 자바 애플리케이션과 데이터베이스 사이에서 엔티티를 저장, 관리하는 논리적인 개념으로 엔티티 매니저를 통해 접근할 수 있다.

 

엔티티의 생명 주기

  • 비영속(new/transient) : 영속성 컨텍스트와 전혀 관계가 없는 새로운 상태 (new Member)
  • 영속(managed) : 영속성 컨텍스트에 관리되는 상태 (em.persist)
  • 준영속 (datached) : 영속성 컨텍스트에 저장되었다가 분리된 상태 (em.detach)
  • 삭제 (removed) : 삭제된 상태 (em.remove)

 

 

특징

 

1) 1차 캐시

 

영속성 컨텍스트는 애플리케이션과 데이터베이스 사이에서 엔티티를 저장, 관리하기 위해서 내부에 1차 캐시 저장소를 가지고 있는데 엔티티를 persist() 하면 1차 캐시에 Key(@Id, PK) Value(Entity), 최초 상태의 엔티티를 복사해둔 스냅샷(변경 감지)을 저장해둔다.

 

2) 동일성

 

엔티티 매니저를 통해 조회(em.find)를 하면 먼저 1차 캐시를 확인해서 엔티티 참조값을 반환해주기 때문에 새로운 객체를 생성하지 않고 동일성을 보장한다. 조회하려는 엔티티가 1차 캐시에 없을 경우 DB에서 조회를 해서 1차 캐시에 저장을 하고 반환을 해준다.

 

3) 쓰기 지연

 

영속성 컨텍스트는 SQL을 쓰기 지연 저장소에 보관해두었다가 flush()가 호출되는 시점에 SQL을 버퍼처럼 모아서 전송한다.

 

4) 변경 감지

 

1차 캐시에 엔티티 스냅샷을 만들어두고 flush() 호출 시, 엔티티와 스냅샷을 비교하여 변경이 있을 경우 쓰기 지연 저장소에 update 쿼리를 추가하고 flush()를 실행한다.

 

5) 지연 로딩

 

JPA에서 테이블 간 연관 관계는 객체의 참조를 통해 이루어지는데 이는 하나의 객체를 조회하려다가 해당 객체가 참조하는 N개의 객체를 전부 조회하게 되는(N+1) 등의 문제를 발생시킬 수 있다. 이를 방지하기 위해 엔티티가 실제 사용되기 전까지 DB 조회를 지연하는 지연 로딩(Lazy Loading) 전략을 지원한다.


JPQL

 

JPQL은 객체 지향 쿼리를 작성하기 위해 JPA가 제공하는 문법으로 SQL을 추상화하여 특정 데이터베이스에 의존하지 않는다.

JPQL은 SQL을 바로 실행하기 때문에 영속 엔티티가 DB 동기화 되지 않아 의도하지 않은 결과가 반환될 수 있어 JPA는 쿼리를 실행할 때 플러시를 자동 호출하는 것을 기본값으로 사용한다. 

em.persist(memberA);
em.persist(memberB);
em.persist(memberC);

// JPQL 실행
query = em.createQuery("select m from Member m", Member.class);

// flush 호출이 안 됐다고 하면 memberA,B,C가 없는 상태인데 의도한 것은 memberA,B,C가 포함된 리스트
List<Member> members= query.getResultList();
더보기

JPQL는 조회 시 영속성 컨텍스트가 아닌 데이터베이스에 우선적으로 조회를 하고 반환값을 1차 캐시에 저장하는데 이때 해당 엔티티가 이미 1차 캐시에 존재하는 경우 반환값은 버리고 1차 캐시의 값을 반환한다.

 

이는 JPQL 조회 기능의 트랜잭션 격리 수준이 REPEATABLE READ이기 때문인데 이 단계에서는 하나의 트랜잭션 내에서 같은 SELECT 쿼리를 계속 실행하면 항상 같은 결과가 반환되어야 한다. (트랜잭션 격리 수준이란 동시에 여러 트랜잭션이 처리될 때, 특정 트랜잭션이 변경, 조회중인 데이터를 다른 트랜잭션에서 어느정도까지 접근 가능한지 레벨을 나눈 것) REPEATABLE READ는 MySQL의 InnoDB 스토리지 엔진에서 기본적으로 사용되는 격리수준인데 이는 트랜잭션의 롤백 가능성에 대비해 변경 전 레코드를 UNDO 영역에 백업해두고 실제 레코드 값을 변경하는 MVCC 방식을 사용한다.

 

모든 InnoDB 트랜잭션은 순차적으로 증가하는 고유한 트랜잭션 번호를 가지며 UNDO 영역에 백업된 레코드에는 변경을 발생시킨 트랜잭션의 번호가 저장되어 있다. 그래서 이 번호를 보고 어떤 데이터를 보여줄지 결정하게 되는데 트랜잭션 10번이 조회한 값을 트랜잭션 12번이 변경하고 커밋한 경우 트랜잭션 10번은 다시 이 값을 조회하면 바뀐 값이 아닌 변경 전 UNDO 영역의 값이 보여지는 것이다. (더티 리드 방지)

 

[참고]

https://cheese10yun.github.io/jpa-jpql/

 

 

플러시 (flush)

 

플러시는 쓰기 지연 SQL 저장소의 쿼리를 DB에 전송하여 영속성 컨텍스트의 변경 내용을 DB에 동기화 하는데 직접 수동으로 호출하거나 트랜젝션 커밋, JPQL 쿼리 실행에 의해 자동 호출이 된다. (JPA가 JPQL 쿼리 실행시 flush를 자동 호출하는 이유는 JPQL은 우선적으로 데이터베이스에서 조회를 하기 때문에 영속 상태의 엔티티가 조회가 안 되는   )

변경 감지를 통해 1차 캐시 엔티티의 변경 사항(스냅샷을 통해 비교)이 있을 경우 update 쿼리를 flush() 직전에 생성해주기 때문에 사용자는 UPDATE가 필요한 경우 엔티티의 값만 바꿔주면 된다. 영속성 컨텍스트를 비우는 것이 아니며 트랜젝션이라는 작업 단위가 중요하다.

 

 

준영속 상태

 

em.detach(entity) : 특정 엔티티만 준영속 상태로 전환

em.clear() : 영속성 컨텍스트를 완전히 초기화

em.close() : 영속성 컨텍스트를 종료

 

 

 

 

[참고]

 

JPA JPQL의 조회 동작 살펴보기 - Yun Blog | 기술 블로그

JPA JPQL의 조회 동작 살펴보기 - Yun Blog | 기술 블로그

cheese10yun.github.io

 

 

[JPA] 영속성 컨텍스트(Persistence Context)란 - Heee's Development Blog

Step by step goes a long way.

gmlwjd9405.github.io

 

자바 ORM 표준 JPA 프로그래밍 - 기본편 - 인프런 | 강의

JPA를 처음 접하거나, 실무에서 JPA를 사용하지만 기본 이론이 부족하신 분들이 JPA의 기본 이론을 탄탄하게 학습해서 초보자도 실무에서 자신있게 JPA를 사용할 수 있습니다., - 강의 소개 | 인프런

www.inflearn.com

 

 

 

[Java] 자바 예외 (Exception)

자바 예외는 최상위 예외 계층인 Throwable, 그 하위로 Exception과 Error가 있는데 Error는 메모리 부족 같이 애플리케이션에서 복구 불가능한 시스템 예외인데 catch로 예외를 잡으면 하위 예외(Error)까

treecode.tistory.com

 

[DB 접근 기술1] 스프링 트랜잭션

일반적인 웹 애플리케이션 구조는 다음과 같다. 프레젠테이션 계층 (@Controller) 서비스 계층 (@Service) 데이터 접근 계층 (@Repository) 여기서 가장 중요한 곳은 핵심 비즈니스 로직이 있는 서비스 계

treecode.tistory.com

 

스프링 트랜잭션 AOP를 사용하면 트랜잭션 로직과 비즈니스 로직을 깔끔하게 분리할 수 있는데 예외 처리에 대한 의존까지는 해결할 수 없다. 리포지토리에서 서비스가 처리할 수 없는 체크 예외를 던지면 서비스 계층에 불필요한 예외 의존 관계가 생기는데 체크 예외를 런타임 예외로 전환해서 던지면 서비스 계층을 순수하게 유지할 수 있다. (체크 예외의 경우 인터페이스에서도 throws를 선언해야 한다.)

 

try {
    ...
} catch (SQLException e) {
    // RuntimeException을 상속 받은 예외 생성
    // 예외 e를 파라미터로 보내줘야 나중에 예외 출력 시 기존 예외를 확인할 수 있다.
    throw new MyException(e);
} finally {
    ...
}

 

데이터베이스에서 발생한 예외중 특정 상황에는 예외를 잡아서 복구하고 싶은 경우 예외의 ErrorCode를 확인해서 새로운 예외로 변환하면 되는데 데이터베이스마다 똑같은 오류라도 ErrorCode가 다 다르기 때문에 직접 ErrorCode를 확인해서 처리하기는 어렵다.

 

스프링은 데이터 접근과 관련된 예외를 추상화해서 제공하는데 각각의 예외는 특정 기술에 종속적이지 않게 설계되어 있고 심지어 스프링이 제공하는 예외로 변환하는 예외 변환기가 ErrorCode를 확인해서 적절한 예외로 변환까지 해준다.

 

스프링이 제공하는 데이터 접근 계층 예외는 RuntimeException을 상속 받은 DataAccessException로 크게 두가지로 나뉜다.

  • Transient : 일시적인 오류로 동일한 SQL로 다시 시도하면 성공할 가능성이 있는 경우 (쿼리 타임아웃, 락 관련 오류 등)
  • NonTransient : SQL 문법 오류, 데이터베이스 제약 조건 위배 등 일시적이지 않은 오류

 

 

 

[참고] 인프런 김영한님 강의를 공부한 내용입니다.

 

 

일반적인 웹 애플리케이션 구조는 다음과 같다.

  • 프레젠테이션 계층 (@Controller)
  • 서비스 계층 (@Service)
  • 데이터 접근 계층 (@Repository)

 

여기서 가장 중요한 곳은 핵심 비즈니스 로직이 있는 서비스 계층으로 비즈니스 로직은 최대한 변경 없이 순수하게 유지되도록 특정 기술에 종속적이지 않게 개발하는 것이 좋다. 서비스 계층을 특정 기술에 종속적이지 않게 개발하기 위해 다른 계층에서 기술에 종속적인 부분을 가지고 가는데 프레젠테이션 계층UI와 관련된 기술인 웹, 서블릿, HTTP와 관련된 부분을 담당하고 데이터 접근 계층JDBC, JPA와 같은 데이터 접근 기술을 담당한다. 서비스 계층은 데이터 접근 계층에 직접 접근하지 않고 인터페이스를 통해 접근하면 비즈니스 로직을 유지보수, 테스트 하기 쉽게 관리할 수 있다.

 

JDBC로 서비스 계층에서 트랜젝션을 시작하면 DataSource, Connection 같은 JDBC 기술에 의존하게 되는데 JDBC 코드를 데이터 계층으로 옮기고 인터페이스로 제공한다 해도 데이터 계층에서 발생한 JDBC 예외가 서비스 계층으로 전파되고  try, catch 코드가 반복되는 등 여러 문제가 발생한다.

 


 

스프링 트랜잭션

 

스프링은 서비스 계층을 순수하게 유지하면서 위의 문제들을 해결할 수 있는 기술을 제공한다.

 

트랜잭션 매니저(TransactionManager)

 

스프링은 PlatformTransactionManager 라는 인터페이스를 제공하는데 이는 트랜잭션 추상화를 통해 데이터 접근 기술이 바뀌면 트랜젝션 코드를 수정해야 하는 등의 문제를 해결해주고 각각의 기술에 맞는 구현체(JpaTransactionManage..)도 직접 만들어 제공한다.

 

트랜잭션 매니저는 내부적으로 트랜잭션 동기화 매니저를 사용하는데 이는 쓰레드 로컬을 사용해서 트랜젝션의 시작부터 끝까지 같은 커넥션을 유지할 수 있도록 커넥션을 동기화 해주고 쓰레드 로컬을 사용하기 때문에 멀티 쓰레드 상황에서 안전하게 커넥션을 동기화 할 수 있다.

 

트랜젝션 매니저는 트랜젝션을 시작하면 트랜젝션 동기화 매니저를 통해 쓰레드 로컬에 커넥션을 보관하는데 리포지토리는 트랜젝션 동기화 매니저를 통해 동기화된 커넥션을 꺼내서 획득한 커넥션으로 SQL을 데이터베이스에 전달해서 실행한다.

트랜잭션은 커밋하거나 롤백하면 종료가 되는데 트랜잭션 동기화 매니저를 통해 동기화된 커넥션을 획득하고 데이터베이스에 트랜잭션을 커밋하거나 롤백한 뒤에 전체 리소스를 정리한다. 트랜잭션 동기화 매니저는 쓰레드 로컬을 사용하기 때문에 사용 후 반드시 리소스를 정리해야 한다.

 

추가로 스프링은 TransactionTemplate이라는 템플릿 클래스를 제공하는데 트랜잭션 템플릿을 사용하면 트랜잭션을 시작하고, 커밋하거나 롤백하는 코드를 전부 제거할 수 있다. (비즈니스 로직이 정상 수행되거나 체크 예외가 발생하면 커밋하고 언체크 예외는 발생 시 롤백)

 

하지만 트랜잭션 템플릿을 사용해도 비즈니스 로직과 트랜잭션 로직을 완전히 분리할 수는 없는데 스프링 AOP를 통해 프록시를 도입하면 해당 문제를 깔끔하게 해결할 수 있다.

 


 

스프링 트랜잭션 AOP

 

스프링 AOP를 통해 트랜잭션 프록시를 사용하면 서비스 계층에서 트랜잭션을 생성했던 것과 달리 트랜잭션 프록시 객체에서 트랜잭션을 생성해서 비즈니스 로직을 호출을 하는 방식으로 바뀌어 트랜잭션과 비즈니스 로직을 처리하는 객체를 명확하게 분리할 수 있다.

스프링이 제공하는 트랜잭션 AOP를 사용하면 프록시를 매우 편리하게 적용할 수 있는데 스프링 부트는 트랜잭션 AOP를 처리하기 위해 필요한 스프링 빈들(어드바이저, 포인트컷, 어드바이스)을 자동으로 등록해준다. 

 

스프링이 제공하는 트랜잭션 AOP를 사용하려면 메서드나 클래스에 @Transactional 애노테이션을 추가하면 되는데 클래스에 붙이면 외부에서 호출 가능한 public 메서드에 AOP 적용이 된다.

 

 

간단한 흐름

 

1. 트랜잭션 AOP 프록시 트랜잭션 시작

2. 스프링 컨테이너에 등록된 트랜잭션 매니저 획득

3. transactionManager.getTransaction()

4. DataSource를 통해 커넥션 조회, 없으면 생성

5. setAutoCommit(false)

6. 트랜잭션 동기화 매니저에 커넥션 보관

7. 트랜잭션 프록시에서 실제 객체의 서비스 로직 호출

8. 리포지토리에서 트랜잭션 동기화 매니저로부터 커넥션 획득

 

스프링 부트는 커넥션 풀을 제공하는 HikariDataSource를 생성해서 스프링 빈으로 등록하고 현재 등록된 라이브러리를 확인하여 데이터 접근 기술에 맞는 PlatformTransactionManager 구현체를 스프링 빈으로 등록(빈 이름 : transactionManager)한다.

트랜젝션

 

데이터를 저장할 때 단순 파일이 아닌 데이터베이스에 저장하는 가장 큰 이유는 데이터베이스가 트랜젝션이라는 개념을 지원하기 때문이다.

트랜젝션은 데이터베이스에서 하나의 거래를 안전하게 처리하도록 보장해주는 것을 뜻하는데 트랜젝션 기능을 사용하면 중간에 문제가 생길 경우 시작 이전으로 되돌릴 수 있다. 작업이 완료되고 데이터베이스에 정상 반영되는 것을 커밋(Commit)이라 하고, 문제가 생겨서 작업 이전으로 되돌리는 것을 롤백(Rollback)이라 한다.

 

 

트랜젝션 ACID 

 

트랜젝션은 원자성(Atomicity), 일관성(Consistency), 격리성(Isolation), 지속성(Durability)를 보장해야 한다.

  • 원자성: 트랜잭션 내에서 실행한 작업들은 마치 하나의 작업처럼 모두 성공 하거나 모두 실패해야 한다.
  • 일관성: 모든 트랜잭션은 일관성 있는 데이터베이스 상태를 유지해야 한다. 예를 들어 데이터베이스에서 정한 무결성 제약 조건을 항상 만족해야 한다.
  • 격리성: 동시에 실행되는 트랜잭션들이 서로에게 영향을 미치지 않도록 격리한다예를 들어 동시에 같은 데이터를 수정하지 못하도록 해야 한다격리성은 동시성과 관련된 성능 이슈로 인해 트랜잭션 격리 수준(JPA  16.1 트랜잭션과 락 참고)을 선택할 수 있다.
  • 지속성: 성공적으로 트랜젝션이 끝나면 그 결과가 항상 기록되어야 한다중간에 시스템에 문제가 발생해도 데이터베이스 로그 등을 사용해서 성공한 트랜잭션 내용을 복구해야 한다.

 


 

데이터베이스 연결 구조와 DB 세션

 

데이터베이스는 커넥션을 연결할 때 내부에 DB 세션을 생성(커넥션 풀이 10개의 커넥션을 생성하면 세션도 10개 생성)하는데 트랜젝션을 시작하고, SQL을 실행하고, 커밋, 롤백, 트랜젝션 종료 등의 모든 요청은 DB 세션을 통해 실행이 된다. 

 

트랜젝션 내부 동작

 

데이터베이스는 커밋을 호출하기 전까지 데이터를 임시로 저장하는데 해당 트랜젝션을 시작한 세션만 변경 데이터(등록, 수정, 삭제)가 보이고 다른 세션은 변경중인 데이터가 보이지 않는데 이는 문제가 생겨서 트랜젝션 롤백이 되는 경우 데이터 정합성에 큰 문제가 생기기 때문이다.

 

트랜젝션에는 자동 커밋과 수동 커밋이 있는데 자동 커밋의 경우 말 그대로 쿼리를 하나 하나 실행할 때마다 자동으로 커밋을 해줘서 편리하지만 트랜젝션 개념이랑 맞지 않기 때문에 수동 커밋 모드(set autocommit false)로 바꾸어 사용하는 것이 트랜젝션의 시작이라고 볼 수 있다.

 

 

DB 락

 

한 세션에서 트랜젝션을 시작하고 데이터를 수정하는 동안 다른 세션이 같은 데이터를 수정하게 되면 트랜젝션의 원자성이 깨지는데 이런 문제를 방지하려면 트랜젝션이 커밋이나 롤백을 하기 전까지 다른 세션에서 해당 데이터를 수정할 수 없도록 막아야 한다.

 

데이터베이스는 이런 문제를 해결하기 위해 락(Lock)이라는 개념을 제공하는데 세션은 트랜젝션을 시작하고 값을 변경하려는 데이터의 row에 대해 먼저 lock을 얻는데 락을 갖고 있는 동안 다른 세션은 해당 row의 데이터를 변경할 수 없다. 락을 획득한 세션이 트랜젝션을 종료하면 락이 반납되고 다른 세션은 대기하다가 락을 획득한 뒤에 데이터를 변경할 수 있으며 락 대기 시간이 넘어가면 타임아웃 오류가 발생한다.

 

데이터베이스마다 다르지만, 락이 걸린 동안에는 다른 세션에서 데이터를 변경하지 못하고 지연이 걸리기 때문에 일반적인 데이터베이스는 조회를 할 때는 락을 사용하지 않고 바로 데이터를 조회할 수 있도록 한다.  조회를 할 때도 락이 필요한 경우  'select for update' 구문을 사용하면 되는데 조회를 통해 중요한 계산을 수행하는 경우 조회 시점에 락을 획득하면 된다.

 


 

트랜젝션 적용

 

트랜젝션은 비즈니스 로직이 있는 서비스 계층에서 시작해야 한다. 비즈니스 로직이 잘못되면 문제가 되는 부분을 함께 롤백해야 하기 때문이다. 서비스 로직에서 트랜젝션을 시작하려면 먼저 커넥션을 획득하고 유지해야 하는데 애플리케이션에서 같은 커넥션을 유지하려면 커넥션을 파라미터로 전달해서 같은 커넥션이 사용되도록 유지해야 한다.

 

하지만 이러한 방법은 서비스 계층이 매우 복잡해지고 커넥션을 유지하기도 어려워지는데 스프링을 사용해서 트랜젝션을 편리하게 사용할 수 있다.

 

 

[참고]  인프런 김영한님 강의를 공부한 내용입니다.

+ Recent posts