JPA(Java Persistence API) 프로그래밍 - 연관관계 매핑 기초

u-nij·2022년 7월 21일
0

JPA 프로그래밍

목록 보기
4/10
post-thumbnail

이 글은 김영한님의 자바 ORM 표준 JPA 프로그래밍 - 기본편 강의를 듣고 정리한 글입니다.

  • 객체와 테이블 연관관계의 차이를 이해
  • 객체의 참조와 테이블의 외래 키를 매핑
  • 용어 이해
    • 방향(Direction): 단방향, 양방향
    • 다중성(Multiplicity): 다대일(N:1), 일대다(1:N), 일대일(1:1), 다대다(N:M) 이해
    • 연관관계의 주인(Owner): 객체 양방향 연관관계는 관리 주인이 필요
  • 연관관계가 필요한 이유
  • 단방향 연관관계
  • 양방향 영관관계와 연관관계의 주인

연관관계가 필요한 이유

‘객체지향 설계의 목표는 자율적인 객체들의 협력 공동체를 만드는 것이다.’ -조영호(객체지향의 사실과 오해)

예제 시나리오

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

연관관계가 없는 객체

객체를 테이블에 맞추어 모델링

Member.java

@Entity
public class Member { 

	@Id @GeneratedValue
    @Column(name = "USER_ID")
    private Long id;

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

    @Column(name = "TEAM_ID") // 💡 참조 대신 외래 키 그대로 사용
    private Long teamId;

 }

Member.java

@Entity
public class Team {

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

	private String name; 

}

JpaMain.java

public class JpaMain {

    public static void main(String[] args) {
        EntityManagerFactory emf = Persistence.createEntityManagerFactory("hello");
        
		EntityManager em = emf.createEntityManager();
        EntityTransaction tx = em.getTransaction();
        tx.begin();

        try {
			// code ..

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

			// 회원 저장
            Member member = new Member();
            member.setUsername("member1");
            member.setTeamId(team.getId()); // 💡 외래 키 식별자를 직접 다룸
            em.persist(member);

            tx.commit();
        } catch (Exception e) {
            tx.rollback();
        } finally {
            em.close();
        }
    }
}

조회

SQL

SELECT * FROM MEMBER;
SELECT * FROM TEAM;

SELECT * FROM MEMBER m
JOIN TEAM t
ON m.team_id=t.team_id;

JpaMain.java

Member findMember = em.find(Member.class, member.getId()); // 조회
Team findTeam = em.find(Team.class, team.getId()); // 연관관계 없음
System.out.println("findTeam = " + findTeam.getName());

→ 서로 연관관계가 없기때문에 식별자로 다시 조회한다. 객체 지향적인 방법은 아니다.

따라서,

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

왜? : 패러다임의 차이

  • 테이블은 외래 키로 조인을 사용해 연관된 테이블을 찾는다.
  • 객체는 참조를 사용해, 연관된 객체를 찾는다.

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

단방향 연관관계

Member.java

@Entity
public class Member {

    @Id @GeneratedValue
    @Column(name = "USER_ID")
    private Long id;

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

	// 💡 객체의 참조와 테이블의 외래 키를 매핑
    @ManyToOne // Member 입장에서 many, Team 입장에서 one
	@JoinColumn(name = "TEAM_ID")   // MEMBER 테이블의 'TEAM_ID' 칼럼과 매핑
    private Team team;

}

JpaMain.java

		try {
        	// 연관관계 저장
           	// 팀 저장
            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());

            Team findTeam = findMember.getTeam(); // 💡 참조를 사용해 연관관계 조회

            System.out.println("findTeam = " + findTeam.getName());

            tx.commit();
        } 

양방향 연관관계

  • 테이블 연관관계는 단방향 연관관계에서의 테이블 연관관계와 차이가 없다.
    왜? 테이블에서는 외래키 하나만 있으면, FK와 PK를 조인하면 양방향 조회 가능
  • 객체에서는 Team에서 Member를 조회하기 위해서 List members가 필요함
    양방향으로 참조가 필요함

Team.java

@Entity
public class Team {

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

    private String name;

