JPA 스터디 (2) 엔티티 매핑, 연관관계 매핑 기초

Jihoon Oh·2022년 6월 16일
0

JPA 스터디

목록 보기
2/4
post-thumbnail

김영한님의 자바 ORM 표준 JPA 프로그래밍 강의와 책을 바탕으로 진행되는 스터디입니다

섹션 4. 엔티티 매핑

객체와 테이블 매핑 어노테이션

@Entity

JPA를 사용해서 테이블과 매핑할 클래스에 붙이는 어노테이션

  • name: JPA에서 사용할 엔티티 이름을 지정(기본값 클래스 이름)
    • 다른 패키지에 이름이 같은 클래스가 있을 경우 이름을 지정해서 충돌을 피해야 함
  • 기본 생성자 필수
    • public 또는 protected가 가능한 데 보통 사용 범위를 최소화하기 위해 protected 사용
  • final, enum, interface, inner 클래스에는 사용 불가
  • 필드 final 불가능

@Table

엔티티와 매핑할 테이블을 지정하는 어노테이션

  • name: 매핑할 테이블 이름(기본값 엔티티 이름)
  • catalog: catalog 기능이 있는 데이터베이스에서 catalog 매핑
  • schema: schema 기능이 있는 데이터베이스에서 schema 매핑
  • uniqueConstraints(DDL): DDL 생성 시에 유니크 제약 조건을 생성
    • 2개 이상의 복합 유니크 제약 조건도 가능
    • 스키마 자동 생성 기능을 사용해서 DDL을 만들 때만 사용

데이터베이스 스키마 자동 생성

JPA는 데이터베이스 스키마를 자동으로 생성하는 기능을 지원

properties

spring.jpa.hibernate.ddl-auto=create
spring.jpa.hibernate.show-sql=true

yaml

spring:
  jpa:
    hibernate:
      ddl-auto: create
      show-sql: true

ddl-auto: 자동으로 테이블을 생성할 지 결정하는 속성

  • create: 기존 테이블 삭제 후 다시 생성(DROP + CREATE)
  • create-drop: create와 같으나 애플리케이션 종료 시점에 테이블 DROP(DROP + CREATE + DROP)
  • update: DB 테이블과 엔티티 매핑 정보를 비교해 변경 사항만 수정
  • validate: 테이블과 엔티티가 정상 매핑 되었는지만 확인
  • none: 사용하지 않음
  • 개발 단계에서는 create 또는 update 사용
  • 초기화 상태의 자동 테스트 서버에서는 create 또는 create-drop 사용
  • 테스트 서버는 update 또는 validate 사용
  • 실제 운영 단계에서는 validate 또는 none 사용
    • 실제로 사용할 때는 그냥 제대로 쿼리 날려서 사용하자!

JPA 자체적으로도 스키마 자동 생성 기능을 지원(update, validate 지원 x)

이름 매핑 전략 변경

  • 기본적으로 자바의 필드 네이밍은 카멜 케이스
  • 데이터베이스의 네이밍은 스네이크 케이스
  • @Column(name="role_type")과 같은 식으로 컬럼명을 지정하지 않으면 필드명을 그대로 컬럼명으로 사용
  • hibernate.naming.implicit-strategy 속성을 통해 전략을 변경해 줄 수 있음
  • hibernate.naming.physical-strategy 속성을 통해 물리적 이름도 변경할 수 있음

DDL 생성 시 제약 조건 걸기

length 속성을 통해 문자 크기 지정(varchar(10)), nullable=false 속성을 통해 not null 제약 조건 적용

@Entity
@Table(name = "MEMBER")
public class Member {

    @Id
    @Column(name = "ID")
    private String id;

    @Column(name = "NAME", nullable = false, length = 10)
    private String username;
    ...
}
  • 이런 속성들은 DDL 자동 생성 시에만 사용

기본 키 매핑

@Id 어노테이션을 통해서 기본 키 매핑 가능

기본 키 조건

  • not null
  • unique
  • 변해서는 안됨

기본키로는 자연 키와 대리 키 사용 가능

  • 자연 키
    • 비즈니스에 의미 있는 키
  • 대리 키(대체 키)
    • 비즈니스와 관련 없는 임의로 만들어진 키
  • 자연 키 보다는 대리 키 사용 권장(현실과 비즈니스 규칙은 생각보다 쉽게 변하므로)

