DAY66 - 연관관계 매핑

은나현·2023년 4월 28일
0

📌 1. 연관관계 매핑 기초

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

📍 1-1. 객체와 테이블의 차이점

  • 테이블과 객체 연관관계 사이에는 큰 간격이 있다.
    • 테이블은 외래 키로 조인을 사용해서 연관된 테이블을 찾는다.
    • 객체는 참조를 사용해서 연관된 객체를 찾는다.
  • 객체를 테이블에 맞추어 데이터 중심으로 모델링하면 협력관계를 만들 수 없다.

📍 1-2. 연관관계의 주인(Owner)

  • 팀 정보를 가진 TEAM 테이블과 팀에 속하는 회원들의 정보를 가진 MEMBER 테이블이 TEAM_ID를 매개로 관계를 맺는다고 가정한다.

    • 테이블에서는 회원-팀의 연관관계는 1개이며 양방향 접근이 가능하다.
    • 객체에서는 양방향 관계를 맺기 위해 사실상 단방향 연관관계를 2개 생성해야 한다.
      • 회원 -> 팀 연관관계 1개(단방향)
      • 팀 -> 회원 연관관계 1개(단방향)
    • 외래키 딜레마
      • Member에서의 Team 참조값, Team에서의 Member 참조값 두 관계 중 하나로 외래키를 관리해야 한다.
      • DB입장에서는 MEMBER에 있는 TEAM_ID만 업데이트되면 됨
        -> 룰(주인)이 생긴다.
  • 연관관계의 주인(Owner)

    • 양방향 매핑 규칙에서는 객체의 두 관계 중에 하나를 연관관계의 주인으로 지정해야 한다.
    • 연관관계의 주인만이 외래 키를 관리할 수 있다. (등록, 수정)
    • 주인이 아닌 쪽은 읽기만 가능하다.
      • 주인은 mappedBy 속성 사용 X
        mappedBy : 내가 누군가에 의해 mapping 되었다는 뜻
    • 항상 외래 키가 있는 곳을 주인으로 정해야 한다.
      • 위의 예와 같은 경우에는 Member.team이 주인이 된다.
      • 비즈니스 로직을 기준으로 연관관계의 주인을 선택하면 안 됨

