[JPA] 연관관계 매핑

보람·2023년 5월 1일
0

Spring

목록 보기
13/18
post-thumbnail

연관관계 매핑

객체 설계를 테이블 설계에 맞춘다면?

1) Member, Team 객체 생성

  • Member 객체생성
    @Entity
    public class Member {

        @Id @GeneratedValue
        private Long id;

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

        @Column(name = "TEAM_ID")
        private Long teamId; 
    }
  • Team 객체생성
    @Entity
    public class Team {

        @Id @GeneratedValue 
        private Long id;

        private String name; 
     }

2) 멤버의 team을 알고 싶을 때

  • Team, Member 영속상태
	//팀 저장
    Team team = new Team();
    team.setName("TeamA");
    em.persist(team);

    // 회원 저장
    Member member = new Member();
    member.setName("member1");
    member.setTeamId(team.getId());
    em.persist(member);
  • 멤버의 team 찾기
	// 멤버 id로 멤버 정보 찾기
	Member findMember = em.find(Member.class, member.getId());
    // 그 결과로 teamId 찾기
	Long findTeamId = findMember.getTeamId();
    // 찾은 teamId로 team정보 가져오기
	Team findTeam = em.find(Team.class, findTeamId);
    // 가져온 team정보로 team 이름 알아내기
	System.out.println("findTeam : " + findTeam.getName());
  • 결론
    • 번거로움
    • 저장된 teamId를 이용해서 팀 조회 -> 객체지향적인 방법이라고 보기 어려움
    • member와 team의 연관관계가 없음

(1) 연관관계 매핑 기초

객체지향적인 모델링을 위해 연관관계 매핑

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

🙆‍♀️ 객체와 테이블 차이

  • 테이블 : 외래 키로 조인을 사용해서 연관된 테이블을 찾음
  • 객체 : 참조를 사용해서 연관된 객체를 찾음

(2) 단방향 연관관계

  • 객체지향적으로 모델링하는 법

  • Member안에 team이라는 객체 넣기
    • 객체가 참조를 바로할 수 있음

1) 연관관계 가진 Member, Team 객체 생성

① Member객체 생성

  • team객체를 통으로 가지고 옴
  • FK를 걸어줘야 하는 곳에 Join 걸어줌
  • @ManyToOne : 많은 것들 중 하나
    • 많은 것 : 멤버 수, 하나 : 팀 이름
  • @JoinColumn
    • 관계 컬럼을 적어줌
@Entity
@Getter @Setter
public class Member {

	@Id @GeneratedValue
	@Column(name="MEMBER_ID")
	private Long id;
    
	@Column(name="USERNAME")
	private String name;
    
    @ManyToOne
	@JoinColumn(name = "TEAM_ID")
	private Team team;
}    

👀 외래키

  • 외래키가 포함된 테이블이 자식 테이블
  • 기준이 되는 테이블의 내용을 참조해서 레코드가 입력

② Team 객체 생성

  • @Column의 name 속성과 @JoinColumn의 name 속성은 반드시 같아야 함
@Entity
@Getter @Setter
public class Team {
	
	@Id @GeneratedValue
	@Column(name="TEAM_ID")
	private Long id;
	private String name;
}    

2) 멤버의 team을 알고 싶을 때

① Team, Member 영속상태

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

//회원 저장
Member member = new Member(); 
member.setName("member1");
// member.setTeamId(team.getId());
// 단방향 연관관계 설정, 참조 저장 
member.setTeam(team); 

em.persist(member);

② 멤버의 team 찾기

	// 멤버 id로 멤버 정보 찾기
	Member findMember = em.find(Member.class, member.getId());
    // 멤버 정보에서 객체 team 찾아냄
	Team findTeam = findMember.getTeam();
    // team의 이름 찾아냄
	System.out.println("findTeamName : " + findTeam.getName());
  • 결론
    • team객체를 넘겨주면 알아서 teamId를 찾아 FK값으로 쓰고 연관관계를 설정
    • team을 getTeam을 이용해서 참조로 가져옴

(3) 양방향 연관관계

  • 단방향 매핑 후, Team의 Member들을 역참조 : 양방향 객체 연관관계

