[JPA] 영속성과 매핑 복습

gyeol·2025년 7월 25일

JPA

목록 보기
11/13
post-thumbnail

영속성 관리

비영속

영속성 컨텍스트에 한 번도 저장

영속

영속성 컨텍스트에 저장되어 JPA에 관리중인 상태
em.persist() 후에 commit() 꼭 필요

준영속

한때 영속이었다가 영속성 컨텍스트에서 분리된 상태
em.detach()로 분리된 적 존재

→ 준영속 상태로 전환하려면?

  • em.detach(entity) : 특정 엔티티 한 것만 분리해 준영속 상태로 만듦
  • em.lclear() : 현재 영속성 컨텍스트를 초기화함
  • em.close() : 영속성 컨텍스트 종료

삭제

영속성 컨텍스트에서 제거되어 삭제된 상태

영속성 컨텍스트 이점

  1. 1차 캐시
  • em.find() 로 조회 시 DB를 조회하기 전에 1차 캐시에서 엔티티 찾음
  • 처음 조회 시, DB에서 조회 후 영속성 컨텍스트에 저장되고 반환
  1. 동일성 보장
  • 동일한 식별자에 대해 em.find()를 여러 번 호출해도 항상 같은 객체 반환
  • ==비교로 동일성 보장받을 수 있음
Member a = em.find(Member.class, "member1");
Member b = em.find(Member.class, "member1");
System.out.println(a == b); // true (동일 객체)
  1. 쓰기 지연
  • em.persist() 호출 시DB에 즉시 SQL을 보내지 않고 저장소에 적재해둠
  • em.commit() 이 실행되어야 DB에 전달됨
  1. 변경 감지(Dirty Checking)
  • 영속 상태의 엔티티 값을 변경만 하면 별도 저장 메서드없이 트랜잭션 커밋 시 자동으로 DB에 저장됨
  • flush 시점에 엔티티 변경 감지해 UPDATE SQL 생성
  1. 지연 로딩 (Lazy Loading)
  • 연관된 엔티티를 실제 사용할 때까지 로딩을 미루는 것

Flush

1차 캐시에 저장된 엔티티들 중 변경된 내용을 찾아 쓰기 지연 SQL 저장소에 모여있던 SQL문을 DB에 전송해 반영함

일반적으로 flush는 트랜잭션 커밋 시 자동 호출되지만 JPQL 쿼리 실행 시에도 자동 flush가 일어남

flush 모드를 선택할 수도 있음 FlushModeType.AUTO/COMMIT 둘 중 선택 가능

연관관계 매핑

엔티티 매핑

@Table엔티티에 매핑될 테이블 이름이나 스키마 지정
@Column필드와 매핑될 테이블의 컬럼을 지정하거나 DDL 제약조건 설정 가능
@Enumeratedenum 타입 필드 매핑에 사용
@Temporal날짜, 시간 등의 매핑에 사용
@LobBLOB/CLOB와 같은 큰 바이너리, 텍스트 데이터를 매핑할 때 사용
@Transient특정 필드를 DB에 아예 저장하도록 지정
@GeneratedValue자동으로 +1씩 지정하도록 하기 위함
주로 id에서 많이 사용

단방향 연관관계

객체 연관관계를 매핑하는 기본 방법은 단방향 관계를 만드는 것

@ManyToOne , @OneToMany 를 사용해 매핑함

// Member 엔티티 (회원)
@Entity
public class Member {
    @Id @GeneratedValue
    private Long id;
    private String username;

    @ManyToOne
    @JoinColumn(name = "TEAM_ID")   // MEMBER 테이블의 FK 컬럼
    private Team team;
    ... 
}

// Team 엔티티 (팀)
@Entity
public class Team {
    @Id @GeneratedValue
    private Long id;
    private String name;
    ...
}
  • 이렇게 되면 Member 클래스에서는 Team에 접근할 수 있지만 Team은 Member 클래스에 접근하지 못함
// 팀 저장
Team team = new Team();
team.setName("TeamA");
em.persist(team);

// 회원 저장 (팀 연관관계 설정)
Member member = new Member();
member.setName("member1");
member.setTeam(team);    // Team 객체 참조 저장
em.persist(member);

// 회원 조회 후 팀 접근
Member findMember = em.find(Member.class, member.getId());
Team findTeam = findMember.getTeam();    // 참조를 통해 팀 조회
System.out.println(findTeam.getName());  // "TeamA"
  • 단방향 연관관계를 매핑하면 회원 엔티티에서 팀을 지정하고 추후에 팀 정보를 가져올 수 있음
  • 이렇게 되면 회원 → 팀 그래프 탐색 가능해짐

양방향 연관관계

// Team 엔티티에 회원 목록 추가 (양방향)
@OneToMany(mappedBy = "team")
private List<Member> members = new ArrayList<>();
  • 위와 동일한 코드이지만 Team 클래스 안에 다음과 같이 추가
  • 이렇게 되면 두방향 모두 객체 그래프 탐색이 가능해짐

mappedBy

  • 객체 양방향 매핑에서 외래키를 관리하는 쪽을 연관관계의 주인이라고 부름
  • 즉!!! mappedBy가 없는 쪽이 주인임
  • 주인만이 DB에 외래키 값 수정/등록 가능
  • 주인이 아닌 쪽은 mappedBy 속성을 지정해 매핑만 설정함
  • 위의 예제와 같이 지정하면 Member.team 필드 값이 변경될 때만 TEAM_ID가 업데이트 되며, Team.members 컬렉션 쪽은 단순 조회용으로 사용
