Spring

RawJPA

sehunbang 2024. 3. 4. 19:10

(아아아앗 mvc jdbc 랑 entity manager 의 노가다 가 생각난다......)

 

 

ORM 의 탄생 

 왜 ORM 의 탄생 배경을 알아야 하나?

1. 내가 입사할 회사에 JPA 가 적용안된 프로젝트가 있을 수 있다!

 

DB 탄생후 수십년 휘에 ORM 이 나오고 

또 수십년 뒤에 진화해서 JPA 가 나왔습니다.

 

JDBC > ( QueryMapper > ORM(JPA))

 

 

복습!

 

JDBC 는 여러 타입의 DB 와 연결 할 수 있는 기능을 제공합니다.

 

JDBC Driver Manager 는 런타임 시점에

 

Connection (연결) 을 생성하여 퀘리를 요청할수 있습니다  // Connection con

Statement(상태) 를 생성하여 퀘리를 요청 하게 주고

ResultSet (결과 셋) 을 생성해 쿼리 결과를 받올 수 있게 해줍니다.

 

🚫 꼭 사용후에는 각각 close() 를 호출해서 자원 해제를 시켜줘야 합니다! 🚫   

 

 

 

 

Docker 설치 

docker run -p 5432:5432 -e POSTGRES_PASSWORD=pass -e POSTGRES_USER=teasun -e POSTGRES_DB=messenger --name postgres_boot -d postgres

 

해설

run :  작동

-p 5432:5432  : 포트 5432 연결

-e  POSTGRES_PASSWORD=pass  :  POSTGRES  패스 워드는 pass

-e POSTGRES_USER=sehun : POSTGRES 유저는 sehun

- POSTGRES_DB = messenger : POSTGRES DB 를 messenger 로

--name postgres boot  : 컨테이너 이름

-d postgress :  -d 는 백그라운드로 띄우겠다, postgres 는 설치 되어 있는서버를 띄운다.

 

 

새로운 컨테이너가 생깁니다.

 

 

Docker 실행

docker exec -i -t postgres_boot bash

 

 

 

su - postgres
// psql --username sehun --dbname messenger

 

Docker 커맨드

\list (데이터 베이스 조회)

 

\dt (테이블 조회)

 

 

Grandle.build

implementation 'org.postgresql:postgresql:42.2.27';

 

 

test1

String url = "jdbc:postgresql://localhost:5432/messenger";
String username = "sehun";
String password = "pass";
try{
  Connection connection = DriverManager.getConnection(url,username,password);
  String creaseSql = "CREATE TABLE ACCOUNT(id SERIAL PRIMARY KEY, username varchar(255),password varchar(255))";
  PreparedStatement statement = connection.prepareStatement(creaseSql);
  statement.execute();
  statement.close();
  connection.close();
} catch (SQLException e) {
  e.printStackTrace();
}

 

JDBC 삽입 조회

try (Connection connection = DriverManager.getConnection(url, username, password)) {
  System.out.println("Connection created: " + connection);

  String insertSql = "INSERT INTO ACCOUNT (id, username, password) VALUES ((SELECT coalesce(MAX(ID), 0) + 1 FROM ACCOUNT A), 'user1', 'pass1')";
  try (PreparedStatement statement = connection.prepareStatement(insertSql)) {
    statement.execute();
  }

  // then
  String selectSql = "SELECT * FROM ACCOUNT";
  try (PreparedStatement statement = connection.prepareStatement(selectSql)) {
    var rs = statement.executeQuery();
    while (rs.next()) {
      System.out.printf("%d, %s, %s", rs.getInt("id"), rs.getString("username"),
          rs.getString("password"));
    }
  }
}

 

 

try () 안에 Connection PreparedStatement 넣으면 자동으로 close 됨

 

 

 

 

DAO 랑 VO 를 사용

 

DAO ( Data Access Object )

를 만들어서 (그냥 sql 커맨드 적는거 귀찬으니 객체로 만들어서 재활용 하는 용도)

public class AccountDAO {

  private Connection conn = null;
  private PreparedStatement stmt = null;
  private ResultSet rs = null;


  String url = "jdbc:postgresql://localhost:5432/messenger";
  String username = "sehun";
  String password = "pass";
  private final String ACCOUNT_INSERT = "INSERT INTO account(ID, USERNAME, PASSWORD) "
      + "VALUES((SELECT coalesce(MAX(ID), 0) + 1 FROM ACCOUNT A), ?, ?)";