1) Member, Team 객체 양방향 연관관계 맺기

① Member객체 생성

  • 단방향과 똑같이
@Entity
@Getter @Setter
public class Member {

	@Id @GeneratedValue
	@Column(name="MEMBER_ID")
	private Long id;
    
	@Column(name="USERNAME")
	private String name;
    
    @ManyToOne
	@JoinColumn(name = "TEAM_ID")
	private Team team;
}  

② Team 객체 생성

  • Team에 members 리스트를 추가
  • @OneToMany 어노테이션 붙이고 mappedBy로 연결할 객체 넣어줌
    • 여기선 Member 클래스의 Team team 객체
@Entity
@Getter @Setter
public class Team {

	@Id @GeneratedValue
    @Column(name="TEAM_ID")
    private Long id;
    
    private String name;
        
	@OneToMany(mappedBy = "team")
	List<Member> members = new ArrayList<Member>();
 }

③ 역참조 가능

team.getMembers();

2) 연관관계의 주인(Owner)

① 테이블, 객체에서의 연관관계

  • 테이블(DB)에서의 연관관계
    • FK값으로 JOIN을 통해서 양방향 접근 가능
  • 객체에서의 연관관계
    • 회원 -> 팀, 팀 -> 회원 연관관계
    • 단방향 연관관계 2개

② 딜레마 : Member에서 Team값을 수정하고 싶을 때

  • DB 입장
    • MEMBER에 있는 TEAM_ID만 UPDATE해도 수정 가능
  • 객체 입장 :
    • TEAM에서 MEMBER 수정? OR MEMBER에서 TEAM 수정?

③ 두 관계중 하나를 연관관계의 주인(Owner)으로 지정

  • 주인이 아닌 쪽에 mappedBy
    • mappedBy : 내가 누군가에 의해 MAPPING 되었다는 뜻
  • 항상 외래키가 있는 곳을 주인 -> 비즈니스 로직을 기준 ❌
    • @ManyToOne을 포함하는 클래스가 주인
    • 여기서는 Member.team 이 연관관계의 주인

④ 연관관계의 주인(Owner)

  • 주인만이 외래키를 관리
  • 주인쪽만 연관관계를 등록하고 수정가능
  • 주인이 아닌 쪽은 읽기만 가능
  • 주인쪽에서 연관관계를 생성하는 코드 추가
	member.setTeam(team);   

3) Team에 담긴 member 리스트 보기

	try {
			Team team = new Team();
			team.setName("TeamA");
			em.persist(team); // 영속상태
			
			Member member = new Member();
			member.setName("member1");
            // 연관관계 생성하는 코드 추가
			member.setTeam(team);
			em.persist(member);
			
			// 양방향 매핑
			Member findSideMember = em.find(Member.class, member.getId());
			// 팀은 멤버를 가지고 있고 
			List<Member> members = findSideMember.getTeam().getMember();
			
			for(Member m : members) {
				System.out.println("result = " + m.getName());
			}
			
			tx.commit(); // db에 적용되는 자리 
		} catch (Exception e) {
			tx.rollback();
		} finally {
			em.close();
			emf.close();
		}

4) 최적화

① 위의 코드 : 실행 ❌ (null)

  • why? 아직 관계 생성 ❌ = DB에 반영 ❌
    • 영속성 컨텍스트에 들어가 있는 데 (1차 캐시) -> 영속성 컨텍스트는 변경된 것을 감지하지 못함

② flush 추가

  • em.persist 뒤에 flush로 DB에 쿼리를 날려주고 캐시를 참고하지 않도록 clear까지 해주면 DB에서 가져올 수 있음
    • 출력 가능한 상태
	try {
			Team team = new Team();
			team.setName("TeamA");
			em.persist(team); // 영속상태
			
			Member member = new Member();
			member.setName("member1");
			member.setTeam(team);
			em.persist(member);
			
            // 강제로 DB쿼리를 보고 싶을 때 사용
            em.flush();
			em.clear();
            
			Member findSideMember = em.find(Member.class, member.getId());
			List<Member> members = findSideMember.getTeam().getMember();
			
			for(Member m : members) {
				System.out.println("result = " + m.getName());
			}
			
			tx.commit(); // db에 적용되는 자리 
		} catch (Exception e) {
			tx.rollback();
		} finally {
			em.close();
			emf.close();
		}

