SQL Mapper의 한계

- Spring JDBC Template

- MyBatis

 

  1. SQL과 자바 간의 매핑이 필요하다.
  2. 필드가 추가되는 등의 객체 구조가 바뀌는 경우에 SQL문도 수정해야 한다.
  3. 패러다임 불일치 문제가 발생한다.
    • 부모 테이블과 자식 테이블에 각각 데이터를 삽입해야 한다. (DB는 상속 개념 X)
    • 두 테이블을 조인해서 데이터를 꺼내와야 한다.
    • => 데이터의 CRUD가 까다롭기 때문에, DB에 저장할 객체는 상속 관계를 사용하지 않게 된다.

 


JPA

  • 자바의 ORM 기술 표준 인터페이스이다.
  • 대표적인 구현체로 Hibernate를 사용한다.

ORM(Object-Relational Mapping) : 객체와 관계형 DB를 자동으로 Mapping하여 패러다임 불일치 문제 해결

 

JPA의 CRUD

// 저장
jpa.persist(tutor);
// 조회
Tutor tutor = jpa.find(Tutor.class, tutorId);
// 수정
tutor.setName("수정할 이름");
// 삭제
jpa.remove(tutor);

 

 

JPA의 패러다임 불일치 해결

jpa.persist(tutor);
// INSERT INTO person ..., INSERT INTO tutor ... 

Tutor tutor = jpa.find(Tutor.class, tutorId);
// SELECT * FROM tutor t JOIN company c ON t.company_id = c.id

tutor.setCompany(company);
jpa.persist(company);
// Collection처럼 setter로 데이터를 바로 수정 가능

Tutor tutor1 = jpa.find(Tutor.class, tutorId);
Tutor tutor2 = jpa.find(Tutor.class, tutorId);
// 동일한 인스턴스를 가져오기 때문에 동일성 비교 가능

 

이때 동일한 인스턴스를 가져오기 위해서는 두 객체가 동일한 트랜잭션 안에서 생성되어야 한다.

조회한 엔티티를 JPA가 내부적으로 캐싱해두고, 같은 트랜잭션 안에서 해당 엔티티를 조회하면 새로 쿼리를 날리지 않고 이미 조회된 인스턴스를 반환하기 때문이다. (-> 1차 캐시)

 

 

JPA의 성능

1차 캐시

DB에 데이터를 저장하기 전, 1차 캐시에 우선적으로 저장하기 때문에, 같은 데이터를 조회할 때 DB를 거치지 않고 조회 가능하다.

Tutor tutor1 = jpa.find(Tutor.class, tutorId); // 실행 결과 1차 캐시에 저장
Tutor tutor2 = jpa.find(Tutor.class, tutorId); // 캐시에서 조회

tutor1 == tutor2; // true

 

 

쓰기 지연

네트워크 통신을 저장소에 한 번에 모아서 요청할 수 있어서 비용이 감소된다.

@GeneratedValue(strategy = GenerationType.IDENTITY)를 엔티티 PK 필드에 적용하면 쓰기 지연을 무시한다.

// 트랜잭션 시작
transaction.begin();

jpa.persist(company);
jpa.persist(tutor1);
jpa.persist(tutor2);

// 트랜잭션 제출, JDBC BATCH SQL
transaction.commit();

 

배치 처리

hibernate.jdbc.batch_size을 application.properties에 설정해서 한 번에 몇 개의 SQL을 모아서 실행할지 지정할 수 있다.

// hibernate.jdbc.batch_size=10 일 때

for (int i = 1; i <= 25; i++) {
    Member member = new Member();
    member.setId((long) i);
    member.setName("User " + i);
    em.persist(member);  // 영속 상태 (쓰기 지연 저장소에 저장)
}

tx.commit(); // INSERT 10개 실행 -> INSERT 10개 실행 -> INSERT 5개 실행 -> 트랜잭션 종료

 

 

즉시 로딩 : 한 번에 조회

지연 로딩 : 필요할 때 조회

@Entity
public class Tutor {
    @Id
    private Long id;

