정환타 개발노트

Spring Boot : JPA 연관관계 매핑 본문

Dev-Spring

Spring Boot : JPA 연관관계 매핑

JungHwanTa 2020. 1. 13. 01:32

RDBMS를 이용할 때, 테이블(Table)하나로 어플리케이션에 사용하는 모든 데이터를 관리하는 것은 불가능하다.

따라서, 여러 데이블에 관련된 데이터들을 나누어 관리하고 필요시에는 테이블을 조인(Join)하여 처리해야 한다.

 

JPA를 이용한다면 일반적인 테이블간의 관계를 이용하는 것 처럼, 엔티티들 사이의 관계를 통해 데이터를 관리할 수 있다.

하지만, JPA를 사용하여 연관관계를 맺기 위해서는 참조 변수를 이용하기 때문에 테이블의 연관과 엔티티의 연관이 정확하게 일치하지 않는다.

 

따라서 이러한 문제를 해결하면서 연관관계를 맵핑하는 방법을 다루어 보겠다.

 

그 이전에 매핑과 관련하여 중요한 용어들은 다음과 같다,

 

방향(Direction)

단방향, 양방향 2가지로 구분.

객체가 참조 변수를 통해 다른 객체를 참조하면 단방향,

만일 다른 객체도 첫번째 객체를 참조한다면 양방향이다.

 

방향은 객체에만 존재, 테이블은 항상 양방향

다중성(Multrplicity)

N:1(다대일), 1:N(일대다), 1:1(일대일), N:N(다대다) 4가지로 구분.

예를들어 회원은 게시글을 여러개 작성할 수 있기에 회원과 게시글은 1:N 관계이다. 

연관관계 주인(Owner)

객체를 양방향 연관관계로 만들기 위해서는 연관관계 주인을 정해야함.

일반적으로, N:1과 1:N관계에서 연관관계 주인은 N에 해당하는 객체이다.

 

1. 단방향 연관관계

 

1.1 N:1(다대일) 단방향 매핑

 

데이터 모델링시에 N:1 관계가 가장 많다고 한다(기본적인 매핑).

 

다대일 관계를 이해하기 위해 아래와 같은 조건을 가정

 

  • 게시판과 회원 존재
  • 게시판과 회원은 다대일 관계
  • 게시글을 통해 작성한 회원정보를 조회할 수 있다.

위의 조건을 이용하여 만든 ERD이다.

객체는 Board.member라는 참조변수를 이용하여 회원 객체와 단방향의 관계를 맺는다.

테이블에서는 BOARD와 MEMBER는 외래키를 이용하여 서로 양방향 관계를 가진다. (조인 사용)

하지만, 객체 관계에서는 Member는 Board에 대한 정보를 알 수 없다.

 

따라서 연관관계 매핑을 하기 위해 회원 클래스를 추가한다.

 

@Getter
@Setter
@ToString
@Entity
public class Member {
	@Id
    @Column(name="MEMBER_ID")
    private String id;
    private String password;
    private String name;
    private String role;
}

Member 클래스를 생성하면 아래와 같이 QMember라는 쿼리 타입 클래스가 생성되는 것을 확인할 수 있다.

다음으로 Board 클래스에 다대일 연관 매핑 설정을 추가한다.

 

 

@Getter
@Setter
@ToString
@Entity
public class Board {

	@Id
	@GeneratedValue
	private Long seq;
	private String title;
//	private String writer;
	private String content;
	@Temporal(value = TemporalType.TIMESTAMP)
	private Date createDate;
	private Long cnt;
	
	@ManyToOne
	@JoinColumn(name="MEMBER_ID")
	private Member member;
}

연관 매핑을 처리하기 위해 member변수를 선언하였고,

다대일 관계 설정을 위한 @ManyToOne 어노테이션을 추가하였다.

또한, 외래 키 매핑을 위한 @JoinColumn 어노테이션을 추가하였다.

 

@JoinColumn 설정을 통해 아래와 같은 매핑 관계가 형성된다.

다음으로 Member 엔티티를 위해 MemberRepository를 작성한다.

 

import org.springframework.data.repository.CrudRepository;

import com.rubypaper.domain.Member;

public interface MemberRepository extends CrudRepository<Member, String> {

}

Repository를 작성한 후, 연관관계 테스트를 위해 글을 등록한다. (src/test/java/com/rubypaper/RelationMappingTest.java)

 

@RunWith(SpringRunner.class)
@SpringBootTest
public class RelationMappingTest {
	@Autowired
	private BoardRepository boardRepo;
	
