SpringStudy

Spring Hibernate

dding-shark 2025. 8. 26. 14:30
728x90

실습 코드 : https://github.com/DDINGJOO/Spring5PlayGround

 

Spring Hibernate

  1. 모듈 개요와 코드 살펴보기
  2. Hibernate 구성 정보 총정리
  3. 엔티티 모델: Singer/Album/Instrument와 관계 매핑
  4. Singer를 AbstractEntity로 리팩토링하기: 상속 전략과 실전 팁
  5. 어노테이션을 필드 vs. 메소드 어디에 둘 것인가: 선택 기준과 주의점
  6. Hibernate 세션과 영속성 컨텍스트 내부 매커니즘
  7. 트랜잭션, 플러시 모드, 지연 로딩, 캐시 최적화 전략
  8. DAO/리포지토리 계층 설계와 예외 처리, NamedQuery 활용
  9. 테스트 데이터와 쿼리 튜닝 팁
  10. 실전 운영 체크리스트와 베스트 프랙티스
  11. 부록: 코드 스니펫 모음, 참고 자료

1. 모듈 개요와 코드 살펴보기

본 저장소의 SpringHibernate 모듈은 간결한 도메인(가수, 앨범, 악기)으로 Hibernate의 기본-중급 기능을 실습하기 좋게 구성되어 있습니다. 주요 파일은 다음과 같습니다.

  • 엔티티
    • entites/Singer.java
    • entites/Album.java
    • entites/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으로 Hibernate SessionFactory를 구성합니다. 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.sqltest-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에서 자동 검증

 


 

728x90

'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