728x90

JPA는 복잡한 검색 조건을 사용해서 엔티티 객체를 조회할 수 있는 다양한 쿼리기술을 지원한다.

JPQL은 가장 중요한 객체지향 쿼리 언어이다. 다른 Criteria나 QueryDSL은 JPQL을 편리하게 사용하도록 도와주는 기술이므로 JPA를 다루는 개발자라면 JPQL을 필수로 학습해야 된다.

객체지향 쿼리 소개

EntityManager.find() 메소드를 사용하면 식별자로 엔티티 하나를 조회할 수 있다.
검색 방법은 두가지다.

  • 식별자 조회 : EntityManager.find();
  • 객체 그래프 탐색 : a.getB().getC();
    이런 단순한 SELECT 쿼리들로는 개발을 하지 않는다.
    복잡 쿼리나 통계형 쿼리같은 복잡한 쿼리들을 수행할 때에는 이런 쿼리로는 택도 없다.
    ORM을 사용하면 DB테이블이 아니라 엔티티 객체를 대상으로 개발하기 때문에 검색도 마찬가지로 엔티티 객체를 대상으로 하는 방법이 필요함.
    JPQL이 이 문제를 해결하기 위해 만들어졌다. 특징은 다음과 같다.
  • 테이블이 아닌 객체를 대상으로 검색하는 객체지향 쿼리
  • SQL을 추상화해서 특정 DB SQL에 의존하지 않는다.

JPQL을 사용하게 되면 JPA는 이 JPQL을 분석한 다음 적절한 SQL을 만들어 DB를 조회한다. 그리고 그 결과로 엔티티 객체를 생성하여 반환해준다.

JPQL은 객체지향 SQL이라고 정의할 수 있다.

JPA 공식 지원 기능

  • JPQL
  • Criteria 쿼리 : JPQL을 편하게 작성하도록 도와주는 API, Builder 클래스 모음
  • 네이티브 SQL : Mybatis처럼 JPQL대신 직접 SQL사용하는 기능
  • QueryDSL : Criteria 쿼리처럼 JPQL을 편하게 작성하도록 도와주는 빌더 클래스 모음, 비표준 오픈소스 프레임워크임.
  • JDBC 직접 사용, SQL 매퍼 프레임워크 사용 : 필요에 의해 JDBC를 직접 사용가능.

@Test
void jpqlTest() {
    Member member = new Member("kim");
    em.persist(member);
    em.flush();

    String jpql = "select m from Member as m where m.name = 'kim'";
    List<Member> resultList = em.createQuery(jpql, Member.class).getResultList();

    assertThat(resultList.get(0).getName()).isEqualTo("kim");
}

실행결과는 다음과 같다.

다른 값들은 생성자에서 설정 안해주었기 때문에 넣지 않았다.
근데 조금 다른 것이 있다.
그것은 원래 우리가 사용하던 방식인 em.find()를 사용했을 때에는 영속성 컨텍스트인 member가 지금 존재하기 때문에 SELECT쿼리를 날리지 않고 1차캐시에서 조회했었다.

그렇지만 JPQL은 직접 SQL을 추상화해서 사용하기 때문에 쿼리를 날려서 조회하게 된다. 그래서 항상 DB에서 찾는데 이 찾은 엔티티가 영속성 컨텍스트에 존재한다면? DB에서 찾는 데이터를 버리고 영속성 컨텍스트에 있는 반환값을 넘긴다

Criteria의 장점은 문자가 아닌 query.select(m).where(...) 처럼 메소드 체이닝으로 JPQL을 작성할 수 있다.
장점이 많지만 모든 장점을 상쇄할 정도로 복잡하고 장황하다. 따라서 사용하기 불편한건 물론이고 Criteria로 작성한 코드도 한눈에 들어오지 않는 단점이 있음.

@Test
void criteriaTest() {
    //Criteria 사용 준비
    CriteriaBuilder cb = em.getCriteriaBuilder();
    CriteriaQuery<Member> query = cb.createQuery(Member.class);

    //루트 클래스(조회를 시작할 클래스)
    Root<Member> m = query.from(Member.class);

    //쿼리 생성
    CriteriaQuery<Member> cq = query.select(m).where(cb.equal(m.get("name"), "kim"));
    List<Member> resultList = em.createQuery(cq).getResultList();        
}

QueryDSL

QueryDSL도 Criteria처럼 JPQL 빌더 역할을 한다. QueryDSL의 장점은 코드 기반이면서 단순하고 사용하기 쉽다. 작성한 코드도 JPQL과 비슷해서 한눈에 들어온다.

QueryDSL은 JPA 표준은 아니고 오픈소스 프로젝트이다.

코드를 보자

//세팅
JPAQuery query = new JPAQuery(em);
QMember member = QMember.member;

//쿼리, 결과조회
List<Member> members = query.from(member)
                            .where(member.name.eq("kim"))
                            .list(member);

여기서 QMember 클래스는 Member 엔티티 클래스를 기반으로 생성한 QueryDSL 쿼리 전용 클래스이다.

여기까지 보면 이제는 JPA와 QueryDSL로 전반적인 코드를 작성할 수 있을것 같다.
그리고 추가적으로 정말 복잡한 것이라면 네이티브 쿼리를 사용하는것도 방법이겠다.

네이티브 쿼리

SQL을 직접 사용하는 기능이 바로 네이티브 쿼리이다. SI회사에 근무했을 당시 MyBatis, iBatis 많이 사용했는데 바꾸려는 시도가 최근에 이루어졌었기 때문에 점점 JPA가 편해져 가는중이지만 그래도 익숙해보인다.

  • 단점
    • 특정 DB에 의존하는 SQL을 작성해야 한다는 것. 그래서 DB를 바꾸면 쿼리문도 수정해주어야 한다.

내가 MySQL이나 MariaDB를 배웠는데 회사에서 Oracle, PostgreSQL 사용한다면 바꿔야 한다는 얘기이다...

String sql = "SELECT id, name, age FROM Member WHERE name='kim'";
List<Member> resultList = em.createNativeQuery(sql, Member.class).getResultList();

JDBC를 사용하는것과 흡사하다. 다른점은 다음에서 설명하겠다.

JDBC 직접 사용, MyBatis 같은 SQL Mapper 사용

Hibernate에서 JDBC 커넥션을 획득하는 방법

Session session = em.unwrap(Session.class);
session.doWork(new Work() {

    @Override
    public void execute(Connection connection) throws SQLException {
       //작업..
    }
});

위에서 다른점이라고 하는것이 이부분인데,
JDBC나 MyBatis를 JPA와 함께 사용하면 영속성 컨텍스트 관리가 안되는 애들이기 때문에 컨텍스트를 적절한 시점에 강제로 플러시 해줘야 한다. 영속성 컨텍스트와 DB 불일치로 데이터 무결성을 훼손해선 절대적으로 안된다.

728x90

'JPA' 카테고리의 다른 글

[JPA] 객체 지향 쿼리 언어 - 3  (0) 2022.08.05
[JPA] 객체 지향 쿼리 언어 - 2  (0) 2022.08.05
[JPA] 값 타입  (0) 2022.08.05
[JPA] 프록시  (0) 2022.08.05
728x90

JPA의 데이터 타입을 크게 분류하면 엔티티 타입과 값 타입으로 나눈다.
엔티티 타입은 @Entity로 정의하는 객체이고, 값 타입은 int, Integer, String 처럼 단순히 값으로 사용하는 자바 기본 타입이나 객체를 말한다.
값 타입은 3가지로 나눌 수 있다.

  • 기본 값 타입
    • 자바 기본타입
    • 래퍼(Wrapper) 클래스
    • String
  • 임베디드 타입(복합 값)
  • 컬렉션 값 타입

