연관관계 매핑 기초

이번 공부의 목표는 객체의 참조와 테이블의 외래 키를 매핑하는 것이 목표입니다.

단방향 연관관계

연관관계 중에선 다대일(N:1) 단방향 관계를 가장 먼저 이해해야 합니다.
예를 회원과 팀의 관계를 통해서 설명하겠습니다.

  • 회원과 팀이 있습니다.
  • 회원은 하나의 팀에만 소속될 수 있다.
  • 회원과 팀은 다대일 관계다.

스크린샷 2019-10-04 오전 2 08 17

객체 연관관계

  • 회원 객체는 Member.team 필드(맴버변수)로 팀 객체와 연관관계를 맺습니다.
  • 회원 객체와 팀 객체는 단방향 관계입니다. 회원은 Member.team 필드를 통해서 팀을 알수 있지만 반대로 팀은 회원을 알 수 없습니다. 예를 들어 member -> team의 조회는 member.getTeam()으로 가능하지만 반대 방향인 team -> member를 접근하는 필드는 없습니다.

테이블 연관관계

  • 회원 테이블은 TEAM_ID 외래 키로 팀 테이블과 연관관계를 맺습니다.
  • 회원 테이블과 팀 테이블은 양방향 관계입니다. 회원 테이블의 TEAM_ID 외래 키를 통해서 회원과 팀을 조인할 수 있고 반대로 팀과 회원도 조인할 수 있습니다. 예를 들어 MEMBER 테이블의 TEAM_ID 외래 키 하나로 MEMBER JOIN TEAM과 TEAM JOIN MEMBER 둘 다 가능합니다.

ex) 아래와 같이 외래 키 하나로 양방향으로 조인하는지 살펴보겠습니다.
SELECT * FROM MEMBER M JOIN TEAM T ON M.TEAM_ID = T.TEAM_ID

다음은 반대인 팀과 회원을 조인하는 SQL 입니다.
SELECT * FROM TEAM T JOIN MEMBER M ON T.TEAM_ID = M.TEAM_ID

객체 연관관계와 테이블 연관관계의 가장 큰 차이점

참조를 통한 연관관계는 언제나 단방향 입니다. 객체간에 연관관계를 양방향으로 만들고 싶으면 반대쪽에도 필드를 추가해서 참조를 보관해야 합니다. 결국 연관관계를 하나 더 만들어야 합니다. 이렇게 양쪽에서 서로 참조하는 것을 양방향 연관관계라고 합니다.
하지만 정확히 이야기하면 이것은 양방향 관계가 아니고 서로 다른 단방향 관계 2개 입니다. 반면에 테이블은 외래 키 하나로 양방향으로 조인이 가능 합니다.

단방향 연관관계

class A{
    B b;
}
class B{}

양방향 연관관계

class A{
    B b;
}

class B{
    A a;
}

객체 연관관계 vs 테이블 연관관계 정리

  • 객체는 참조(주소)로 연관관계를 맺는다.
  • 테이블은 외래 키로 연관관계를 맺습니다.

객체는 참조를 사용하지만 테이블은 join을 사용합니다.

  • 참조를 사용하는 객체의 연관관계는 단방향입니다.
    A -> B(a.b)

  • 외래키를 사용하는 테이블의 연관관게는 양방향입니다.
    A JOIN B 가 가능하면 반대로 B JOIN A 도 가능합니다.

  • 객체를 양방향으로 참조하려면 단방향 연관관계를 2개 만들어야 합니다.
    A -> B(a.b)
    B -> A(b.a)

순수한 객체 연관관계

아래 코드는 순수하게 객체만 사용한 연관관계를 알기 위해서 작성하였습니다.
JPA를 사용하지 않는 순수한 회원과 팀 클래스 코드입니다.

public class Member {

    private String id;

    private Team team_id; //팀의 참조를 보관

    private String username;

    public String getId() {
        return id;
    }

    public void setId(String id) {
        this.id = id;
    }

    public Team getTeam_id() {
        return team_id;
    }

    public void setTeam_id(Team team_id) {
        this.team_id = team_id;
    }

    public String getUsername() {
        return username;
    }

    public void setUsername(String username) {
        this.username = username;
    }
}
public class Team {

    private String id;
    private String name;

    public String getId() {
        return id;
    }
    public void setId(String id) {
        this.id = id;
    }

    public String getName() {
        return name;
    }

    public void setName(String name) {
        this.name = name;
    }
}

회원1과 회원2를 팀에 소속시키는 코드입니다.

public static void main(String[] args){

    Member member1 = new Member("member1", "회원1");
    Member member2 = new Member("member2", "회원2");
    Team team1 = new Team("team1", "팀1");

    member1.setTeam(team1);
    member2.setTeam(team1);

    Team findTeam = member1.getTeam();
}

위의 코드를 보면 회원1과 회원2는 팀1에 소속 되었습니다. 그리고 다음 코드로 회원1이 속한 팀을 조회할 수 있습니다.

Team findTeam = member1.getTeam();

이처럼 객체는 참조를 사용해서 연관관계를 탐색할 수 있는데 이것을 객체 그래프 탐색이라고 합니다.

객체 관계 매핑

이제 JPA를 사용해서 둘을 매핑해 보겠습니다.

package com.web.community.domain;

import javax.persistence.*;

@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_id(Team team) {
        this.team = team;
    }

    // Getter, Setter ...
}
package com.web.community.domain;

import javax.persistence.Column;
import javax.persistence.Entity;
import javax.persistence.Id;

@Entity
public class Team {

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

    private String name;

    // Getter, Setter ...
}

위의 코드는 Member, Team 엔티티를 매핑한 코드입니다.

  • 객체 연관관계: 회원 객체의 Member.team 필드 사용
  • 테이블 연관관계: 회원 테이블의 MEMBER.TEAM_ID 외래키 컬럼을 사용
@ManyToOne
@JoinColumn(name = "TEAM_ID")
private Team team;

회원 엔티티에 있는 연관관계 패밍부분인데 연관관계를 매핑하기 위한 새로운 어노테이션들 있습니다.

@ManyToOne: 이름 그대로 다대일(N:1)관계라는 매핑 정보입니다. 회원과 팀은 다대일 관계입니다. 연관관계를 매핑 할 때 이렇게 다중성을 나타내는 어노테이션을 필수로 사용해야 합니다.

@JoinColumn(name = “TEAM_ID”): 조인 컬럼은 외래 키를 매핑할 때 사용합니다.
name 속성에는 매핑할 외래 키 이름을 지정합니다. 회원과 팀 테이블은 TEAM_ID 외래 키로 연관관계를 맺으므로 이 값을 지정하면 됩니다. 이 어노테이션은 생략할 수 있습니다.

  • @ManyToOne 주요 속성
    스크린샷 2019-10-04 오전 2 08 37

  • targetEntity 속성의 사용 예 입니다.

@OneToMany
private List<Member>members; //제네릭으로 타입 정보를 알 수 있습니다.

@@OneToMany(targetEntity=Member.class)
private List members; // 제네릭이 없으면 타입 정보를 알 수 없습니다.

연관관계 사용

실제로 연관관계를 등록, 수정, 삭제, 조회하는 예쩨를 코드로 알아보겠습니다.

저장

연관관계를 매핑한 엔티티를 아래와 같은 코드로 저장해보았습니다.

@RunWith(SpringRunner.class)
@SpringBootTest
public class CommunityApplicationTests {