📍 1-3. 객체 연관관계 생성

  • DB에서 다음과 같은 연관관계를 갖는 두 객체를 설정하고자 한다.
  • Member 클래스
    @Entity @Getter @Setter
    public class Member {
    	// 자동 생성 primary key MEMBER_ID
    	@Id	@GeneratedValue	@Column(name="MEMBER_ID")
    	private Long id;
        // USERNAME 컬럼
    	@Column(name="USERNAME")
    	private String name;
    	// N:1 관계, 외래키가 있는 N측 객체가 주인이다.
        // Team 객체를 통째로 필드에 주입하고 관계 컬럼을 정의해 준다. 
    	@ManyToOne @JoinColumn(name="TEAM_ID")
    	private Team team;
    }
    • @ManyToOne : 1대 다의 개념 주입, 여기서는 Member가 Many
    • @JoinColumn : 관계 컬럼을 정의(TEAM_ID를 기준으로 조인)
  • Team 클래스
    @Entity @Getter @Setter
    public class Team {
    	// 자동 생성 primary key TEAM_ID (Member 측에서 상속하는 외래키)
    	@Id	@GeneratedValue	@Column(name="TEAM_ID")
    	private Long id;
    	private String name;
    	// Member 객체들로 이루어진 ArrayList로 1대 다 관계성 주입
    	@OneToMany(mappedBy = "team")
    	private List<Member> member = new ArrayList<>();
    • @OneToMany : 1대 다의 개념 주입, 여기서는 Team이 One
    • mappedBy : 주인이 아닌 쪽을 뜻하는 속성
      mappedBy가 적힌 곳은 읽기만 가능하며, 값을 넣는 등의 업데이트 시도를 해도 작동하지 않음

📍 1-4. 연관된 객체 동기화

  • 다음과 같은 코드를 통해 Team, Member 객체를 각각 하나씩 생성하고 Team에 담긴 member 리스트 결과값을 보고자 한다.
    			try {
    				// team 생성, 1차 캐시 저장
    				Team team = new Team();
    				team.setName("TeamA");
    				em.persist(team);
    				// member 생성, 1차 캐시 저장
    				Member member = new Member();
    				member.setName("member1");
    				member.setTeam(team);
    				em.persist(member);
    				// team에 담긴 member 리스트 정보 셀렉트 시도
    				Team findTeam = em.find(Team.class, team.getId());
    				List<Member> members = findTeam.getMember();
    				for(Member m : members) {
    					System.out.println("member = " + m.getName());
    				}
    				tx.commit();
    			}catch(Exception e) {
    				tx.rollback();
    			}finally {
    				em.close();
    				emf.close();
    			}
  • 그러나 이 상태에서는 member 값을 가져오지 못하는데, team이 영속성 컨텍스트 즉 1차 캐시에 저장되고 아직 flushcommit이 호출되지 않아 변경을 감지하지 못한 상태에서 검색했기 때문이다. 즉 1차 캐시에 담긴 내용(null)이 그대로 조회된다.
  • 강제적으로 flush를 호출하고 조회하면 검색은 가능하다.
    		em.flush();
    		em.clear();
  • 그러나 비즈니스 로직에 따라 원할 때마다 flush를 호출할 수 없으므로, 양방향 매핑 시에는 양쪽 객체에 값을 모두 입력시켜 주어야 한다. 이렇게 하면 DB를 다녀오지 않고 순수 객체 상태로만 사용할 수 있다.
     team.getMember().add(member);

    ➕ 참고로 member.setTeam(team);을 하지 않고 team.getMember().add(member);만으로 자료를 업데이트하려고 하면 동작하지 않는다.
    해당 객체 매핑에서 연관관계의 주인이 Member.team에 있고 Team에서의 관계는 읽기 전용이기 때문이다.

  • 그러나 또한 이와 같이 항상 수동으로 값을 입력해 주는 것에는 실수가 많을 수 있기 때문에 자동 연관관계 편의 메서드를 생성하는 것이 권장된다. 양방향 매핑 시에는 무한루프를 주의한다.
  • Member를 기준으로 team을 넣을 경우
    • 다음은 Member 클래스에서 기존의 세터를 변형한 메서드이다.
       	public void changeTeam(Team team) {
      		this.team = team;
      		// this : 나 자신의 인스턴스(Member)
      		team.getMember().add(this);
      	} 
      ➕ 일반적인 setter의 형태를 벗어나면 추후 소스코드를 볼 때 파악하기 쉽도록 이름을 바꿔 주는 것이 좋다.
      ➕ lombok을 통한 일괄 세터 생성에서 제외하고자 할 때는 필드에 다음과 같은 어노테이션을 적용할 수 있다.
      -> @Setter(value=AccessLevel.NONE)
    • 이와 같이 변형한 세터를 기존의 setTeam 대신 사용하면 자동으로 team 쪽에도 값이 입력된다.
  • Team을 기준으로 member를 넣을 경우
    • 다음은 Team 클래스에서 추가한 메서드이다.
      	public void addMember(Member member) {
      		member.setTeam(this);
      		this.member.add(member);
      	}
    • 해당 메서드를 사용하면 team에 멤버를 입력할 때 자동으로 member측에도 team 값이 설정된다.

📍 1-5. 양방향 매핑 정리

  • 단방향 매핑으로도 이미 연관관계 매핑은 완료된다.
    • 양방향 매핑은 반대 방향으로 조회기능을 추가하기 위해 사용하는 것이다.
      • 사용 이유 : JPQL에서 역방향으로 탐색할 일이 많음
  • 객체 입장에서 양방향 매핑은 이득이 별로 되지 않으므로 필수가 아니며 테이블에 영향을 주지 않는다.
    • 따라서 필요 시에 추가해도 늦지 않은 옵션이다.

0개의 댓글