본문 바로가기

백엔드/JPA

JPA 엔티티 매핑1

엔티티와 PK

JPA를 사용해서 테이블과 매핑하고 싶은 클래스는 @Entity 어노테이션을 달아줘야한다. 테이블과 매핑을 할거니 pk가 필요한데 @Id 어노테이션을 필드위에 달아줌으로써 이 필드가 pk이다 라는 것을 jpa에게 알려줘야한다. @GeneratedValue 라고 하면 jpa가 알아서 id를 할당해준다. 여기서 JPA의 영속성을 통해서 id 값을 확인해보면

JPA가 알아서 id 값을 세팅해주었으므로 p.getId() 를 출력해보면 어떤 id 가 할당되었는지 확인이 가능하다.
id 값을 생성하는 전략은 총 4가지가 있다. 수업에서는 SEQUENCE 만 알려주셨는데 나머지는 궁금해서 찾아봤다.

1. SEQUENCE
수업에서 알려주셨던 방법

이렇게 auto increment를 해주면서 id값이 정해진다.

 

2. IDENTITY

SEQUENCE 전략이랑 뭐가 다른거지...? 싶어서 한번 찾아봤다.

IDENTITY : 일단 엔티티를 디비에 저장해서 디비가 기본키 생성하도록 함. 그 후 조회해서 가져온다.
SEQUENCE : 데이터베이스 시퀀스를 사용하여 식별자 조회 후 엔티티에 할당
라는데 한번 로그를 찍으며 확인해봤다. 

무식한 방법이지만 이제 저 ==== 가 어디에 찍히는지를 보면 되겠다 ㅎ

SEQUENCE 전략 로그

em.persist 할때마다 hibernate_sequence로부터 값을 가져와서 할당하는 듯 하다. 그 이후에 트랜젝션이 커밋되면 플러시되면서 insert문들이 연달아 나가는듯 하다.

IDENTITY 전략

IDENTITY 전략은 이렇게 persist 할때마다 insert 문이 나간다. 신기 ㅋㅋㅋ IDENTITY 전략은 id의 타입이 String 이어도 pk 값이 1 2 3 4 이렇게 순차적으로 증가하면서 할당된다.

3. TABLE

키생성 전용 테이블을 하나 만들어서 id를 할당하는 전략이다. 직접해보니 update문과 select문이 엄청 많이 나가는게 동시성 문제가 많을 것으로 보인다.. 찾아보니 역시나 성능상의 이슈가 있다고 한다.

4. AUTO

기본값으로 데이터베이스에 따라 자동으로 지정해준다고 한다. H2의 경우엔 아무것도 지정하지 않으면 SEQUENCE 전략으로 나간다. 그동안 @GeneratedValue 만 썼을땐 이 AUTO 옵션이 적용되었던 것이었다..!

 

시퀀스 전략은 다음과 같은 옵션이 있다.

id 값이 3부터 할당되는 것을 확인했고

allocSize를 5로 줬으니 한번에 5개씩 가져와서 할당하는 것도 확인이 가능했다. 

단방향 º 양방향 연관관계

 

그래서 데이터베이스 중심의 설계 문제는 어떻게 해결하는데?

JPA가 Id 할당은 이런식으로 한다는건 알겠고.. 데이터베이스 중심으로 설계하면 

이렇게 매우 어색한 코드가 나오는 것이다. 그래서 JPA에서는 id 값을 가지는게 아니고 참조값을 가질 수 있도록 연관관계 매핑이라는 것이 등장한다. 

- 단방향
먼저 단방향 연관관계는 간단하다. 

public class Person {

    @Id @GeneratedValue(strategy = GenerationType.SEQUENCE,
            generator = "person_seq")
    private Long id;

    private String name;

    @ManyToOne(fetch = FetchType.LAZY)
    @JoinColumn(name="family_id")
    private Family family;
    
    /// getter setter
    
 }
 
 @Entity
public class Family {
    @Id @GeneratedValue
    private Long id;

    private String name;
}

이렇게 한쪽 클래스에서만 JoinColumn을 해주면된다. 여러명의 person이 하나의 family에 속해있다고 설계하고 ManyToOne으로 매핑했다. JoinColumn의 name 옵션은 person테이블에서 family의 외래키 컬럼명을 무엇으로 할 것이냐이다. 

이렇게 family_id 라고 설정된것이 보인다. referencedColumnName 옵션으로 상대 테이블의 외래키 컬럼명을 지정해주는데 아무것도 설정안하면 JPA가 알아서 찾아준다고 한다. 거의 쓸일이 없을 것으로 보인다. 
단방향에서 짚고 넘어가야할 것은 OneToMany는 없을 거라는 것이다. 연관관계의 주인은 항상 외래키를 들고있는 n 쪽
이기 때문이다. 위의 예시에서는 person이 n이고 family가 1이므로 person이 연관관계의 주인이 된다. 그럼 이제 양방향 연관관계에 대해서 알아보자.

- 양방향
다음은 양방향이다. 우선 단방향이나 양방향이나 테이블은 변하지 않는다. 애초에 한쪽에서 다른쪽의 pk를 외래키로 가지고 있으면서 join이 이뤄지기 때문이다. 객체에서는 서로가 서로에 대한 참조를 가지고 객체 그래프에 대한 탐색이 가능해야한다. 그래서 Person, Family의 예제에서는 Family가 자신에게 속해있는 Person들을 가지고 있도록 수정해야한다.

public class Person {

    @Id @GeneratedValue(strategy = GenerationType.SEQUENCE,
            generator = "person_seq")
    private Long id;

    private String name;

