티스토리 뷰

연관관계 매핑

객체와 관계형 데이터베이스 테이블을 서로 매핑하는 것

 

연관관계를 매핑할 때 생각해야할 3가지가 존재한다.

  • 방향 : 단방향, 양방향 (객체 간의 참조)
  • 다중성 : 일대일, 일대다, 다대일, 다대다
  • 연관관계의 주인 : 양방향일 때 연관관계에서 관리의 주체가 되는 곳

단방향, 양방향

데이터베이스에서 테이블들은 외래키 하나를 가지로 양 쪽 테이블 조인이 가능하다. 따라서 DB에서는 단방향이니 양방향이니 나눌 필요가 없다.

 

하지만 객체는 참조용 필드가 있는 객체만이 다른 객체를 참조하는 것이 가능하다. 따라서 두객체 사이에 하나의 참조용 필드만을 가지고 참고하게 되면 단방향 관계이고, 두 객체 모두가 참조용 필드를 갖고 참조하면 양방향 관계가 된다.

 

정확히 말하면 단방향 관계 하나만 가지냐 or 단방향 관계 두개를 가지냐의 차이이다.

 

이러한 단방향, 양방향의 선택은 정해져있는게 아니라 비지니스 로직에 맞게 개발자가 선택해서 정해야한다.

  • member.getReservations() 처럼 예약목록에 대한 참조가 필요하면 Member -> Reservation 단방향 참조
  • reservation.getMember() 처럼 예약자에 대한 참조가 필요하면 Reservation -> Member 단방향 참조

이런식으로 필요한 참조마다 설계를 해주면 된다. 위에서 두개 다 필요하다면 양방향이 되고 회원이 예약을 한개 이상 가질 수 있다면 다대일 양방향이 되는 것이다.

 

양방향의 무분별한 사용 지양

모든 객체간의 관계를 양방향으로 하면 걱정이 없을 거라 생각할 수도 있다.

 

하지만 회원과 같은 다른 객체와 관계를 정말 많이 맺는 경우에는 무분별하게 관계를 맺다보면 해당 객체가 너무 복잡해지는 문제가 발생한다. 또한 다른 객체들도 복잡성이 증가하게 된다.

 

따라서 관계가 필요하다면 일단 단방향으로 설계를하고 정말 양방향이 필요하다면 그때 양방향으로 설계를 하는 방식으로 접근하는게 좋겠다.

 

다중성

데이터베이스에서 테이블간 관계를 기준으로 결정하게 된다.

  • 회원 : 마이페이지 (1 : 1)
  • 회원 : 예약 (1 : N)
  • 회원 : 팀 (N : 1)

 

연관관계의 주인

연관관계의 주인은 양방향 관계일 때 누가 이 관계의 주인으로서 외래키를 가지고, 테이블을 INSERT, UPDATE를 하는 권한을 갖는 것이며, 이를 지정하는것은 JPA에게 누가 주인인지 알려주는 용도이다.

 

이때 주인이 아닌 객체는 SELECT 만 수행해야 하고, mappedBy 속성을 통해 상대편이 주인임을 지정해줘야 한다.

 

mappedBy

mappedBy는 양방향 매핑에서 관계 미소유 객체가 주인 객체를 가리키기 위해 사용하는 것이다. 
사용하는 방식은 @OneToMany(mappedBy = “참조하는 엔티티에 있는 변수명”)으로 사용한다.
mappedBy를 사용하므로서 현재 자신의 참조가 참조하는 해당 엔티티에 어떤 변수로 지정되었는지 JPA에게 알려줄수 있다.

 

Fetch Type

연관관계를 갖고있는 객체를 조회하면 JPA는 해당 객체가 들고있는 연관관계의 객체들을 모두 조회해서 끌고오게 된다. 이때 발생하는
문제가 1 + N 쿼리 문제이다.

 

예를들어, 하나의 서점을 조회했는데 해당 객체가 3개의 책을 갖고있고 각 책들은 2개의 편집자 정보를 갖고있다고 해보자.

 

그럼 서점 -> 책 -> 편집자 정보로 이어지는 조회가 발생하게 된다. 따라서 1번 (서점) + 3번 (책) + 6번 (편집자) 해서 1 + 9번의 쿼리가 발생하게 된다. 따라서 일대다 방식에서는 Lazy 방식을 지향한다.

 

