jpa 스프링으로 시작하기

스프링과 JPA

  • 스프링이 다른 라이브러리를 쉽게 개발하도록 보조하듯, JPA 역시 상당 부분 자동 세팅을 한다.

의존성 및 설정

  • 현재 환경은 spring boot, gradle, h2 이다.
  • spring-boot-starter-data-jpa 을 사용한다.
  • JPA는 기본적으로 로깅 기능을 제공한다. 더 좋은 로깅을 위하여 p6spy-spring-boot-starter 을 사용한다.
  • 데이타베이스는 com.h2database:h2을 사용한다.
plugins {
    id 'org.springframework.boot' version '2.6.3'
    id 'io.spring.dependency-management' version '1.0.11.RELEASE'
    id 'java'
}

group = 'qoch'
version = '0.0.1-SNAPSHOT'
sourceCompatibility = '11'

repositories {
    mavenCentral()
}

dependencies {
    implementation 'org.springframework.boot:spring-boot-starter-data-jpa'
    implementation 'org.springframework.boot:spring-boot-starter-validation'
    implementation 'org.springframework.boot:spring-boot-starter-thymeleaf'
    implementation 'org.springframework.boot:spring-boot-starter-web'
    implementation('org.springframework.boot:spring-boot-devtools')
    implementation('com.github.gavlyukovskiy:p6spy-spring-boot-starter:1.8.0')
    compileOnly 'org.projectlombok:lombok'
    runtimeOnly 'com.h2database:h2'
    annotationProcessor 'org.projectlombok:lombok'
    implementation 'org.springframework.boot:spring-boot-starter'
    testImplementation 'org.springframework.boot:spring-boot-starter-test'
}

tasks.named('test') {
    useJUnitPlatform()
}
  • applictaion.yml 을 설정한다.
spring:
  datasource:
    url: jdbc:h2:tcp://localhost/~/jpashop
    username: sa
    password:
    driver-class-name: org.h2.Driver

  jpa:
    hibernate:
      ddl-auto: create
    properties:
      hibernate:
        # show_sql: true # -> system.out
        format_sql: true # -> log

logging.level:
  org.hibernate.SQL: debug # SQL 전체.
  org.hibernate.type: trace # SQL의 파라미터의 값

# p6spy-spring-boot-starter 는 의존성을 주입할 경우 프로퍼티 설정없이 자동적으로 적용 된다. 

코드 작성

  • 영속성 컨텍스트를 가지고 온다.
@Entity
@Getter
@Setter
public class Member {
    @Id
    @GeneratedValue
    private Long id;

    private String userName;
}

@Repository
public class MemberRepository {

    @PersistenceContext
    private EntityManager em;

    // 커맨드와 쿼리를 분리해라.
    // 사이드 이펙트가 있을 수 있으므로 insert의 경우 id 정도만 리턴한다.
    public Long save(Member member){
        em.persist(member);
        return member.getId();
    }

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

  • 테스트 및 수행한다.
  • 트랜잭션으로 엔티티매니저가 동작한다. 반드시 @Transaction을 선언해야 한다.
  • 리포지토리에서 em.persist로 영속화한 객체와 em.find로 찾은 객체의 동일성 비교가 true로 나온다. 실제로 find 할 때, select 쿼리가 발생하지 않았다.
@SpringBootTest
// 엔티티매니저는 트랜잭션이 없으면 동작하지 않는다.
// Transactional은 자바 표준이 아닌 스프링 사용
@Transactional 
class MemberRepositoryTest {

    @Autowired
    MemberRepository memberRepository;