    @Autowired
    MemberRepository memberRepository;

    @Autowired
    TeamRepository teamRepository;

    @Test
    public void testSave() {

        Team team1 = new Team("team1", "팀1");
        teamRepository.save(team1);

        Member member1 = new Member("member1", "맴버1");
        member1.setTeam(team1);

        memberRepository.save(member1);
    }
}

가장 핵심은 member1.setTeam(team1); 회원 -> 팀 참조 부분입니다.
memberRepository.save(member1);

회원 엔티티는 팀 엔티티를 참조하고 저장했습니다. JPA는 참조한 팀의 식별자(Team_id)를 외래 키로 사용해서 적절한 등록 쿼리를 생성합니다. 이때 실행한 SQL은 다음과 같습니다.

insert into team (team_id, name) values ('team1', '팀1')
insert into member (team_id, username, member_id) values ('team1', 'member1', '맴버1')

실제로 데이터가 잘 입력되었는지 MySQL 데이터베이스에서 확인해보았습니다.

스크린샷 2019-10-04 오전 2 08 47

조회

연관관계가 있는 엔티티를 조회하는 방법은 크게 2가지 입니다.

  1. 객체 그래프 탐색
  2. 객체지향 쿼리 사용(JPQL)
  • 객체 그래프 탐색
  • 방금 저장한 대로 회원1이 팀1에 소속해 있다고 가정합니다.
    member.getTeam()을 사용해서 member와 연관된 team 엔티티를 조회할 수 있습니다.
Optional<Member> findMember = memberRepository.findById("member1");
findMember.ifPresent(member -> System.out.println(member.getId() +" " + member.getUsername() + " " + member.getTeam().getId()));

이처럼 객체를 통해 여관된 엔티티를 조회하는 것을 객체 그래프 탐색이라고 합니다.

  • 객체지향 쿼리 사용
    객체지향 쿼리인 JPQL에서 연관관계를 어떻게 사용하는지 알아보겠습니다.
    예를들어서 회원을 대상으로 조회하는데 팀1에 소속된 회원만 조회하려면 회원과 연관된 팀 엔티티를 검색 조건으로 사용해야 합니다. SQL은 연관된 테이블을 조인해서 검색조건을 사용하면 됩니다. JPQL도 조인을 지원하지만 문법이 약간 다릅니다.

저같은 경우에는 스프링 부트로 개발을 하고 있기 때문에 책에 내온 내용과 다르게 JPQL을 짜서 조인을 해보았습니다.

//Repository 클래스에서 쿼리메소드를 작성하였습니다.
public interface MemberRepository extends CrudRepository<Member, String> {
    //테이블이 아니라 엔티티를 중심으로 쿼리를 작성하면 JPA가 분석하여 SQL을 만들어서 데이터베이스에 해당쿼리를 전송합니다. 
    @Query("select m from Member m join m.team t where t.name = ?1")
    public List<Member> findByMember(String id);   
}

 @Test
    public void testSave() {

        Team team1 = new Team("team1", "팀1");
        teamRepository.save(team1);

        Member member1 = new Member("member1", "맴버1");
        member1.setTeam(team1);

        memberRepository.save(member1);

        List<Member> resultList = memberRepository.findByMember("팀1");

        for(Member member : resultList){
            System.out.println(member.getUsername());
        }
    }
    // 결과 값: 맴버1

JPQL의 from Member m join m.team t 부분을 보면 회원이 팀과 관계를 가지고 있는 필드(m.team)를 통해서 Member와 Team을 조인했습니다. 그리고 where 절을 보면 조인한 t.name를 검색조건으로 사용해서 팀1에 속한 회원만 검색했습니다.

참고로 ?1은 첫번째로 들어온 파라미터를 바인딩 받는 문법입니다.

이때 실행되는 SQL은 다음과 같습니다.

SELECT m from Member m join Team t on m.TEAM_ID = t.TEAM_ID
WHERE TEAM_NAME = '팀1';

실행된 SQL과 JPQL을 비교하면 JPQL은 객체(엔티티)를 대상으로 하고 SQL보다 간결합니다.

수정

@Test
    public void updateRelation(){

        Team team1 = new Team("team1", "팀1");
        teamRepository.save(team1);

        Team team2 = new Team("team2", "팀2");
        teamRepository.save(team2);

        Member member1 = new Member("member1", "맴버1");
        member1.setTeam(team1);

        memberRepository.save(member1);


        Member findMember = memberRepository.findByMemberId("member1");

        System.out.println(findMember.getUsername());

        findMember.setTeam(team2);
}

실행되는 수정 SQL은 다음과 같습니다.

UPDATE MEMBER
SET TEAM_ID='team2', ...
WHERE ID='member1'

수정은 update() 같은 메소드가 없습니다. 단순히 불러온 엔티티의 값만 변경해두면 커밋할 때 플러시가 일어나면서 변경감지 기능이 작동합니다.
이것은 연관관계를 수정할 때도 같은데, 참조하는 대상만 변경하면 나머지는 JPA가 자동으로 처리합니다.

연관관계 제거

@Test
public void deleteRelation(){
    Member member1 = MemberRepository.findByMemberId("member1");
    member1.setTeam(null);
}

연관된 엔티티 삭제

연관된 엔티티를 삭제하려면 기존에 있던 연관관계를 먼저 제거하고 삭제해야 합니다. 그렇지 않으면 외래 키 제약조건으로 인해, 데이터베이스에서 오류가 발생합니다.
팀1에는 회원1과 회원2가 소속되어 있습니다. 이때 팀1을 삭제하려면 연관관계를 먼저
끊어야 합니다.

member1.setTeam(null);
member2.setTeam(null);
em.remove(team);

다음에는 양방향 연관관계에 대해서 자세히 공부 후 리뷰해보겠습니다.

양방향 연관관계

회원에서 팀으로 접근하는 다대일 단방향 매핑을 공부해보았습니다. 이번에는 반대 방향인 팀에서 회원으로 접근하는 관계를 추가해보겠습니다. 그래서 회원에서 팀으로 접근하고 반대 방향인 팀에서 회원으로 접근 할 수 있도록 양방향 연관관계로 매핑하겠습니다.

스크린샷 2019-10-04 오전 2 09 30

먼저 객체 연관관계를 보면 회원과 팀은 다대일 관계입니다. 반대로 팀에서 회원은 일대다 관게 입니다. 일대다 관계는 여러 건과 여러관계를 맺을 수 있으므로 컬렉션을 사용해야 합니다. Team.members를 List 컬렉션으로 추가했습니다.

  • 회원 -> 팀(member.team)
  • 팀 -> 회원(Team.members)

데이터베이스 테이블에서는 외래 키 하나로 양방향으로 조회 할 수 있습니다.
따라서 처음부터 양뱡향 관게입니다. 그러므로 데이터베이스에 추가 할 내용은 전혀 없습니다…

왜냐하면 처음에 언급한것 처럼 MEMBER JOIN TEAM <-> TEAM JOIN MEMBER도 가능하기 때문이죠

양방향 연관관계 매핑

회원 엔티티

@Entity
@NoArgsConstructor
@ToString(exclude = "team")
public class Member {

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

    private String username;

    //@ManyToOne(fetch = FetchType.LAZY)
    @ManyToOne
    @JoinColumn(name = "TEAM_ID")
    private Team team; //팀의 참조를 보관

    public Member(String id, String username) {
        this.id = id;
        this.username = username;
    }