Team team = new Team();
team.setName("TeamA");
em.persist(team);

Member member = new Member();
member.setName("member1");

// 역방향(주인이 아닌 방향)으로만 연관관계 설정
team.getMembers().add(member);
em.persist(member);

→ 주인이 아닌 방향으로 설정하면 결국 MEMBER.TEAM_ID에는 NULL이 들어감

→ 주인 방향으로 설정해야 함

N:1

  • 예를 들면 많은 Member가 하나의 Team에 속하는 관계
  • 보통 N 쪽이 외래키를 가짐
  • JPA 매핑 시 Many 쪽 엔티티에 @ManyToOne 필드를 두는 것이 일반적
// 다대일 양방향 매핑 예시
class Member {
    @ManyToOne(fetch = FetchType.LAZY)
    @JoinColumn(name = "TEAM_ID")
    private Team team;
    ...
}
class Team {
    @OneToMany(mappedBy = "team")
    private List<Member> members = new ArrayList<>();
    ...
}

1:N

  • Team → Member 관계를 Team 입장에서 일대다 단방향으로 정의할 수 있음
  • 실무에서는 권장되지 않음
  • 테이블에서는 항상 N쪽에 FK가 존재하는데 일대다 단방향으로 정의하려면 1쪽에서 FK를 관리하는 구조가 되어 관리 복잡
  • @OneToMany 를 단방향으로 사용하면 중간에 조인 테이블이 만들어짐
    → 이를 피하기 위해서는 @JoinColumn 으로 FK 컬럼을 지정해야 함

1:1

  • 일대일 관계에서는 둘 중 어느 테이블에 FK를 둘지 선택 가능
  • 주 테이블에 외래키를 둠. 이게 일반적임
    예를 들면 Member-Locker 관계에서 Member가 외래키를 관리하도록
    하지만 이렇게 되면 Member가 Locker를 갖지 않으면 FK가 NULL을 허용해야 하므로 테이블 설계상 NULL 허용 컬럼이 생겨버림
  • 대상 테이블에 외래키
    FK를 Locker 테이블에 두는 방식
    Member가 Locker를 모르면 Locker에 Member을 알 방법이 없기에 결국 양방향 구성이 되버림
    하지만 이렇게 되면 프록시를 통한 지연로딩이 제한됨

N:N

  • 관계형 DB는 다대다를 직접 표현할 수 없기에 연결 테이블을 만들어 1:N + N:1로 풀어서 제공해야 함
  • 편의를 위해 @ManyToMany 애노테이션을 제공하며, 이 경우 연결 테이블을 자동으로 관리. 하지만 실무에선 거의 사용하지 않음
    대신 분리하는 것 추천

상속 관계 매핑

자바 객체는 상속 관계가 있지만 관계형 DB에는 상속이라는 개념이 없음

이를 대응하기위해 슈퍼타입(부모), 서브타입(자식) 모델링으로 RDB에서 상속 구조 구현

조인 전략 (Joined)

슈퍼타입과 서브타입 각각 테이블을 만들어 조인하여 조회

  • 부모 클래스에 @Inheritance(strategy = InheritanceType.JOINED) 지정
  • @DiscriminatorColumn(name="DTYPE") 을 적어 차별화 컬럼을 생성하면 각 레코드가 어떤 타입인지 식별 가능
  • 저장하면 부모, 자식 테이블 각각 INSERT가 필요하며 총 2번 이뤄짐
  • 정규화된 설계로 중복 데이터가 없고 참조 무결성이 보장됨
  • 하지만 조회시 매번 조인이 필요하기에 성능이 저하될 수 있음

단일 테이블 전략 (Single Table)

한 테이블에 모든 클래스를 저장, 구분 컬럼으로 타입 구분

  • 부모 클래스에 InheritanceType.SINGLE_TABLE 만 지정하면 됨
  • 실행 시 모든 엔티티가 한 테이블에 저장되며 DTYPE 컬럼으로 타입 구분
  • 모든 데이터가 한 테이블에 있기에 조회 단순+빠름
  • 사용하지 않는 컬럼들은 모두 테이블에 존재하므로 많은 NULL이 저장됨

구현 클래스별 테이블 전략 (Table per Class)

각 구체 클래스마다 자체 테이블 만듦

  • InheritanceType.TABLE_PER_CLASS 만 적으면 됨
  • 실행 시 부모 entity에 해당하는 테이블은 없고 자식별 테이블만 생성
  • 거의 사용되지 않음
  • 여러 자식 테이블을 동시에 조회하기 어렵기 때문

@MappedSuperclass (매핑 정보 상속)

공통 매핑 정보를 여러 엔티티에서 사용해야할 때는 @MappedSuperclass 애노테이션 사용

  • 여러 엔티티에 동일한 필드를 넣고 싶다면 BaseEntitty 라는 추상 클래스를 만들고 여기에 필드를 정의하고 @MappedSuperclass 로 지정
  • 다양한 클래스가 BaseEntity 를 상속받기만 해도 BaseEntity 의 필드들이 각 엔티티 테이블의 컬럼으로 포함됨
  • @MappedSuperclass 는 엔티티가 아니기에 DB 테이블과 매핑되지 않고, 별도 조회도 불가능. 그냥 공통 필드를 전달하는 것 뿐
profile
공부 기록 공간 '◡'

0개의 댓글