	@Autowired
	private MemberRepository memberRepo;
	
	@Test
	public void testManyToOneInsert() {
		Member member1 = new Member();
		member1.setId("member1");
		member1.setPassword("member111");
		member1.setName("Park");
		member1.setRole("User");
		memberRepo.save(member1);
		
		Member member2 = new Member();
		member2.setId("member2");
		member2.setPassword("member222");
		member2.setName("Kim");
		member2.setRole("Admin");
		memberRepo.save(member2);
		
		for(int i=1;i<=3;i++) {
			Board board = new Board();
			board.setMember(member1);
			board.setTitle("board made by Park " + i);
			board.setContent("content made by Park" + i);
			board.setCreateDate(new Date());
			board.setCnt(0L);
			boardRepo.save(board);
		}
		
		for(int i=1;i<=3;i++) {
			Board board = new Board();
			board.setMember(member2);
			board.setTitle("board made by Kim " + i);
			board.setContent("content made by Kim" + i);
			board.setCreateDate(new Date());
			board.setCnt(0L);
			boardRepo.save(board);
		}
	}
	
}

여기서 주의할 점은 엔티티를 저장할 때, 연관관계가 있는 엔티티가 있으면 해당 엔티티도 영속 상태에 있어야 한다.

여기서는 board와 member가 연관관계를 맺기위해 member 엔티티를 먼저 영속성 컨테이너에 저장하고 이후에 board 엔티티를 저장했다.

 

위의 테스트를 실행하여 테이블을 확인하면,

 

조회 결과에서 확인할 수 있듯이 board 테이블의 외래키에 해당하는 member_id 컬럼이 자동으로 member 테이블의 기본 키 값으로 저장되었다.

 

연관관계를 맺었으니 상세조회를 통해 관계가 바르게 설정 되었는지 확인한다.(src/test/java/com/rubypaper/RelationMappingTest.java)

 

@RunWith(SpringRunner.class)
@SpringBootTest
public class RelationMappingTest {
	@Autowired
	private BoardRepository boardRepo;

	@Autowired
	private MemberRepository memberRepo;

	@Test
	public void testManyToOneSelect() {
		Board board = boardRepo.findById(5L).get();
		System.out.println("Title : " + board.getTitle());
		System.out.println("Content : " + board.getContent());
		System.out.println("Writer : " + board.getMember().getName());
		System.out.println("Writer's role : " + board.getMember().getRole());
	}

}

따라서 테스트 케이스를 실행하게 되면 자동으로 조인이 실행되어 연관관계에 있는 member 정보까지 함께 조회할 수 있다.

 

하지만 위의 실행에서는 외부조인을 사용하였고, 외부조인은 우리가 원하는 조회에서 비효율적이므로 내부조인으로 변경을 한다.

내부조인으로 실행하기 위해서는 이전에 설정한 Board 클래스에서 @JoinColumn 어노테이션에 nullable 속성을 추가한다.

 

 

public class Board {
	@Id @GeneratedValue
    private Long seq;
    
    ...
    
    @ManyToOne
    @JoinColumn(name="MEMEBER_ID", nullable=false)
   	private Member member;
}

저장 후 다시 실행하면 내부조인으로 바뀌는 것을 확인할 수 있다.

 

 

 

2. 양방향 연관관계

 

위에서 Board에서 Member로 접근하는 단방향 매핑을 적용했다. 이번에는 반대로 Member에서 Board로 접근하는 관계를 추가하여 양방향 관계 매핑을 적용해 보겠다. 

 

앞선 관계와 반대로 Member에서 Board로 가는 관계는 일대다 관계이고, 일대다 관계를 여러 객체와 연관 관계를 맺을 수 있으니 List와 같은 컬렉션을 사용해야 한다.

 

일대다 관계를 적용하기 위해 먼저 Member 클래스를 수정한다.

 

 

@Getter
@Setter
@ToString
@Entity
public class Member {
	@Id
    @Column(name="MEMBER_ID")
    private String id;
    private String password;
    private String name;
    private String role;
    
    @OneToMany(mappedBy="member", fetch=FetchType.EAGER)
    private List<Board> boardList = new ArrayList<Board>();
}

이전과 다르게 boardList를 추가하였고 @OneToMany라는 어노테이션을 추가하여 일대다 관계를 매핑하였고, mappedBy 속성을 통해 member가 관계의 주인이라는 것을 지정하였다.

또한, 기본적인 일대다 관계의 fetch 속성은 LAZY이나 회원정보를 가져올 때, 등록한 게시글 목록도 가져오기 위해 EAGER 옵션으로 설정하였다.

 