기본 값 타입은 말그대로 자바가 제공하는 기본 데이터 타입이고, 임베디드 타입은 JPA에서 사용자가 직접 정의한 값이다. 컬렉션 타입은 하나 이상의 값 타입을 저장할 때 사용한다.

기본은 다뤘으므로 생략하도록 하겠다.

임베디드 타입(복합 값 타입)

직접 정의한 임베디드 타입도 int, String 처럼 값 타입이다.

공통적으로 쓰는 어떤것(ex - 시간, 주소)들을 엔티티 클래스 마다 그대로 가지고 있으면 객체지향적이지 않으며 응집력이 떨어진다. 이런 공통 타입이 생기면 더 명확해진다.

임베디드 타입을 사용하려면 2가지 어노테이션이 필요하다.
둘 중 하나는 생략해도 된다.

  • @Embeddable : 값 타입을 정의하는 곳에 표시
  • @Embedded : 값 타입을 사용하는 곳에 표시

임베디드 타입은 엔티티의 값일 뿐이다. 따라서 값이 속한 엔티티의 테이블에 매핑한다.
이 임베디드 타입 덕분에 객체와 테이블을 아주 세밀하게 매핑하는것이 가능하기 때문에 잘 설계된 애플리케이션은 매핑한 테이블 수보다 클래스의 수가 더 많다.

Mybatis로 개발을 한다면 테이블, 객체 1:1매핑을 한다. 그렇기에 객체지향으로 개발하려고 해도 이미 SQL에 너무나 의존적인 개발을 진행했기에 여러 클래스를 매핑하는 작업이 수월하지 않았다.

ORM인 JPA를 사용하면 귀찮은 반복 작업은 JPA에게 할당하고 모델을 설계하는데 집중할 수 있다.

이 기능으로 연관된 테이블은 모조리 @Embedded로 묶어 사용하는 그림이 그려진다❗️

@AttributeOverride : 속성 재정의

만약 주소가 집주소 그리고 회사 주소가 있다고 가정할때 클래스는 똑같은데 컬럼 값을 다르게 줘야한다면 속성을 재정의해서도 값을 줄수가 있다.

@Embeddable
@NoArgsConstructor
@AllArgsConstructor
@Builder
@Getter
public class Address {
    @Column(name = "city")
    private String city;

    private String street;

    private String zipcode;
}

@Embedded
private Address homeAddress;

@Embedded
@AttributeOverrides({
    @AttributeOverride(name = "city", column = @Column(name = "COMPANY_CITY")),
    @AttributeOverride(name = "street", column = @Column(name = "COMPANY_STREET")),
    @AttributeOverride(name = "zipcode", column = @Column(name = "COMPANY_ZIPCODE")),
})
private Address companyAddress;

이렇게해서 재정의를 해주면 companyAddress에는 override한 @Column 들이 매핑되게 된다.
그래서 SQL 쿼리문은 아래와 같이 출력된다.

여기서 column이 소문자로 나온 이유는 내가 JPA Buddy로 column설정을 무조건 언더바에 lowerCase로 나오게해서 그렇다.

설정이 없다면 대문자로 나오게 될 것이다.

이런식으로 공통적으로 쓰는것은 저번 강의에서 봤던 @MappedSuperClass와 같이 사용한다면 시너지가 극대화 될 것이라고 생각한다❗️

임베디드 타입과 null

임베디드 타입이 null이면 매핑한 컬럼 값은 모두 null이 된다.

member.setAddress(null); //null
em.persist(member);

멤버 테이블의 주소와 관련된 값은 모두 null이 된다.

값 타입과 불변 객체

값 타입은 단순하고 안전하게 다룰 수 있어야 한다.

값 타입 공유 참조

임베디드 타입 같은 값 타입을 여러 엔티티에서 공유하면 위험하다.

member1.setHomeAddress(new Address("OldCity"));
Address address = member1.getHomeAddress();

address.setCity("NewCity"); //멤버1의 address 공유
member2.setHomeAddress(address);

이렇게 update를 하게되면 멤버2만 NewCity로 변경이 되는 것이 아니라 멤버1의 주소도 NewCity로 변경된다. 이것은 영속성 컨텍스트가 멤버1, 2 둘 다 city 속성값이 변경된 것으로 생각하기 때문에 둘다 update 쿼리를 날리게 된다.

이런식으로 예상치 못한 곳에서 문제가 발생하는 것은 부작용이라고 한다. 이 부작용을 막기 위해선 값을 복사해서 사용하면 된다.

값 타입 복사

값을 복사해서 사용하면 공유 참조로 인해 발생하는 부작용을 피할 수 있다. 임베디드 타입처럼 직접 정의한 값 타입은 자바의 기본타입이 아니라 객체 타입이다.
자바 객체는 CallByReference 이기 때문에 참조값을 전달한다.
clone()이 아니라 예를 들어

Address a = new Address("주소1");
Address b = a;
b.setCity("주소테스트");

이렇게 b Address에 a가 참조하는 인스턴스의 참조값 자체를 b에 넘기면 둘은 같은 인스턴스를 공유참조 한다. 이렇게되면 a의 city값도 변하게 되는 것이다.

인스턴스를 복사해서 대입하면 공유 참조를 피할 수 있는데 복사하지 않고 원본 참조 값을 직접 넘기는 실수를 완전하게 막을 수는 없다. 그래서 객체의 공유 참조는 피할 수 없다.

책에서 해결책은 setter메소드를 모두 제거하면 된다고한다. 제거하면 부작용의 발생을 막을 수 있다.

불변 객체

객체를 불변하게 만들면 값을 수정할 수 없다. 그렇기에 부작용 원천 차단이 가능하다.
따라서 값 타입은 될 수 있으면 불변 객체로 설계해야 한다.
불변 객체의 값은 조회할 수 있지만 수정할 수 없다. 이 불변 객체도 객체기에 참조값 공유를 피할 수는 없지만 수정이 불가능하므로 부작용이 발생할 우려는 없다.

이런데에서 깨달은 것이 바로 생성자에서 값을 할당하는 것이다.

@Embeddable
@NoArgsConstructor(access = AccessLevel.PROTECTED)
@Getter
public class Address {
    private String city;

    public Address(String city) {
        this.city = city;
    }
}

멤버1의 주소값을 조회하여 새로운 주소 생성

Address address = member1.getHomeAddress();
Address newAddress = new Address(address.getCity());
member2.setHomeAddress(newAddress);

이렇게 해서 불변이라는 작은 제약조건으로 부작용이라는 에러를 막을 수 있다.

값 타입의 비교

이것은 너무나 잘 알고들 있을거라고 생각한다.

  • 동일성 비교 : 인스턴스의 참조 값을 비교(주소 값을 비교하는것)
    • == 사용
  • 동등성 비교 : 인스턴스의 단순 값을 비교
    • equals() 사용

요약

엔티티 타입과 값 타입의 특징은 다음과 같다.

엔티티 타입의 특징

  • 식별자(@Id) 가 있다.
    • 엔티티 타입은 식별자가 있고 식별자로 구별할 수 있다.
  • 생명 주기가 있다.
    • 생성 → 영속화 → 소멸 의 주기가 있다.
    • em.persist(entity) 로 영속화
    • em.remove(entity) 로 제거
  • 공유할 수 있다.
    • 참조 값 공유할 수 있다. 이것이 공유 참조라고 한다.
    • ex : 멤버 엔티티가 있으면 다른 엔티티에서 얼마든지 멤버 엔티티 참조 가능

값 타입 특징

  • 식별자 없음
  • 생명 주기를 엔티티에 의존함
    • 스스로 생명주기를 가지지 않고 엔티티에 의존한다. 의존하는 엔티티를 제거하면 같이 제거된다.
  • 공유하지 않는 것이 안전하다.
    • 엔티티 타입과는 다르게 공유하지 않는 것이 안전하다. 대신 값을 복사하자 ‼️
    • 오직 하나의 주인만 관리해야 함.
    • 불변 객체로 만드는 것이 안전