  private final String ACCOUNT_GET = "SELECT * FROM account WHERE ID = ?";


  public Integer insertAccount(AccountVO vo) throws SQLException {
    var id = -1;
    String[] returnId = {"id"};
    try {
      conn = DriverManager.getConnection(url,username,password);
      assert conn != null;
      stmt = conn.prepareStatement(ACCOUNT_INSERT, returnId);
      stmt.setString(1, vo.getUsername());
      stmt.setString(2, vo.getPassword());
      stmt.executeUpdate();

      try (ResultSet rs = stmt.getGeneratedKeys()) {
        if (rs.next()) {
          id = rs.getInt(1);
        }
      }
    } catch (Exception e) {
      e.printStackTrace();
    } finally {
      conn.close();
    }

    return id;
  }

  public AccountVO selectAccount(Integer id) throws SQLException {
    AccountVO account = null;
    try {
      conn = DriverManager.getConnection(url,username,password);
      assert conn != null;
      stmt = conn.prepareStatement(ACCOUNT_GET);
      stmt.setInt(1, id);
      rs = stmt.executeQuery();
      if (rs.next()) {
        account = new AccountVO();
        account.setId(rs.getInt("ID"));
        account.setUsername(rs.getString("USERNAME"));
        account.setPassword(rs.getString("PASSWORD"));
      }
    } catch (Exception e) {
      e.printStackTrace();
    } finally {
      conn.close();
    }

    return account;
  }
}

 

