ORM 표준 JPA (2)

이정원·2025년 1월 23일
post-thumbnail

1.엔티티 매핑

  • 객체와 테이블 매핑: @Entity,@Table
    JPA를 사용해서 DB테이블과 매핑할 클래스는 @Entity가 필수이다.

    주의: 기본 생성자 필수(파라미터가 없는 public or protected 생성자)

  • 필드와 컬럼 매핑: @Column

  • 기본 키 매핑: @Id

  • 연관관계 매핑: @ManyToOne,@JoinColumn

1-1.데이터베이스 스키마 자동 생성

DDL(Data Definition Language): 데이터베이스 스키마(테이블,컬럼,제약조건)을 정의하거나 관리하는 명령어

주요 기능

1.테이블,데이터베이스 생성

  • 테이블, 데이터베이스, 스키마 등을 새로 만든다.

2.구조 변경

  • 기존 테이블의 컬럼 추가,삭제,타입 변경등이 가능하다.

3구조 삭제

  • 더 이상 필요 없는 테이블,데이터베이스 등을 삭제한다.

DDL의 주요 명령어
1.CREATE: 새로운 데이터베이스 객체 생성

CREATE TABLE users (
    id INT PRIMARY KEY,
    name VARCHAR(50),
    email VARCHAR(100)
);

2.ALTER: 기존 데이터 구조 변경

-- 컬럼 추가
ALTER TABLE users ADD age INT;

-- 컬럼 수정
ALTER TABLE users MODIFY name VARCHAR(100);

-- 컬럼 삭제
ALTER TABLE users DROP COLUMN age;

3.DROP: 데이터베이스 객체 삭제

DROP TABLE users; -- 테이블 삭제
DROP DATABASE my_database; -- 데이터베이스 삭
제

JPA에서 hibernate.hbm2ddl.auto 속성을 사용하여 애플리케이션 시작 시점에 어떤 DDL 작업을 수행할지 결정한다.

DDL 생성 기능은 데이터베이스 스키마를 정의하거나 수정하는 데만 사용되며 JPA 실행 로직에는 영향을 주지 않는다.

1-2.필드와 컬럼 매핑

@Id
private Long id;

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

private Integer age;

@Enumerated(EnumType.STRING)
private RoleType roleType;
   
@Temporal(TemporalType.TIMESTAMP)
private Date createdDate;

@Temporal(TemporalType.TIMESTAMP)
private Date lastModifiedDate;

@Lob
private String description;

@Column:

@Enumerated:DB에는 enum 타입이 존재하지 않기 때문에 @Enumerated를 달아주면 된다.

@Temporal: 날짜 타입에서 DB는 DATE(날짜),TIME(시간),TIMESTAMP(날짜+시간)이 있기 때문에 @Temporal(TemporalType.TIMESTAMP)를 할당 해야한다. -> LocalDate(년,월),LocalDateTime 을 사용하면 된다.
@Lob:DB에 VARCHAR을 넘어서는 컨텐츠를 넣고 싶다면 @Lob을 통해 나타낸다.
@Transient: DB와 상관없는 필드를 객체에 넣고 싶다면 @Transient를 할당하면 된다.

1-3.기본키 매핑

기본 키 매핑 애노테이션

IDENTITY는 초기 애플리케이션에서 id(PK)에 값을 넣지 않고 INSERT를 해야한다. 즉, 저장된 이후 id 값을 알수 있다. 새로운 객체를 생성하고 저장되기 위해 영속성 컨텍스트에서 관리되려면 PK값이 있어야 하는데 IDENTITY 전략은 초기 애플리케이션에서 PK값을 알수 없기 때문에 em.persist()를 호출하면 db에 flush 이후 id값을 가져온다.(원래 commit() 시점에 insert 쿼리가 나가지만 IDENTITY만 특별하게 persist()하면 insert 쿼리가 날라간다.) 쓰기 저장 SQL을 사용하지 않기 때문에 성능면에서 단점이 있다. SEQUENCE는 DB에서 해당 객체의 PK값을 가져온뒤 commit() 될때 insert 쿼리가 실행된다.

SEQUENCE는 JPA가 Sequence 객체를 사용해 id를 할당하므로 사전에 id 값을 활용하여 배치 INSERT를 효율적으로 처리할수 있다. 또한 데이터베이스와 독립적으로 Primary Key 생성을 관리할수 있다.

allocation_size: 엔티티의 ID 값을 DB 시퀀스에서 미리 웹 서버 메모리로 얼마나 업로드할지를 설정하는 값

Table 전략은 키 생성용 테이블을 사용하며 범용적이지만 최적화가 어려워 성능적으로 단점이 있다.

권장하는 식별자 전략

  • 기본 키 제약 조건: Not null,unique,변하면 안됨
  • 대리키를 사용 (자연키:비지니스 적으로 의미있는 키 -> 사용X)
  • 권장:Long + 대체키 + 키

2.연관관계

Order 클래스에서 주문을 찾고 해당 주문의 Member를 찾으려면 다음과 같은 절차가 필요하다.

Order order=em.find(Order.class,1L); // EM 1번 호출
Long memberId= order.getMemberId();

Member member=em.find(Member.class,memberId); // EM 2번 호출

DB 테이블 구조에 맞춰 엔티티를 설계하면 애플리케이션에서 객체의 참조를 가져오는것이 아닌 테이블의 외래키를 가져오고,EntityManager를 통해 반복적으로 ID를 조회하는 비객체지향적인 코드가 발생할 수 있다. 이를 해결하려면 Order 클래스에 Member 타입을 선언하여 객체 그래프를 자연스럽게 탐색해야 한다.