	// 💡 컬렉션 추가
    @OneToMany(mappedBy = "team") // 무엇과 연결되어 있는지 (Team 변수명)
    private List<Member> members = new ArrayList<>();
	// OneToMany이기 때문에, List 컬렉션이 필요
    // null 포인트 안뜨게 하기 위해 `new ArrayList<>()`로 초기화하는 것이 관례

}

Member.java (단방향과 동일)

@Entity
public class Member {

    @Id @GeneratedValue
    @Column(name = "USER_ID")
    private Long id;

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

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

}

JpaMain.java

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

            Member member = new Member();
            member.setUsername("member1");
            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 = " + m.getUsername());
            }

            tx.commit();
        } catch (Exception e) {
            tx.rollback();
        } finally {
            em.close();
    	}
  • 반대 방향으로 객체 그래프 탐색
            // 조회
            Team findTeam = em.find(Team.class, team.getId());

            int memberSize = findTeam.getMembers().size(); // 💡 역방향 조회

연관관계의 주인

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

  • 객체와 테이블간에 연관관계를 맺는 차이를 이해해야 한다.
  • 용어 이해
    • 방향(Direction): 단방향, 양방향
  • 객체 연관관계 = 2개
    • 회원 → 팀 (단방향)
    • 팀 → 회원 (단방향)
  • 테이블 연관관계 = 1개
    - 회원 ↔ 팀의 연관관계 (양방향)
    사실 양방향 관계가 아니라 서로 다른 단방향 2개이다.
    ⇒ 객체를 양방향으로 참조하려면 단방향 연관관계 2개를 만들어야 한다.
    외래키 하나로 두 테이블의 연관관계를 가짐 (사실, 테이블에는 방향성이 없다.)

참조 2개 중 어떤 것으로 매핑을 해야 하는가?

DB 입장에서는 MEMBER 테이블의 TEAM_ID(FK)만 업데이트되면 된다.
→ 둘 중 하나로 외래 키를 관리해야 한다.
⇒ 연관관계의 주인(Owner)

연관관계의 주인(Owner)

양방향 매핑 규칙

  • 객체의 두 관계 중 하나를 연관관계의 주인으로 지정
  • 연관관계의 주인만이 외래 키를 관리(등록, 수정) (ex. @JoinColumn(name = "FK 칼럼 이름"))
  • 주인이 아닌 쪽은 읽기만 가능
  • 주인은 mappedBy 속성 사용 X
  • 주인이 아닌 쪽은 mappedBy 속성으로 주인 지정 (ex. @OneToMany(mappedBy = "주인 이름") )

외래 키가 있는 객체를 주인으로!

Member.java (주인)

    @ManyToOne
    @JoinColumn(name = "TEAM_ID") // 테이블과 매핑
    private Team team;

Team.java (주인X)

    @OneToMany(mappedBy = "team")
    private List<Member> members = new ArrayList<>();
  • 객체 Team과 MEMBER 테이블을 매핑을 할 수도 있지만,
    Team 값을 변경했을 때 MEMBER 테이블의 쿼리가 뜰 경우에 햇갈리고,
    성능 이슈가 있기 때문에 외래 키가 있는 객체를 주인으로 설정하는 것이 좋다.
  • 또한, DB입장에서 외래키가 없는 테이블이 ‘1’이고, 외래키가 있는 테이블이 ‘N’이다.
  • 즉, N’이 무조건 연관관계의 주인이 된다. & @ManyToOne

양방향 매핑시 가장 많이 하는 실수

  • 연관관계의 주인이 아닌 것에 값을 입력할 경우

    JpaMain.java

    		try {
    			Team team = new Team();
                team.setName("TeamA");
                em.persist(team);
    
                Member member = new Member();
                member.setUsername("member1");
    
    			// 역방향(주인이 아닌 방향)만 연관관계 설정
    			team.getMembers().add(member);
    
                em.persist(member);
              
                // ...
    		}

    Team.java

    @OneToMany(mappedBy = "team")
    private List<Member> members = new ArrayList<>();
  • JPA에서 DB를 변경할 때 가짜 맵핑(mappedBy 객체)을 보지 않음.
    team.getMembers().add(member);가 아닌, member.setTeam(team);

  • 양방향 매핑시 연관관계의 주인에 값을 입력해야 한다.

    JpaMain.java

    		try {
    			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();
                // 1차 캐시가 깨끗해진 상태
    
                Team findTeam = em.find(Team.class, team.getId());
                // 1) DB에서 새로 데이터를 가져와 1차 캐시에 저장
    
                // members 리스트에 들어간 것이 없음
    			List<Member> members = findTeam.getMembers();
                // 2) 데이터들을 리스트 컬렉션에 넣음
    
                // 리스트에 세팅한 것이 없음에도 값이 출력됨
                for (Member m : members) {
                    System.out.println("m = " + m.getUsername());
                }
              
                // ...
    		}