    @ManyToOne(fetch = FetchType.LAZY)
    @JoinColumn(name="family_id")
    private Family family;
    
    /// getter setter
    
 }
 
 @Entity
public class Family {
    @Id @GeneratedValue
    private Long id;

    private String name;
    
    @OneToMany(mappedBy="team")
    List<Person> persons = new ArrayList<>();
}

Family에게 person 리스트를 갖고있고 mappedBy로 연결해주면 된다. mappedBy는 연관관계의 주인이 아닌쪽 객체에다가 설정하는 어노테이션이다.

연관관계에서 중요한것은 Family에서 Person의 목록을 수정하든 Person이 자신이 속한 Family를 변경하든 디비의 테이블에서 변하는 값은 결국 Person의 fk 값이다. 그러므로 Person이 연관관계의 주인이 되며 Family는 데이터를 읽기만 가능하도록 mappedBy를 준다. 이렇게 되면 말그대로 읽기만 가능하기 때문에 코드 상에서 Family의 Person 리스트에다가 new Person()으로 Person객체를 하나 만들어서 넣어줘도 디비에는 반영이 되지 않게된다. 

유사한 예제로 Team Member를 가지고 테스트를 해보면 아래와 같다.

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

Member mem = new Member();
mem.setUsername("멤버");
team.getMembers().add(mem);   // 1
mem.setTeam(team);            // 2

2를 주석 처리하고 1을 실행해보면 결과는 값이 들어가지 않는다. 연관관계의 주인이 아니기 때문이다.

반대로 1을 주석처리하고 2를 실행하면 실제 데이터가 들어가는 것을 확인할 수 있다.

그렇다고 해서 연관관계의 주인에만 값을 입력하는 것은 객체관계를 고려하면 좋지 않다. 1과 2 모두 실행해서 양쪽을 다 연결시켜주도록 해야한다.

만약 2번 코드만 실행한 다음 team을 persist한 후 해당 team을 find 하면 영속성 컨텍스트의 1차 캐시로부터 가져오므로 member가 아직 세팅되어있지 않은 그런 상황이 발생할 수 있다. 이러한 상황을 해결하기 위해서 객체지향적으로 양쪽 서로 다 연결하는 연관관계 편의 메소드를 만들어줘야한다.

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

팁이라 하면 단순 getter setter의 이름처럼 setTeam보다는 저렇게 살짝 다르게 해주는것이 나중에 코드를 봤을때 알아보기 편할 것이다. 주의 사항으로는 양방향 매핑시에는 무한루프를 조심해야한다. 롬복 라이브러리에서 toString등을 정의해줄때 필드들을 출력해주는데 문제는 이 과정에서 무한루프가 발생할 수 있다는 것이다.

Member.toString 에서 Team.toString 호출
Team.toString 에서 Member.toString 호출
Member.toString 에서 Team.toString 호출
Team.toString 에서 Member.toString 호출
Member.toString 에서 Team.toString 호출
Team.toString 에서 Member.toString 호출

...

그래서 lombok으로는 toString을 안쓰는게 좋다.

선생님의 추천은 기본적으로 단방향으로 다 끝내고 실제 애플리케이션 개발에서 양방향 넣는걸 고민하는 게 좋다고 하신다. 양방향 연관관계를 넣는 이유는 온전히 개발상의 편의 때문이다.

상속매핑

데이터베이스에서는 당연히 상속이라는 개념은 없고 그나마 슈퍼타입과 서브타입 관계라는 모델링 기법이 유사하다.
JPA에서는 부모 클래스에 @Inheritance 라는 어노테이션을 주면 된다. 이때 strategy 옵션을 줘야하는데 쓸만한 방법으로는 싱글테이블 전략과 조인전략 두가지가 있다.

싱글테이블
 자식 클래스의 모든 컬럼을 한 테이블에 때려넣는 전략이다. join을 하지 않아도 돼서 조회 쿼리가 단순하고 성능이 좋다. 하지만 다른 클래스의 컬럼은 모두 null이어야하므로 데이터 무결성이 보장되지 않는다는 단점이 있다.. 그리고 단일 테이블에 다 저장하므로 공간적으로 비효율적이다.

Family <- SecondFamily 가 상속
SecondFamily <- ThirdFamily 가 상속
이렇게 했을 때는 어떻게 동작하는지 확인해봤다.

결과는 역시나 모든 필드가 한 테이블에 들어갔다. 만약 상속하는 클래스들이 많아진다면 상당히 비효율적일 수 있는 전략인 것 같다. 그래도 단순한 케이스에는 딱일듯 하다 ㅋㅋㅋㅋ

조인테이블
 부모 클래스에 있는 공통 필드들은 부모 테이블에 들어가고 자식 클래스들은 각자의 테이블이 생성되는 방식이다. 생성해보면 아래와 같이 테이블에 값이 들어간다.

em.find( ThirdFamily ) 하면 테이블 3개가 조인되며 select 문이 나갈것이다.

상당히 빡센 조회쿼리가 나가는 것을 볼 수 있다 ㅋㅋㅋㅋㅋ 단점으로는 위에서 보이듯 복잡한 join 쿼리와 그로인한 성능 저하가 있겠다. 장점은 테이블이 정규화되어 구조가 깔끔하고 저장공간을 효율적으로 쓸 수 있다는 점이다. 

'백엔드 > JPA' 카테고리의 다른 글

JPA 값타입  (0) 2021.08.08
JPA 프록시와 영속성 전이  (0) 2021.08.06
JPA 엔티티 매핑2  (0) 2021.08.05
스프링부트 테스트 영속성과 JPQL  (0) 2021.07.27
JPA와 영속성  (0) 2021.07.27