실습 코드 : https://github.com/DDINGJOO/Spring5PlayGround
Spring Hibernate
- 모듈 개요와 코드 살펴보기
- Hibernate 구성 정보 총정리
- 엔티티 모델: Singer/Album/Instrument와 관계 매핑
- Singer를 AbstractEntity로 리팩토링하기: 상속 전략과 실전 팁
- 어노테이션을 필드 vs. 메소드 어디에 둘 것인가: 선택 기준과 주의점
- Hibernate 세션과 영속성 컨텍스트 내부 매커니즘
- 트랜잭션, 플러시 모드, 지연 로딩, 캐시 최적화 전략
- DAO/리포지토리 계층 설계와 예외 처리, NamedQuery 활용
- 테스트 데이터와 쿼리 튜닝 팁
- 실전 운영 체크리스트와 베스트 프랙티스
- 부록: 코드 스니펫 모음, 참고 자료
1. 모듈 개요와 코드 살펴보기
본 저장소의 SpringHibernate 모듈은 간결한 도메인(가수, 앨범, 악기)으로 Hibernate의 기본-중급 기능을 실습하기 좋게 구성되어 있습니다. 주요 파일은 다음과 같습니다.
- 엔티티
entites/Singer.javaentites/Album.javaentites/Instrument.java
- DAO
dao/SingerDao.java,dao/SingerDaoImpl.java
- 인프라 구성
config/AppConfig.java(SessionFactory, 트랜잭션 설정, H2 임베디드 DB 설정 등)src/main/resources/sql/schema.sql,test-data.sql(스키마 및 초기 데이터)src/main/resources/application.properties
- 공통 베이스 (현재 샘플)
dao/AbstractEntity.java
주목 포인트
- 현재
Singer,Album,Instrument는 메소드(게터)에 JPA 어노테이션을 부여하는 접근 방식을 사용하고 있습니다. 이는 JPA의 “프로퍼티 접근(property access)” 전략을 의미하며, 이에 따른 동작 차이가 존재합니다. AppConfig는 H2 임베디드 데이터베이스를 사용하고,LocalSessionFactoryBean으로 HibernateSessionFactory를 구성합니다.hibernate.hbm2ddl.auto=update로 설정되어 있어, 애플리케이션 시작 시 스키마 동기화를 시도합니다.- DAO 구현은
SessionFactory.getCurrentSession()을 사용하여 현재 트랜잭션에 바인딩된 세션을 얻어 쿼리를 실행합니다. 이는 Spring의 트랜잭션 관리와 밀접하게 연결됩니다.
2. Hibernate 구성 정보 총정리
2.1 DataSource 구성
AppConfig#dataSource()는 임베디드 H2 데이터베이스를 빌드합니다.
@Bean
public DataSource dataSource() {
EmbeddedDatabaseBuilder dbbuilder = new EmbeddedDatabaseBuilder();
dbbuilder.setType(EmbeddedDatabaseType.H2);
dbbuilder.addScript("classpath:db/schema.sql");
dbbuilder.addScript("classpath:db/test-data.sql");
return dbbuilder.build();
}
- H2: 개발·테스트에 적합한 인메모리(또는 파일) DB.
- 스크립트: 초기 스키마 및 데이터 로딩.
2.2 Hibernate SessionFactory 구성
@Bean
public SessionFactory sessionFactory() throws IOException {
LocalSessionFactoryBean sessionFactoryBean = new LocalSessionFactoryBean();
sessionFactoryBean.setDataSource(dataSource());
sessionFactoryBean.setPackagesToScan("com.springstudy.playground.springhibernate.entities");
sessionFactoryBean.setHibernateProperties(hibernateProperties());
sessionFactoryBean.afterPropertiesSet();
return sessionFactoryBean.getObject();
}
2.3 Hibernate 속성 요약
private Properties hibernateProperties(){
Properties p = new Properties();
p.put("hibernate.dialect", "org.hibernate.dialect.H2Dialect");
p.put("hibernate.show_sql", "true");
p.put("hibernate.connection.autocommit", "false");
p.put("hibernate.use_sql_comments", "true");
p.put("hibernate.format_sql", "true");
p.put("hibernate.hbm2ddl.auto", "update");
p.put("hibernate.max_fetch_depth", "3");
p.put("hibernate.jdbc.batch_size", "10");
p.put("hibernate.jdbc.fetch_size", "50");
p.put("hibernate.order_inserts", "true");
return p;
}
- dialect: H2에 최적화된 방언 사용
- show_sql/use_sql_comments/format_sql: 콘솔 로깅 가독성 향상
- connection.autocommit=false: 트랜잭션 기반 커밋
- hbm2ddl.auto=update: 개발 편의성 높으나 운영에서 주의 필요
- max_fetch_depth: 연관 로딩 최대 깊이 조정
- jdbc.batch_size: 배치 인서트/업데이트 크기 설정 (엔티티 매핑과 ID 전략에 따라 실제 배치 효과 좌우)
- fetch_size: 드라이버 힌트
- order_inserts: INSERT 순서 최적화로 FK 제약 충돌 완화 및 배치 효율 향상
2.4 트랜잭션 매니저
@Bean
public HibernateTransactionManager transactionManager() throws IOException {
return new HibernateTransactionManager(sessionFactory());
}
- Spring의 선언적 트랜잭션(@Transactional)과 결합하여, 메서드 경계에서 트랜잭션을 시작/커밋/롤백합니다.
getCurrentSession()호출이 동작하려면 스레드-바인딩된 세션이 트랜잭션 경계 내에 존재해야 합니다.
3. 엔티티 모델: Singer/Album/Instrument와 관계 매핑
본 프로젝트의 엔티티는 음악가(Singer), 앨범(Album), 악기(Instrument)를 모델링합니다. 다음은 핵심 요약입니다.
3.1 Singer
- 테이블:
singer - 주석 기반 매핑: 메소드(게터)에
@Id,@Column,@OneToMany,@ManyToMany등 선언 - NamedQuery:
Singer.findById: 앨범과 악기를 left join fetch로 함께 로딩하여 N+1 방지Singer.findAllWithAlbum: 전체 가수 목록을 앨범/악기를 페치 조인으로 로딩
중요 코드 포인트:
@Entity
@Table(name = "singer")
@NamedQueries({
@NamedQuery(name = "Singer.findById",
query = "select distinct s from Singer s " +
"left join fetch s.albums a " +
"left join fetch s.instruments i " +
"where s.id = :id"),
@NamedQuery(name = "Singer.findAllWithAlbum",
query = "select distinct s from Singer s " +
"left join fetch s.albums a" +
"left join fetch s.instruments i")
})
public class Singer {
private Long id;
private String firstName;
private String lastName;
private String birthDay; // (스키마는 BIRTH_DATE DATE)
private int version;
private Set<Album> albums = new HashSet<>();
private Set<Instrument> instruments = new HashSet<>();
@OneToMany(mappedBy = "singer", cascade = CascadeType.ALL, orphanRemoval = true)
public Set<Album> getAlbums() { return albums; }
@ManyToMany
@JoinTable(name = "singer_instrument",
joinColumns = @JoinColumn(name = "singer_id"),
inverseJoinColumns = @JoinColumn(name = "instrument_id"))
public Set<Instrument> getInstruments() { return instruments; }
@Id @GeneratedValue(strategy = IDENTITY)
@Column(name = "id")
public Long getId() { return id; }
@Version @Column(name = "version")
public int getVersion() { return version; }
@Column(name = "first_name")
public String getFirstName() { return firstName; }
// ... 이하 생략
}
3.2 Album
- 테이블:
album - Singer:
@ManyToOne관계 - 날짜:
releaseDate는@Temporal(TemporalType.DATE)로 Date 타입 적절히 매핑
@Entity
@Table(name = "album")
public class Album {
private Long id;
private String title;
private Date releaseDate;
private int version;
private Singer singer;
@ManyToOne
@JoinColumn(name = "singer_id")
public Singer getSinger() { return singer; }
@Id @GeneratedValue(strategy = GenerationType.IDENTITY)
@Column(name = "id")
public Long getId() { return id; }
@Version @Column(name = "version")
public int getVersion() { return version; }
@Column(name = "title")
public String getTitle() { return title; }
@Temporal(TemporalType.DATE)
@Column(name = "release_date")
public Date getReleaseDate() { return releaseDate; }
}
3.3 Instrument
- 테이블:
instrument - Singer와 다대다
@ManyToMany
@Entity
@Table(name = "instrument")
public class Instrument implements Serializable {
private String instrumentId;
private Set<Singer> singers = new HashSet<>();
@ManyToMany
@JoinTable(name = "singer_instrument",
joinColumns = @JoinColumn(name = "instrument_id"),
inverseJoinColumns = @JoinColumn(name = "singer_id"))
public Set<Singer> getSingers() { return singers; }
@Id
@Column(name = "instrument_id")
public String getInstrumentId() { return instrumentId; }
}
관계 매핑 요약
- Singer 1 : N Album (양방향 구현 가능, 현재 Album -> Singer ManyToOne, Singer -> Album OneToMany)
- Singer N : M Instrument (연결 테이블
singer_instrument)
4. Singer를 AbstractEntity로 리팩토링하기: 상속 전략과 실전 팁
현재 저장소에는 dao/AbstractEntity.java가 존재합니다.
public class AbstractEntity implements Serializable {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
@Column(updatable = false)
protected Long id;
@Version
@Column(name = "version")
private int version;
public int getVersion() { return version; }
public Long getId() { return id; }
public void setId(Long id) { this.id = id; }
public void setVersion(int version) { this.version = version; }
}
이 클래스를 Singer/Album 등 공통 상위 클래스로 활용하면 다음 이점이 있습니다.
- 중복 제거: id, version, 공통 유틸 메서드(equals/hashCode/toString) 통합
- 낙관적 락킹(Version) 일관화
- 감사(auditing) 필드(createdAt, updatedAt) 확장 용이
그러나 JPA에서 상속 매핑을 올바르게 적용하려면 반드시 @MappedSuperclass 또는 상속 전략 어노테이션을 사용해야 합니다. 위 코드는 어노테이션이 없으므로, 엔티티 상속 시 매핑 인식이 되지 않을 수 있습니다.
권장 리팩토링:
@MappedSuperclass
public abstract class AbstractEntity implements Serializable {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
@Column(updatable = false)
protected Long id;
@Version
@Column(name = "version")
protected int version;
public Long getId() { return id; }
public int getVersion() { return version; }
}
@MappedSuperclass: 이 클래스를 상속받는 모든 엔티티에 필드 매핑을 전달- abstract로 선언하여 직접 엔티티 인스턴스화 방지
Singer가 이를 상속하도록 변경:
@Entity
@Table(name = "singer")
public class Singer extends AbstractEntity {
private String firstName;
private String lastName;
private Date birthDate; // LocalDate 권장
@Column(name = "first_name")
public String getFirstName() { return firstName; }
// @Id, @Version 등은 상위에서 이미 매핑됨
}
주의 사항
- 접근 전략: 상위 클래스가 필드 접근(Field Access)을 사용한다면, 하위도 일관되게 필드 접근을 권장합니다. 상·하위 클래스 접근 전략 혼합은 미묘한 버그를 유발할 수 있습니다(5장 참고).
- 패키지: AbstractEntity는
dao패키지보다는entites(또는entities)와 같은 도메인 패키지에 두는 것이 응집도 관점에서 더 바람직합니다. - equals/hashCode: 식별자 기반 vs. 비즈니스 키 기반 구현 선택. 식별자 기반은 영속 전(Transient)에는 id가 없다는 점을 고려해 주의가 필요합니다.
상속 전략 대안
@MappedSuperclass: 가장 일반적인 공통 속성 상속용, 테이블 미생성.@Inheritance(strategy = InheritanceType.JOINED/SINGLE_TABLE/TABLE_PER_CLASS): 실제 테이블 상속이 필요한 경우 사용. 본 케이스는 공통 필드 공유가 목적이므로@MappedSuperclass가 적합.
5. 어노테이션을 필드 vs. 메소드 어디에 둘 것인가
JPA는 엔티티에 대한 접근 전략을 두 가지로 나눕니다.
- 필드 접근(Field Access): 필드에 @Id가 선언되면 필드에 선언된 어노테이션을 기준으로 매핑. 프록시 바이트코드가 필드를 직접 접근합니다.
- 프로퍼티 접근(Property Access): getter에 @Id가 선언되면 getter에 선언된 어노테이션을 기준으로 매핑.
현재 프로젝트는 대체로 “메소드(게터) 어노테이션”을 사용합니다. 이 선택은 다음과 같은 의미와 트레이드오프를 가집니다.
장점(프로퍼티 접근)
- 캡슐화: 게터/세터 로직에 유효성 검사, 변환 로직 삽입 용이
- Lombok 없이도 접근자 중심 코드 가독성 유지
- 일부 프레임워크/라이브러리가 자바빈 관례에 친화적
단점(프로퍼티 접근)
- 게터 호출 부작용: 게터에 로직이 있다면 영속성 컨텍스트 동작(스냅샷 비교 등)과 예기치 않게 상호작용할 수 있음
- 리플렉션 기반 프레임워크와 혼재 시 접근 전략 착오 발생 가능
장점(필드 접근)
- 더티 체킹 단순화: 필드를 직접 참조하므로 스냅샷 비교가 직관적
- 게터/세터에 로직이 있어도 JPA 측면에서 영향이 적음
- 성능상 약간의 이점이 보고되기도 함(케이스 바이 케이스)
단점(필드 접근)
- 캡슐화 저해: JPA가 필드를 직접 건드리므로 접근 제어에 민감한 팀에서는 거부감
- 테스트에서 목킹/스파잉 등 접근자 기반 툴 체인과의 어울림이 떨어질 수 있음
선택 가이드
- 팀 합의와 일관성이 최우선. 혼용은 피하자.
- 상속 구조에서는 상위 클래스와 하위 클래스의 접근 전략을 일치시켜야 한다.
- 게터에 비즈니스 로직이 많다면 필드 접근을, 도메인 규칙을 접근자에서 보장하려면 프로퍼티 접근을 고려.
- 성능 이슈가 큰 도메인에서는 필드 접근이 유리한 경우가 있음.
클래스 단위 강제
@Access(AccessType.FIELD)또는@Access(AccessType.PROPERTY)를 클래스/필드/메소드 단위로 선언해 일관성 확보 가능.
예시: 필드 접근 일괄 적용
@Entity
@Access(AccessType.FIELD)
public class Singer extends AbstractEntity {
@Column(name = "first_name")
private String firstName;
@Column(name = "last_name")
private String lastName;
@Temporal(TemporalType.DATE)
@Column(name = "birth_date")
private Date birthDate;
@OneToMany(mappedBy = "singer", cascade = CascadeType.ALL, orphanRemoval = true)
private Set<Album> albums = new HashSet<>();
@ManyToMany
@JoinTable(name = "singer_instrument",
joinColumns = @JoinColumn(name = "singer_id"),
inverseJoinColumns = @JoinColumn(name = "instrument_id"))
private Set<Instrument> instruments = new HashSet<>();
// 필요 시 getter/setter 제공
}
6. Hibernate 세션과 영속성 컨텍스트 내부 매커니즘
핵심 개념 정리
- Session = JPA EntityManager에 해당. 1차 캐시를 보유, Flush/Dirty Checking/쓰기 지연/지연 로딩 담당.
- 영속성 컨텍스트 = Session이 관리하는 엔티티 객체의 집합과 스냅샷.
- 트랜잭션과 세션 바인딩: Spring에서
@Transactional경계 안에서getCurrentSession()은 스레드 로컬에 바인딩된 세션을 반환합니다.
내부 동작 흐름
1) find/get/load
- Session 1차 캐시에서 먼저 조회
- 없으면 SQL SELECT 발행, 엔티티 매핑, 1차 캐시에 저장
2) 변경 감지(Dirty Checking)
- Flush 시점에 스냅샷과 현재 값을 비교해 UPDATE 필요 여부 판단
- Access 전략에 따라 스냅샷 기준이 달라질 수 있음
3) Flush
- 트랜잭션 커밋 직전(기본 Auto) 또는 JPQL/HQL 실행 시점에 동기화
- INSERT/UPDATE/DELETE SQL을 일괄 보냄(쓰기 지연)
4) 지연 로딩(Lazy Loading)
- 프록시를 통해 연관 엔티티/컬렉션을 필요 시점에 로딩
- 세션이 닫힌 상태에서 접근하면 LazyInitializationException
5) 캐시
- 1차 캐시: 세션 범위
- 2차 캐시: 세션 팩토리 범위(옵션). 엔티티/컬렉션/쿼리 캐시
6) 락킹/버전 관리
@Version기반의 낙관적 락. 병행 업데이트 시 OptimisticLockException 발생.
세션 수명 주기와 Spring 연동
@Transactional메서드 시작 시 세션 열림(Open), 종료 시 Flush+Close- 전파(Propagation.REQUIRED 등)에 따라 세션 공유/분리
7. 트랜잭션, 플러시 모드, 지연 로딩, 캐시 최적화 전략
7.1 트랜잭션 전파와 격리 수준
- 기본 전파 REQUIRED: 기존 트랜잭션이 있으면 참여, 없으면 새로 시작
- 읽기 전용 트랜잭션:
@Transactional(readOnly = true)는 플러시를 억제하고 일부 최적화 유도 - 격리 수준 조정: DB/드라이버/스프링 설정을 통해 Phantom Read 등 제어
7.2 Flush Mode
- AUTO(기본): 질의 실행 전/커밋 전 flush
- COMMIT: 커밋 직전에만 flush → 일부 읽기 쿼리 성능 향상 가능
- MANUAL: 프로그래머가 명시적으로 flush 호출 필요, 주의 요망
7.3 지연 로딩 전략
- fetch = LAZY(기본 권장), 필요한 곳에서 페치 조인 또는 엔티티 그래프 사용
- N+1 회피: JPQL 페치 조인, BatchSize, Subselect 패치 등
- 컬렉션 페치 조인 한계: 다중 컬렉션 동시 페치 시 카테시안 곱 주의, 페이징 제한
7.4 배치 쓰기와 성능
hibernate.jdbc.batch_size: IDENTITY 전략은 DB가 키를 즉시 생성하므로 배치 이점이 줄어들 수 있음. SEQUENCE 전략과 함께hibernate.jdbc.batch_versioned_data=true,order_inserts=true등을 조합하여 최대화- 드라이버/DB에 따라 fetch_size/batch_size 체감 차이 큼
7.5 2차 캐시·쿼리 캐시(옵션)
- 실전에서는 Ehcache/Redis/Infinispan 등 조합 고려
- 읽기 중심 엔티티, 참조 테이블 캐싱에 효과적
8. DAO/리포지토리 설계와 예외 처리, NamedQuery 활용
SingerDaoImpl은 다음과 같이 구현되어 있습니다.
@Transactional
@Repository("singerDao")
public class SingerDaoImpl implements SingerDao {
private SessionFactory sessionFactory;
@Resource(name ="sessionFactory")
public void setSessionFactory(SessionFactory sessionFactory) {
this.sessionFactory = sessionFactory;
}
@Transactional(readOnly = true)
public List<Singer> findAll() {
return sessionFactory.getCurrentSession()
.createQuery("from Singer s").list();
}
@Transactional(readOnly = true)
public List<Singer> findAllWithAlbum() {
return sessionFactory.getCurrentSession()
.getNamedQuery("Singer.findAllWithAlbum").list();
}
public Singer findById(Long id) {
return (Singer) sessionFactory.getCurrentSession()
.getNamedQuery("Singer.findById")
.setParameter("id", id).uniqueResult();
}
public Singer save(Singer singer) {
sessionFactory.getCurrentSession().saveOrUpdate(singer);
return singer;
}
public void delete(Singer singer) {
sessionFactory.getCurrentSession().delete(singer);
}
}
코멘트
- getCurrentSession(): 스프링 트랜잭션 경계 내에서 안전. 세션 라이프사이클을 스프링이 관리
- NamedQuery: 정적 쿼리로 재사용성과 문맥 안전성 향상, 애플리케이션 시작 시 파싱 오류 조기 발견 장점
- 예외 변환: 스프링이 HibernateException을 DataAccessException 계층으로 변환하여 상위 계층 안정성 향상
개선 아이디어
- 반환 타입 제네릭 지정: createQuery("from Singer s", Singer.class)
- Optional 사용으로 Null 안전성
- 분리된 읽기/쓰기 트랜잭션 경계로 성능/격리 최적화
9. 테스트 데이터와 쿼리 튜닝 팁
schema.sql과 test-data.sql이 제공됩니다.
- Singer/Album/Instrument/Singer_Instrument 테이블 및 제약 구성
- 인덱스: 기본 PK 외, 유니크 제약 존재. 조회 패턴에 따라 보조 인덱스 고려
- 데이터 로딩 순서와 FK 제약 주의(특히 다대다 연결 테이블)
튜닝 팁
- 다대다의 페치 전략: 자주 참조되는 측을 캐시하거나 별도 조회로 분리
- 페치 조인 남용 주의: 페이징과 카테시안 곱 문제. 필요한 화면 단위로 DTO 프로젝션 고려
- 읽기 최적: 읽기 전용 트랜잭션, read-only 힌트, 2차 캐시 활용
10. 실전 운영 체크리스트와 베스트 프랙티스
체크리스트
1) 패키지 스캔 경로 정확성: setPackagesToScan("...entites")로 오타 교정
2) 리소스 경로: addScript("classpath:sql/schema.sql") 등 실제 리소스 위치 반영
3) 접근 전략 일관성: 프로젝트 전반에 FIELD 또는 PROPERTY 중 하나로 통일, @Access 사용
4) 날짜/시간 타입: String → LocalDate/LocalDateTime 권장, @Temporal 대신 JPA 변환기 또는 Hibernate Java 8 타입 모듈 사용
5) hbm2ddl.auto: 운영에서는 validate 또는 none, 마이그레이션 툴(Flyway/Liquibase) 도입
6) 배치 처리: ID 전략 재검토(SEQUENCE+allocationSize), batch_size 최적화
7) N+1: 페치 조인/엔티티 그래프/BatchSize 조합, 레포트 도구로 탐지
8) 로그: SQL 파라미터 로깅은 민감정보 주의, 운영 레벨 조절
9) 락: @Version 기반 낙관적 락과 재시도 정책, 필요 시 비관적 락 옵션 검토
10) 테스트: 슬라이스 테스트(@DataJpaTest), Testcontainers로 실제 DB 근접 환경
베스트 프랙티스
- 도메인 모델 주도 설계: 엔티티에 비즈니스 규칙을 위치시키되 JPA 동작과 충돌하지 않도록 주의
- 레이어링: 서비스 계층에서 트랜잭션 경계를 명확히, DAO/리포지토리는 얇게
- 성능 관측: slow query 로그, Hibernate statistics, 2차 캐시 hit/miss 모니터링
11. 부록: 코드 스니펫 모음, 개선안, 예상 결과
11.1 AbstractEntity 적용 예 (필드 접근으로 통일)
@MappedSuperclass
@Access(AccessType.FIELD)
public abstract class AbstractEntity implements Serializable {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
@Column(updatable = false)
protected Long id;
@Version
@Column(name = "version")
protected int version;
public Long getId() { return id; }
public int getVersion() { return version; }
}
``;
```java
@Entity
@Table(name = "singer")
@Access(AccessType.FIELD)
public class Singer extends AbstractEntity {
@Column(name = "first_name")
private String firstName;
@Column(name = "last_name")
private String lastName;
@Column(name = "birth_date")
private LocalDate birthDate; // Java 8 타입 권장
@OneToMany(mappedBy = "singer", cascade = CascadeType.ALL, orphanRemoval = true)
private Set<Album> albums = new HashSet<>();
@ManyToMany
@JoinTable(name = "singer_instrument",
joinColumns = @JoinColumn(name = "singer_id"),
inverseJoinColumns = @JoinColumn(name = "instrument_id"))
private Set<Instrument> instruments = new HashSet<>();
// business methods
public void addAlbum(Album album) {
albums.add(album);
album.setSinger(this);
}
}
11.2 AppConfig 경로 교정 예
sessionFactoryBean.setPackagesToScan("com.springstudy.playground.springhibernate.entities");
// H2 스크립트 경로도 실제 리소스 구조에 맞춤
new EmbeddedDatabaseBuilder()
.setType(EmbeddedDatabaseType.H2)
.addScript("classpath:sql/schema.sql")
.addScript("classpath:sql/test-data.sql")
.build();
11.3 DAO 타입 안전성과 Optional 적용
@Transactional(readOnly = true)
public List<Singer> findAll() {
return sessionFactory.getCurrentSession()
.createQuery("from Singer s", Singer.class)
.getResultList();
}
@Transactional(readOnly = true)
public Optional<Singer> findById(Long id) {
return sessionFactory.getCurrentSession()
.createNamedQuery("Singer.findById", Singer.class)
.setParameter("id", id)
.uniqueResultOptional();
}
11.4 예상 실행 결과 예시
findAll()실행 시 콘솔- select s1_0.id, s1_0.first_name, s1_0.last_name, s1_0.birth_date, s1_0.version from singer s1_0;
findAllWithAlbum()실행 시- left join fetch가 적용되어 singer, album, instrument를 한 번에 로딩. distinct로 중복 제거
11.5 더티 체킹/플러시 시나리오
1) 트랜잭션 시작 → Session open
2) Singer s = session.get(Singer.class, 1L);
3) s.setLastName("NewName"); (변경만 하고 저장 메소드 호출 없음)
4) 메서드 종료 시점 커밋 → Flush → UPDATE SQL 자동 발행 → Commit
11.6 지연 로딩 주의 시나리오
- 서비스 계층
@Transactional종료 후, 컨트롤러에서s.getAlbums().size()접근 → LazyInitializationException - 해결: 서비스 계층 내에서 접근(초기화)하거나, DTO 변환 시 필요한 데이터만 페치 조인으로 가져오기
11.7 동시성 제어 시나리오(@Version)
- 사용자 A와 B가 동일 Singer를 조회
- A가 lastName 변경 후 커밋(version: 0→1)
- B가 firstName 변경 후 커밋 시도 → OptimisticLockException (B의 버전 0이 DB 1과 불일치)
- 재시도 정책 또는 병합 전략 필요
11.8 마이그레이션/스키마 관리
- 운영 전환 시 hbm2ddl.auto=validate로 고정, Flyway/Liquibase로 스키마 버전관리
- SQL 스크립트 운영 DB와 정합성 테스트: Testcontainers로 CI에서 자동 검증
'SpringStudy' 카테고리의 다른 글
| JDBC 최종 (4) | 2025.08.21 |
|---|---|
| JDBC 학습 정리(1) (0) | 2025.08.21 |
| Pointcut 문법 정리 (0) | 2025.08.20 |
| SpringAop -final (0) | 2025.08.20 |
| Spring AOP (1) | 2025.08.19 |