엔티티와 값 타입을 혼동해서 엔티티를 값 타입으로 만드는 실수는 범하지 말자!!!

728x90

'JPA' 카테고리의 다른 글

[JPA] 객체 지향 쿼리 언어 - 2  (0) 2022.08.05
[JPA] 객체지향 쿼리 언어 - 1  (0) 2022.08.05
[JPA] 프록시  (0) 2022.08.05
[JPA] 고급 매핑  (0) 2022.08.04
728x90

프록시와 즉시로딩, 지연로딩

객체는 객체 그래프로 연관된 객체들을 탐색.
그렇지만 객체가 DB에 저장되어 있으므로 연관된 객체를 마음껏 탐색하기는 어렵다.
JPA구현체들은 이것을 해결하기 위해 프록시라는 기술을 사용한다. 프록시를 사용하면 연관된 객체를 처음부터 DB에서 조회하는 것이 아니라, 실제 사용하는 시점에 DB에서 조회할 수 있다.
자주 함께 사용하는 객체들은 조인을 사용해서 함께 조회 하는것이 효과적이다.
JPA는 즉시로딩과 지연로딩이라는 방법으로 둘을 모두 지원한다.

영속성 전이와 고아 객체

JPA는 연관된 객체를 함께 저장하거나 함께 삭제할 수 있는 영속성 전이와 고아 객체 제거라는 편리한 기능을 제공한다.

프록시

엔티티를 조회할 때 연관된 엔티티들이 항상 사용되는 것은 아니다. 멤버 엔티티를 조회할 때 연관된 팀 엔티티는 비즈니스 로직에 따라 사용될 때도 있지만, 그렇지 않을 때도 있다.

@Entity @Getter
@NoArgsConstructor
public class Member implements Serializable {

    @Id @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;
    private String name;
    private int age;

    @ManyToOne
    private Team team;

    @Builder
    public Member(Long id, String name, int age, Team team) {
        this.id = id;
        this.name = name;
        this.age = age;
        this.team = team;
    }
}

@Entity
@Getter
public class Team {

    @Id
    @GeneratedValue
    private Long id;

    private String name;


    @Builder
    public Team(String name){
        this.name = name;
    }
}

아래는 테스트 코드이다.

@Test
@DisplayName("프록시 테스트")
void proxyTest(){
    //given
    tx.begin();

    Team team = Team.builder()
                    .name("team1")
                    .build();


    Member member = Member.builder()
                          .name("홍길동")
                          .age(27)
                          .team(team)
                          .build();

    em.persist(team);
    em.persist(member);
    em.flush();

    //when
    Member resultMember = em.find(Member.class, member.getId());
    Team resultTeam = resultMember.getTeam();

    //then
    assertThat(resultMember.getName()).isEqualTo("홍길동");
    assertThat(resultMember.getAge()).isEqualTo(27);
    assertThat(resultTeam.getName()).isEqualTo("team1");
}

이 코드는 멤버 아이디로 회원 엔티티를 찾으면서 연관된 팀의 이름도 출력한다.
그런데 만약 멤버 엔티티만 조회한다고 한다면 팀 엔티티까지 DB에서 조회해두는 것은 비효율적이다.

JPA는 이러한 문제를 해결하려고 엔티티가 실제 사용될 때까지 DB 조회를 지연하는 방법을 제공해준다.
이것을 지연로딩이라고 한다.
정리하자면 실제 팀 엔티티 값을 사용하는 시점에 DB에서 팀 엔티티에 필요한 데이터를 조회하는 것이다.

지연 로딩을 사용하기 위해선 실제 엔티티 객체 대신 DB조회를 지연할 수 있는 가짜 객체가 필요한데 이것을 프록시 객체라고 한다.

프록시 기초

JPA에서 식별자로 엔티티 하나를 조회할 때에는 EntityManager.find()를 사용한다.
이 메소드는 영속성 컨텍스트에 엔티티가 존재하지 않으면 DB를 조회한다.
이런식으로 엔티티를 직접 조회하게 되면 조회한 엔티티 사용유무에 관계없이 DB를 조회하게 된다.

엔티티를 실제 사용시점까지 DB조회를 미루고 싶다면 EntityManager.getReference()를 사용하면 된다. 이 메소드를 호출할 때 JPA는 DB를 조회하지 않고 실제 엔티티 객체도 생성하지 않는다. 대신 DB접근을 위임한 프록시 객체를 반환한다.

프록시의 특징

프록시 클래스는 실제 클래스를 상속받아 만들어진 것이므로 실제 클래스와 겉 모양이 같다. 사용하는 입장에서
이게 진짜 객체인지 프록시 객체인지 구분하지 않고 사용해도 된다.

실제 객체의 참조를 보관하기 때문에 프록시 객체의 메소드를 호출하면 프록시 객체는 실제 객체의 메소드를 호출한다.

프록시의 특징 7가지

  • 프록시 객체는 처음 사용할 때 한번만 초기화된다.
  • 프록시 객체를 초기화한다고 프록시 객체가 실제 엔티티로 바뀌는 것은 아니다. 프록시 객체가 초기화되면 프록시 객체를 통해서 실제 엔티티에 접근할 수 있다.
  • 프록시 객체는 원본 엔티티를 상속받은 객체이므로 타입 체크 시에 주의해서 사용해야 한다.
  • 영속성 컨텍스트에 찾는 엔티티가 이미 있으면 DB를 조회할 필요가 없으므로 프록시를 호출해도 실제 엔티티를 반환한다.
  • 초기화는 영속성 컨텍스트의 도움을 받아야 가능하다. 그래서 준영속 상태의 프록시를 초기화하면 org.hibernate.LazyInitializationException 예외 발생시킨다.

프록시 객체 초기화

프록시 객체는 실제 사용될 때 DB를 조회해서 실제 엔티티 객체를 생성하는데 이것이 프록시 객체 초기화이다.

초기화 과정은 다음과 같다.

프록시와 식별자

엔티티를 프록시로 조회할 때 식별자(PK) 값을 파라미터로 전달하는데 프록시 객체는 이 식별자 값을 보관한다.
프록시 객체는 식별자 값을 가지고 있으므로 식별자 값을 조회하는 메소드를 호출해도 프록시를 초기화하지 않는다. 단, 엔티티 접근 방식을 프로퍼티(@Access(AccessType.PROPERTY))로 설정한 경우에만 초기화하지 않는다. 접근방식을 필드로 설정하면 JPA는 실행한 메소드가 그것만 조회하는 것인지 아니면 부가적인 기능이 있는지를 알지 못하기에 프록시 객체를 초기화한다.

프록시 확인

JPA에서 isLoaded(Object entity) 를 사용하여 프록시 인스턴스의 초기화 여부를 확인할 수 있다.

즉시 로딩과 지연 로딩

프록시 객체는 연관된 엔티티를 지연로딩 할때 주로 사용된다.

JPA의 조회시점 두가지 방법

  • 즉시 로딩 : 엔티티를 조회할 때 연관된 엔티티도 함께 조회
    • 설정 방법 : @ManyToOne(fetch = FetchType.EAGER)
  • 지연 로딩 : 연관된 엔티티를 실제 사용할 때 조회한다.
    • 설정 방법 : @ManyToOne(fetch = FetchType.LAZY)

즉시 로딩

즉시 로딩(EAGER Loading)은 위에서 본것 처럼 FetchType을 EAGER로 설정해준다. 위 코드로 보면 멤버를 조회하는 순간 팀도 함께 조회한다.
두 테이블을 조회하기에 조회쿼리를 2번 실행할 것이라고 예상하지만, 대부분의 JPA 구현체는 즉시 로딩을 최적화 하기 위해 가능하면 조인 쿼리를 사용한다.