    @ManyToOne(fetch = FetchType.EAGER) // 즉시 로딩
    private Company company;
}

Tutor tutor = tutorRepository.find(tutorId); // JOIN 사용하여 Tutor, Company 모두 조회
Company company = tutor.getCompany();
String companyName = company.getName();
@Entity
public class Tutor {
    @Id
    private Long id;

    @ManyToOne(fetch = FetchType.LAZY) // 지연 로딩
    private Company company;
}

Tutor tutor = tutorRepository.find(tutorId); // Tutor만 조회
Company company = tutor.getCompany(); // Company 조회 X
String companyName = company.getName(); // Company 조회

 

 


hibernate.dialect

SQL 표준을 지키지 않는 특정 데이터베이스의 고유한 기능(dialect, 방언)을 지정해서, 데이터베이스와 Hibernate가 상호작용할 때 사용 중인 데이터베이스에 맞게 SQL 구문을 자동으로 조정한다.

JPA가 생성하는 SQL과 JPQL(@Query 또는 em.createQuery())로 작성한 SQL문이 자동으로 조정된다.

 


hibernate.hbm2ddl.auto

애플리케이션 실행 시점에 DB 테이블을 어떻게 생성/관리할지 결정한다.


영속성 컨텍스트

Entity 객체를 영속성 상태로 관리하는 캐시 역할을 하는 공간이다.

DB와 자동으로 동기화되며 같은 트랜잭션 안에서는 동일한 객체가 유지된다.

영속성 컨텍스트에 접근하기 위해서는 Entity Manager를 통해야 한다.

 

Entity의 생명주기

JPA에서 Entity란 데이터베이스의 테이블을 나타내는 클래스를 의미한다.

  1. 비영속(new/transient)
    • 영속성 컨텍스트가 모르는 새로운 상태
    • 데이터베이스와 전혀 연관이 없는 객체 (=자바 세상에만 데이터가 존재하는상태)
  2. 영속(managed)
    • 영속성 컨텍스트에 저장되고 관리되고 있는 상태
    • 1차 캐시에 저장
  3. 준영속(detached)
    • 영속성 컨텍스트에 저장되었다가 분리되어 더 이상 기억하지 않는 상태
  4. 삭제(removed)
    • 영속성 컨텍스트에 의해 삭제로 표시된 상태
    • 트랜잭션이 끝나면 데이터베이스에서 제거
public static void main(String[] args) {
    // EntityManagerFactory 생성
    EntityManagerFactory emf = Persistence.createEntityManagerFactory("test");
    // EntityManager 생성
    EntityManager em = emf.createEntityManager();
    // Transaction 생성
    EntityTransaction transaction = em.getTransaction();
    // 트랜잭션 시작
    transaction.begin();

    try {
        Tutor tutor = new Tutor(1L, "wonuk", 100); // 비영속

        System.out.println("persist 전");
        em.persist(tutor); // EntityManager가 Tutor 객체 저장 -> 영속
        System.out.println("persist 후");

        // SQL 실행
        transaction.commit();
    } catch (Exception e) {
        // 실패 -> 롤백
        e.printStackTrace();
        transaction.rollback();
    } finally {
        // 엔티티 매니저 연결 종료
        em.close();
    }
    emf.close();
}
준영속 상태로 만드는 법

// 1. 특정 Entity만 준영속 상태로 변경
em.detach()

// 2. 영속성 컨텍스트 초기화
em.clear()

// 3. 영속성 컨텍스트 종료
em.close()

 


변경 감지(Dirty Checking)

영속성 컨텍스트가 엔티티의 초기 상태를 저장하고 트랜잭션 커밋 시점에 현재 상태와 비교해 변경 사항이 있는지 확인한다.

 

내부 동작

1. 값을 조회한 최초 시점의 상태를 1차 캐시와 snapshot에 저장

...

2. flush()가 수행될 때, JPA가 snapshot과 현재 엔티티 상태를 비교 (변경 감지)

3. 엔티티에 변경이 있으면 DB에 commit()

 

