Spring ORM JPA 관련 정리 - 2

BYEONGMIN CHOI·2022년 6월 4일
0

SPRING ORM JPA

목록 보기
2/5

인프런 이영한님의 강의를 바탕으로 작성된 내용입니다.

Entity Mapping

객체와 테이블 매핑

@Entity

  • @Entity가 붙은 클래스는 JPA가 관리
  • JPA를 사용해서 테이블과 매핑할 클래스는 @Entity 필수
  • 주의
    - 기본 생성자 필수 ( 파라미터가 없는 public 또는 protected 생성자)
    • final 클래스, enum, interface, inner 클래스 사용 X
    • 저장할 필드에 final 사용 X

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

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

  • DDL을 애플리케이션 실행 시점에 자동생성
    * DDL(Data Definition Language) : 데이터베이스를 정의하는 언어이며, 데이터를 생성, 수정, 삭제하는 등의 데이터의 전체의 골격을 결정하는 역할을 하는 언어 -> create : 데이터베이스, 테이블등을 생성 alter : 테이블을 수정 drop : 데이터베이스, 테이블을 삭제 truncate : 테이블을 초기화

  • 테이블 중심 \rightarrow 객체 중심

  • 데이터베이스 방언을 활용해서 데이터베이스에 맞는 적절한 DDL생성
  • 이렇게 생성된 DDL은 개발 장비에서만 사용
  • 생성된 DDL은 운영서버에서는 사용하지 않거나, 적절히 다듬 은 후 사용

데이터베이스 스키마 자동 생성 - 주의

  • 운영 장비에는 절대 create, create-drop, update 사용하면 안된다.
  • 개발 초기 단계는 create 또는 update
  • 테스트 서버는 update 또는 validate
  • 스테이징과 운영 서버는 validate 또는 none

\rightarrow 여러명이 사용하는 테스트 서버/ 개발 서버에 왠만하면 사용안하는게 나음

  • DDL 생성 기능
  • 제약 조건/ 유니크 제약조건
    @Column(nullable = false, length = 10)
    @Column(Unique = true) -> Column에 잘 안쓴다 column 이름이 랜덤으로 생성되기때문에 이름을 반영하기 어려워 실무에서는 잘 안쓴다. @Table(UniqueConstraint = ture) 로 사용

\rightarrow DDL을 자동 생성할 때만 사용되고 JPA의 실행 로직에는 영향을 주지 않는다.

필드와 컬럼 매핑

@Enumerated

• EnumType.ORDINAL: enum 순서를 데이터베이스에 저장
• EnumType.STRING: enum 이름을 데이터베이스에 저장

\rightarrow @Enumerated(EnumType.STRING) 로 사용 : 이유) 예 enum 목록이 초기 {USER, ADMIN} 에서 {GUEST, USER, ADMIN} 로 변경시 ORDINAL을 사용하면 순서가 뒤로 밀려 치명적이 버그가 발생 할 수 있다.

기본 키 매핑

- @Id

- 직접 할당

- @GeneratedValue

  • IDENTITY : 데이터베이스에 위임, MYSQL
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;

기본키 생성을 데이터베이스에 위임하기 때문에 id값을 처음에 알고있지 못함
JPA는 보통 트랜잭션 커밋 시점에 INSERT SQL 실행
AUTO_ INCREMENT는 데이터베이스에 INSERT SQL을 실행한 이후에 ID 값을 알 수 있음
IDENTITY 전략은 em.persist() 시점에 즉시 INSERT SQL 실행 하고 DB에서 식별자를 조회

  • SEQUENCE : 데이터베이스 시퀀스 오브젝트 사용, ORACLE 데이터 베이스에서 많이 사용
    - 데이터베이스 시퀀스는 유일한 값을 순서대로 생성하는 특별한 데이터베이스 오브젝트(예: 오라클 시퀀스)
@Entity
@SequenceGenerator(
name = “MEMBER_SEQ_GENERATOR",
sequenceName = “MEMBER_SEQ", //매핑할 데이터베이스 시퀀스 이름
initialValue = 1, allocationSize = 1)  // 1부터 id값을 시작
public class Member {
	@Id
    @GeneratedValue(strategy = GenerationType.SEQUENCE, generator = "MEMBER_SEQ_GENERATOR")
    private Long id;

\rightarrow stratege 가 sequence 인것을 확인후 id 값을 가져오기 위해 데이터베이스와 통신을 계속하면 성능이 떨어지지 않을까?

\rightarrow allocationSize = 50 속성을 통해 다음 Sequence id를 얻기전까지 어플리케이션 메모리의 id를 하나씩 높여가면서 id를 부여, 만약 51에 도달하면 다시 DB로 부터 그 다음 시퀀스를 받아 온다.