nullable 설정에 따른 조인 전략

  • @JoinColumn(nullable = true) : NULL 허용(기본값), 외부 조인 사용
  • @JoinColumn(nullbale = false) : NULL 허용하지 않음, 내부 조인 사용

지연 로딩

지연 로딩(LAZY Loading)을 사용하려면 FetchType을 LAZY로 설정해준다. 위 코드로 보면 find를 호출할 시에 멤버만 조회하고 팀은 조회하지 않지만 조회한 회원의 team 멤버변수에 프록시 객체를 넣어둔다.
반환된 팀 객체는 프록시 객체이므로 실제 사용될 때까지 데이터 로딩을 미룬다. 이것을 지연 로딩이라 한다.

즉시 로딩, 지연 로딩 정리

  • 지연 로딩 : 연관된 엔티티를 프록시로 조회한다. 프록시를 실제 사용할 때 초기화 하면서 DB를 조회한다.
  • 즉시 로딩 : 연관된 엔티티를 즉시 조회한다. Hibernate는 가능하면 SQL조인을 사용해서 한번에 조회한다.

프록시와 컬렉션 래퍼

Hibernate는 엔티티를 영속 상태로 만들 때, 엔티티에 컬렉션이 있으면 컬렉션을 추적하고 관리할 목적으로 원본 컬렉션을 Hibernate가 제공하는 내장 컬렉션으로 변경하는데 이것을 컬렉션 래퍼라고 한다.
엔티티를 지연 로딩하면 프록시 객체를 사용해서 지연 로딩을 수행하지만 주문내역 같은 컬렉션은 컬렉션 래퍼가 지연 로딩을 처리해준다.

JPA 기본 페치 전략

fetch 속성의 기본 설정값은 다음과 같다.

  • @ManyToOne, @OneToOne : 즉시 로딩(FetchType.EAGER)
  • @OneToMany, @ManyToMany : 지연 로딩(FetchType.LAZY)

책에 나와있는 추천하는 방법은 모든 연관관계에 지연 로딩을 사용하는 것 이라고 한다. 그리고는 실제 사용하는 상황을 봐서 꼭 필요한 곳에만 즉시 로딩을 사용하도록 최적화 하면 된다고 한다.

컬렉션에 FetchType.EAGER 사용 시 주의점

  • 컬렉션을 하나 이상 즉시 로딩하는 것은 권장하지 않는다.
    • 일대다 조인은 결과 데이터가 다(N) 쪽에 있는 수만큼 증가하기 때문에 너무 많은 데이터를 반환할 우려가 있다.
  • 컬렉션 즉시 로딩은 항상 외부 조인을 사용한다.

Eager 설정과 조인 전략

  • @ManyToOne, @OneToOne
    • optional = false : 내부 조인
    • optional = true : 외부 조인
  • @OneToMany, @ManyToMany
    • optional = false : 외부 조인
    • optional = true : 외부 조인

영속성 전이: CASCADE

특정 엔티티를 영속 상태로 만들 때 연관된 엔티티도 함께 영속 상태로 만들고 싶으면 영속성 전이기능을 사용하면 된다. JPA는 CASCADE 옵션으로 영속성 전이를 제공한다. 영속성 전이를 사용하면 부모 엔티티를 저장할 때 자식 엔티티도 함께 저장할 수 있다.

JPA에서 엔티티를 저장할 때 연관된 모든 엔티티는 영속 상태여야 한다.

영속성 전이: 저장

@Entity
@Getter
@Setter
@NoArgsConstructor
@AllArgsConstructor
public class Parent {
    @Id
    @GeneratedValue(strategy = GenerationType.AUTO)
    private Long id;

    @OneToMany(mappedBy = "parent", cascade = CascadeType.PERSIST)
    private List<Child> children = new ArrayList<>();
}

@Entity
@Getter
@Setter
@NoArgsConstructor
@AllArgsConstructor
public class Child {
    @Id
    @GeneratedValue
    private Long id;

    @ManyToOne
    private Parent parent;
}

@Test
void 영속성전이테스트(){
    tx.begin();

    Child child1 = new Child();
    Child child2 = new Child();

    Parent parent = new Parent();

    child1.setParent(parent); //연관관계 추가
    child2.setParent(parent); //연관관계 추가
    parent.getChildren().add(child1);
    parent.getChildren().add(child2);

    em.persist(parent);
    em.flush();

    tx.commit();
}

위 코드처럼 부모를 영속화할 때, 자식도 함께 영속화하기 위해 cascade PERESIST 옵션을 설정했다.
이렇게하면 한번에 영속화 가능하다.

영속성 전이는 연관관계 매핑하는 것과는 아무 관련이 없다. 엔티티를 영속화할 때 연관된 엔티티도 같이 영속화하는 편리함을 제공할 뿐이다.

영속성 전이 : 삭제

삭제도 마찬가지로 영속성 전이 사용 가능하다. CasCadeType.REMOVE로 설정하여 부모 엔티티를 삭제하면 연관된 자식 엔티티들도 같이 삭제된다.

remove설정을 하지않고 부모를 삭제하면 부모 엔티티만 삭제가 된다. 그런데 이 부모를 삭제하는 순간 외래키 제약조건 때문에 DB에서 외래키 무결성 예외가 발생하게 된다.

CASCADE 종류

  • ALL : 모두 적용
  • PERSIST : 영속
  • MERGE : 병합
  • REMOVE : 삭제
  • REFRESH : refresh
  • DETACH : detach

cascade 옵션은 여러 속성을 같이 사용할 수 있다.

참고❗️ PERSIST, REMOVE는 persist, remove메소드를 실행할때 전이가 바로 발생하지 않고 flush를 호출할 때 전이가 발생한다.

고아 객체

JPA는 부모 엔티티와 연관관계가 끊어진 자식 엔티티를 자동으로 삭제하는 기능을 제공하는데 이걸 고아 객체 제거라고 한다. 이 기능을 사용해서 부모 엔티티의 컬렉션에서 자식 엔티티의 참조만 제거하면 자식 엔티티가 자동으로 삭제하도록 할 수 있다.

orphanRemoval = true 옵션을 설정하면 컬렉션에서 엔티티를 제거할 때 DB의 데이터도 삭제된다. 마찬가지로 이 제거 기능도 영속성 컨텍스트를 flush할때 적용되므로 flush시점에 DELETE SQL이 실행된다.

모든 엔티티를 제거하려면 컬렉션을 비워주면 된다.

고아 객체 정리

고아 객체 제거는 참조가 제거된 엔티티는 다른 곳에서 참조하지 않는 고아 객체로 보고 삭제하는 기능이다. 그렇기에 이 기능은 한군데에서만 참조할 때 사용해야 한다. 만약 삭제한 엔티티를 다른 곳에서도 참조한다면 문제가 발생할 수 있다. 이런 이유때문에 orphanRemoval 옵션은 @OneToMany, @OneToOne에만 사용할 수 있다.

고아 객체 제거에는 기능이 또 한가지가 있는데, 개념적으로 볼 때 부모를 제거하면 자식은 고아가 된다. 따라서 부모를 제거하면 자식도 같이 제거된다.

영속성 전이 + 고아 객체, 생명주기

여태 배운 두 옵션을 (persist + orphanRemoval = true) 같이 사용하면 부모 엔티티를 통해 자식의 생명주기를 관리할 수가 있다.

Parent parent = em.find(Parent.class, parentId);
parent.addChild(child1);

//delete
Parent parent = em.find(Parent.class, parentId);
parent.getChildren().remove(removeObject);

정리

이번 포스팅에서는 프록시의 동작 원리에 대해 학습하고 즉시 로딩 그리고 지연 로딩에 관해 알아보았다.