    //연관관계 설정
    public void setTeam(Team team) {
        this.team = team;
    }
}

팀 엔티티

@Entity
@NoArgsConstructor
public class Team {

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

    private String name;

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

    public Team(String id, String name) {
        this.id = id;
        this.name = name;
    }
}

위의 팀 엔티티 코드에서 팀과 회원은 일대다 관계입니다. 따라서 List members를 추가했습니다. 그리고 일대다 관계를 매핑하기 위해서 @OneToMany 매핑 정보를 사용했습니다. mappedBy 속성은 양방향 매핑일 때 사용하는데
반대쪽 매핑이 Member.team이므로 team을 값으로 주었습니다.

일대다 컬렉션 조회

@Test
public void biDirection(){
List<Member> lists = teamRepository.getMemberList("team1");
    for (Member member : lists) {
        logger.info(member.getId() + " " + member.getUsername());
    }
}
//결과 member1 맴버1
//결과 member2 맴버2

연관관계의 주인

@OneToMany는 직관적으로 이해가 됩니다. 문제는 mappedBy 속성입니다. 단순히 @OneToMany만 있으면 되지 mapperBy는 왜 사용하는지 모르겠습니다. 사실 객체에는 연관관게/라는 것이 없습니다. 서로 다른 단방향 연관관계 2개를 애플리케이션 로직으로 잘 묶어서 양방향인 것처럼 보이게 할 뿐 입니다. 반면에 데이터베이스 테이블은 앞서 말한것 처럼 외래 키 하나로 양쪽이 서로 조인할 수 있습니다. 따라서 테이블은 외래 키 하나만으로 양방향 연관관계를 맺습니다.

객체 연관관계

  • 회원 -> 팀 연관관계 1개(단방향)
  • 팀 -> 회원 연관관계 1개(단방향)

테이블 연관관계

  • 회원 <-> 팀의 연관관계 1개(양방향)

다시 말해서 테이블은 외래 키 하나로 두 테이블의 연관관계를 관리합니다.
엔티티를 단방향으로 매핑하면 참조를 하나만 사용하므로 이 참조로 외래 키를 관리하면 됩니다. 그런데 엔티티를 양방향으로 매핑하면 회원 -> 팀, 팀 -> 회원 두곳에서 서로를 참조합니다. 따라서 객체의 연관관계를 관리하는 포인트는 2곳으로 늘어납니다.

여기서 문제점이 있습니다. 엔티티를 양뱡향으로 설정하면 객체의 참조는 둘인데 외래 키는 하나입니다. 따라서 둘 사이에 차이가 발생합니다.
이런 차이로 인해 JPA에서는 두 객체 연관관계 중 하나를 정해서 테이블의 외래키를 관리해야 하는데 이것을 연관관계의 주인이라 합니다.

양방향 매핑의 규칙: 연관관계의 주인

양방향 연관관계 매핑 시 지켜야할 규칙이 있는데 두 연관관계 중 하나를 연관관계의 주인으로 정해야 합니다. 연관관계의 주인만이 데이터베이스 연관관계와 매핑되고 외래 키를 관리(등록, 수정, 삭제)할 수 있습니다. 반면에 주인이 아닌 쪽은 읽기만 할 수 있습니다.

어떤 연관관계를 주인으로 정할지는 mappedBy 속성을 사용하면 됩니다.

  1. 주인은 mappedBy 속성을 사용하지 않습니다.
  2. 주인이 아니면 mappedBy 속성을 사용해서 속성의 값으로 연관관계의 주인을 지정해야 합니다.
  • 회원 -> 팀(member.team) 방향
public class Member {
    @ManyToOne
    @JoinColumn(name = "TEAM_ID")
    private Team team; //팀의 참조를 보관
}
  • 팀 -> 회원(team.members) 방향
public class Team { 
    @OneToMany(mappedBy = "team")
    List<Member> members = new ArrayList<>();
}

연관관계의 주인을 정한다는 것은 사실 외래 키 관리자를 선택하는 것입니다.

여기서는 회원 테이블에 있는 TEAM_ID 외래 키를 관리할 관리자를 선택해야 합니다.
만약에 회원 엔티티에 있는 Member.team을 주인으로 선택하면 자기 테이블에 있는 외래 키를 관리하면 됩니다. 하지만 팀 엔티티에 있는 Team.members를 주인으로 선택하면 물리적으로 전혀 다른 테이블의 외래 키를 관리해야 합니다. 왜냐하면 이 경우 Team.members가 있는 Team 엔티티는 TEAM 테이블에 매핑되어 있는데 관리해야할 외래 키는 MEMBER 테이블에 있기 때문입니다.

연관관계의 주인은 외래 키가 있는 곳으로 설정하면 됩니다.
주인이 아닌 Team.members에는 mappedBy=“team” 속성을 사용해서 주인이 아님을 설정하면 됩니다. 여기서 mappedBy의 값으로 사 용된 team은 연관관계의 주인인 Member 엔티티의 team 필드를 말합니다.

결론은 연관관계의 주인만 데이터베이스 연관관계와 매핑되고 외래 키를 관리할 수 있습니다. 주인이 아닌 반대편은 읽기만 가능하고 외래키를 변경하지는 못합니다.

양방향 연관관계의 주의점

양방향 연관관계를 설정하고 가장 흔히 하는 실수는 연관관계의 주인에는 값을 입력하지 않고, 주인이 아닌 곳에만 값을 입력하는 경우입니다. 데이터베이스에 외래 키 값이 정상적으로 저장되지 않으면 이것부터 의심해봐야 합니다.

이것도 코드로 예제를 확인해 보겠습니다.

public void testSaveNonOwner(){

        Team team1 = new Team("team1", "팀1");
        teamRepository.save(team1);

        //맴버1 저장
        Member member1 = new Member("member1", "맴버1");
        memberRepository.save(member1);
        //맴버2 저장
        Member member2 = new Member("member2", "맴버2");
        memberRepository.save(member2);

        //주인이 아닌 곳만 연관관계 설정
        team1.getMembers().add(member1);
        team1.getMembers().add(member2);
        teamRepository.save(team1);
}

회원을 조회한 결과 값

스크린샷 2019-10-04 오전 2 09 41

외래 키 TEAM_ID에 team1이 아닌 null 값이 입력되어 있는데, 연관관계의 주인이 아닌 Team.members에만 값을 저장했기 때문입니다. 다시 한 번 강조하지만 연관관계의 주인만이 외래 키의 값을 변경할 수 있습니다.

순수한 객체까지 고려한 양방향 연관관계

객체지향 관점에서 양쪽 방향에 모두 값을 입력해주는것이 가장 안전합니다.
양쪽 방향 모두 값을 입력하지 않으면 JPA를 사용하지 않는 순수한 객체 상태에서 심각한 문제가 발생할 수 있습니다.

예를 들어 JPA를 사용하지 않고 엔티티에 대한 테스트 코드를 작성한다고 가정해봅시다. ORM은 객체와 관계형 데이터베이스 둘 다 중요합니다. 데이터베이스뿐만 아니라 객체도 함께 고려해야 합니다.

public void test순수한객체_양방향(){

    //팀1
    Team team1 = new Team("team1", "팀1");
    //맴버1 저장
    Member member1 = new Member("member1", "맴버1");
    //맴버2 저장
    Member member2 = new Member("member2", "맴버2"); 

    member1.setTeam(team1);
    member2.setTeam(team1);

    List<Member> members = team1.getMembers();
    System.out.println("members.size = " + members.size());
}
//결과: members.size = 0