@GeneratedValue 어노테이션을 추가하여기본 키 직접 생성 대신 데이터베이스에서 제공하는 기본 키 자동 생성 기능 사용 가능

  • IDENTITY: 기본 키 생성을 데이터베이스에 위임(ex MySQL의 AUTO_INCREMENT)
    • MySQL, PostgreSQL, SQL Server, DB2 등에서 사용
    • DB에 INSERT를 한 후에 기본 키 값 조회 가능
    • 트랜잭션을 지원하는 쓰기 지연 불가
      • 영속 상태를 만들기 위해 식별자를 구해야 하는데 DB에 INSERT 전에는 기본 키 값을 조회할 수 없으므로 em.persist와 동시에 INSERT를 해줘야 함
  • SEQUENCE: 데이터베이스 시퀀스를 사용해 기본 키 할당
    • 오라클, PostgreSQL, DB2, H2 등에서 사용
    • @SequenceGenerator를 사용해서 시퀀스 생성기 등록
      • name: 식별자 생성기 이름
      • sequenceName: 데이터베이스에 등록되어 있는 시퀀스 이름(기본값 hibernate_sequence)
      • initialValue: 시퀀스 DDL을 생성할 때 처음 시작하는 수 지정(기본값 1)
      • allocationSize: 시퀀스 1회 호출에 증가하는 수(기본값 50)
      • catalog, schema: DB catalog, schema 이름
    • em.persist를 호출할 때 데이터베이스 시퀀스를 사용해서 식별자 조회 → 엔티티에 식별자 할당 → 영속성 컨텍스트에 저장 → 트랜잭션 플러시할 때 데이터베이스에 저장
  • TABLE: 키 생성 테이블 사용
    • 모든 데이터베이스에서 사용 가능
    • 키 생성 전용 테이블을 만들고 이름과 값으로 컬럼을 만들어 데이터베이스 시퀀스를 흉내내는 전략
    • @TableGenerator 를 사용해서 테이블 키 생성기를 등록
      • name: 식별자 생성기 이름
      • table: 키생성 테이블명(기본값 hibernate_sequences)
      • pkColumnName: 시퀀스 컬럼명(기본값 sequence_name)
      • valueColumnName: 시퀀스 값 컬럼명(기본값 next_val)
      • pkColumnValue: 키로 사용할 값 이름(기본값 엔티티 이름)
      • initialValue: 초기 값(기본값 0)
        • 시퀀스와는 다르게 마지막으로 생성된 값이 기준
      • allocationSize: 시퀀스 한 번 호출에 증가하는 수(기본값 50)
      • catalog, schema: DB catalog, schema 이름
      • uniqueConstraints(DDL): 유니크 제약 조건 지정
    • 기본 키 조회를 위해 먼저 SELECT 쿼리를 사용하고 값을 증가시키기 위해 UPDATE 쿼리 사용
      • DB와 한 번 더 통신
  • AUTO: 데이터베이스에 따라 IDENTITY, SEQUENCE, TABLE 중 하나를 선택