③ 순수 객체로 사용하기

  • flush 호출을 매번하는 것은 번거롭고 실수할 가능성 높임
    • flush 자리에 team.getMembers().add(member); 추가
    • Team은 주인이 아니므로 읽기만 가능
    • 양쪽 객체에 값을 모두 입력시키는 것
    • DB에 다녀오지 않고 객체 상태로만 사용 가능

④ 주의사항

  • 수동으로 값을 입력하는 것 : 실수 유발
  • 무한루프 주의
    • toString 실행 시, member객체는 team객체를 부르고 team객체는 member객체를 부름

⑤ 최종 해결

  • Member.java
    • Member를 기준으로 team 넣을 경우
    • 기존의 setter 변경한 다음 메서드 추가
    	    public void changeTeam(Team team) {
    			this.team = team;
    			// this : 나 자신의 인스턴스를 넣어준다.
    			team.getMember().add(this);
    		}
    • 기존의 setter 변경 : 이름 바꾸기
    • @Setter(value = AccessLevel.NONE)
      • lombok에서 자동으로 setter생성을 막아줌
@Entity
@Getter @Setter
public class Member {

	@Id @GeneratedValue
	@Column(name = "MEMBER_ID")
	private Long id;
	@Column(name = "USERNAME")
	private String name;
	
	@ManyToOne
	@JoinColumn(name = "TEAM_ID")
	@Setter(value = AccessLevel.NONE)	// setter생성을 막아줌
	private Team team;

	/*
	 * 일반적인 setter의 형태(자바에서의 관례 형태를 벗어났다)가 아니면 이름을 바꿔준다. 
	 * 그럼 추후 소스코드를 봤을때 단순 settet작업이 아닌 중요한 작업을 진행하는지를 파악 할 수 있다.
	 */
	public void changeTeam(Team team) {
		this.team = team;
		// this : 나 자신의 인스턴스를 넣어준다.
		team.getMember().add(this);
	}
	
	public void setTeam(Team team) {
		this.team = team;
	}	
}
  • Team.java
    • Team을 기준으로 member를 넣을 경우
    • team에 멤버 입력 시 자동으로 member측에도 team값 설정
@Entity
@Getter @Setter
public class Team {
	@Id @GeneratedValue
	@Column(name = "TEAM_ID")
	private Long id;
	private String name;
	
	@OneToMany(mappedBy = "team")
	private List<Member> member = new ArrayList<>();
	
	public void addMember(Member member) {
		member.setTeam(this);
		this.member.add(member);
	}
}
  • 최종 최적화 코드
	try{
    	Team team = new Team();
		team.setName("TeamB");
		em.persist(team);
			
		Member member = new Member();
		member.setName("member2");

		//member.changeTeam(team);	// 1안 : member를 기준으로 team을 넣는다.
		em.persist(member);
			
		team.addMember(member);		// 2안 : team을 기준으로 member를 넣는다.
			
		Team findTeam = em.find(Team.class, team.getId());
		List<Member> members = findTeam.getMember();
			
		System.out.println("members = " + findTeam);
			
		for( Member m : members ) {
				System.out.println("result = " + m.getName());
		}
			
		tx.commit();
        
	}catch (Exception e) {
			tx.rollback();
	} finally {
		em.close();
		emf.close();
	}

5) 양방향 매핑 정리

  • 단방향 매핑만으로도 이미 연관관계 매핑은 완료
  • 양방향 매핑은 반대 방향으로 조회 기능이 추가
    • 양방향 사용 이유 : JPQL에서 양방향으로 탐색할 일이 많음
  • 단방향 매핑을 메인으로 사용하고 양방향 매핑은 필요할 때 추가하기
    • 양방향 : select를 좀 더 직관적으로 하고 싶을 때 사용, 테이블에 영향을 주지 않음

결론

  • 객체입장에서 양방향 매핑은 이득이 별로 안되므로 필수가 아님.
  • 필요시에 생성하기 (옵션)
profile
안녕하세요, 한보람입니다.

0개의 댓글