JAVA ORM JPA

김소희·2024년 11월 7일

JDBC를 JDBC 템플릿을 이용하게 됨으로써 코드는 확 줄었지만 여전히 개발자가 sql문을 작성해야하는 숙제가 남아있다. 그런데 JPA를 사용하면 쿼리도 자동으로 작성해준다.
구글 트렌드에서 검색해보면 세계적으로 JPA가 압도적이고, 2015년 이후로 국내에서도 마이바티스보다 JPA가 보급이 많이 되었다.
스프링에서도 JPA를 굉장히 많이 지원하고 있으므로 추후에 김영한님의 자바 ORM 표준 JPA 프로그래밍 책을 사서 심도있게 다뤄볼 예정이다.

JPA

JPA(Java Persistence API)는 Java Persistence API의 줄임말로 자바 진영의 ORM 기술 표준이다. JPA는 인터페이스 모음이므로, 이 인터페이스를 구현한 실제 클래스가 필요하다. JPA를 구현한 실제 클래스에는 대표적으로 하이버네이트(Hibernate)가 있다.

ORM이란 무엇일까?
ORM(Object-Relational Mapping)은 이름 그대로 객체와 관계형 데이터베이스를 매핑 한다는 뜻이다. ORM 프레임워크는 객체와 테이블을 매핑 해서 패러다임의 불일치 문제를 개발자 대신 해결해준다.
따라서 객체 측면에서는 정교한 객체 모델링을 할 수 있고 관계형 데이터베이스는 데이터베이스에 맞도록 모델링하면 된다. 그리고 둘을 어떻게 매핑 해야 하는지 매핑 방법만 ORM 프레임워크에게 알려주면 된다.

  • JPA는 기존의 반복코드는 물론이고, 기본적인 SQL도 JPA가 직접 만들어서 실행해 준다.
  • JPA를 사용하면, SQL과 데이터 중심의 설계에서 객체 중심의 설계로 패러다임을 전환할 수 있다.
    데이터 엑세스 기술유형 - SQL중심기술과 객체중심기술의 차이점을 자세히 알고싶다면
  • JPA를 사용하면 개발 생산성을 크게 높일 수 있다.
  • JPA는 자바진영의 표준 인터페이스로 구현체로 Hibernate, EclipseLink등 구현 기술로 여러개의 벤더들이 있는데 기본적으로 Hibernate 구현체를 사용된다고 보면 된다.
  • JPA는 ORM기술로 object(객체와), reletional(관계형 데이터베이스를), mapping(매핑)한다고 보면 되는데 mapping은 @Entity 애너테이션을 이용한다.

JPA와 데이터베이스 방언(Dialect)

JPA는 특정 DB에 종속되지 않는 기술이다.
하지만 DB마다 SQL 문법과 함수, 타입이 다르다.
이 차이를 해결하기 위해 방언(Dialect) 이 존재한다.
→ JPA가 사용하는 SQL을 해당 DB 문법에 맞게 변환해주어 차이를 흡수한다.
따라서 DB를 변경하더라도 코드 수정 없이 Dialect 설정만 바꾸고 DB를 교체할 수 있다.

2. DB별 차이 예시

구분MySQLOracle
문자형 타입VARCHARVARCHAR2
문자열 자르기SUBSTRING()SUBSTR()
페이징LIMITROWNUM

JPA 기본 키 생성 전략 정리

JPA는 다양한 데이터베이스 환경에 맞게 여러 키 생성 전략을 제공한다.
각 전략의 동작 시점과 성능 특성을 이해하고, 사용하는 DB에 맞게 선택하는 것이 중요하다.

1. IDENTITY 전략

  • DB가 기본 키를 생성한다.
  • AUTO_INCREMENT를 사용하는 MySQL 등에 적합하다.
  • em.persist() 시점에 즉시 INSERT 쿼리가 실행된다.
  • INSERT 후에 식별자 값을 조회할 수 있다.
  • 쓰기 지연(transactional write-behind)이 동작하지 않는다.
private static void logic(EntityManager em) {  
    Board board = new Board();  
    em.persist(board);  
    System.out.println("board.id = " + board.getId());  
}
// 출력: board.id = 1

2. SEQUENCE 전략

  • 데이터베이스의 시퀀스 객체를 사용한다.
  • Oracle, PostgreSQL 등에 적합하다.
@Entity
@SequenceGenerator(
    name = "BOARD_SEQ_GENERATOR",
    sequenceName = "BOARD_SEQ",  // 실제 DB 시퀀스 이름
    initialValue = 1,
    allocationSize = 1
)
public class Board {