필드와 컬럼 매핑

  • @Column: 객체 필드를 테이블 컬럼에 매핑
    • name: 필드와 매핑할 테이블의 컬럼 이름(기본값 필드 이름)
    • insertable: false로 설정하면 이 필드는 데이터베이스에 저장하지 않음(기본값 true)
      • 거의 사용 x
    • updatable: false로 설정하면 데이터베이스에 수정하지 않음
      • 거의 사용 x
    • table: 하나의 엔티티를 두 개 이상의 테이블에 매핑할 때 사용(기본값 현재 클래스의 테이블)
      • 거의 사용 x
    • nullable(DDL): null 값의 허용 여부를 설정(기본값 true)
    • unique(DDL): 컬럼에 유니크 제약조건을 걸 때 사용
    • columnDefinition(DDL): 데이터베이스 컬럼 정보를 직접 입력
    • length(DDL): 문자 길이 제약. String에만 사용
    • precision, scale(DDL): BigDecimal, BigInteger 타입에서 사용
      • precision: 소수점을 포함한 전체 자릿수(기본값 19)
      • scale: 소수부의 자릿수(기본값 2)
    • @Column을 생략하면 대부분 @Column 속성의 기본값이 적용
      • 단, nullable은 primitive 타입이냐 wrapper 타입이냐에 따라 다름
      • primitive 타입의 경우 nullable = false 적용
  • @Enumerated: enum 타입을 매핑할 때 사용
    • value
      • EnumType.ORDINAL: enum 순서를 저장(기본값)
        • DB에 저장되는 크기가 작지만 이미 저장된 enum의 순서를 변경 불가능
        • 주의해서 사용 필요
      • EnumType.STRING: enum 이름을 저장
        • 저장된 enum의 순서가 바뀌거나 enum이 추가되어도 안전
        • DB에 저장되는 크기가 ORDINAL에 비해 큼
  • @Temporal: 날짜 타입(Date, Calendar)을 매핑할 때 사용
    • value
      • TemporalType.DATE
        • 데이터베이스의 date(ex 2022-06-16) 타입과 매핑
      • TemporalType.TIME
        • 데이터베이스의 time(ex 21:55) 타입과 매핑
      • TemporalType.TIMESTAMP
        • timestamp 타입(ex 2022-06-16 21:55) 타입과 매핑
    • 생략 시 자바의 Date와 가장 유사한 datetime(MySQL) 또는 timestamp(H2, 오라클, PostgreSQL) 타입으로 매핑
  • @Lob: BLOB, CLOB 타입과 매핑
    • BLOB: 이진 대형 객체(이미지, 동영상, MP3…)
    • CLOB: 문자 대형 객체
    • 매핑하는 필드 타입이 문자면 CLOB, 나머지는 BLOB 매핑
  • @Transient: 해당 필드를 매핑하지 않음
  • @Access: JPA가 엔티티 데이터에 접근하는 방식 지정
    • AccessType.FIELD: 필드에 직접 접근(private 필드도 접근 가능)
    • AccessType.PROPERTY: getter로 접근
    • 설정하지 않으면 @Id 어노테이션이 붙은 위치를 기준으로 결정

섹션 5. 연관관계 매핑 기초

객체와 DB의 연관관계 차이점

MemberTeam을 필드로 가지는 연관관계

  • member → team 조회는 meber.getTeam으로 가능
  • team → member 접근은 불가능
    • 객체 참조는 항상 단방향
    • 양방향을 만들려면 단방향 2개가 필요(Member → Team 참조, Team → Member 참조)
  • 반면 DB는 참조가 아니라 외래 키로 연관관계를 맺음
    • DB는 외래 키 하나만 가지고도 양방향 JOIN이 가능(MEMBER JOIN TEAM, TEAM JOIN MEMBER)

단방향 연관관계

가장 기초적인 다대일[N:1] 연관관계(Member(N)가 Team(1)을 필드로 가지는 연관관계)

@Entity
public class Member {

    @Id
    @Column(name = "MEMBER_ID")
    private String id;

    private String username;

    @ManyToOne
    @JoinColumn(name = "TEAM_ID")
    private Team team;

    public void setTeam(Team team) {
        this.team = team;
    }

    ...
}
@Entity
@Getter
public class Team {

    @Id
    @Column(name = "TEAM_ID")
    private String id;

    private String name;

    ...
}

MemberTeam 단방향 연관관계(Member → Team만 조회 가능)

  • @ManyToOne
    • 다대일관계를 나타내는 매핑 정보
    • 연관관계 매핑 시 나타내는 어노테이션을 필수로 사용
    • optional: false로 설정하면 연관된 엔티티가 항상 있어야 함(기본값 true)
    • fetch: 글로벌 페치 전략 설정
      • @ManyToOne의 기본값은 FetchType.EAGER
      • @OneToMany의 기본값은 FetchType.LAZY
    • cascade: 영속성 전이 기능 설정
    • targetEntity: 연관된 엔티티의 타입 정보 설정
      • 거의 사용 x (컬렉션을 쓰더라도 제네릭으로 타입 정보 알 수 있음)
  • @JoinColumn(name=”TEAM_ID”)
    • 외래키를 매핑할 때 사용
    • name 속성에 매핑할 외래 키 이름을 지정
      • 참조로 연관관계를 맺는 Member의 필드인 Team에 해당 어노테이션을 붙여주면 데이터베이스에서는 TEAM_ID를 FK로 하여 연관관계를 맺음
    • name: 매핑할 외래 키 이름(기본값 필드명_참조하는 테이블의 기본 키 컬럼명)
    • referencedColumnName: 외래 키가 참조하는 대상 테이블의 컬럼명(기본값 참조하는 테이블의 기본 키 컬럼명)
    • foreignKey(DDL): 외래 키 제약조건 직접 지정
    • unique, nullable, insertable, updatable, columnDefinition, table
      • @Column의 속성과 같음
    • 생략 가능
      • 생략에 대한 자세한 테스트는 참고
      • 위 상황에서 생략하면 team_TEAM_ID FK를 사용하게 됨

