본문 바로가기
IT/JPA

@OneToMany 단방향을 @ManyToOne 양방향으로

by 최고영회 2019. 8. 28.
728x90
반응형
SMALL

JPA 에서 Entity 간의 "관계" 가 중요한데 

Parent - Child 가 있을 때 

쉽게 생각하면 아래와 같이 @OneToMany 로 매핑할 수 있다. 

@Entity
@Getter @Setter
@Builder
@NoArgsConstructor
@AllArgsConstructor
public class Parent {
	
	@Id @Column(name = "parent_id")
	@GeneratedValue(strategy = GenerationType.IDENTITY)
	private long id;
	
	private String parentValue;
	
	@OneToMany
	@Builder.Default
	private List<Child> childList = new ArrayList<Child>();
}


@Entity
@Getter @Setter
public class Child {

	@Id
	@Column(name = "child_id")
	@GeneratedValue(strategy = GenerationType.IDENTITY)
	private long id;
	
	private String childValue;
}

이렇게 단방향 일대다 매핑을 하게 되면 

Parent와 Child 간의 관계를 관리하는 별도의 테이블이 생성된다. (#조인테이블전략)

간단히 테스트 해 보면

@RestController
@RequestMapping("test")
public class TestController {

	@Autowired	
	private TestService s;

	@PostMapping
	public ResponseEntity<Parent> save(@RequestBody Parent p){
		return new ResponseEntity<Parent>(s.save(p), HttpStatus.OK);
	}
}


// Service
@Transactional
public Parent save(Parent p) {
	return repo.save(p);
}

잘 될까?

에러가 발생한다. 

hibernate 가 수행하는 것을 debug 로 살펴보면 

Hibernate: 
    insert 
    into
        parent
        (parent_value) 
    values
        (?)
binding parameter [1] as [VARCHAR] - [abcdssss]
Natively generated identity: 2
...
...
insert 
    into
        parent_child_list
        (parent_parent_id, child_list_child_id) 
    values
        (?, ?)
binding parameter [1] as [BIGINT] - [2]

이 다음에 

ERROR o.h.i.ExceptionMapperStandardImpl - HHH000346: Error during managed flush [org.hibernate.TransientObjectException: object references an unsaved transient instance - save the transient instance before flushing: com.yhkim.study.test.Child] 예외가 발생한다. 

예외를 보면 unsaved instance 를 참조해서 발생한 문제다. 

Parent @OneToMany 에 영속성 전이를 설정해 보자. 

public class Parent {
	...
	
	@OneToMany(cascade = CascadeType.ALL)
	@Builder.Default
	private List<Child> childList = new ArrayList<Child>();
}

CascadeType 은 여러 종류가 있으나 편하게 테스트를 위해 All 로 설정

다시 테스트 해 보면

 insert 
    into
        parent
        (parent_value) 
    values
        (?)
binding parameter [1] as [VARCHAR] - [p value]

...

insert 
    into
        child
        (child_value) 
    values
        (?)
binding parameter [1] as [VARCHAR] - [c value]

...

insert 
    into
        parent_child_list
        (parent_parent_id, child_list_child_id) 
    values
        (?, ?)
binding parameter [1] as [BIGINT] - [2]
binding parameter [2] as [BIGINT] - [3]

정상적으로 잘 동작 한다. 

그런데 parent_child_list 라는 테이블이 영 마음에 들지 않는다. 

Join 을 위한 테이블이 하나 더 있는 것 자체로 성능과 관리 측면에서 좋지 않다. 

parent_child_list 테이블을 없애보자.

public class Parent {
	...
		
	@OneToMany(cascade = CascadeType.ALL)
	@JoinColumn(name = "parent_id")
	@Builder.Default
	private List<Child> childList = new ArrayList<Child>();
}

간단하다. 

@OneToMany 에 @JoinColumn 을 넣어주는 것이다. 

일대다 관계에서 외래키는 '다' 쪽에 있기 때문에 위와 같이 설정하면 child 테이블에 parent_id 라는 Join 을 위한 컬럼이 추가 된다. 

hibernate 가 어떻게 동작하는 지 살펴보자. 

insert 
    into
        parent
        (parent_value) 
    values
        (?)
binding parameter [1] as [VARCHAR] - [p value]

...

insert 
    into
        child
        (child_value) 
    values
        (?)
binding parameter [1] as [VARCHAR] - [c value]

...

update
        child 
    set
        parent_id=? 
    where
        child_id=?
binding parameter [1] as [BIGINT] - [1]
binding parameter [2] as [BIGINT] - [1]

일다다 단방향 매핑의 단점은 

매핑한 객체가 관리하는 외래 키가 다른 테이블에 있다는 점이다. 

본인 테이블에 외래 키기 있으면 엔티티의 저장과 연관관계 처리를 INSERT SQL 한번에 끝낼 수 있지만, 다른 테이블에 외래 키가 있으면 연관관계 처리를 위한 UPDATE SQL을 추가로 실행해야 한다. 

위에서도 말했듯이 이것은 성능 문제도 있지만 관리도 부담스럽다. 

이를 해결하기 위해 다대일 양방향 매핑을 사용해 보자. 

public class Parent {
	...  
    
    @OneToMany(mappedBy = "parent", cascade = CascadeType.ALL)
	@Builder.Default
	private List<Child> childList = new ArrayList<Child>();
}

public class Child {
    ...   
	
    @ManyToOne
	@JoinColumn(name = "parent_id")
	@JsonIgnore
	private Parent parent;
}

Parent 의 @OneToMany에 @JoinColumn이 빠지고 mappedBy 가 추가되었다. 

그리고 Child 에는 @ManyToOne 으로 Parent 를 선언한다. 

이렇게 양방향으로 매핑을 하게 되면 일대다 단방향 매핑보다 더 자유롭게 객체 탐색에서도 유리하다. 

이제 다시 테스트를 해보자. 

Hibernate: 
insert 
    into
        parent
        (parent_value) 
    values
        (?)
binding parameter [1] as [VARCHAR] - [p value]
Natively generated identity: 1
...
Hibernate: 
    insert 
    into
        child
        (child_value, parent_id) 
    values
        (?, ?)
binding parameter [1] as [VARCHAR] - [c value]
binding parameter [2] as [BIGINT] - [null]

update 구문은 사라졌다. 

그런데 마지막줄을 보면 Join 컬럼인 parent_id 에 null 이 들어가고 있다. 

controller 를 살펴보면 @RequestBody 로 Parent 를 받고 있다. 

실제 테스트 한 데이터는 아래와 같다.

@RequestBody 를 통해 JSON 데이터가 Parent 객체에 매핑될 것이다.

@RequestBody 를 통해 생성된 Parent 안에 참조된 Child 입장에서는 @ManyToOne 으로 private Parent parent; 변수에 set 된 적이 없기 때문에 Parent 정보를 알 수 없다. 

때문에 null 이 insert 된 것으로 보인다. 

Service 에서 Child 에게 "내가 너의 부모야!" 라고 알려줘 보자. 

@Transactional
public Parent save(Parent p) {
	for (Child c: p.getChildList()) {
		c.setParent(p);
	}
	return repo.save(p);
}
Hibernate: 
insert 
    into
        parent
        (parent_value) 
    values
        (?)
binding parameter [1] as [VARCHAR] - [p value]
Natively generated identity: 1
...
Hibernate: 
    insert 
    into
        child
        (child_value, parent_id) 
    values
        (?, ?)
binding parameter [1] as [VARCHAR] - [c value]
binding parameter [2] as [BIGINT] - [1]

잘된다...

당연히 I'm your father가 자동으로 인식될것이라고 생각했기 때문에 이 문제를 해결하는데 오랜 시간이 걸렸다...

어제 이것 때문에 늦게까지 야근을......ㅜㅜ

만약 Parent Entity 에 아주 많은 @OneToMany 가 있고 Child Entity 에도 또 @OneToMany 가 많다면?

또 depth 가 점점 깊어진다면???

Loop - Loop - Loop 반복....

Service 에서 이렇게  I'm your fater 설정을 해줘야 하는 걸까..... 

public void setChildList(List<Child> childList) {
	this.childList = childList;
	if (this.childList != null && this.childList.size() > 0) {
		for (Child c : childList) {
			c.setParent(this);
		}
	}
}

이렇게 Parent Entity에 childList 에 대한 Setter 를 구현해 두면 

@RequestBody에 의해 객체가 생성되면서 Setter가 호출될 때 Child에게 Parent 가 자신임을 알려줄 수 있게 된다. 

각 Entity별로 필요에 따라 위와 같이 Setter를 만들어 두면 Service 에서 다중 Loop, 반복 등이 사라진다. 

728x90
반응형
LIST