AccountVO (정보, //sql 에 서 뽑아올)

public class AccountVO {

  private Integer id;

  private String username;

  private String password;


  public AccountVO() {
  }

  public AccountVO(String username, String password) {
    this.username = username;
    this.password = password;
  }

  public Integer getId() {
    return id;
  }

  public String getUsername() {
    return username;
  }

  public String getPassword() {
    return password;
  }

  public void setId(Integer id) {
    this.id = id;
  }

  public void setUsername(String username) {
    this.username = username;
  }

  public void setPassword(String password) {
    this.password = password;
  }
}

 

 

@Test
@DisplayName("JDBC DAO 삽입/조회 실습")
void jdbcDAOInsertSelectTest() throws SQLException {
  // given
  AccountDAO accountDAO = new AccountDAO();

  // when
  var id = accountDAO.insertAccount(new AccountVO("new user", "new password"));

  // then
  var account = accountDAO.selectAccount(id);
  assert account.getUsername().equals("new user");
}

 

 

 

 

2. JDBC 의 여러 문제로 QueryMapper 이 탄생했다.

  • DBC 로 직접 SQL을 작성했을때의 문제
    • SQL 쿼리 요청시 중복 코드 발생
    • DB별 예외에 대한 구분 없이 Checked Exception (SQL Exception) 처리
    • Connection, Statement 등.. 자원 관리를 따로 해줘야함
      • 안해주면 메모리 꽉차서 서버가 죽음

 

문제 해결을 위해 처음으로 Persistence Framework 등장!

Persistence Framework 의 두가지!

  • SQL Mapper : JDBC Template, MyBatis 👈 요게 먼저나옴
  • ORM : JPA, Hibernate

 

  • SQL Mapper (QueryMapper)
    • SQL ↔ Object
    • SQL 문과 객체(Object)의 필드를 매핑하여 데이터를 객채화

JDBC Template

  • SQL Mapper 첫번째 주자로 JDBCTemplate 탄생
    • 쿼리 수행 결과와 객채 필드 매핑
    • RowMapper 로 응답필드 매핑코드 재사용
    • Connection, Statement, ResultSet 반복적 처리 대신 해줌
    • 😵‍💫 But, 결과값을 객체 인스턴스에 매핑하는데 여전히 많은 코드가 필요함

 

 

예제

 

 

grandle 에 lib 추가

// 3. PostgreSQL 의존성 추가
implementation 'org.postgresql:postgresql'

// 4. SpringBoot 의존성 추가
implementation 'org.springframework.boot:spring-boot-starter-web'
implementation 'org.springframework.boot:spring-boot-starter-jdbc'

// JUNIT 테스트를 위한 기본 의존성
testImplementation 'org.junit.jupiter:junit-jupiter-api:5.8.1'
testRuntimeOnly 'org.junit.jupiter:junit-jupiter-engine:5.8.1'

// 5. JDBC Template 등.. Spring 의존성을 받기위한 의존성
testImplementation 'org.springframework.boot:spring-boot-starter-test'

 

 

새파일 추가 resource 에

application.yml

spring:
  datasource:
    driver-class-name: org.postgresql.Driver
    url: jdbc:postgresql://localhost:5432/messenger
    username: sehun
    password: pass

 

 

JDBC Template

@JdbcTest // Jdbc Slice Test
@AutoConfigureTestDatabase(replace = Replace.NONE) // 테스트용 DB 쓰지 않도록
@Rollback(value = false) // Transactional 에 있는 테스트 변경은 기본적으론 롤백 하도록 되어있다.
public class JDBCtemplateTest {

    @Autowired
    private JdbcTemplate jdbcTemplate;

    @Test
    @DisplayName("SQL Mapper - JDBC Template 실습")
    void sqlMapper_JDBCTemplateTest() throws SQLException {
      // given
      var accountTemplateDAO = new AccountTemplateDAO(jdbcTemplate);

      // when
      var id = accountTemplateDAO.insertAccount(new AccountVO("new user2", "new password2"));

      // then
      var account = accountTemplateDAO.selectAccount(id);
      assert account.getUsername().equals("new user2");
    }
  }

 

DAO

public class AccountTemplateDAO {
  private JdbcTemplate jdbcTemplate=null;

  //query
  private final String ACCOUNT_INSERT = "INSERT INTO account(ID, USERNAME, PASSWORD) "
      + "VALUES((SELECT coalesce(MAX(ID), 0) + 1 FROM ACCOUNT A), ?, ?)";

  private final String ACCOUNT_GET = "SELECT * FROM account WHERE ID = ?";

  public Integer insertAccount(AccountVO vo) throws SQLException {
    KeyHolder keyHolder = new GeneratedKeyHolder();

    jdbcTemplate.update(con -> {
      PreparedStatement ps = con.prepareStatement(ACCOUNT_INSERT,new String[]{"id"});
      ps.setString(1,vo.getUsername());
      ps.setString(2,vo.getPassword());
      return ps;
    },keyHolder);

    return (Integer) keyHolder.getKey();
  }

  AccountVO selectAccount(Integer id){
    return jdbcTemplate.queryForObject(ACCOUNT_GET,new AccountRowMapper(),id);
  }
  public AccountTemplateDAO(JdbcTemplate jdbcTemplate) {
    this.jdbcTemplate = jdbcTemplate;
  }
}


ROW MAPPER

public class AccountRowMapper implements RowMapper<AccountVO> {


  @Override
  public AccountVO mapRow(ResultSet rs, int rowNum) throws SQLException {
    var account = new AccountVO();
    account.setId(rs.getInt("ID"));
    account.setUsername(rs.getString("USERNAME"));
    account.setPassword(rs.getString("PASSWORD"));

    return account;
  }
}

 

 

 

 

 

 

 

SQL Mapper 두번째 주자로 MyBatis 탄생

  • 반복적인 JDBC 프로그래밍을 단순화
  • SQL 쿼리들을 XML 파일에 작성하여 코드와 SQL 을 분리!
  • 😵‍💫 But, 결국 SQL을 직접 작성하는것은 피곤하다…(DB 기능에 종속적) 😫
  • 😵‍💫 But, 테이블마다 비슷한 CRUD 반복, DB타입 및 테이블에 종속적이다.

 

 

// 코드

Grandle.build

// 6. MyBatis 실습을 위한 의존성
implementation 'org.mybatis.spring.boot:mybatis-spring-boot-starter:3.0.0'
//testCompileClasspath 'org.jpastudy.spring.boot:mybatis-spring-boot-starter-test:3.0.1'
runtimeOnly 'org.postgresql:postgresql'

 

resources 에 AccountMapper 추가

<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
  "http://mybatis.org/dtd/mybatis-3-mapper.dtd">

<!--
    [템플릿 설명]
    - 해당 파일은 SQL 문을 작성하는 곳입니다.
-->
<mapper namespace="me.whitebear.jpastudy.mybatis.mapper.AccountMapper">

  <select id="selectAccount" resultType="me.whitebear.jpastudy.mybatis.vo.AccountMyBatisVO">
    SELECT id,
           username,
           password
    FROM account
    WHERE id = #{id}
  </select>

  <insert id="insertAccount" parameterType="me.whitebear.jpastudy.mybatis.vo.AccountMyBatisVO">
    INSERT INTO account(username, password)
    VALUES (#{username}, #{password});
  </insert>

</mapper>

 

DB Configuration 추가

@Configuration
@MapperScan(basePackages = "me.sehun.*")
@EnableTransactionManagement
public class DBConfiguration {

  @Bean
  public SqlSessionFactory sqlSessionFactory(DataSource dataSource) throws Exception {
    final SqlSessionFactoryBean bean = new SqlSessionFactoryBean();
    bean.setDataSource(dataSource);
    // mapping xml 파일 위치를 bean mapper location 에 등록
    PathMatchingResourcePatternResolver resolver = new PathMatchingResourcePatternResolver();
    bean.setMapperLocations(resolver.getResources("classpath:mappings/*.xml"));
    return bean.getObject();
  }

  @Bean
  public SqlSessionTemplate sqlSessionTemplate(SqlSessionFactory sqlSessionFactory) {
    return new SqlSessionTemplate(sqlSessionFactory);
  }

}

 

 

메인 테스트 클래스

@SpringBootTest
@AutoConfigureTestDatabase(replace = AutoConfigureTestDatabase.Replace.NONE)
@Import(DBConfiguration.class)
public class MyBatisTest {

  // Mapper 클래스를 받으려면 mapper.xml 빌드 해야하고, 그러려면 main 으로 옮겨서 해야함...
  @Autowired
  AccountMapper accountMapper;

  @Autowired
  AccountMapperV2 accountMapperV2;

  @Test
  @DisplayName("SQL Mapper - MyBatis 실습 XML 사용시")
  void sqlMapper_MyBatisTest() {
    // given

    // when
    accountMapper.insertAccount(new AccountMyBatisVO("new user3", "new password3"));
    var account = accountMapper.selectAccount(1);

    // then
    assert !account.getUsername().isEmpty();
  }

  @Test
  @DisplayName("SQL Mapper - MyBatis V2 실습 어노테이션 사용시")
  void sqlMapper_MyBatisV2Test() {
    // given

    // when
    accountMapperV2.insertAccount(new AccountMyBatisVO("new user4", "new password4"));
    var account = accountMapperV2.selectAccount(1);

    // then
    assert !account.getUsername().isEmpty();
  }
}

 

Mapper & Mapperv2 & VO

 

@Mapper
public interface AccountMapper {

  AccountMyBatisVO selectAccount(@Param("id") int id);

  void insertAccount(AccountMyBatisVO vo);
}

 

@Mapper
public interface AccountMapperV2 {

  @Select("SELECT * FROM account WHERE id = #{id}")
  AccountMyBatisVO selectAccount(Integer id);

  @Insert("INSERT INTO ACCOUNT (username, password) VALUES (#{username}, #{password})")
  void insertAccount(AccountMyBatisVO vo);
}
public class AccountMyBatisVO {

  private Integer id;

  private String username;

  private String password;


  public AccountMyBatisVO() {
  }

  public AccountMyBatisVO(String username, String password) {
    this.username = username;
    this.password = password;
  }

  public Integer getId() {
    return id;
  }

  public String getUsername() {
    return username;
  }

  public String getPassword() {
    return password;
  }

  public void setId(Integer id) {
    this.id = id;
  }

  public void setUsername(String username) {
    this.username = username;
  }

  public void setPassword(String password) {
    this.password = password;
  }
}

QueryMapper 의 DB의존성 및 중복 쿼리 문제로 ORM 이 탄생했다.

ORM 은 DB의 주도권을 뺏어왔다고 표현해도 과언이 아닙니다.

  • ORM 은 DAO 또는 Mapper 를 통해서 조작하는것이 아니라 테이블을 아예 하나의 객체(Object)와 대응시켜 버립니다.
  • 말이 쉽지…. 객체지향(Object) 을 관계형 데이터베이스(Relation) 에 매핑(Mapping) 한다는건 정말 많은 난관이 있습니다.

 

릴레이션(관계형 데이터베이스)를 객체(도메인 모델)로 매핑 하려는 이유?

  • 객체 지향 프로그래밍의 장점을 활용할 수 있다.
  • 이를 통해, 비즈니스 로직 구현 및 테스트 구현이 편리함
  • 각종 디자인 패턴 사용하여 성능 개선 가능
  • 코드 재사용

하지만, 객체를 릴레이션에 맵핑하려니 발생하는 문제들

ORM 이 해결해야하는 문제점 과 해결책

 

상속의 문제

  • 객체 : 객체간에 멤버변수나 상속관계를 맺을 수 있다.
  • RDB : 테이블들은 상속관계가 없고 모두 독립적으로 존재한다.

 해결방법 : 매핑정보에 상속정보를 넣어준다. (@OneToMany, @ManyToOne)

 

관계 문제

  • 객체 : 참조를 통해 관계를 가지며 방향을 가진다. (다대다 관계도 있음)
  • RDB : 외래키(FK)를 설정하여 Join 으로 조회시에만 참조가 가능하다. (즉, 다대다는 매핑 테이블 필요)

해결방법 : 매핑정보에 방향정보를 넣어준다. (@JoinColumn, @MappedBy)

탐색 문제

  • 객체 : 참조를 통해 다른 객체로 순차적 탐색이 가능하며 콜렉션도 순회한다.
  • RDB : 탐색시 참조하는 만큼 추가 쿼리나, Join 이 발생하여 비효율적이다.

해결방법 : 매핑/조회 정보로 참조탐색 시점을 관리한다.(@FetchType, fetchJoin())

밀도 문제

  • 객체 : 멤버 객체크기가 매우 클 수 있다.
  • RDB : 기본 데이터 타입만 존재한다.

해결방법 : 크기가 큰 멤버 객체는 테이블을 분리하여 상속으로 처리한다. (@embedded)

식별성 문제

  • 객체 : 객체의 hashCode 또는 정의한 equals() 메소드를 통해 식별
  • RDB : PK 로만 식별

해결방법 : PK 를 객체 Id로 설정하고 EntityManager는 해당 값으로 객체를 식별하여 관리 한다.(@Id,@GeneratedValue )

 

 

대신, ORM 이 얻은 최적화 방법

 

1차 2 차 캐시

 

 

캐싱 기능은 객체지향 프로그래밍이 가진 가장 큰 장점이다.

 

 

  • 1차 캐시
    • 영속성 컨텍스트 내부에는 엔티티를 보관하는 저장소가 있는데 이를 1차 캐시라고 한다.
    • 일반적으로 트랜잭션을 시작하고 종료할 때까지만 1차 캐시가 유효하다.
    • 1차 캐시는 한 트랜잭션 계속해서 원본 객체를 넘겨준다.
    • 자세한 내용은 밑에서…👇
  • 2차 캐시
    • 애플리케이션 범위의 캐시로, 공유 캐시라고도 하며, 애플리케이션을 종료할 때 까지 캐시가 유지된다.
    • 2차 캐시는 캐시 한 객체 원본을 넘겨주지 않고 복사본을 만들어서 넘겨준다.
    • 복사본을 주는 이유는 여러 트랜잭션에서 동일한 원본객체를 수정하는일이 없도록 하기 위해서이다.

 

Entity에 @Cacheable 적용 후 설정 추가

 @Entity
  @Cacheable
  public class Team {
  @Id @GeneratedValue
  private Long id;
  ...
}

 

 

# application.yml

spring.jpa.properties.hibernate.cache.use_second_level_cache: true
# 2차 캐시 활성화합니다.

spring.jpa.properties.hibernate.cache.region.factory_class: XXX
# 2차 캐시를 처리할 클래스를 지정합니다.

spring.jpa.properties.hibernate.generate_statistics: true
# 하이버네이트가 여러 통계정보를 출력하게 해주는데 캐시 적용 여부를 확인할 수 있습니다.


spring.jpa.properties.javax.persistence.sharedCache.mode: ENABLE_SELECTIVE

 

 

영속성 컨텍스트(1차 캐시)를 활용한 쓰기지연

  • 영속성 이란?영속성을 갖지 않으면 데이터는 메모리에서만 존재하게 되고 프로그램이 종료되면 해당 데이터는 모두 사라지게 된다.
  • 그래서 우리는 데이터를 파일이나 DB에 영구 저장함으로써 데이터에 영속성을 부여한다.
  • 데이터를 생성한 프로그램이 종료되어도 사라지지 않는 데이터의 특성을 말한다.

 

  • 영속성 4가지 상태 ( 비영속 > 영속 > 준영속 | 삭제)② 영속(managed) - 엔티티가 영속성 컨텍스트에 저장되어, 영속성 컨텍스트가 관리할 수 있는 상태④ 삭제(removed) - 엔티티를 영속성 컨텍스트와 데이터베이스에서 삭제하겠다고 표시한 상태
    • 객체의 영속성 상태는 Entity Manager 의 메소드를 통해 전환된다.
    • 💁‍♂️ Raw JPA 관점에서 순서대로 요약정리 해보자면
      • persist(),merge() > (영속성 컨텍스트에 저장된 상태) > flush() > (DB에 쿼리가 전송된 상태) > commit() > (DB에 쿼리가 반영된 상태)
  • ③ 준영속(detached) - 엔티티가 영속성 컨텍스트에 저장되어 있다가 분리된 상태로, 영속성컨텍스트가 더 이상 관리하지 않는 상태
  • ① 비영속(new/transient) - 엔티티 객체가 만들어져서 아직 저장되지 않은 상태로, 영속성컨텍스트와 전혀 관계가 없는 상태

 

예제 코

Item item = new Item();    // 1
item.setItemNm("테스트 상품");

EntityManager em = entityManagerFactory.createEntityManager();  // 2
EntityTransaction transaction = em.getTransaction();       // 3

transaction.begin();
em.persist(item);      // 4-1
em.flush(item).     // 4-2 (DB에 SQL 보내기/commit시 자동수행되어 생략 가능함)
transaction.commit();      // 5

em.close();       // 6

 

1️⃣ 영속성 컨텍스트에 담을 상품 엔티티 생성

2️⃣ 엔티티 매니저 팩토리로부터 엔티티 매니저를 생성

3️⃣ 데이터 변경 시 무결성을 위해 트랜잭션 시작

4️⃣ 영속성 컨텍스트에 저장된 상태, 아직 DB에 INSERT SQL 보내기 전

5️⃣ 트랜잭션을 DB에 반영, 이 때 실제로 INSERT SQL 커밋 수행

6️⃣ 엔티티 매니저와 엔티티 매니저 팩토리 자원을 close() 호출로 반환

 

  • 쓰기 지연이 발생하는 시점
    • flush() 동작이 발생하기 전까지 최적화한다.
    • flush() 동작으로 전송된 쿼리는 더이상 쿼리 최적화는 되지 않고, 이후 commit()으로 반영만 가능하다.
  • 쓰기 지연 효과
    • 여러개의 객체를 생성할 경우 모아서 한번에 쿼리를 전송한다.
    • 영속성 상태의 객체가 생성 및 수정이 여러번 일어나더라도 해당 트랜잭션 종료시 쿼리는 1번만 전송될 수 있다.
    • 영속성 상태에서 객체가 생성되었다 삭제되었다면 실제 DB에는 아무 동작이 전송되지 않을 수 있다.
    • 즉, 여러가지 동작이 많이 발생하더라도 쿼리는 트랜잭션당 최적화 되어 최소쿼리만 날라가게된다.

키 생성전략이 generationType.IDENTITY 로 설정 되어있는 경우 생성쿼리는 쓰기지연이 발생하지 못한다.

  • why? 단일 쿼리로 수행함으로써 외부 트랜잭션에 의한 중복키 생성을 방지하여 단일키를 보장한다.
Team teamA = new Team();
teamA.setName("TeamA");
em.persist(teamA);

Team teamB = new Team();
teamB.setName("TeamB");
em.persist(teamB);

Member member_A = new Member();
member_A.setName("memberA");
member_A.setTeam(teamA);

em.persist(member_A);

// em.flush();

Member findMember = em.find(Member.class, member_A.getId());
Team findTeam= findMember.getTeam();

System.out.println(findTeam.getName());

 

 

//flush가 있는 경우

create member
create team
insert team      // flush로 인해 쓰기지연이 발생하지 않음
insert member    // flush로 인해 쓰기지연이 발생하지 않음
print "TeamA" (memberA.getTeam())

 

 

 

///flush가 없는 경우

create member
create team
print "TeamA" (memberA.getTeam()) // 쓰기 지연이 발생하더라도 영속성 컨텍스트에서 조회해옴
insert team      // 쓰기 지연이 발생한 부분
insert member    // 쓰기 지연이 발생한 부분