연관 관계 저장

Team team1 = new Team("team1", "팀1");
em.persist(tema1);

Member member1 = new Member("member1", "회원1");
member1.setTeam(team1);
em.persist(member1);
  • 엔티티 저장 시 연관된 엔티티는 반드시 영속상태여야 함
    • MemberTeam 참조이므로 연관관계를 맺어줄 때 Team은 반드시 영속상태여야 함
    • MEMBER 테이블의 TEAM_ID 값으로 team1의 id값이 들어감

연관관계 조회

연관관계를 조회하는 방법은 2가지

  • 객체 그래프 탐색
    Member member = em.find(Member.class, "member1");
    Team team = member.getTeam();
  • 객체지향 쿼리 사용(JPQL)
    String jpql = "select m from Member m join m.team t where t.name = :teamName";
    
    List<Member> members = em.createQuery(jpql, Member.class)
            .setParameter("teamName", "팀1")
            .getResultList();
    • 회원이 팀과 관계를 가지고 있는 필드(m.team)을 통해 Member와 Team을 조인

연관관계 수정

Team team2 = new Team("team2", "팀2");
em.persist(team2);

Member member = em.find(Member.class, "member1");
member.setTeam(team2);
  • member.setTeam으로 참조하는 대상만 바꿔주면 JPA가 트랜잭션이 커밋되는 시점에 변경 사항을 데이터베이스에 반영

연관관계 제거

Member membe1 = em.find(Member.class, "member1");
member.setTeam(null);
  • 참조를 null로 바꿔주면 FK도 null로 바꿔서 연관관계가 제거됨
  • 연관된 엔티티를 삭제하려면 기존에 있던 연관관계를 먼저 제거하고 삭제해야 함

양방향 연관관계

TeamMember로 접근하는 관계를 추가

  • 회원 → 팀 (Member.team)
  • 팀 → 회원 (Team.members; 다대일이므로 컬렉션)
  • 데이터베이스는 외래 키 하나로 양방향 조회가 가능하므로 DB는 변화 없음
  • 객체 매핑만 추가해주면 됨
@Entity
@Getter
public class Team {

    @Id
    @Column(name = "TEAM_ID")
    private String id;

    private String name;

    @OneToMany(mappedBy = "team")
    private List<Member> members = new ArrayList<>();

    ...
}
  • @OneToMany
    • @ManyToOne의 반대
    • 양방향일 때 mappedBy 속성을 지정

연관관계의 주인

엄밀히 말해 객체에는 양방향 연관관계란 존재하지 않음. 단방향 연관관계 2개가 존재.

반면 데이터베이스 테이블은 외래 키 하나로 양방향 조인이 가능.

  • 단방향 연관관계 사용 시 참조를 하나만 사용하므로 해당 참조로 외래 키 관리
  • 양방향 연관관계 사용 시 참조가 2개가 됨
    • 반면 외래 키는 하나이므로 외래 키를 관리할 참조를 지정해줘야 함
    • 외래 키를 관리하는 엔티티를 연관관계의 주인이라 함
  • 연관관계의 주인만이 데이터베이스 연관관계와 매핑됨
    • 외래 키를 관리(등록, 수정, 삭제) 가능
    • 반대 쪽 엔티티는 읽기만 가능
  • mappedBy 속성을 통해 연관관계의 주인을 설정
    • 주인은 mappedBy 속성을 사용하지 않음
    • Member.team이 연관관계의 주인이므로 Team.members에는 mappedBy 속성을 지정해주어야 함
    • team으로 매핑되므로 mappedBy 값으로 team을 주면 됨
      • 즉, 연관관계의 주인인 엔티티의 연관된 필드 이름을 mappedBy 값으로 주면 됨
    • 참고) mappedBy 없으면 조인 테이블이 하나 추가로 생성됨
  • 다대일 관계에서는 반드시 다 쪽을 연관관계의 주인으로!
    • 따라서 @ManyToOne에는 mappedBy 속성이 존재하지 않음