목표는 객체의 참조와 테이블의 외래키를 매핑시키는 것이다.

2-1.단방향 연관관계

객체지향 스러운 모델링은 다음과 같이 객체 설계에서 외래키가 아닌 참조값을 그대로 가져온다.

객체에 연관관계가 있는 객체를 선언한다면 1:N과 같은 관계를 명시해줘야 한다. member 입장에서 team은 1개의 속하기 때문에 @ManyToOne을 설정하고 DB의 외래키와 같이 @JoinColumn 설정한다.

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

2-2.양방향 연관관계 & 연관관계의 주인

양방향 연관관계란 매핑된 객체의 참조를 통해 자유로이 양 객체를 탐색할수 있는것이다. 테이블에선 Foreign Key를 넣으면 Join을 통해 방향 상관없이 자유로운 탐색이 가능하다. 그러나 객체는 Team에서 Members라는 List를 넣어야 양쪽으로 탐색이 가능하다.

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

위와 같이 Member의 변수명을 mappedBy로 명시한다.

실행 코드

 Member findMember = em.find(Member.class, member.getId());
 List<Member> members = findMember.getTeam().getMembers();
   for(Member m:members){
        System.out.println("m.getUsername() = " + m.getUsername());
   }

이렇게 양쪽 객체에 설정을 해줘야 자유로운 탐색이 가능하다. 여기서 mappedBy란 무엇일까?

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

  • 객체 연관관계=2개 (회원->팀, 팀->회원)
  • 테이블 연관관계=1개 (회원<->팀)

객체의 양방향 관계는 서로 다른 2개의 단방향 관계다.

그렇다면 member의 team을 변경할때 Member의 team을 변경해야 할까 아니면 team의 Member 리스트를 변경해야 할까??(DB 입장에서 TEAM_ID 값만 업데이트 되면 된다.) -> 그래서 둘중 하나로 외래키를 관리한다.

양방향 매핑 규칙

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

주인이 아닌쪽에서 값을 수정해봐야 아무일도 일어나지 않는다. 조회만 가능하다. 따라서 연관관계 주인 객체를 변경해줘야한다.(team1.getmember(memberId).setTeam("B"))

누구를 주인으로?

설계된 테이블을 참고하여 FK가 포함된 객체를 연관관계 주인으로 정한다. (DB 테이블에서 외래키가 있으면 무조건 N이고 반대는 1이다.)

흔히 하는 실수

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

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

            em.flush();
            em.clear();

            Member findMember = em.find(Member.class, member.getId());
            List<Member> members = findMember.getTeam().getMembers();
            for(Member m:members){
                System.out.println("m.getUsername() = " + m.getUsername());
            }
            tx.commit();

해당 코드를 살펴보면 Team 객체의 리스트에 member를 추가하지 않고도 로직이 정상 실행된다. 이유는 DB 반영시 JPA가 내부적으로 관계를 관리하여 추가된다. 만약 flush를 하지 않으면 DB 반영전 JPA가 내부적으로 team.getMembers().add(member)를 호출하지 않아 1차 캐시에만 저장이 된 상태이기 때문에 리스트 출력 테스트시 오류가 발생한다. 정확한 방법은 Member에서 team을 set할때 Team 객체를 통해 리스트에 추가한다.

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

이렇게 되면 양쪽으로 세팅이 되고 이를 연관관계 편의 메서드라 한다. 이때 Controller에서 엔티티를 반환하게 되면 ToString(),JSON생성이 연관관계로 인해 무한루프에 빠질 위험이 있다. 따라서 엔티티 반환은 DTO를 통해 반환한다.

@JoinColumn: 외래 키(Foreign Key, FK) 컬럼을 지정하는 애노테이션, 해당 필드를 가진 엔티티는 연관관계의 주인이다. 상대 테이블 PK가 존재하지 않는다면 JPA가 추가한다.

@OneToMany: 기본적으로 연관관계의 주인이 아니며(1인 team에 FK값인 member_id를 가지면 불필요한 데이터 증가) 연관관계 주인인 테이블에 추가적인 update SQL이 진행된다.

name=mappedby: 연관관계의 주인이 아닌 쪽에서 mappedBy를 설정하여 한쪽에서만 관리하도록 한다. 이를 통해 JOIN시 불필요한 중간 테이블 생성이 방지된다. mappedBy가 설정된 필드는 읽기 전용이며, 실제 FK 관리는 연관관계의 주인이 담당한다.

3.일대일,다대다

3-1.일대일

3-2.다대다

DB 테이블에서 N:M 관계를 각각 상대의 FK를 가지는 것으로 설계하는것은 잘못된 설계이다. 왜냐하면 테이블에 FK를 가진다는것은 해당 row는 하나의 값만 참조가 가능하기 때문에 1:N 관계로 설정이되어 여러 값을 가지지 못한다. 객체는 각각 Collection을 통한 구현이 가능하지만 DB 테이블은 방법이 중간 테이블(FK,FK)을 통해 해결이 가능하다.
중간 테이블에서 주문시간,수량 같은 데이터를 넣지 못하고 쿼리가 복잡하기 때문에 실무에서 사용하지 않는다.

해결방법:@ManyToMany를 @OneToMany,@ManyToOne으로 바꾸고 중간 테이블을 엔티티로 승격시킨다.

0개의 댓글