=> setter로 값을 변경하고 update 과정을 거치지 않아도 자동으로 변경된 부분을 DB에 커밋

flush()를 수행하지 않는 경우는 commit() 호출 시 자동으로 flush()가 실행되면서 변경 감지

 


필드 매핑

JPA로 관리되는 클래스인 Entity의 필드는 테이블의 컬럼과 매핑된다.

@Entity
@Table(name = "board")
public class Board {
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;

    // @Column을 사용하지 않아도 자동으로 매핑된다.
    private Integer view;

    // 객체 필드 이름과 DB 이름을 다르게 설정할 수 있다.
    @Column(name = "title")
    private String bigTitle;

    // DB에는 기본적으로 enum이 없다.
    @Enumerated(EnumType.STRING)
    private BoardType boardType;

    // VARCHAR()를 넘어서는 큰 용량의 문자열을 저장할 수 있다.
    @Column(columnDefinition = "longtext")
    private String contents;

    // 날짜 타입 DATE, TIME, TIMESTAMP를 사용할 수 있다.
    @Temporal(TemporalType.TIMESTAMP)
    private Date createdDate;

    @Temporal(TemporalType.TIMESTAMP)
    private Date lastModifiedDate;
    
    @Transient
    private int count;

    public Board() {
    }
}

 

EnumType.ORDINAL을 사용하면 Enum 값이 추가될 때 마다 순서가 바뀌기 때문에 실제로 사용하지 않는다.

 


연관관계 Mapping

단방향

@Entity
@Table(name = "tutor")
public class Tutor {
    @Id @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;
    private String name;
    
    // N:1 단방향 연관관계 설정
    @ManyToOne
    @JoinColumn(name = "company_id")
    private Company company;
		
    // 기본 생성자, getter/setter
}

양방향

 

@Entity
@Table(name = "company")
public class Company {
    @Id @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;
    private String name;
	
    // 양방향 연관관계 설정(mappedBy)
    @OneToMany(mappedBy = "company")
    // null을 방지하기 위해 ArrayList로 초기화 한다.(관례)
    private List<Tutor> tutors = new ArrayList<>(); 
		
    // 기본 생성자, getter/setter
}

 

mappedBy는 두 엔티티 간의 관계에서 연관관계의 주인이 아닌 쪽에 선언한다.

연관관계의 주인 선정 기준

- 항상 FK가 있는 곳을 연관관계의 주인으로 지정한다.

- Company가 주인인 경우 Company를 수정할 때 Tutor를 Update하는 SQL이 실행된다.(SQL문 반복 실행 발생)


JPA와 Spring Data JPA

JPA

  • 자동으로 내부에서 EntityManagerFactory와 TransactionManager를 싱글톤으로 관리한다.
  • @PersistenceContext를 통해 자동으로 생성된 EntityManager를 주입받아 사용할 수 있다.
@Repository
public class TutorRepository {
    
    @PersistenceContext
    private EntityManager em;

    public void save(Tutor tutor) {
        em.persist(tutor);
    }

    public Tutor findById(Long id) {
        return em.find(Tutor.class, id);
    }

    public List<Tutor> findAll() {
        return em.createQuery("SELECT * FROM tutor", Tutor.class).getResultList();
    }

    public void delete(Tutor tutor) {
        em.remove(tutor);
    }
}

 

Spring Boot는 프록시(가짜 객체)를 싱글톤으로 등록해 요청마다 별도의 EntityManager 인스턴스를 제공하여,
각 요청은 독립적으로 EntityManager를 사용해 안전하게 데이터베이스 작업을 처리할 수 있게 된다.
따라서 EntityManager는 동시성 문제 방지를 위해 싱글톤으로 등록되지 않는다.


+ JPA는 지연 로딩 기능을 지원할 때도, 프록시 객체를 생성한다.
프록시 객체는 엔티티의 기본 생성자를 호출하여 빈 객체를 만들고 필드값을 채워넣는 방식으로 생성되기 때문에, 엔티티는 기본 생성자를 필수로 가져야 한다.

 

Spring Data JPA