            Member member1 = new Member();
            member1.setUsername("A");
            Member member2 = new Member();
            member2.setUsername("B");
            Member member3 = new Member();
            member3.setUsername("C");


            System.out.println("===========================");
            // DB SEQ 1| 1  
            // DB SEQ 51| 1 -> 성능 최적화를 하기위해 두번 호출된다. 50개씩 메모리를 사용하기 위해 
            // DB SEQ 51| 2
            // DB SEQ 51| 3
            em.persist(member1);
            em.persist(member2);
            em.persist(member3);
            System.out.println("member1.getId : "+ member1.getId());
            System.out.println("member2.getId : "+ member2.getId());
            System.out.println("member3.getId : "+ member3.getId());
            System.out.println("===========================");

Table 전략

  • 키 생성 잔영 테이블을 하나 만들어서 데이터베이스 시퀀스를 흉내내느 전략
    - 장점 : 모든 데이터베이스에 적용가능
    • 단점 : 성능
- 키 생성 전용 테이블을 하나 만들어서 데이터베이스 시퀀스를 흉 내내는 전략
- 장점: 모든 데이터베이스에 적용 가능 
- 단점: 성능
@Entity
@TableGenerator(
        name = "MEMBER_SEQ_GENERATOR",
        table = "MY_SEQUENCES",
        pkColumnValue = "MEMBER_SEQ", allocationSize = 1 )
public class Member {
    @Id
    @GeneratedValue(strategy = GenerationType.TABLE, generator = "MEMBER_SEQ_GENERATOR")
    private Long id;
  • AUTO: 방언에 따라 자동지정, 기본값

권장하는 식별자 전략

  • 기본 키 제약 조건: null 아님, 유일, 변하면 안된다.
  • 미래까지 이 조건을 만족하는 자연키는 찾기 어렵다. 대리키(대체키)를 사용하자.
    예를 들어 주민등록번호도 기본 키로 적절하기 않다.
  • 권장: Long형 + 대체키 + 키 생성전략 사용

실전 예제

테이블 설계

엔티티 설계와 매핑

OrderItem 객체를 통해 orderId와 itemId 를 다 가지고 있음 \rightarrow 테이블과 똑같이 설계한 것임, 이 모델을 통해 개선해 나가는 작업을 진행할 것이다.

  • 가급적 매핑할때 제약조건이나 Column 명을 명시하는 것을 추천
  • spring boot jpql 에선 객체의 필드값이 카멜케이스가 테이블에 언더케이스로 명시되는 것이 기본값이다.

데이터 중심 설계의 문제점

  • 현재 방식은 객체 설계를 테이블 설계에 맞춘 방식
  • 테이블의 외래키를 객체에 그대로 가져옴
  • 객체 그래프 탐색이 불가능
  • 객체 그래프 탐색이 불가능 참조가 없으므로 UML도 잘못됨

연관관계 매핑 기초

연관관계가 필요한 이유

  • 외래키 식별자를 직접 다룸
    // 조회 -> jpa 에게 계속 물어봐야한다. 객체지향 스럽지않다.
    Member findMember = em.find(Member.class, member.getId());
    Long findTeamId = findMember.getTeamId();
    Team findTeam = em.find(Team.class, findTeamId);

객체를 테이블에 맞추어 데이터 중심으로 모델링하면, 협력 관계를 만들 수 없다.

  • 테이블은 외래 키로 조인을 사용해서 연관된 테이블을 찾는다.

  • 객체는 참조를 사용해서 연관된 객체를 찾는다.

  • 테이블과 객체 사이에는 이런 큰 간격이 존재

단방향 연관관계

객체 지향 모델링 (객체 연관관계 사용)

// 저장
       Team team1 = new Team();
       team1.setName("TeamA");
       em.persist(team1);

       Member member = new Member();
       member.setUsername("member1");
       //member.setTeamId(team1.getTeamId()); // 테이블 맞춤 설계
       member.setTeam(team1); // 객체지향 모델링
       em.persist(member);


       em.flush();// 영속성 컨텍스트에 쌓여있는 1차캐시 commit 후 싱크를 맞춘후
       em.clear(); // 영속성 컨텍스트 비운다 -> DB에서 데이터를 찾아오기 위해

       Member findMember = em.find(Member.class, member.getId());
       // 조회 -> jpa 에게 계속 물어봐야한다. 객체지향 스럽지않다.
//     Long findTeamId = findMember.getTeamId();
//     Team findTeam = em.find(Team.class, findTeamId);

       // 조회 객체 지향 모델링
       Team findTeam = findMember.getTeam();
       System.out.println("findTeam = " + findTeam.getName());

       //
       Team newTeam= em.find(Team.class, 100L); // team_id : 100L 있다고 가정
       findMember.setTeam(newTeam); // DB의 외래키가 100L으로 수정

양방향 연관관계와 연관관계의 주인