예제코드는 JPA를 사용하지 않는 순수한 객체입니다. 코드를 보면 Member.team에만 연관관계를 설정하고 반대 방향은 연관관계를 설정하지 않았습니다. 그래서 결국 팀에 소속된 회원이 몇 명인지를 출력해보면 0이 출력됩니다. 이것은 우리가 기대하는 양방향 연관관계가 아닙니다.

양방향은 양쪽 다 관계를 설정해야 합니다.

public void test순수한객체_양방향(){  

    Team team1 = new Team("team1", "팀1");
    Member member1 = new Member("member1", "맴버1");
    Member member2 = new Member("member2", "맴버2"); 

    member1.setTeam(team1);
    team1.getMembers().add(member1);

    member2.setTeam(team1);
    team1.getMembers().add(member2);

    List<Member> members = team1.getMembers();
    System.out.println("members.size = " + members.size());
}
//결과: members.size = 2

위의 코드는 양쪽 모두 관계를 설정했고, 기대했던 2가 출력되었습니다.
이제 JPA를 사용해서 위의 코드를 완성해봅십다.

public void testORM_양방향(){  

    Team team1 = new Team("team1", "팀1");
    teamRepository.save(team1);

    Member member1 = new Member("member1", "맴버1");

    member1.setTeam(team1);
    team.getMembers().add(member1);
    memberRepository.save(member1);

    Member member2 = new Member("member2", "맴버2"); 

    member2.setTeam(team1);
    team.getMembers().add(member2);
    memberRepository.save(member2);
}

위 코드에서 양쪽에 연관관계를 설정했습니다. 순수한 객체 상태에서도 동작하며, 테이블의 외래 키도 정상 입력됩니다. 물론 외래 키 값은 연관관계의 주인인 Member.team 값을 사용합니다.

member.team: 연관관계의 주인, 이 값으로 외래 키를 관리합니다.
Team.getMembers().add(member1); // 주인이 아님. 저장 시 사용되지 않습니다.

결론은 객체의 양방향 연관관계는 객체 테이블 모두 관계를 맺어주여야 합니다.

연관관계 편의 메소드 작성 시 주의사항

사실 setTeam() 메소드에는 버그가 있습니다.

member1.setTeam(teamA); //1
member1.setTeam(teamA); //2
List<Member> memberList = teamA.getMembers(); //여전히 member1이 조회

위의 코드를 보면 먼저 member1.setTeam(teamA)를 호출한 직후 모습입니다.(그림실력이... 악마수준이라 생각하시고 보시기를...)

스크린샷 2019-10-04 오전 2 09 49

다음으로 member.setTeam(teamB)를 호출한 직후 객체 연관관계인 그림을 봅시다.

스크린샷 2019-10-04 오전 2 09 56

문제는 teamB로 변경할 때 teamA -> member1 관계를 제거하지 않는 겁니다…
연관관계를 변경할 때는 기존 팀이 있으면 기존 팀과 회원의 연관관계를 삭제하는 코드를 추가해야 합니다. 아래 코드처럼 수정합니다.

public void setTeam(Team team){

    if(this.team != null){
        this.team.getMembers().remove(this);
    }
    this.team = team;
    team.getMembers().add(this);
}

이 코드는 객체에서 서로 다른 단방향 연관관계 2개를 양뱡향인 것처럼 보이게 하려고 얼마나 많은 고민과 수고가 필요한지를 보여주고 있습니다. 반면에 관계형 데이터베이스는 외래 키 하나로 문제를 단순히 해결합니다.
결국엔 양방향 연관관계를 사용하려면 로직을 견고하게 작성해야 합니다.

양방향의 장점은 반대방향으로 객체 그래프 탐색 기능이 추가된 것뿐입니다.

  • 단방향 매핑만으로 테이블과 객체의 연관관계 매핑은 이미 완료되었습니다.
  • 단방향을 양방향으로 만들면 반대방향으로 객체 그래프 탐색 기능이 추가됩니다.
  • 양방향 연관관계를 매핑하려면 객체에서 양쪽 방향을 모두 관리해야 합니다.
출저: 자바 ORM 표준 JPA 프로그래밍

엔티티 매핑

JPA를 사용하는데 가장 중요한 일은 엔티티와 테이블을 정확히 매핑하는 일입니다.
매핑 어노테이션을 숙지하고 사용해야 하는데 JPA는 다양한 매핑 어노테이션을 지원합니다.
아래와 같이 크게 4가지로 분류할 수 있습니다.

  • 객체와 테이블 매핑: @Entity, @Table
  • 기본 키 매핑: @Id
  • 필드와 컬럼 매핑: @Column
  • 연관관계 매핑: @ManyToOne, @JoinColumn, @OneToMany

@Entity

JPA를 사용해서 테이블과 매핑할 클래스는 @Entity 어노테이션을 필수로 붙여야 합니다.
@Entity가 붙은 클래스는 JPA가 관리하는 것으로, 엔터티라 부릅니다.

스크린샷 2019-10-04 오전 1 38 40

@Entity 적용 시 주의사항

  • 기본 생성자는 필수다(파라미터가 없는 public 또는 protected 생성자).
  • final 클래스, enum, interface, inner 클래스에는 사용 할 수 없다.
  • 저장할 필드에 final을 사용하면 안 된다.

JPA가 엔터티 객체를 생성할 때 기본 생성자를 사용하므로 이 생성자는 반드시 있어야 합니다. 자바는 생성자가 하나도 없으면 다음과 같이 기본 생성자를 자동으로 만듭니다.
ex)

public Member() {} //기본 생성자

만약 생성자를 하나 이상 만들면 자바는 기본 생성자를 자동으로 만들지 않기 때문에 이때는 개발자가 직접 기본 생성자를 만들어야 합니다.

public Member() {} //직접 만든 기본 생성자

//임의의 생성자
public Member(String name){
this.name = name;
}

@Table

@Table은 엔터티와 매핑할 테이블을 지정한다. 생략하면 매핑한 엔터티 이름을 테이블 이름으로 사용합니다.

스크린샷 2019-10-04 오전 1 38 50

다양한 매핑 사용

JPA 시작하기 장에서 개발하던 회원 관리 프로그램에 다음 요구사항이 추가 되었습니다.

  1. 회원은 일반 회원과 관리자로 구분됩니다.
  2. 회원 가입일과 수정일이 있어야 합니다.
  3. 회원을 설명할 수 있는 필드가 있어야 합니다. 이 필드는 길이 제한이 없습니다.
    아래와 같이 위 요구사항을 만족하는 회원 엔터티에 기능을 추가 할 수 있습니다.
@Entity
@Table(name = "MEMBER") // Member 엔티티를 테이블 명 MEMBER로 매핑
public class Member

@Id //인스턴스 변수 id를 DB 테이블 MEMBER 기본키 ID로 매핑
@Column(name = "ID")
private String id;

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

private Integer age;
//자바의  enum을 사용해서 회원의 타입을 구분하였습니다. 일반 회원은 USER, 관리자는
//ADMIN입니다. 자바의 enum을 사용하려면 @Enumerated 어노테이션으로 매핑해야 합니다.
@Enumerated(EnumType.STRING)
private RoleType roleType;