주요 내용

  • JPA구현체들은 객체 그래프를 맘껏 탐색할 수 있도록 지원하는데 이때 프록시 기술이 사용된다.
  • 객체 조회할 때 연관된 객체를 즉시 로딩하는 것이 즉시 로딩, 지연해서 로딩하는 것이 지연 로딩
  • 객체를 저장하거나 삭제할 때 연관된 객체도 함께 저장하거나 삭제할 수가 있는데 이것을 영속성 전이라고 한다.
  • 부모 엔티티와 연관관계가 끊어진 자식 엔티티를 자동으로 삭제하려면 고아 객체 제거 기능을 사용하면 됨
728x90

'JPA' 카테고리의 다른 글

[JPA] 객체지향 쿼리 언어 - 1  (0) 2022.08.05
[JPA] 값 타입  (0) 2022.08.05
[JPA] 고급 매핑  (0) 2022.08.04
[JPA] 다양한 연관관계 매핑  (0) 2022.08.04
728x90

깃허브 바로가기
이번 장에서 다루는 내용들이다.

  • 상속 관계 매핑
  • @MappedSuperclass
  • 복합 키와 식별 관계 매핑
  • 조인 테이블
  • 엔티티 하나에 여러 테이블 매핑하기

상속 관계 매핑

RDBMS에는 객체지향 언어처럼 상속이라는 개념이 없다.

대신 그림과 같이 슈퍼타입 서브타입 관계 라는 모델링 기법이 상속 개념과 유사하다.
ORM에서의 상속 관계 매핑은 객체의 상속 구조와 DB의 슈퍼타입 서브타입 관계를 매핑하는 것이다.

이 슈퍼타입 서브타입 논리 모델을 실제 테이블로 구현할 때는 3가지 방법중 하나를 선택할 수 있다.

  • 각각의 테이블로 변환
    • 모두 테이블로 만들고 조회할 때 조인을 사용한다. JPA에서는 조인 전략이라고 한다.
  • 통합 테이블로 변환
    • 테이블을 하나만 사용하여 통합한다. JPA에서는 단일 테이블 전략이라고 한다.
  • 서브타입 테이블로 변환
    • 서브 타입마다 하나의 테이블을 만든다. JPA에서는 구현 클래스마다 테이블 전략 이라고 한다.

조인 전략

조인 전략은 엔티티 각각을 모두 테이블로 만들고 자식 테이블이 부모 테이블의 기본 키를 받아서 기본 키 + 외래 키로 사용하는 전략이다. 따라서 조회할 때 조인을 자주 사용한다.

❗️ 주의 사항

  • 객체는 타입으로 구분할 수 있지만 테이블은 타입의 개념이 없다.
    • 타입을 구분하는 컬럼을 추가해야 한다. 여기서는 DTYPE 컬럼을 구분 컬럼으로 사용한다.

예제 코드

@Entity
@Inheritance(strategy = InheritanceType.JOINED) //1
@DiscriminatorColumn(name = "DTYPE") //2
public abstract class Item {
    @Id
    @GeneratedValue
    @Column(name = "ITEM_ID")
    private Long id;

    private String name;
    private int price;
}

@Entity
@DiscriminatorValue("A") // 3
public class Album extends Item{
    private String artist;
}

@Entity
@DiscriminatorValue("M") // 3
public class Movie extends Item{
    private String director;
    private String actor;
}
  • @Inheritance(strategy = InheritanceType.JOINED)
    • 상속 매핑은 부모 클래스에 @Inheritance 를 사용해야 한다. 그리고 매핑 전략 중 조인 전략을 사용해서 JOINED를 사용
  • @DiscriminatorColumn(name = "DTYPE")
    • 부모 클래스에 구분 컬럼 지정.
    • 이 컬럼으로 자식 테이블 구분 가능.
    • 기본값이 DTYPE이므로 생략 가능하다.
  • @DiscriminatorValue("M")
    • 엔티티를 저장할 때 구분 컬럼에 입력할 값을 지정
    • 어떤 엔티티를 저장하면 구분 컬럼인 DTYPE에 Value 설정한 값이 들어간다.

기본값으로 자식 테이블은 부모 테이블의 ID 컬럼명을 그대로 사용하는데, 만약 자식 테이블의 기본 키 컬럼명을 변경하고 싶으면 @PrimaryKeyJoinColumn 을 사용하면 된다.

조인전략 정리

  • 장점
    • 테이블이 정규화됨
    • 외래 키 참조 무결성 제약조건 활용 가능
    • 저장공간 효율적으로 사용
  • 단점
    • 조회할 때 조인이 많이 사용되므로 성능 저하 우려됨
    • 조회 쿼리가 복잡함
    • 데이터를 등록할 INSERT SQL을 두번 실행한다.
  • 특징
    • JPA 표준 명세는 구분 컬럼을 사용하도록 하지만 Hibernate를 포함한 몇 구현체는 구분 컬럼 없이도 동작한다.
  • 관련 어노테이션
    • @PrimaryKeyJoinColumn
    • @DiscriminatorColumn
    • @DiscriminatorValue

단일 테이블 전략

이름 그대로 테이블을 하나만 사용
구분 컬럼으로 어떤 자식 데이터가 저장됐는지 구분한다. 조회할 때 조인을 사용하지 않으므로 일반적으로 가장 빠르다.

❗️ 주의사항

자식 엔티티가 매핑한 컬럼은 모두 null을 허용해야 한다.