즉시로딩 _ (FetchType.EAGER)

  • 연관된 객체를 즉시 가져오는 방식이다.
  • 연관된 객체가 사용되지 않더라도 일단 모두 조회해온다.

지연로딩 _ (FetchType.LAZY)

  • 연관 객체를 프록시 객체로 치환하여 껍데기 객체로 대치하여 추후 직접적인 조회가 발생하면 가져오는 방식이다.
  • 조회를 미루는 것이다.

참고로 일대일에서는 단방향 관계에서만 LAZY로딩이 가능하다. 양방향이면 LAZY로 해도 EAGER로 작동한다.


@OneToOne

하나의 객체와 다른 객체간 1 : 1 연관관계를 맺을 때 사용한다.

 

단방향 일대일

 

회원과 마이페이지 (1 : 1)

@Entity
public class Member {
    @Id
    private Long id;
    
    @OneToOne
    @JoinColumn(name = "MYPAGE_ID")
    private MyPage mypage;
    
    ...
}

@Entity
public class MyPage {
    @Id
    private Long Id;
    
    ...
}

 

@OneToOne

  • 일대일 (1 : 1) 관계 매핑 정보

@JoinCloumn(name = "xxx")

  • 관계의 주인이 갖는다.
  • 외래키를 매핑할 컬럼이다.
  • name 속성은 매핑할 외래키의 컬럼 명을 지정해준다.
  • 디폴트 값은 외래키의 Entity명 ("MyPage") + _ + 참조된 해당 객체의 PK명 ("Id") 이다.

 

양방향 일대일

 

 

 

마이페이지 객체

@Entity
public class MyPage {
    @Id
    private Long Id;
    
    @OneToOne(mappedBy = "mypage")
    private Member member;
    ...
}

주인이 아닌 비소유 객체에 주인객체를 컬럼으로 넣어주면 된다. 즉 단방향을 추가해줌으로서 양방향이 된다.

단, 이때 주인이 아닌 객체는 mappedBy를 통해 관계의 주인이 상대편임을 명시해줘야 한다.

 

참고사항

  • 일대일 관계에서 단방향이 아닌 양방향을 하게되면 Lazy Loading이 작동하지 않는다. 따라서 1 + N문제가 발생할 수 있으므로 조회시 Fetch join을 쓰거나 단방향으로 수정해서 Lazy loading이 되도록 해야한다.

@ManyToOne

서점과 책의 관계를 예로 들어보자.

  • 서점은 여러개의 책을 갖는다.
  • 하나의 책은 하나의 서점에만 존재할 수 있다.
  • 책과 서점은 다대일 관계를 갖는다.

데이터베이스를 기준으로 다대일의 다중성을 갖는 객체들이다. 책(N)이 다쪽이므로 책이 외래키를 관리하는 형태이다. 참고로 데이터베이스는 항상 다(N)쪽이 외래키를 갖는다.

 

다대일 단방향

@Entity @Getter @Setter
public class Book {
    @Id
    private Long id;
    
    @Column 
    String title;
    
    @ManyToOne
    @JoinColumn(name = "LIBRARY_ID")
    private Libarary library;    
}

@Entity @Getter @Setter
public class Libarary {
    @Id
    private Long id;
    
    @Column
    private String libararyName;
}

@ManyToOne

  • 다대일 단방향에서 다 쪽인 책이 갖고 있다. 이에반해 일 쪽인 서점은 책을 참조하고 있지 않다.

 

다대일 양방향

@Entity @Getter @Setter
public class Book {
    @Id
    private Long id;
    
    @Column 
    String title;
    
    @ManyToOne
    @JoinColumn(name = "LIBRARY_ID")
    private Libarary library;    
}

@Entity @Getter @Setter
public class Libarary {
    @Id
    private Long id;
    
    @Column
    private String libararyName;
    
    @OneToMany(mappedBy = "libarary")
    private List<Book> books = new ArrayList<>();
}

@OneToMany

  • 일쪽에서 달아줌으로서 양방향 매핑이 성사되었다.
  • mappedBy 특성을 통해 상대쪽이 관계의 주인임을 명시해준다.

 


@OneToMany