    @Id
    @GeneratedValue(
        strategy = GenerationType.SEQUENCE,
        generator = "BOARD_SEQ_GENERATOR"
    )
    private Long id;
}

allocationSize 기본값이 50인 이유

  • 시퀀스를 한 번에 50 증가시켜 메모리에서 1~50을 순차로 사용한다.
  • 시퀀스 호출 횟수를 줄여 성능 최적화 가능.
  • 여러 JVM이 동시에 동작해도 PK 충돌이 발생하지 않는다.

단, DB에 직접 접근해 데이터를 삽입할 경우 시퀀스 값이 크게 증가한 것처럼 보일 수 있다.
hibernate.id.new_generator_mappings=true 설정 시 위 최적화가 적용된다.


3. TABLE 전략

  • 키 생성 전용 테이블을 별도로 만들어 시퀀스를 흉내낸다.
  • 어떤 DB에서도 사용 가능하지만 성능이 가장 느리다.
  • 새로운 키를 조회하기 위해 SELECT
  • 다음 키로 증가시키기 위해 UPDATE 실행
  • DB와 두 번 통신하기 때문에 SEQUENCE보다 느리다.
  • allocationSize를 활용해 최적화할 수 있다.
@Entity
@TableGenerator(
    name = "BOARD_SEQ_GENERATOR",
    table = "MY_SEQUENCES",
    pkColumnValue = "BOARD_SEQ",
    allocationSize = 1
)
public class Board {

    @Id
    @GeneratedValue(
        strategy = GenerationType.TABLE,
        generator = "BOARD_SEQ_GENERATOR"
    )
    private Long id;
}

4. AUTO 전략

  • JPA가 사용하는 DB 방언(Dialect) 에 따라
    IDENTITY, SEQUENCE, TABLE 중 자동 선택한다.
  • DB를 변경해도 코드를 수정할 필요가 없다.
  • 단, 선택된 전략이 SEQUENCE나 TABLE이라면 시퀀스나 키 생성용 테이블을 미리 만들어 두어야 한다.
@Id
@GeneratedValue(strategy = GenerationType.AUTO)
private Long id;

기본 키 생성 요약

전략키 생성 주체대표 DB장점단점
IDENTITYDBMySQL단순, 설정 간단쓰기 지연 불가
SEQUENCEDB 시퀀스Oracle, PostgreSQL빠르고 안전DB 의존
TABLE키 생성 테이블모든 DB이식성 높음가장 느림
AUTOJPA 자동 선택모든 DBDB 교체 용이전략 예측 어려움

JPA 사용전 작업

build.gradle 파일에 JPA와 데이터베이스(여기서는 H2) 관련 라이브러리를 추가한다.
jpa는 jdbc관련 라이브러리를 포함하고 있으므로 Jdbc 라이브러리는 지워도 된다.

dependencies {
    implementation 'org.springframework.boot:spring-boot-starter-thymeleaf'
    implementation 'org.springframework.boot:spring-boot-starter-web'
    // implementation 'org.springframework.boot:spring-boot-starter-jdbc'
    implementation 'org.springframework.boot:spring-boot-starter-data-jpa'
    runtimeOnly 'com.h2database:h2'
    testImplementation('org.springframework.boot:spring-boot-starter-test') {
        exclude group: 'org.junit.vintage', module: 'junit-vintage-engine'
    }
}

resources/application.properties에 설정을 추가한다.

spring.datasource.url=jdbc:h2:tcp://localhost/~/test
spring.datasource.driver-class-name=org.h2.Driver
spring.jpa.show-sql=true
spring.jpa.hibernate.ddl-auto=none
  • show-sql은 JPA가 생성하는 SQL을 출력한다.
  • ddl-auto는 JPA가 테이블을 자동으로 생성하는 기능을 제공하는데 none을 사용하면 해당 기능을 끄고 create를 사용하면 엔티티정보를 바탕으로 테이블을 직접 생성해준다.

이후에 엔티티에 매핑을 한다.
DB에서 ID를 생성(관리)하는 것을 설정하기 위해 IDENTITY를 지정해준다.
스프링은 EntotyManager를 만들어 인젝션(주입)을 해준다.
또한 JPA는 회원가입처럼 데이터가 변경이 일어날 때 항상 트랙젝션 안에서 실행 되어야하므로 service클래스나 회원가입 메소드위에 @Transactional을 붙여준다.

JPA 사용

package jpabook.start; 
import javax.persistence.*; 
import java.util.Date; 