    @Test
    void test(){
        //given
        Member member = new Member();
        member.setUserName("memberA");

        //when
        final Long memberId = memberRepository.save(member);

        final Member findMember = memberRepository.find(memberId);

        //then
        Assertions.assertThat(member).isEqualTo(findMember); // 동등성 비교. 같은 엔티티임.
    }
}
  • persist에서 insert 쿼리가 발생하고, find 때 select 쿼리가 발생하지 않음.
  • repository로 값이 리턴된다 하더라도, Transaction으로 묶여 있으면, 같은 영속성 컨텍스트임을 알 수 있다.

영속성 컨텍스트의 초기화

final Long memberId = memberRepository.save(member);

// 영속성 컨텍스트의 초기화
em.flush(); // commit이 동작한다.
em.clear(); // 영속성 컨텍스트를 초기화 한다.

final Member findMember = memberRepository.find(memberId);
  • 만약 위와 같이 수행할 경우 flush 할 때 insert 쿼리가 발생함을 확인할 수 있다.
  • 동일한 영속성 컨텍스트 내부에서 insert는 트랜잭션이 종료하는 커밋 시점에서 진행된다. 이 말은, 하나의 트랜잭션에서 insert(persist)를 하고 select(find)한 객체는, db에 아예 들어가지도 않는, 영속성 컨텍스트 내부의 자바 메모리라는 의미이다. 그리고 rollback이 자동으로 수행되기 때문에, insert 자체를 수행할 일이 없다.
  • 그러므로 영속성 컨텍스트를 초기화하는 방식과 더불어, @Rollback(falue)를 하더라도 insert 쿼리가 발생함을 확인할 수 있다. 왜냐하면 commit을 날리기 때문이다.

JPA 사용의 주의점

setter를 사용하지 않는다.

  • mybatis의 경우 query를 생성하기 전까지는 DB가 변경되지 않는다.
  • jpa는 영속 상태의 객체를 setter로의 변경하면 데이터가 바뀐다. setter를 사용하지 않고 별도의 매서드를 사용한다.

연관관계를 설정 할 떄 패치 전략을 lazy로 한다.

  • 필요시 fetch join 을 사용한다.

컬렉션은 필드에서 초기화 한다.

  • 널포인트예외로부터 안전하다.
  • 하이버네이트는 영속성 컨텍스트 동작 과정에서 엔티티 컬렉션에 대하여 상속 및 주입한다. 그러니까 클래스 자체가 변경된다. 초기화 되지 않은 컬렉션 필드는 영속성 컨텍스트에 종속된 소스블럭에서 초기화 및 사용할 가능성이 있다. 이러한 가능성을 원천적으로 차단한다.
Member member = new Member();
System.out.println(member.getOrders().getC lass());
em.persist(team);
System.out.println(member.getOrders().getClass());
//출력 결과
class java.util.ArrayList
class org.hibernate.collection.internal.PersistentBag

스프링의 테이블, 컬럼명 생성 전략

  • SpringPhysicalNamingStrategy 전략에 따른다.
  • 카멜케이스(자바) -> 언더스코어(sql) 전략을 사용한다.

연관관계 편의 메서드를 사용한다.

  • 양방향을 사용할 경우, 영속성 컨텍스트가 자바 - DB간 패러다임의 불일치가 일어난다.
  • 연관관계 주인만 대상 엔티티에 대한 데이터를 가져도 대상 엔티티의 데이터가 영속화 된다. 자바 객체의 측면에서는 대상 엔티티의 주인 필드의 데이터가 채워져 있지 않다. 엔티티와 자바 객체 간 격차를 없애기 위하여 연관관계 편의 메서드를 사용한다.
  • 테스트 코드 작성에도 유리하다.
public void setMember(Member member){
        this.member = member;
        member.getOrders().add(this);
    }

@Transactional(readOnly = true)

  • 트랜잭션이 있어야 영속성 컨텍스트를 사용할 수 있으며, DB와 통신 가능하다.
  • 트랜잭션을 열어 놓으면 setter만으로 데이타가 변경된다. 이러한 위험을 보호하기 위하여, 서비스 레이어는 readonly를 한다.
  • 실제 데이터를 변경할 메서드에만 @Transaction 을 붙인다.