//자바의 날짜 타입은 @Temporal을 사용해서 매핑해야 합니다. 
@Temporal(TemporalType.TIMESTAMP)
private Date createDate;

@Temporal(TemporalType.TIMESTAMP)
private Date lastModifiedDate;

//회원을 설명하는 필드는 길이 제한이 없고, 따라서 데이터베이스 varchar 타입 대신에
//CLOB 타입으로 저장해야 합니다. @Lob를 사용한다면 CLOB, BLOB 타입을 매핑할 수 있습니다.
@Lob
private String description;


public enum RoleType{
    ADMIN, USER
}

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

JPA는 데이터베이스 스키마를 자동으로 생성하는 기능을 지원합니다. 클래스의 매핑정보를 보면 어떤 테이블에 어떤 컬럼을 사용하는지 알 수 있습니다. JPA는 이 매핑정보와 데이터베이스 방언을 사용해서 데이터베이스 스키마를 생성합니다.

저 같은경우에는 스프링 부트에서 생성한 application.yml 파일에 아래와 같이 정의합니다.

spring:
  jpa:
    hibernate:
      ddl-auto: create

이 속성을 추가하면 어플리케이션 실행 시점에 데이터베이스 테이블을 자동으로 생성합니다.

스크린샷 2019-10-04 오전 1 39 03

운영 서버에서 create, create-drop, update처럼 DDL을 수정하는 옵션은 절대 사용하면 안됩니다. 오직 개발 서버나 개발 단계에서만 사용해야 합니다. 이 옵션들은 운영 중인 데이터베이스 컬럼을 삭제할 수 있기 때문입니다.

  • 개발 초기 단계는 create 또는 update
  • 초기화 상태로 자동화된 테스트를 진행하는 개발자 환경과 CI 서버는 create 또는 create-drop
  • 테스트 서버는 update 또는 validate
  • 스테이징과 운영 서버는 validate 또는 none

DDL 생성 기능

회원 이름은 필수로 입력되어야 하고, 10자를 초과하면 안 되는 제약조건이 추가되었습니다.
스키마 자동 생성하기를 통해 만들어지는 DDL에 이 제약조건을 추가해 봅시다.

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

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

    @Column(name = "NAME", nullable = false, length = 10) // 추가
    private String username;
}

위 코드에서 @Column 매핑정보의 nullable 속성 값을 false로 지정하면 자동 생성되는 DDL에 not null 제약조건을 추가할 수 있습니다. 그리고 length 속성 값을 사용하면 자동 생성되는 DDL에 문자의 크기를 지정할 수 도 있습니다.

기본키 매핑

JPA가 제공하는 데이터베이스 기본 키 생성 전략은 다음과 같습니다.

  • 직접할당: 기본 키를 어플리케이션에서 직접 할당합니다.
  • 자동생성: 대리 키 사용 방식

IDENTITY: 기본 키 생성을 데이터베이스에 위임한다.

SEQUENCE: 데이터베이스 시퀀스를 사용해서 기본 키를 할당한다.

TABLE: 키 생성 테이블을 사용한다.

자동 생성 전략이 이렇게 다양한 이유는 데이터베이스 벤더마다 지원하는 방식이 다르기 때문입니다. 오라클은 시퀀스를 제공하지만, MySQL은 시퀀스를 제공하지 않고 대신에 기본 키 값을 자동으로 채워주는 AUTO_INCREMENT 기능을 제공합니다. 따라서 SEQUENCE나 IDENTITY 전략은 사용하는 데이터베이스에 의존합니다.

//IDENTITY 매핑 코드
@Entity
public class Board{

    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;
}
//IDENTITY 사용 코드
private static void logic(EntityManager em){

    Board board = new Board();
    em.save(board);
    System.out.println("board.id = " + board.getId());
}

위의 코드는 em.save()를 호출해서 엔터티를 저장한 직후에 할당된 식별자 값을 출력하였습니다. 출력된 값 1은 저장 시점에 데이터베이스가 생성한 값을 JPA가 조회한 것입니다.

IDENTITY 전략과 최적화

IDENTITY 전략은 데이터를 데이터베이스에 INSERT한 후에 기본 키 값을 조회할 수 있다.따라서 엔터티에 식별자 값을 할당하려면 JPA는 추가로 데이터베이스를 조회해야 합니다.

주의사항
엔터티가 영속 상태가 되려면 식별자가 반드시 필요합니다. 그런데 IDENTITY 식별자 생성 전략은 엔터티를 데이터베이스에 저장해야 식별자를 구할 수 있으므로 em.save()를 호출하는 즉시 INSERT SQL이 데이터베이스에 전달됩니다. 따라서 이전 략은 트랜잭션을 지원하는 쓰기지연이 동작하지 않습니다.

SEQUENCE 전략

데이터베이스 시퀀스는 유일한 값을 순서대로 생성하는 특별한 데이터베이스 오브젝트 입니다.
SEQUENCE 전략은 이 시퀀스를 사용해서 기본 키를 생성합니다. 이 전략은 시퀀스를 지원하는 오라클, H2, PostgreSQL에서 사용할 수 있습니다.

@Entity
@SequenceGenerator(
    name = "BOARD_SEQ_GENERATOR",
    sequenceName = "BOARD_SEQ", //매핑할 데이터베이스 시퀀스 이름
    initialValue = 1, allocationSize = 1)
public class Board{


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

위의 코드에서 BOARD_SEQ_GENERATOR라는 시퀀스 생성기를 등록했습니다. 이 시퀀스 생성기를 실제 데이터베이스 BOARD_SEQ 시퀀스와 매핑합니다.
이제부터 id 식별자 값은 BOARD_SEQ_GENERATOR 시퀀스 생성기가 할당합니다.

//SEQUENCE 사용 코드
private static void logic(EntityManager em){

    Board board = new Board();
    em.save(board);
    System.out.println("board.id = " + board.getId());
}

IDENTITY 전략과 SEQUENCE 전략의 차이점
시퀀스 사용코드는 IDENTITY 전략과 같지만 내부 동작 방식은 다릅니다. SEQUENCE 전략은 em.save()를 호출 할 때 먼저 데이터베이스 시퀀스를 사용해서 식별자를 조회합니다. 그리고 조회한 식별자를 엔터티에 할당한 후에 엔터티를 영속성 컨텍스트에 저장합니다. 이후 트랜잭션을 커밋해서 플러시가 일어나면 데이터베이스에 저장을 합니다. 반대로 이전에 설명했던 IDENTITY 전략은 먼저 엔터티를 데이터 베이스에 저장한 후에 식별자를 조회해서 엔터티의 식별자에 할당합니다.

필드와 컬럼 매핑: 레퍼런스

@Enumerated

자바의 enum 타입을 매핑할 때 사용합니다.

스크린샷 2019-10-04 오전 1 39 21

ex)

enum RoleType{
    ADMIN, USER
}

다음은 enum 이름으로 매핑합니다.
@Enumerated(EnumType.STRING)
private RoleType roleType;

member.setRoleType(RoleType.ADMIN); //-> DB에 문자 ADMIN으로 저장됩니다.

@Enumerated를 사용하면 편리하게 enum 타입을 데이터베이스에 저장할 수 있습니다.