public interface MemberRepository extends JpaRepository<Member, Long> {
		// JPA Query Methods
		public Member findById(Long id);
}

  1. JPA 추상화 Repository 제공
    • CrudRepository, JpaRepository 인터페이스를 제공한다.
    • SQL이나 EntityManager를 직접 호출하지 않아도 기본적인 CRUD 기능을 손쉽게 구현할 수 있다.
  2. JPA 구현체와 통합
    • 일반적으로 Hibernate를 통해 자동으로 SQL이 생성된다.
  3. QueryMethods
    • Method 이름만으로 SQL을 자동으로 생성한다.
    • @Query 를 사용하여 JPQL 또는 Native Query를 정의할 수 있다.
      • 복잡한 SQL을 직접 구현할 때 사용
  4. 트랜잭션 관리와 LazyLoading
    • 트랜잭션 기능을 Spring과 통합하여 제공한다.
    • 연관된 Entity를 필요할 때 로딩하는 지연로딩 기능을 지원한다.

 

SimpleJpaRepository

Spring Data JPA의 기본 Repository 구현체로 JpaRepository 인터페이스의 기본 메서드들을 실제로 수행하는 클래스이다.

내부적으로 EntityManager를 사용하여 JPA Entity를 DB에 CRUD 방식으로 저장하고 관리하는 기능을 제공한다.

public interface MemberRepository extends JpaRepository<Member, Long> {} 
// JpaRepository< entity타입, id타입 >
  • Spring이 실행되면서 JpaRepository 인터페이스를 상속받은 인터페이스가 있다면, 해당 인터페이스의 정보를 토대로 SimpleJpaRepository 를 생성하고 Bean으로 등록한다.
  • 인터페이스의 구현 클래스를 직접 만들지 않아도 JpaRepository 의 기능을 사용할 수 있다.
  • 개발자가 직접 SimpleJpaRepository를 사용하거나 참조할 필요는 없다.
  • 제네릭에 선언된 엔티티와 매핑되는 테이블의 SQL이 생성된다.

 

Query Methods

public interface MemberRepository extends JpaRepository<Member, Long> {
    // Query Methods
    Member findByNameAndAddress(String name, String address);
}

// 자동으로 생성되어 실제로 실행되는 SQL
SELECT * FROM member WHERE name = ? AND address = ?;
  1. find : Entity에 매핑된 테이블(member)을 조회한다.
  2. ByName : 조건은 member 테이블의 name 필드이다.
  3. AndAddress : 또다른 조건은 member 테이블의 address 필드이다.

https://docs.spring.io/spring-data/jpa/reference/jpa/query-methods.html

 

JPA Query Methods :: Spring Data JPA

By default, Spring Data JPA uses position-based parameter binding, as described in all the preceding examples. This makes query methods a little error-prone when refactoring regarding the parameter position. To solve this issue, you can use @Param annotati

docs.spring.io

 

JPA Auditing

엔티티의 생성 및 수정 시간을 자동으로 관리해주는 기능이다.

@EnableJpaAuditing // JPA Auditing 기능을 활성화
@SpringBootApplication
public class SpringDataJpaApplication {
    public static void main(String[] args) {
        SpringApplication.run(SpringDataJpaApplication.class, args);
    }
}
@Getter
// 클래스를 상속받는 Entity에 공통 매핑 정보 제공
@MappedSuperclass 
// Entity를 DB에 적용하기 전, 커스텀 콜백 요청 (내부적으로 @PrePersist 사용)
@EntityListeners(AuditingEntityListener.class)
public class BaseEntity{
    
    @CreatedDate // 생성 시점의 날짜 자동 기록
    @Column(updatable = false)
    @Temporal(TemporalType.TIMESTAMP) // 날짜 타입을 세부적으로 지정
    private LocalDateTime createdAt;

    @LastModifiedDate // 수정 시점의 날짜 자동 기록
    @Temporal(TemporalType.TIMESTAMP)
    private LocalDateTime modifiedAt;
}
@Entity
public class User extends BaseEntity{
    @Id
    private Long id;
    private String name;
}