@Entity 
@Table (name="MEMBER") 
public class Member { 
	
	@Id 
	@Column (name = "ID") 
	private String id; 
	
	@Column (name - "NAME") 
	private String username; 
	
	private Integer age; 

	//== 추가 == 
	@Enumerated (EnumType. STRING) 
	private RoleType roleType; // 1

	@Temporal (TemporalType. TIMESTAMP) 
	private Date createdDate; // 2

	@Temporal (TemporalType. TIMESTAMP)  
	private Date lastModifiedDate; // 2
 
	@Lob 
	private String description; //3
}

//Getter, Setter 
package jpabook.start; 

public enum RoleType { 
	ADMIN, USER
}

////////////////////////////////////////////////////////////

@Configuration
public class SpringConfig {
	
    @PersistenceContext
    private EntityManager em;
    
    @Autowired
    public SpringConfig(EntityManager em) {
    	this.em = em;
    }
    
    @Bean
    public MemberService memberService() {
    	return new MemberService(memberRepository());
    }
    
    @Bean
    public MemberRepository memberRepository() {
    	return new JpaMemberRepository(em);
    }
    
}

////////////////////////////////////////////////////////

public class JpaMemberRepository implements MemberRepository {
	
    private final EntityManager em;
    
    public JpaMemberRepository(EntityManager em) {
    	this.em = em;
    }
    
    @Override
    public Member save(Member member) {
    	em.persist(member);
        return member;
    }
    
    @Override
    public Optional<Member> findById(Long id) {
    	Member member = em.find(Member.class, id);
        return Optional.ofNullable(member);
    }
    
    @Override
    public Optional<Member> findByName(String name) {
    	List<Member> result = em.createQuery("select m from Member m where m.name = :name", Member.class)
        	.setParameter("name", name)
            .getResultList();
        return result.stream().findAny();
    }
    
    @Override
    public List<Member> findAll() {
    	return em.createQuery("select m from Member m", Member.class)
        	.getResultList()
    } //JPA 쿼리인데 select의 대상이 테이블이 아닌 객체를 대상으로 쿼리를 날리고 있다.  


}

Spring Spring Data Jpa 사용

위에서 살펴본 바와 같이 JPA를 사용하기만 해도 코드가 줄어드는 기적을 볼 수 있었다.
그런데 한단계 더 나아가 스프링 데이터 JPA 프레임워크를 사용하면 리포지토리에 구형 클래스 없이 인터페이스만으로 개발을 완료할 수 있다.(마법과 다름없다.)
기존에 반복적으로 작성하면 CRUD기능도 스프링 데이터 JPA가 모두 제공한다.
따라서 개발자들은 핵심 비즈니스 로직을 개발하는데, 집중 할 수 있게 되었다.

이제는 스프링 데이터 JPA가 선택이 아닌 필수이다.

하지만 JPA를 편리하게 도와주는 라이브러리일 뿐이기 때문에 JPA를 공부하지 않으면 실무에서 활용하기가 어려울 것이다. (결국엔 모두 배워야 한다...)

위의 Repository코드를 스프링 데이터 Jpa를 사용하여 작성해보았는데 코드가 거의 없다시피 하는 것을 볼 수 있다.
CRUD기능은 인터페이스로 제공되고 findByName()처럼 메소드 명으로 조회 기능을 제공한다.
페이징 기능도 자동으로 제공한다.
Spring Data Jpa을 통해 EntityManger를 직접 다루지 않고도 JPA 기술을 사용할 수 있다.

import hello.hellospring.domain.Member;
import org.springframework.data.jpa.repository.JpaRepository;

import java.util.Optional;

public interFace SpringDataJpaMemberRepository extends JpaRepository<Member, Long>, MemberRepository {
	
    @Override
    Optional<Member> findByname(String name);
    
    
}

실무에서는 JPA와 스프링 데이터 JPA를 기본으로 사용하고, 복잡한 동적쿼리는 QueryDSL이라는 라이브러리를 사용하면 된다. QueryDSL을 사용하면 쿼리도 자바코드로 작성할 수 있고, 동적쿼리도 편리하게 작성할 수 있다. 이 조합으로 해결하기 어려운 쿼리는 JPA가 제공하는 네이티브 쿼리를 사용하거나 스프링 JdbcTemplate를 사용하면 된다.

참고자료

ORM 표준 JPA 프로그래밍 요약 사이트
JPA로 데이터베이스 사용하기

profile
백엔드 개발자의 노트

0개의 댓글