EnumType.ORDINAL은 enum에 정의된 순서대로 ADMIN은 0, USER은 1값이 데이터베이스에 저장됩니다.

  • 장점: 데이터베이스에 저장되는 데이터 크기가 작습니다.

  • 단점: 이미 저장된 enum의 순서를 변경할 수 없습니다.
    EnumType.STRING은 enum 이름 그대로 ADMIN은 ‘ADMIN’, USER는 'USER’라는 문자로 데이터베이스에 저장된다.

  • 장점: 저장된 enum의 순서가 바뀌거나 enum이 추가되어도 안전합니다.

  • 단점: 데이터베이스에 저장되는 데이터 크기가 ORDINAL에 비해서 큽니다.

@Transient

이 필드는 매핑하지 않습니다. 따라서 데이터베이스에 저장하지 않고 조회되지도 않습니다. 객체에 임시로 어떤값을 보관하고 싶을때 사용합니다.

@Transient
private Integer temp;

@Access

JPA가 엔터티 테이블에 접근하는 방식을 지정합니다.
테이블에 매핑할때 필드에 적용된 매핑정보를 읽은 다음에 테이블에 매핑을 하기 때문에 필드 접근방식을 지정하는 방법을 다르게 설정할 수 있습니다.

  • 필드 접근: AccessType.FIELD로 지정합니다. 필드에 직접 접근합니다. 필드 접근 권한이 private이어도 접근할 수 있습니다.

  • 프로퍼티 접근: AccessType.PROPERTY로 지정합니다. 접근자 Getter를 사용합니다.

//필드 접근 코드
@Entity
@Access(AccessType.FIELD)
public class Member{

    @Id
    private String id;

    private String data1;

    private String data2;

    ...
}

@Id가 필드에 있으므로 @Access(AccessType.FIELD)로 설정한 것과 같습니다. 따라서 @Access는 생략해도 됩니다.

//프로퍼티 접근 코드
@Entity
@Access(AccessType.PROPERTY)
public class Member{


    private String id;

    private String data1;

    private String data2;

     @Id
     public String getId(){
         return id;
     }

     @Column
     public String getData1(){
         return data1;
     }

     public String getData2(){
         return data2;
     }
}

@Id가 프로퍼티에 있으므로 @Access(AccessType.PROPERTY)로 설정한 것과 같습니다. @Access는 생략해도 됩니다.

//필드, 프로퍼티 접근 코드 함께 사용
@Entity
public class Member{

    @Id
    private String id;

    @Transient
    private String firstName;

    @Transient
    private String lastName;

     @Access(AccessType.PROPERTY)
     public String getFullName(){
         return firstName + lastName;
     }
}

@Id가 필드에 있으므로 기본은 필드 접근 방식을 사용하고, getFullName()만 프로퍼티 접근 방식을 사용합니다. 따라서 회원 엔터티를 저장하면 회원 테이블의 FULLNAME 컬럼에 firstName + lastName의 결과가 저장됩니다.

출저: 자바 ORM 표준 JPA 프로그래밍

영속성 컨텍스트의 특징

영속성 컨텍스트는 식별자 값(@Id로 테이블의 기본 키와 매핑한 값)으로 구분합니다.
따라서 영속 상태는 식별자 값이 반드시 있어야 합니다. 식별자 값이 없으면 예외가 발생하는걸 알아두셔야 합니다.
영속성 컨텍스트에 엔터티를 저장하면 이 엔터티는 바로 데이터베이스에 저장되지 않습니다. 트랜잭션 커밋이 수행되기 전까지 영속성 컨텍스트에 있다가 커밋을 수행하면 플러시라는걸 호출하여 쓰기지연 SQL 저장소라는 메모리 공간에 쌓인 sql문을 데이터베이스에 동기화하는 작업을 수행하고 이것을 플러시라고 합니다. 플러시는 아래서 더 자세하게 다루어 보겠습니다.

영속성 컨텍스트(Persistence Context)의 장점

1차 캐시
동일성 보장
트랜잭션을 지원하는 쓰기 지연
변경 감지
지연 로딩
위에서 말한 영속성 컨텍스트의 장점에 대해서 리뷰하고 어떤 이점이 있는지 엔터티를 CRUD하는 예제를 보면서 하나씩 살펴보겠습니다.

엔터티 조회

Member member = new Member();
member.setId("member1");
member.setUsername("회원");

em.persist(member);

이 코드를 실행하면 영속 컨텍스트 내부에 존재하는 1차 캐시에 회원 엔터티를 저장합니다.
회원 엔터티는 아직 데이터베이스에 저장되지 않는 상태입니다. 1차 캐시는 Collection인 Map 형태의 자료구조로 Key와 value 값을 가지고 있는 영역이라고 생각하시면 됩니다. Key는 식별자 값으로 @Id이고, value 값으로는 해당 member 인스턴스 즉 엔터티 값이 들어가 있습니다. 그리고 식별자 값은 데이터베이스 기본 키와 매핑되어 있습니다.

따라서 영속성 컨텍스트에 데이터를 저장하고 조회하는 모든 기준은 데이터베이스의 기본 키 값입니다.

엔터티 조회는 아래코드와 같습니다.

Member member = em.find(Member.class, "member1");

find() 메소드를 보면 첫 번째 파라미터는 엔터티 클래스 타입이고, 두번째는 조회할 엔터티의 식별자 값 입니다. 이 메소드를 호출하면 먼저 1차 캐시에서 엔터티를 찾고 만약 존재하지 않는다면 데이터베이스에서 조회를 하여 엔터티를 생성합니다. 그리고 1차 캐시에 저장한 후에 영속 상태의 엔터티를 반환합니다.

스크린샷 2019-10-04 오전 1 26 19

이러한 방식으로 한번 DB에서 조회를 한 후에 영속 컨텍스트에 저장하면 메모리에 있는 1차 캐시에서 바로 불러오기 때문에 성능상의 이점을 누릴 수 있습니다.

엔터티 등록

엔터티 매니저를 사용해서 엔터티를 영속성 컨텍스트에 등록하는 과정을 살펴보겠습니다.

EntityManager em = emf.createEntityManager();
EntityTransaction transaction = em.getTransaction();
//엔터티 매니저는 데이터 변경 시 트랜잭션을 시작합니다.
transaction.begin(); // 트랜잭션 시작

em.save(memberA);
em.save(memberB);
//여기까지 Insert SQL을 데이터베이스에 보내지 않습니다.

transaction.commit(); // 트랜잭션 커밋

엔터티 매니저는 트랜잭션을 커밋하기 직전까지 데이터베이스에 엔터티를 저장하지 않고 내부 쿼리를 쓰기지연 SQL저장소에 Insert 쿼리문들을 차곡차곡 쌓아두었다가 실제로 트랜잭션을 커밋하는 시점에서 모아둔 쿼리를 데이터베이스에 보내는데 이것을 트랜잭션을 지원하는 쓰기지연이라고 합니다.

트랜잭션을 커밋하는 시점에 엔터티 매니저는 우선 영속성 컨텍스트를 플러시를 합니다. 전에 올렸던 포스팅에서 플러시를 언급했었었는데요. 플러시란 영속성 컨텍스트의 변경 내용을 데이터 베이스에 동기화하는 작업입니다. 이때 등록, 수정, 삭제한 엔터티를 데이터베이스에 반영합니다. 한마디로 쓰기 지연 SQL 저장소에 모인 쿼리를 데이터베이스에 보내는 작업이라고 생각하시면 됩니다. 이렇게 영속성 컨텍스트 변경 내용을 데이터베이스에 동기화한 후에 실제 데이터베이스 트랜잭션을 커밋합니다.