양방향 연관관계 주의

em.flush(); em.clear();를 실행하지 않았을 경우

JpaMain.java

		try {
			Team team = new Team();
            team.setName("TeamA");
            em.persist(team); // 순수한 team 객체 상태

            Member member = new Member();
            member.setUsername("member1");
            member.setTeam(team); // member 객체에 team 객체 설정
            em.persist(member);
            // members 리스트에 들어간 것이 없음
      
      		team.getMembers().add(member); // team 객체에 member 객체 추가
      
            // em.flush();
            // em.clear();

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

            // 1차 캐시에 있는 것들이 출력됨
            for (Member m : members) {
                System.out.println("m = " + m.getUsername());
            }
            
            // ...
		}
  • em.persist(team); 실행으로 영속성 컨텍스트에 있는 것들이 출력됨
  • List 컬렉션(members)에는 값이 존재하지 않는다.
  • 출력 결과: 1차 캐시에 있는 것들이 출력되기 때문에, select 쿼리가 나타나지 않음.
  • team.getMembers().add(member);가 없을 경우
    • 출력 결과: 아무것도 출력되지 않는다
    • 1차 캐시에만 있는 상태(em.persist();)로는 해당 명령어가 없을 경우 못 읽어들인다.
    • 영속성 컨텍스트에 있는 1

실습

  • 순수 객체 상태를 고려해 항상 양쪽에 값을 설정해야 한다.
  • 양방향 매핑시에 무한 루프 조심
    • ex) toString(), lombok, JSON 생성 라이브러리 (서로 계속 호출)
  • 연관관계 편의 메소드를 생성 :
    Member의 Team을 설정하는 시점에, Team에도 설정해주는 것.
    JpaMain.java의 team.getMembers().add(member);을 지우고,
    Member.java에 changeTeam() 메소드 추가

JpaMain.java

		try {
			Team team = new Team();
            team.setName("TeamA");
            em.persist(team); // 순수한 team 객체 상태

            Member member = new Member();
            member.setUsername("member1");
            member.changeTeam(team);
            em.persist(member);
      
            // em.flush();
            // em.clear();

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

            // 1차 캐시에 있는 것들이 출력됨
            for (Member m : members) {
                System.out.println("m = " + m.getUsername());
            }
            
            // ...
		}

Member.java

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

Team을 기준으로 Member 집어넣기

  • 단, 양방향으로 편의 메소드를 작성할 경우에 문제를 일으킬 수 있기 때문에,
    Member.java의 changeTeam() 메소드를 지워주어야 함. (한 쪽에서만 연관관계 편의 메소드 작성)

JpaMain.java

    		team.addMember(member);

Team.java

	public void addMember(Member2 member) {
		member.setTeam(this);
    	members.add(member);
	}

양방향 매핑 정리

  • 단방향 연관관계 매핑 & 연관관계의 주인을 누구로 할 것인가가 중요
  • 단방향 매핑만으로도 이미 연관관계 매핑은 완료.
  • 양방향 매핑은 반대방향으로 조회(객체 그래프 탐색) 기능이 추가된 것 뿐.
    - 대부분 읽기(조회), JPQL 작성할 때 편의를 위해 넣게 된다.(주인이 아닌 쪽은 읽기만 가능하다.)
  • JPQL에서 역방향으로 탐색할 일이 많음
  • 가급적이면 단방향으로 설계하는 것이 좋다. 단방향 매핑을 잘 하고 양방향은 필요할 때 추가해도 된다. (테이블에 영향을 주지 않음)

연관관계의 주인을 정하는 기준

  • 비즈니스 로직을 기준으로 연관관계의 주인을 선택하면 안된다.
  • 연관관계의 주인은 외래 키가 있는 객체로 정해야 한다.
profile
삶은 달걀이다

0개의 댓글