  • Team Entity
@Id
@GeneratedValue
@Column(name = "TEAM_ID")
private Long teamId;
private String name;

@OneToMany(mappedBy = "team")
private List<Member> members = new ArrayList<>(); // 관례 : 초기화를 해놔야 null point가 뜨지 않기 때문에
  • Member Entity
 @Id
 @GeneratedValue
 @Column(name = "MEMBER_ID")
 private Long id;

 @Column(name = "USERNAME")
 private String username;

//@Column(name = "TEAM_ID")
//private Long teamId;
@ManyToOne
@JoinColumn(name = "TEAM_ID")
private Team team;

연관관계의 주인과 mappedBy

객체와 테이블간에 연관관계를 맺는 차이를 이해해야 한다.

객체와 테이블이 관계를 맺는 차이

객체의 양방향 관계

  • 객체의 양방향 관계는 사실 양방향 관계가 아니라 서로 다른 단방향 관계 2개이다.

테이블의 양방향 관계

  • MEMBER.TEAM_ID 외래 키 하나로 양방향 연관관계를 가짐(양쪽으로 조인 가능)

객체의 외래 키 관리

  • 둘 중 하나로 외래 키를 관리해야한다.

연관관계의 주인 (Owner)

  • 객체의 두 관계중 하나를 연관관계의 주인으로 지정
  • 주인은 mappedBy 속성 사용X
  • 연관관계의 주인만이 외래 키를 관리(등록, 수정) 주인이 아닌쪽은 읽기만 가능
  • 주인이 아니면 mappedBy 속성으로 주인 지정

외래 키가 있는 곳을 주인으로 정해라.

ManyToOne 쪽이 주인이 된다. -> 다(Many) 쪽이 주인이 된다.

양방향의 경우 연관관계 Column을 모두 세팅해주는 것이 맞다.

  • 순수 객체 상태를 고려해서 항상 양쪽에 값을 설정하자
  • 연관관계 편의 메소드를 생성하자
// 연관 관계 메서드 또는 상태를 변경하는 메서드 명에 set을 쓰지 않는다. Getter Setter 관례 때문에
    public void changeTeam(Team team) {
        this.team = team;
        team.getMembers().add(this);
    }

단방향 매핑만으로도 이미 연관관계 매핑은 완료

  • 양방향 매핑은 반대 방향으로 조회(객체 그래프 탐색) 기능이 추가된 것 뿐
  • JPQL에서 역방향으로 탐색할 일이 많음
  • 단방향 매핑을 잘 하고 양방향은 필요할 때 추가 가능 (테이블에 영향을 주지 않는다.)

양방향 연관관계 사용시 주의점

주인이 아닌쪽은 읽기만 가능하기 때문에 team.getMembers().add(member) 를 호출 하더라도 데이터베이스를 조회해 보면 데이터가 추가되지 않는것을 확인할 수 있다.

그렇다면 주인쪽에서만 member.setTeam(team) 을 호출해서 연관관계 설정을 해주면 될까?

주인쪽에만 설정해주면 돠는게 맞긴 하지만 가장 권장하는 방법은 주인이 아닌쪽에도 설정을 해주것이다.

Team team = new Team();
team.setName("teamA");
em.persist(team);

Member member = new Member();
member.setUsername("member1");
member.setTeam(team);

// team.getMembers().add(member);

Team findTeam = em.find(Team.class, team.getId()); //1차 캐시에 있음
List<Member> members = findTeam.getMembers();

for (Member m : members) {
	System.out.println("m = " + m.getUsername());
}

em.persist(team);

→ team 엔티티가 영속성 컨텍스트의 1차 캐시에 저장됨

Team findTeam = em.find(Team.class, team.getId());

→ em.find()로 team 엔티티를 조회하면 위에서 1차 캐시에 저장된 team 엔티티를 반환한다.

List<Member> members = findTeam.getMembers();

→ 조회시 아무것도 출력되지 않는다.

why?

// team.getMembers().add(member);

→ 이 부분이 주석처리가 되어있기 때문이다.

이 부분이 주석처리가 되어있어도 tx.commit() 을 호출하는 시점에는 데이터베이스에 정상적으로 반영이 되기 때문에 DB에 저장하는 것 자채는 문제가 없지만 저 시점에서 team.getMembers() 로 회원을 조회해오면 컬렉션에는 저장되어 있지 않아서 뭔가 굉장히 헷갈리고 실수를 하게 된다.

참고) team.getMembers()를 호출하면 1차 캐시에서 가져오는 것이 아니라 팀에 소속된 회원들을 조회하는 SELECT 쿼리가 실행된다.

그러므로 양방향 연관관계를 설정할 때에는 양쪽에다가 값을 전부 세팅하는것을 권장한다.

profile
스스로 성장하는 개발자가 되겠습니다.

0개의 댓글