일대다는 위에서 말한 관계의 주인을 다(N)이 아닌 일(1)에 두는것을 말한다. 데이터베이스는 무조건 다쪽에 외래키를 두고 관리하게 하는데 외래키 위치와 별개로 일 쪽에서 다 쪽 객체를 관리(등록, 수정) 하는 것이다.

 

일대다 단방향

@Entity 
@Getter @Setter
public class Book {
	@Id
	@GeneratedValue(strategy = GenerationType.IDENTITY)
	@Column(name = "id", nullable = false)
	private Long id;

	@Column
	private String name;
}

@Entity 
@Getter @Setter
public class Library {
	@Id
	@GeneratedValue(strategy = GenerationType.IDENTITY)
	@Column(name = "id", nullable = false)
	private Long id;

	@Column
	private String libraryName;

	@OneToMany
	@JoinColumn(name = "LIBRARY_ID")
	private List<Book> books = new ArrayList<>();
}

이러한 구조가 된다. 이때 @JoinColumn을 꼭 넣어줘야 한다. 이를 통해 해당 테이블들이 조인을 하게 된다.

 

하지만 이러한 일대다 단방향 형태는 사용을 지양하는데 몇가지 단점이 존재하기 때문이다.

  1. 엔티티가 관리하는 외래키가 반대쪽에 있다. (DB에서는 다쪽에 외래키가 존재하므로 Library가 아닌 Book에 존재하게 된다)
  2. 일쪽을 저장할 때 다쪽에 대한 update 쿼리가 발생하게 된다.

그러니깐 현재 일(1) 쪽인 서점이 책들(N) 을 갖고 있고 자기가 책들을 관리하겠다고 (관계의 주인이되겠다고) 하는 상황이다. 따라서 @JoinColumn도 서점이 들고 있게 된다.

 

하지만 정작 외래키는 데이터베이스 원칙상 다(N)인 BOOK이 들고 있게 되므로 관리 객체가 아닌 반대쪽에 외래키가 존재하는 이상한 상황이 나오게 된다.

정작 외래키 컬럼은 Book 엔티티에 존재하는 모습

또한, 책을 저장할 때는 문제가 없지만 Library에 Book을 넣고, Library를 저장할 때, Library가 보유한 Book에 대해서 update 쿼리가 발생하게 된다. 코드를 통해 확인해보자.

 

테스트

@Test
	public void LibrarySave() throws Exception
	{
	    //given
		Book book1 = new Book();
		book1.setName("THIS IS BOOK1");

		Book book2 = new Book();
		book2.setName("THIS IS BOOK2");
        
		Library library = new Library();
		library.setLibraryName("ABC LIBRARY");
        
		//when
		System.out.println("================QUERY START================");
		bookRepo.save(book1);
		bookRepo.save(book2);
		System.out.println("================QUERY END================\n");

		library.addBook(book1);
		library.addBook(book2);
		System.out.println("================QUERY START================");
		libraryRepo.save(library);
		System.out.println("================QUERY END================\n");
		entityManager.flush();

	    //then
	}

서점 객체는 1쿼리로 하나를 저장했다. 하지만 Library에 들어가있는 Book객체들에 대해 update 쿼리가 발생하는 것을 볼 수 있다.

 

이는 Library 테이블에 Book객체의 외래키를 저장한 컬럼이 없으므로 해당 Book 테이블에 접근해서 외래키가 담겨있는 컬럼에 update쿼리를 날리므로서 발생하는 것이다. 즉 외래키를 관리 주체가 아닌 곳이 갖고있으므로 발생하는 문제.

 

따라서 이러한 문제점이 상주하므로 일대다 단방향 구조를 지양하고 다대일 양방향의 설계를 지향해야한다. 즉, 다쪽에서 외래키를 가지고 관리하게 하는 것이다.

 

참고로 1 쪽이 관계의 주인이 되는 일대다 양방향 관계는 JPA에서 지원하지 않으므로 고려하지 않는다.


@ManyToMany

  • JPA스펙상 존재하긴하지만 실무에서 사용하지 않는다.
  • 사용자가 모르는 쿼리가 발생할 경우가 많다.
  • 일대다 & 다대일 구조로 풀어서 만드는 방식 (중간에 중간 엔티티를 통해) 으로 대처할 수 있다.
반응형
Comments
반응형
최근에 올라온 글
최근에 달린 댓글
Total
Today
Yesterday