테스트를 위해 이전에 작성한 테스트 코드에 추가로 작성을 한다.

 

@RunWith(SpringRunner.class)
@SpringBootTest
public class RelationMappingTest {
	@Autowired
	private BoardRepository boardRepo;

	@Autowired
	private MemberRepository memberRepo;
	
	@Test
	public void testTwoWayMapping() {
		Member member = memberRepo.findById("member1").get();
		
		System.out.println("========================");
		System.out.println(member.getName() + "'s Board List");
		System.out.println("========================");
		List<Board> list = member.getBoardList();
		for (Board board : list) {
			System.out.println(board.toString());
		}
	}

}

 

코드만 본다면 meber1의 이름과 member1이 작성한 board의 데이터들이 출력 될 것이다. 하지만, 양방향관계에서 @ToString이 상호 호출을 하기에 StackOverFlowError가 발생하고 출력결과는 나오지 않는다.

즉, A 객체에서 toStirng() 메소드를 호출하면 그 안에서 B 객체의 toString() 호출하고 또 다시 이전의 과정이 반복되는 순환 참조에 빠지게 된다.

 

따라서, Board와 Member 엔티티 클래스에서 설정한 @ToString 어노테이션에 exclude 속성을 추가하여 상호 호출을 중단하여야 한다.

 

@Getter
@Setter
@ToString(exclude="member")
@Entity
public class Board {

 

@Getter
@Setter
@ToString(exclude="boardList")
@Entity
public class Member {

클래스를 수정하고 다시 테스트를 하면 정상적인 결과가 출력된다. 

 

 

 

 

3. 영속성 전이

 

특정 엔티티를 영속 상태로 만들거나 삭제를 할 때,  연관된 엔티티도 함께 처리할 경우 영속성 정의를 사용하면 관리가 쉽다.

JPA에서 cascade 속성을 통해 부모 엔티티를 저장,삭제 할 때 자식 엔티티도 저장, 삭제할 수 있다.

 

member와 board 관계에서 member는 부모 엔티티이고 게시판은 자식 엔티티이다.(member-PK, board-FK)

위의 관계에 영속성 전이를 적용하기 위해 Member 클래스를 수정한다.

 

public class Member {
	...
    
    @OneToMany(mappedBy="member", fetch=FetchType.EAGER, cascade=CascadeType.ALL)
    private List<Board> boardList = new ArrayList<Board>();
}

 

Cascade.ALL을 적용하여 Member와 관련된 게시판들도 Member와 함께 수정, 삭제 되도록 지정 할 수 있다.

 

테스트를 위해 이전에 사용했던 testManyToOneInsert() 메소드를 수정한다.

 

@RunWith(SpringRunner.class)
@SpringBootTest
public class RelationMappingTest {
	@Autowired
	private BoardRepository boardRepo;

	@Autowired
	private MemberRepository memberRepo;
    
	@Test
	public void testManyToOneInsert() {
		Member member1 = new Member();
		member1.setId("member1");
		member1.setPassword("member111");
		member1.setName("Park");
		member1.setRole("User");
//		memberRepo.save(member1);
		
		Member member2 = new Member();
		member2.setId("member2");
		member2.setPassword("member222");
		member2.setName("Kim");
		member2.setRole("Admin");
//		memberRepo.save(member2);
		
		for(int i=1;i<=3;i++) {
			Board board = new Board();
			board.setMember(member1);
			board.setTitle("board made by Park " + i);
			board.setContent("content made by Park" + i);
			board.setCreateDate(new Date());
			board.setCnt(0L);
			boardRepo.save(board);
		}
		
		for(int i=1;i<=3;i++) {
			Board board = new Board();
			board.setMember(member2);
			board.setTitle("board made by Kim " + i);
			board.setContent("content made by Kim" + i);
			board.setCreateDate(new Date());
			board.setCnt(0L);
//			boardRepo.save(board);
		}
	}
}

 

핵심은 이전에는 객체를 영속화하기 위해 매번 save()를 했지만 영속성 전이를 통해 Member 객체만 영속화하면 Member가 가진 boardList에 있는 모든 Board객체도 자동으로 영속화 되는 것이다.

 

이상 무!

 

 

 

 

 

 

이 글은 코드프레소 DevOps Roasting 코스를 수강하면서 작성한 글입니다.

'Dev-Spring' 카테고리의 다른 글

Spring Boot Redis 환경 추가하기  (2) 2020.01.29
[Srping Project - 1] Jungstagram(Simple SNS)  (0) 2020.01.16
Comments