일대다 컬렉션 조회

Team team = em.find(Team.class, "team1");
List<Member> members = team.getMembers();

양방향 연관관계 저장

Team team1 = new Team("team1", "팀1");
em.persist(team1);

Member member1 = new Member("member1", "회원1");
member1.setTeam(team1);
em.persist(member1);

// 연관관계의 주인이 아닌 방향은 값을 설정하지 않아도 데이터베이스에 외래 키 값이 정상 입력됨
// team1.getMembers().add(member1);

주의

Member member1 = new Member("member1", "회원1");
em.persist(member1);

Team team1 = new Team("team1", "팀1");
team1.getMembers().add(member1);
em.persist(team1);
  • 연관관계의 주인이 아닌 방향에는 값을 설정하지 않아도 데이터베이스에 외래 키 값이 정상 입력됨
  • 연관관계의 주인이 아닌 방향에만 값을 설정하고 연관관계의 주인에 값을 설정하지 않으면 데이터베이스에 외래 키 값이 입력되지 않음
  • 그러나 객체 관점에서 양쪽 방향에 모두 값을 입력해 주는 것이 안전
Team team1 = new Team("team1", "팀1");
em.persist(team1);

Member member1 = new Member("member1", "회원1");
member1.setTeam(team1);
em.persist(member1);

team1.getMembers().add(member1);
  • why? JPA 없이 순수한 객체 상태로 사용할 때 문제를 방지하기 위함

연관관계 편의 메서드

member.setTeam(team)team.getMembers().add(member)를 모두 호출할 때 실수로 누락 가능성 있음

→ 두 코드가 하나의 코드인 것 처럼 사용해야 안전

public class Member {

    ...
    private Team team;

    public void setTeam(Team team) {
        this.team = team;
        team.getMembers().add(this);
    }
}

이렇게 Member.setTeam 하나로 양방향의 연관관계를 모두 입력해주도록 하면 연관관계를 맺어주는 메서드를 한 번만 호출해도 됨 (연관관계 편의 메서드)

  • 단, 객체 관점에서는 주인이 아닌 부분의 연관관계가 사라지지 않는다는 것에 주의
    member1.setTeam(teamA);
    member1.setTeam(teamB);
    
    Member findMember = teamA.getMembers.get(0); // member1 조회됨
    • 기존 연관관계를 끊지 않으면 외래 키는 정상적으로 변경됨
    • 주인인 쪽의 객체 연관관계도 정상적으로 수정됨
    • 그러나 같은 영속성 컨텍스트 내에서 기존의 team 객체의 members 에는 아직 member 이 남아있음
  • 따라서 다음과 같이 기존 관계를 제거하는 것이 안전
    public void setTeam(Team team) {
        if (this.team != null) {
            this.team.getMembers().remove(this);
        }
        this.team = team;
        team.getMembers().add(this);
    }

양방향 연관관계 시 순환참조 주의

Member.toString에서 getTeam을 호출하고, Team.toString에서 getMember를 호출하면 어느쪽에서 toString을 호출하든 무한루프에 빠짐(엔티티를 JSON으로 변환할 때 많이 생기는 문제)

  • JSON 라이브러리들은 보통 무한루프에 빠지지 않도록 하는 어노테이션이나 기능 제공
  • Lombok의 @ToString 사용 시에 해당 문제가 자주 발생

정리

  • 단방향 매핑만으로 테이블과 객체의 연관관계 매핑은 이미 완료
  • 단방향을 양방향으로 만드는 것의 이점은 반대 방향의 객체 그래프 탐색 기능 추가 뿐
  • 양방향 연관관계를 매핑하려면 객체에서 양쪽 방향을 모두 관리해야 함
  • 따라서 기본적으로는 단방향으로 매핑하고 필요 시에 양방향 연관관계 추가를 고려
profile
Backend Developeer

0개의 댓글