어차피… 데이터를 저장하는 즉시 등록 쿼리를 데이터베이스에 보내거나 데이터를 저장하면 등록 쿼리를 데이터베이스에 보내지않고 메모리에 모아두다가 트랜잭션을 커밋할 때 모아둔 쿼리를 데이터베이스에 보내는 방법 모두 다 트랜잭션 범위 안에서 실행되므로 둘의 결과는 같습니다. 결과적으로… 트랜잭션을 커밋하기 전까진 아무 소용이 없는거죠 어떻게든 커밋 직전에만 데이터베이스에 SQL문을 전달하기만 하면 됩니다. 이것이 트랜잭션을 지원하는 쓰기 지연이 가능한 이유입니다.

이러한 기능을 잘만 활용한다면 모아둔 등록 쿼리를 데이터베이스에 한 번에 전달해서 성능을 최적화 할 수 있습니다.

엔터티 수정

SQL을 사용하면 수정 쿼리를 직접 작성해야 합니다. 그런데 프로젝트가 점점 커지고 요구사항이 늘어나면서 수정 쿼리도 점점 추가됩니다. 다음 아래와 같은 수정쿼리가 있습니다.

update MEMBER
SET
    NAME=?
    AGE=?
WHERE
    id=?

회원의 이름과 나이를 변경하는 기능을 개발했는데 회원의 등급을 변경하는 기능이 추가되면 회원의 등급을 다음과같이 수정 쿼리를 추가로 작성해야합니다.

UPDATE MEMBER
SET
   GRADE=?
WHERE
   id=?

보통은 이렇게 2개의 수정 쿼리를 작성한다. 물론 둘을 합쳐서 다음과 같이 하나의 수정 쿼리만을 사용해도 됩니다.

UPDATE MEMBER
SET 
   NAME=?
   AGE=?
   GRADE=?
WHERE
   id=?

하지만 이렇게 합쳐진 쿼리를 사용해서 이름과 나이를 변경하는 데 실수로 등급 정보를 입력하지 않거나, 등급을 변경하는데 실수로 이름과 나이를 입력하지 않을 수도 있습니다.
이런 개발방식의 문제점은 수정 쿼리가 많아지는 것은 물론이고 비즈니스 로직을 분석하기 위해 SQL을 계속 확인해야 합니다. 결국 직접적이든 간접적이든 비즈니스 로직이 SQL에 의존하게 됩니다.

이러한 문제점을 해결하기 위해서 JPA에서는 변경감지 기능을 제공합니다.
JPA로 엔터티를 수정할 때는 단순히 엔터티를 조회해서 데이터만 변경하면 됩니다.
엔터티의 데이터만 변경했는데 어떻게 데이터베이스에 반영이 될까… 이렇게 엔터티의 변경사항을 데이터베이스에 자동으로 반영하는 기능을 변경감지라고 합니다.

JPA는 엔터티를 영속성 컨텍스트에 보관할 때, 최초 상태를 복사해서 저장해두는데 이것을 스냅샷이라고 합니다. 그리고 플러시 시점에 스냅샷과 엔터티를 비교하여 변경된 엔터티를 찾습니다.

스크린샷 2019-10-04 오전 1 26 28

위의 그림이 가장 잘 설명하는거 같아서 첨부하였습니다 ㅎㅎ… 변경 감지는 영속성 컨텍스트가 제공하는 기능으로… 엔터티가 영속성 컨텍스트에 존재하지 않으면 당연히 변경된 데이터를 가진 엔터티는 DB에 변경된 상태로 저장되지 않습니다. 오직 영속상태의 엔터티만 적용됩니다.

엔티티 삭제

엔터티를 삭제하려면 먼저 삭제 대상 엔터티를 조회해야 합니다.