사용법은 @Inheritance(strategy = InheritanceType.SINGLE_TABLE 을 사용하면 단일 테이블 전략을 사용한다. 테이블 하나에 모든 것을 통합하므로 구분 컬럼을 필수로 사용해야 한다.

단일 테이블 전략 정리

  • 장점
    • 조인이 필요 없으므로 일반적으로 조회 성능이 빠르다.
    • 조회 쿼리가 단순하다.
  • 단점
    • 자식 엔티티가 매핑한 컬럼은 모두 null을 허용해야 한다.
    • 단일 테이블에 모든 것을 저장하므로 테이블이 커질 수 있다. 그러므로 상황에 따라서는 조회 성능이 오히려 느려질 수 있다.
  • 특징
    • 구분 컬럼을 꼭 사용해야 한다.
    • @DiscriminatorValue를 지정하지 않으면 기본으로 엔티티 이름을 사용한다.

구현 클래스마다 테이블 전략

자식 엔티티마다 테이블을 만든다. 그러면서 자식 테이블 각각에 필요한 컬럼이 모두 있다.

@Inheritance(strategy = InheritanceType.TABLE_PER_CLASS) 를 사용
일반적으로 추천하지 않는 전략

  • 장점
    • 서브 타입을 구분해서 처리할 때 효과적
    • not null 제약조건 사용 가능
  • 단점
    • 여러 자식 테이블을 함께 조회할 때 성능이 느리다(SQL의 UNION을 사용해야 한다)
    • 자식 테이블을 통합해서 쿼리하기 어렵다.
  • 특징
    • 구분 컬럼을 사용하지 않는다.

추천하지 않는 방법이므로, 조인이나 단일 테이블 전략을 고려하자

@MappedSuperclass

상속 관계는 부모,자식 클래스 모두 DB테이블에 매핑 시켰는데 이것은 부모 클래스는 매핑하지 않고 상속받은 자식 클래스에게 매핑 정보만 제공하고 싶을때 사용하는 것이다.
@Entity 와는 다르게 실제 테이블과 매핑되지 않는다.

@MappedSuperclass의 특징

  • 테이블과 매핑되지 않고 자식 클래스에 엔티티의 매핑 정보를 상속하기 위해 사용
  • @MappedSuperclass로 지정한 클래스는 엔티티가 아니므로 em.find()나 JPQL에서 사용할 수 없다.
  • 직접 사용할 일이 없으므로 추상 클래스로 만들자.

@MappedSuperclass 를 사용하면 등록일자, 수정일자, 등록자, 수정자 같은 여러 엔티티에서 공통으로 사용하는 속성을 효과적으로 관리할 수 있다.

복합 키와 식별관계 매핑

식별관계 vs 비식별관계

DB 테이블 사이의 관계는 외래 키가 기본 키에 포함되는지 여부에 따라 식별, 비식별로 구분된다.

식별관계

식별 관계는 부모 테이블의 기본 키를 내려 받아 자식 테이블의 기본키 + 외래키로 사용하는 관계

비식별관계

비식별 관계는 부모 테이블의 기본 키를 받아서 자식 테이블의 외래 키로만 사용하는 관계다.
비식별 관계는 외래키에 NULL을 허용하는지에 따라 필수적, 선택적으로 갈린다.

  • 필수적 비식별 관계
    • 외래 키에 NULL 허용 ❌
    • 연관관계를 필수적으로 맺어야함
  • 선택적 비식별 관계
    • 외래 키에 NULL 허용 ⭕️
    • 연관관계를 맺을지 말지 선택 가능

최근 동향은 DB테이블 설계할 때 비식별 관계를 주로 사용하며 꼭 필요한 곳에만 식별 관계를 사용하는 추세이다. JPA는 모두 지원한다.

복합키 : 비식별 관계 매핑

식별자 필드가 2개 이상이면 별도의 식별자 클래스를 만들고 equals와 hashcode를 override해야 한다.
JPA는 복합키를 지원하기 위해 @IdClass, @EmbeddedId 2가지 방법을 제공하는데 전자는 관계형 DB에 가까운 방법이고 후자는 좀 더 객체지향에 가까운 방법이다.

@IdClass

@Entity
@IdClass(ParentId.class)
public class Parent {
    @Id
    @Column(name = "PARENT_ID1")
    private String id1;

    @Id
    @Column(name = "PARENT_ID2")
    private String id2;

    private String name;
}

public class ParentId implements Serializable {
    private String id1;
    private String id2;

    public ParentId() {
    }

    public ParentId(String id1, String id2) {
        this.id1 = id1;
        this.id2 = id2;
    }

    @Override
    public boolean equals(Object o) {...}

    @Override
    public int hashCode() {...}
}

@IdClass를 사용할 때 식별자 클래스는 다음 조건을 만족해야 한다.

  • 식별자 클래스의 속성명과 엔티티에서 사용하는 식별자의 속성명이 같아야 한다.
  • Serializable 인터페이스를 구현해야 한다.
    • equals, hashCode를 구현해야 함.
  • 기본 생성자가 있어야 한다.
  • 식별자 클래스는 public이어야 한다.

위와같이
Parent.id1, Parent.id2 값을 사용해서 식별자 클래스인 ParentId를 생성하고 영속성 컨텍스트의 키로 사용한다.

복합키로 조회하는건 아래와 같다.

식별자 클래스인 ParentId를 사용하여 엔티티를 조회함.

@EmbeddedId

다음 조건을 만족해야 한다.

  • @Embeddable 어노테이션을 붙여주어야 함.
  • Serializable 인터페이스 구현해야 함.
  • equals, hashCode 구현해야 함
  • 기본 생성자가 있어야함
  • 식별자 클래스는 public이어야 한다.

복합키와 equals(), hashCode()

영속성 컨텍스트는 엔티티의 식별자로 키를 사용해서 엔티티를 관리한다. 식별자를 비교하는 것으로는 equals, hashCode가 있다. 식별자 객체의 동등성이 지켜지지 않으면, 다른 엔티티가 조회되거나 엔티티를 찾을 수 없는 등 영속성 컨텍스트가 엔티티를 관리하는데 심각한 문제가 발생한다.
그래서 복합키는 equals, hashCode를 필수로 구현해야 한다.

@IdClass와 식별관계

필요한 키만큼 JoinColumn으로 자식에서 필요하다면 불러오면 된다.

@EmbeddableId와 식별관계

@MapsId 사용
Id클래스의 변수값을 @MapsId("childId") 식으로 매핑

비식별 관계 구현

@Entity
public class Parent {
    @Id
    @GeneratedValue
    @Column(name = "PARENT_ID")
    private Long id;
    private String name;
    ...
}

@Entity
public class Child {
    @Id
    @GeneratedValue
    @Column(name = "CHILD_ID")
    private Long id;
    private String name;

    @ManyToOne
    @JoinColumn(name = "PARENT_ID")
    private Parent parent;
}

식별관계 복합 키를 사용한 코드에 비하면 코드가 단순하다.

일대일 식별 관계

일대일 식별 관계는 자식 테이블의 기본 키값으로 부모 테이블의 기본 키 값만 사용한다.

식별, 비식별 관계의 장단점

DB설계 관점에서 보면 비식별 관계를 선호하는데 이유는 다음과 같다.

  • 식별 관계는 부모 테이블의 기본 키를 자식 테이블로 전파하면서 자식 테이블의 기본 키 컬럼이 점점 늘어난다. 조인할 때 SQL이 복잡해지고 기본 키 인덱스가 불필요하게 커질 수 있다.
  • 식별 관계는 2개 이상의 컬럼을 합해서 복합 기본 키를 만들어야 하는 경우가 많다.
  • 식별 관계를 사용할 때 기본 키로 비즈니스 의미가 있는 자연 키 컬럼을 조합하는 경우가 많다.
  • 식별 관계는 부모 테이블의 기본 키를 자식 테이블의 기본 키로 사용하므로 비식별 관계보다 테이블 구조가 유연하지 못하다.
  • 일대일 관계를 제외하고 식별 관계는 2개 이상의 컬럼을 묶은 복합 키를 사용한다. JPA에서 복합 키는 별도의 복합 키 클래스를 만들어서 사용해야 한다.
  • 비식별 관계의 기본 키는 대리 키를 사용하는데 JPA는 @GeneratedValue처럼 대리 키를 생성하기 위한 편리한 방법을 제공한다.

ORM 신규 프로젝트 진행 시 될 수 있으면 비식별 관계를 사용하고 기본 키는 Long 타입의 대리 키를 사용❗️

그리고 선택적 비식별 관계보다는 필수적 비식별 관계를 사용하자
이유 - 선택적 비식별 관계는 null허용이므로 외부 조인을 사용해야 한다.

조인 테이블

DB테이블의 연관관계 설계 방법

  • 조인 컬럼 사용(외래 키)
    • 테이블 간의 관계는 주로 조인 컬럼이라 부르는 외래 키 컬럼을 사용하여 관리
  • 조인 테이블 사용(테이블 사용)
    • 연관관계를 관리하는 조인 테이블을 추가하고 조인하려는 두 테이블의 외래 키를 가지고 연관관계를 관리함

일대일 조인

일대일 관계를 만드려면 조인 테이블의 외래 키 컬럼 각각에 유니크 제약조건을 걸어야 함.

@Entity
public class Parent {
    @Id
    @GeneratedValue
    @Column(name = "PARENT_ID")
    private Long id;
    private String name;

    @OneToOne
    @JoinTable(name = "PARENT_CHILD", joinColumns = @JoinColumn(name = "PARENT_ID"), inverseJoinColumns = @JoinColumn(name = "CHILD_ID"))
    private Child child;
}

@Entity
public class Child {
    @Id
    @GeneratedValue
    @Column(name = "CHILD_ID")
    private Long id;
    private String name;
}

@JoinTable의 속성

  • name : 매핑할 조인 테이블 이름
  • joinColumns : 현재 엔티티를 참조하는 외래 키
  • inverseJoinColumns : 반대방향 엔티티를 참조하는 외래 키

일대다 조인 테이블

일대다 관계를 만드려면 조인 테이블의 컬럼중 N과 관련된 컬럼에 유니크 제약조건을 걸어야한다.

다대일 조인 테이블

일대다 에서 방향만 반대이므로 생략하도록 하겠다.

다대다 조인 테이블

다대다 관계를 만드려면 조인 테이블의 두 칼럼을 합해서 하나의 복합 유니크 제약조건을 걸어주어야 한다.


엔티티 하나에 여러 테이블 매핑

@SecondaryTable 을 사용해서 @Table 로 먼저 매핑해주고 난다음 추가로 매핑할 수 있다.

속성

  • @SecondaryTable.name : 매핑할 다른 테이블의 이름
  • @SecondaryTable.pkJoinColumns : 매핑할 다른 테이블의 기본 키 컬럼 속성

여러 테이블에 접목하려면 @Column(table = "두번째 테이블") 로 이어주면 된다.
더 많은 테이블을 매핑하려면 @SecondaryTables 사용

728x90

'JPA' 카테고리의 다른 글

[JPA] 값 타입  (0) 2022.08.05
[JPA] 프록시  (0) 2022.08.05
[JPA] 다양한 연관관계 매핑  (0) 2022.08.04
[JPA] 연관관계 매핑  (0) 2022.08.04
728x90

깃허브 바로가기
엔티티 연관관계를 매핑할때는 다음 3가지를 고려해야 한다.

  • 다중성
    • 다대일(@ManyToOne)
    • 일대다(@OneToMany)
    • 일대일(@OneToOne)
    • 다대다(@ManyToMany)
  • 단방향, 양방향
    • 테이블은 외래키 하나로 조인을 사용하여 양방향 쿼리가 가능한데 비해, 객체는 참조용 필드를 가지고 있어야만 연관된 객체 조회가 가능하다. 한쪽만 참조하는것이 단방향, 양쪽 서로 참조하는 것이 양방향이다.
  • 연관관계의 주인
    • 연관관계의 주인은 외래키 관리자이고, fk를 소유하고 있는 테이블을 기준으로 관리자 역할을 주면 된다고 생각한다.

다대일

다대일 관계의 반대 방향은 항상 일대다 관계이고 일대다 관계의 반대 방향은 항상 다대일 관계이다.
DB테이블의 1, N 관계에서 외래 키는 항상 N쪽에 있다.
따라서 객체의 양방향 관계에서 연관관계의 주인은 항상 N쪽이다.

다대일 양방향관계

양방향은 외래 키가 있는 쪽이 연관관계의 주인이다.

아까도 말했듯이 1:N, N:1 연관관계는 항상 N에 외래 키가 있다.

양방향 연관관계는 항상 서로를 참조해야 한다.

양방향 연관관계는 항상 서로 참조해야 한다. 어느 한쪽만 하게되는 순간 양방향 연관관계는 성립되지 않는다.
편의 메소드는 한곳에만 작성하거나 양쪽에 작성을 할 수가 있는데 양쪽에 다 작성하게 되면 무한루프에 빠지므로 주의해야 한다.

일대다

다대일 관계의 반대 방향이다. 일대다 관계는 엔티티를 하나 이상 참조할 수가 있으므로 Java Collection중 Collection, List, Set, Map 중에 하나를 사용해야 한다.

일대다 단방향

하나의 팀은 여러 멤버를 참조할 수 있는데 이런 관계를 일대다 관계라고 한다. 팀은 멤버를 참조하지만 멤버는 팀을 참조하지 않으면 둘의 관계는 단방향이다.

일대다 단방향 매핑의 단점

일대다 단방향 매핑의 단점은 매핑한 객체가 관리하는 외래 키가 다른 테이블에 있다는 점이다.
본인 테이블에 외래 키가 있으면 엔티티의 저장과 연관관계 처리를 INSERT 한번으로 끝낼 수가 있지만, 다른 테이블에 외래키가 존재한다면 연관관계 처리를 위한 UPDATE SQL을 추가로 실행해야 한다.

일대일

일대일 관계는 양쪽이 서로 하나의 관계만 갖는다.

일대일 관계의 특징

  • 일대일 관계는 그 반대도 일대일 관계
  • 테이블 관계에서 일대다, 다대일은 항상 다쪽이 외래키를 가진다. 반면 일대일 관계는 주 테이블이나 대상 테이블 중 어느 곳이나 외래키를 가질 수 있다.
    • 주 테이블에 외래 키
      • 주 객체가 대상 객체를 참조하는 것처럼 주 테이블에 외래 키를 두고 대상 테이블을 참조함.
      • 장점 - 주 테이블이 외래 키를 가지고 있으므로 주 테이블만 확인해도 대상 테이블과 연관관계가 있는지 알 수 있다.
    • 대상 테이블에 외래 키
      • 전통적인 DB개발자들은 보통 대상 테이블에서 외래 키를 두는 것을 선호한다. 이 방법의 장점은 테이블 관계를 일대일에서 일대다로 변경할 때 테이블 구조를 그대로 유지할 수 있다.

주의할점❗️

프록시를 사용할 때 외래 키를 직접 관리하지 않는 일대일 관계는 지연 로딩으로 설정해도 즉시 로딩된다.

프록시에 해당하는 설명은 이후 포스팅에서 다루도록 하겠다.

다대다

관계형 데이터베이스는 정규화된 테이블 2개로 다대다 관계를 표현할 수 없다. 그래서 보통 다대다 관계를 일대다, 다대일 관계로 풀어내는 연결 테이블을 사용한다.
그런데 객체는 테이블과는 다르게 객체 2개로 다대다 관계를 만들 수 있다.

@ManyToMany@JoinTable 을 사용해서 연결 테이블을 바로 매핑한 것이다.

@JoinTable의 속성은 아래와 같다.

  • @JoinTable.name
    • 연결 테이블을 지정한다.
  • @JoinTable.joinColumns
    • 현재 방향인 회원과 매핑할 조인 컬럼 정보를 지정한다.
  • @JoinTable.inverseJoinColumns
    • 반대 방향인 상품과 매핑할 조인 컬럼 정보를 지정한다.

매핑의 한계와 극복(연결엔티티 사용)

@ManyToMany 를 사용하면 연결 테이블을 자동으로 처리해주므로 도메인 모델이 단순해지고 여러 가지로 편리하다.
연결 테이블에 컬럼을 추가하면 더 이상 @ManyToMany 를 사용할 수 없다.
다른 엔티티에는 추가한 컬럼들을 매핑할 수 없기 때문이다.

JPA에서 복합키를 사용하려면 별도의 식별자 클래스를 만들어야 한다. 그리고 엔티티에 @IdClass 를 사용해서 식별자 클래스를 지정하면 된다.
이 식별자 클래스의 특징은 다음과 같다.

  • 복합 키는 별도의 식별자 클래스로 만들어야 한다.
  • Serializable을 구현해야 한다.
  • equals와 hashCode 메소드를 구현해야 한다.
  • 기본 생성자가 있어야 한다.
  • 식별자 클래스는 public이어야 한다.
  • @IdClass 를 사용하는 방법 외에 @EmbeddedId 를 사용하는 방법도 있다.

식별관계

부모 테이블의 기본키를 받아서 자신의 기본키 + 외래 키로 사용하는 것을 데이터베이스 용어로 식별 관계라고 한다.

정리

다대일, 일대다, 일대일, 다대다 연관관계 매핑에 관해 살펴봤는데 다대다 연관관계는 사용하기엔 JPA가 다 알아서 처리해주므로 편리했지만 연결 테이블에 필드가 추가된다고 하면 더는 사용할 수 없어서
실무에서 사용하기엔 다대일 매핑이 훨씬 괜찮아 보였다.

728x90

'JPA' 카테고리의 다른 글

[JPA] 프록시  (0) 2022.08.05
[JPA] 고급 매핑  (0) 2022.08.04
[JPA] 연관관계 매핑  (0) 2022.08.04
[JPA] 엔티티 매핑  (0) 2022.08.04
728x90

깃허브 바로가기
엔티티들은 대부분 다른 엔티티와 연관이 있다.

연관관계 매핑 핵심 키워드

  • 방향 : 객체관계에서만 존재하고 실제 테이블 관계는 항상 양방향
    • 단방향 : 두 엔티티중 하나만 참조하는 것이
    • 양방향 : 두 엔티티가 서로 참조하는 것
  • 다중성 : 다대일(N:1), 일대일(1:1), 일대다(1:N), 다대다(N:M)
  • 연관관계의 주인 : 객체를 양방향 연관관계로 만들면 연관관계의 주인을 정해야 한다.

단방향 연관관계

Member와 Team이 있다고 가정할 때, 팀은 멤버를 여러명 가질 수 있지만, 멤버는 하나의 팀만 가질 수 있는 구조이다. 이것이 다대일관계이다.

public class Member {
    private String id;
    private String name;
    private Team team;
}

public class Team {
    private String id;
    private String name;
}

이런 관계로 있다고 한다면 멤버 객체와 팀 객체는 단방향 관계이다.
멤버는 Member.team 필드를 통해 팀을 알 수 있는데에 반해, 팀은 회원을 알 수가 없는 구조이다.

다시말해,
Get메소드를 추가한다면 member.getTeam() 으로 가능한데 반대는 불가하다는 것이다.

참조를 통한 연관관계는 언제나 단방향 구조이다. 이 객체간의 연관관계를 양방향으로 하고 싶다면 반대에도 필드를 추가해줘서 참조를 보관해야 한다.

그렇게 하려면 위 구조에서 Team을 아래와 같이 바꿔야 한다.

public class Team {
    private String id;
    private String name;
    private Member member;
}

객체는 양방향을 만드려면 서로 참조하게끔 단방향 연관관계를 2개 만들어야 하고, 테이블은 JOIN을 사용하기 때문에 외래 키를 사용하는 테이블의 연관관계는 양방향이다.

JPA매핑

멤버와 팀 객체들을 JPA를 사용하여 매핑해보면 아래와 같다.

@Entity
public class Member {

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

    private String name;

    //연관관계 매핑
    @ManyToOne
    @JoinColumn(name = "TEAM_ID")
    private Team team;
}

@Entity
public class Team {
    @Id
    @Column(name = "TEAM_ID")
    private String id;

    private String name;
}
  • 객체 연관관계 : 멤버 객체의 Member.team 필드 사용

  • 테이블 연관관계 : 멤버 테이블의 MEMBER.TEAM_ID 외래키 컬럼을 사용

  • @ManyToOne

    • 이름처럼 다대일 관계라는 매핑 정보들 함축하고 있다. 멤버와 팀은 다대일 관계이다.
      연관관계를 매핑할때 다중성을 나타내는 어노테이션을 필수로 사용해야 한다.
  • @JoinColumn(name = "TEAM_ID") - 외래키를 매핑할 때 사용

    • name속성에는 매핑할 외래 키 이름을 지정. 멤버와 팀 테이블은 TEAM_ID 외래 키로 연관관계를 맺으므로 이 값을 지정하여 맞춰주면 된다. 근데 이 어노테이션은 생략 가능하다.

@JoinColumn의 속성

속성 기능 기본값
name 매핑할 외래 키 이름 필드명 + _ + 참조하는 테이블의 기본키 컬럼명
referencedColumnName 외래 키가 참조하는 대상 테이블의 컬럼명 참조하는 테이블의 기본키 컬럼명
foreignKey(DDL) 외래 키 제약조건을 직접 지정할 수 있다.
이 속성은 테이블을 생성할 때만 사용한다.
unique
nullable
insertable
updatable
columnDefinition
table
@Column의 속성과 같다.

@ManyToOne의 속성

속성 기능 기본값
optional false로 설정하면 연관된 엔티티가 항상 있어야 한다. true
fetch 글로벌 페치 전략을 설정한다. • @ManyToOne = FetchType.EAGER
• @OneToMany = FetchType.LAZY
cascade 영속성 전이 기능을 사용한다.
targetEntity 연관된 엔티티의 타입 정보를 설정한다.
이 기능은 거의 사용하지 않는다.
컬렉션을 사용해도 제네릭으로 타입 정보를 알 수 있다.

연관된 엔티티 삭제

연관된 엔티티를 삭제하려면 기존에 있던 연관관계들을 먼저 제거하고 삭제해야 한다.
그렇지 않으면 외래키 제약 조건으로 DB에서 오류가 발생한다.
팀을 삭제하려면 팀에 소속된 멤버 연관관계를 먼저 전부 제거해야한다.
그런 뒤에 em.remove(team)을 해줘야 한다.

양방향 연관관계

아까 말했듯 팀은 여러 멤버를 가질 수 있다. 그렇기 때문에 컬렉션을 사용해야한다.
JPA는 List를 포함해서 Collection, Set, Map 같은 다양한 컬렉션을 지원한다.

  • 멤버 → 팀 (Member.team)
  • 팀 → 멤버 (Team.members)

DB 테이블은 외래 키 하나로 양방향 조회가 가능하기 때문에 따로 설정해줄 것은 없다.

@Entity
public class Team {
    @Id
    @Column(name = "TEAM_ID")
    private String id;

    private String name;

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

이렇게 설계하여도 테이블은 외래키 하나로 두 테이블의 연관관계를 관리한다.

엔티티를 양방향의 연관관계로 설정하면 객체 참조는 둘인데 외래키는 하나이다.
그래서 둘 사이에 차이가 발생
이러한 이유로 JPA에서는 연관관계중 하나를 정해 테이블의 외래키를 관리해야 한다.
이것을 연관관계의 주인이라고 한다.

  • 주인은 mappedBy 속성을 사용하지 않는다.
  • 주인이 아니라면 mappedBy 속성을 사용하여 속성의 값으로 연관관계의 주인을 지정해야 한다.

그래서 테이블에 외래 키가 존재하는 테이블에 연관관계 관리자 역할을 주어야한다.

주의점

양방향 연관관계는 연관관계의 주인이 외래 키를 관리하기 때문에 주인이 아닌 방향은 값을 설정하지 않아도 DB에 외래 키 값이 정상 입력된다.

그렇기 때문에 주인이 아닌곳에서만 값을 할당한다면 null을 반환하게 되는 결과를 초래한다.
이 부분을 굉장히 주의깊게 사용하여야 할 것이라고 생각한다.

객체까지 고려하여 주인이 아닌 곳에도 값을 할당해줘야 추후에 테스트 코드를 작성했을때 size 0인것을 반환하는 오류를 막을 수 있다.

결론 : 객체의 양방향 연관관계는 양쪽 모두 관계를 맺어주자

정리

양방향 매핑은 단방향 매핑에 비해 복잡하다. 복잡한 것에 비해 양방향의 장점은 반대방향으로 객체 그래프 탐색 기능이 추가된 것뿐이다.

  • 단방향 매핑만으로 테이블과 객체의 연관관계 매핑은 이미 완료되었다.
  • 단방향을 양방향으로 만들면 반대방향으로 객체 그래프 탐색 기능이 추가된다.
  • 양방향 연관관계를 매핑하려면 객체에서 양쪽 방향을 모두 관리해야 한다.

그리고 양방향 매핑 시에는 무한루프에 빠지지 않게 조심해야 한다. 서로가 서로를 무한으로 호출하는지 생각하면서 사용하자.

728x90

'JPA' 카테고리의 다른 글

[JPA] 고급 매핑  (0) 2022.08.04
[JPA] 다양한 연관관계 매핑  (0) 2022.08.04
[JPA] 엔티티 매핑  (0) 2022.08.04
[JPA] 영속성 관리  (0) 2022.08.04

+ Recent posts