Member memberA = em.find(Member.class, "memberA); //삭제 대상 엔터티 조회
em.remove(memberA);

em.remove();

em.remove()에 삭제 대상 엔터티를 넘겨주면 엔터티를 삭제합니다. 물론 엔터티를 즉시 삭제하는 것이 아니라 엔터티 등록과 비슷하게 삭제쿼리를 쓰기 지연 SQL 저장소에 등록합니다.
트랜잭션을 커밋해서 플러시를 호출하면 실제 데이터베이스에 삭제 쿼리를 전달합니다. 참고로 em.remove(memberA)를 호출하는 순간 memberA는 영속성 컨텍스트에서 제거됩니다.
이렇게 삭제된 엔터티는 재사용하지 말고 자연스럽게 GC(가비지 컬렉션)의 대상이 되도록 두는 것이 좋습니다.

플러시란?

플러시는(flush())는 영속성 컨텍스트의 변경 내용을 데이터베이스에 반영합니다. 플러시를 실행하면 구체적으로 다음과 같은 일이 일어납니다.

  1. 변경 감지가 동작해서 영속성 컨텍스트에 있는 모든 엔터티를 스냅샷과 비교해서 수정된 엔터티를 찾습니다. 수정된 엔터티는 수정 쿼리를 만들어 쓰기 지연 SQL 저장소에 등록합니다.

  2. 쓰기 지연 SQL 저장소의 쿼리를 데이터 베이스에 전송합니다.(등록, 수정, 삭제, 쿼리)

영속성 컨텍스트를 플러시하는 방법은 총 3가지 입니다.

  1. em.flush()를 직접 호출합니다.

  2. 트랜잭션 커밋 시 플러시가 자동으로 호출됩니다.

  3. JPQL 쿼리 실행 시 플러시가 자동 호출 됩니다.

직접 호출

엔터티 매니저의 flush()의 메소드를 직접 호출해서 영속성 컨텍스트를 강제로 플러시 합니다.
테스트나 다른 프레임워크와 JPA를 함께 사용할 때를 제외하고 거의 사용하지 않습니다.

트랜잭션 커밋 시 플러시 자동 호출

데이터베이스에 변경 내용을 SQL로 전달하지 않고 트랜잭션만 커밋하면 어떤 데이터도 데이터베이스에 반영되지 않습니다. 따라서 트랜잭션을 커밋하기 전에 꼭 플러시를 호출해서 영속성 컨텍스트의 변경 내용을 데이터베이스에 반영해야 합니다.
JPA는 이런 문제를 예방하기 위해서 트랜잭션을 커밋할 때 플러시를 자동으로 호출합니다.

JPQL 쿼리 실행 시 플러시 자동 호출

JPQL이나 Criteria같은 객체지향 쿼리를 호출할 때도 플러시가 실행된다.
예시를 통해서 한번 알아보겠습니다.

//save 메소드는 JPA 구현체인 하이버네이트에서 제공해주는 함수로 persist처럼 해당 엔터티를 영속화 시킵니다.
em.save(memberA);
em.save(memberB);
em.save(memberC);

//중간에 JQPL 실행
query = em. createQuery("select m from Member m", Member.class);
List<Member> members = query.getResultList();

먼저 em.save()나 em.persist()를 호출해서 엔터티 memberA, memberB, memberC를 영속 상태로 만들었습니다. 이 엔터티들은 영속성 컨텍스트에는 있지만 아직 데이터베이스에는 반영되지 않았습니다. 이때 JPQL를 실행하면 JQPL은 SQL로 변환되어 데이터베이스에서 엔터티를 조회합니다. 그런데 memberA, memberB, memberC는 아직 데이터베이스에 없으므로 쿼리 결과로 조회되지 않습니다. 따라서 쿼리를 실행하기 직전에 영속성 컨텍스트를 플러시해서 변경 내용을 데이터베이스에 반영해야 합니다.
마찬가지로 JPA는 이런 문제를 예방하기 위해 JPQL을 실행할 때도 플러시를 자동 호출합니다.

지금까지 영속 컨텍스트가 제공하는 기능과 엔터티를 조회, 등록, 수정, 삭제 작업을 수행할 때 어떠한 과정을 거치는지… 그 과정속에서 플러시를 호출하는 메커니즘까지 살펴보았습니다.

출저: 자바 ORM 표준 JPA 프로그래밍

'SpringFramework > JPA' 카테고리의 다른 글

스프링 부트 테스트 : @DataJpaTest  (0) 2019.10.09
JPA 다양한 연관관계 매핑  (0) 2019.10.04
JPA 연관관계 매핑기초  (0) 2019.10.04
JPA가 지원하는 엔티티 매핑  (0) 2019.10.04
JPA를 이용한 영속성 관리  (0) 2019.10.04

JPA 그게 뭔데?

JPA(Java Persistence API)는 ORM(Object Relational Management) 기술 표준으로 스프링 부트 교재를 보면 대부분 실려있는 기술 스펙 입니다.

JPA가 어떤기술인지 알아보기전에 대체 왜 JPA가 세상에 나오게 됬는지에 대해서나 알게되면 확실히 기억에도 오래남는거 같아서 저의 얄팍한 지식으로 살펴보겠습니다.
스프링 프레임워크로 프로젝트를 해본 사람이라면 간단하든 복잡하든 DB를 사용하면서 대부분의 객체들을 테이블에 매핑하는 데이터베이스 관련 작업을 Spring JDBC Template이나 Mybatis를 이용하여 해보셨을 겁니다.
하지만 프로젝트 규모가 커지면서 객체와 테이블을 매핑하기 위해서 반복적이고 지루한 JDBC API를 호출하게 되고 이 과정에서 코드의 가독성과 유지보수적 측면에서 많은 효율성이 떨어지게 됩니다. 간단하게 개발자들이 해당 객체를 DB에 저장하기 위해서 테이블을 설계하는데 많은 시간을 소모하게 됩니다.
이렇게 되면 핵심 비즈니스 로직을 테스트하는데 소모하는 시간도 줄어들기 때문에 심각한 에러를 초래할 수 있고, 이러한 상황이 계속 무한루프 되는 악순환이 발생하는 것이죠.

이러한 문제점을 해결하기 위해서 JPA라는 걸출한 기술이 등장하게 되었습니다. JPA는 위에서 설명한것처럼 ORM을 정의하는 자바진영의 기술 표준입니다. 이러한 JPA 구현체는 여러개가 있지만 제가 아는 대표적인 구현체는 Hibernate 입니다.
저는 Hibernate를 기준으로 이야기 하겠습니다.

스크린샷 2019-10-04 오전 1 09 22

JPA가 제공하는 기능은 크게 두가지가 있습니다.

엔터티와 테이블을 매핑하는 설계하는 부분
매핑한 엔터티를 실제 사용하는 부분
위에 말하는 엔터티는 쉽게말해서 클래스이지만 그냥 클래스가 아니고 JPA에게 테이블과 매핑한다고 알려주고 클래스 위에 @Entity가 적용된 클래스를 엔터티라고 부릅니다.
이렇게 매핑한 엔터티를 엔터티 매니저를 통해서 영속성 컨테스트(Persistence Context)에 관리하게 됩니다.

스크린샷 2019-10-04 오전 1 11 07

엔터티 매니저는 엔터티를 저장, 수정, 삭제, 조회하는 등 엔터티와 관련된 모든 일을 처리합니다.

즉, 엔터티를 관리하는 관지라인 샘이죠 개발자 입장에서는 엔터티 매니저는 엔터티를 저장하는 가상의 데이터베이스로 생각하면 이해하기 쉽습니다.

엔티티 매니저 팩토리와 엔티티 매니저

스크린샷 2019-10-04 오전 1 11 23

데이터베이스를 하나만 사용하는 애플리케이션은 일반적으로 EntityManagerFactory를 하나만 생성합니다. EntityManager를 생성하기 위해서는 META-INF/persistence.xml이라는 클래스 패스에서 설정 정보를 사용해서 EntityManagerFactory를 먼저 생성해야 합니다.

스크린샷 2019-10-04 오전 1 11 33

EntityManagerFactory는 EntityManager를 생산하는 공장이라고 생각하시면 됩니다. 공장을 만드는 비용은 상당히 크기때문에 한개만 만들어서 애플리케이션 전체에서 공유도록 설계되어 있습니다. 반면에 EntityManager를 생산하는 비용은 거의 들지 않습니다.

EntityManager는 Entity를 영속성 컨텍스트에 관리하고 있습니다. 이것은 JPA를 이해하는데 가장 중요한 이유입니다. 영속성 컨텍스트는 엔터티를 영구 저장하는 환경이라는 뜻입니다.

엔터티의 생명주기

스크린샷 2019-10-04 오전 1 19 30

비영속일때는 엔터티 객체를 생성했했지만 아직 순수한 객체 상태이며 아직 저장하지 않았습니다.
따라서 영속성 컨텍스트나 데이터베이스와는 전혀 관련이 없기때문에 이것을 비영속 상태라고 합니다.

ex) 비영속 상태

Member member = new Member();
member.setId("member1");
member.setUsername("회원1");

영속상태는 엔터티 매니저를 통해서 엔터티를 영속성 컨텍스트에 저장합니다. 이렇게 영속성 컨텍스트가 관리하는 엔터티를 영속 상태라고 합니다. 이제 회원 엔터티는 비영속 상태에서 영속상태가 되었습니다.

ex) 영속 상태

//비영속 상태인 member 인스턴스를 영속상태로 변경
em.persist(member);

em.find()나 JPQL을 사용해서 조회한 엔터티도 영속성 컨텍스트가 관리하는 영속상태입니다.

스크린샷 2019-10-04 오전 1 22 37

결국 영속 상태라는 것은 영속성 컨텍스트에 의해 관리된다는 뜻입니다.

준영속은 영속성 컨텍스트가 관리하던 영속상태의 엔터티를 영속성 컨텍스트가 관리하지 않으면 준영속 상태가 됩니다. 특정 엔터티를 준영속 상태로 만들려면 em.detach()와 영속성컨텍스트를 닫는 em.close(), 영속성 컨텍스트를 초기화하는 em.clear()를 호출하면 영속상태의 엔터티는 준영속상태가 됩니다.

ex) 준영속 상태

em.detach(member);

삭제는 엔터티를 영속성 컨텍스트와 데이터베이스에서 삭제합니다.
ex) 삭제 상태
em.remove(member);

오늘은 간단하게 JPA의 개념과 등장이유 그리고 엔터티 매니저를 생성하는 엔터티매니저 팩토리,엔터티를 관리하는 엔터티 매니저, 영속성 컨텍스트의 개념, 엔터티 생명주기에 대해서만 리뷰하였습니다.

다음 포스팅에서는 영속성 컨텍스트의 특징과 영속성 컨텍스트에서 관리하는 엔터티를 데이터 베이스에 동기화하는 플러시(flush)에 대해서 더 자세하게 다루어 보겠습니다.

출저: 자바 ORM 표준 JPA 프로그래밍

+ Recent posts