0. 개요
- 이전 포스팅에서 JPA를 이용한 양방향 연관 관계를 구현하는 방법에 대해 알아보았다.
- 이번 포스팅은 양방향 연관 관계를 구현할 때 자주 실수하는 부분에 대해서 알아보자.
* 이전 포스팅과 이어지는 내용입니다!
1. 연관 관계의 주인만 FK의 값을 생성할 수 있다.
a) 예시 코드
- 양방향 매핑을 구현하면 mappedBy 옵션을 사용하여 연관 관계의 주인을 명시한다.
- 이로써 JPA에서도 FK처럼 작동한다고 판단하여 자주 하는 실수 중 하나이다.
- 다음 코드 예시를 살펴보자.
- 우선 선수 객체(player1)를 만들었다.
- 그리고 팀 객체(team)를 만들었다.
- team 객체를 이용하여 앞서 만들었던 선수 객체를 저장하였다.
- 이 코드는 올바르게 작동할까?
- 위의 결과처럼, 위의 코드는 올바르게 작동하지 않는다.
- 그렇다면 그 이유는 무엇일까?
b) 문제의 원인
- 다음 Team 객체의 코드를 살펴보자.
@Entity
public class Team {
@Id
@GeneratedValue
@Column(name = "TEAM_ID")
private Long id;
@Column(name = "TEAM_NAME")
private String name;
@OneToMany(mappedBy = "teamId")
private List<Player> players = new ArrayList<>();
}
- Team 객체는 mappedBy 옵션을 이용하여 관계의 주인을 명시하였다.
- 이전 포스팅에서 mappedBy를 명시하는 객체는 관계의 대상이라고 했다.
- 즉, 관계의 주인이 아니다.
- 관계의 주인이 아니므로 FK에 해당하는 필드에 값을 생성할 수 없다.
- FK에 해당하는 필드에 값을 생성하고 싶다면, 관계의 주인이 해야 한다.
- 즉, Player 객체를 이용해야 teamId 값을 설정할 수 있는 것이다.
c) 해결 방법 (1) - 관계의 주인을 사용한다.
- 그러므로 다음과 같이 코드를 작성해야 문제가 발생하지 않는다.
- 위의 코드처럼, Team 객체를 먼저 생성한다.
- 그리고 Player 객체를 생성하여 setTeamId() 메서드를 통해 teamId 필드에 값을 생성한다.
- 이처럼 코드가 올바르게 작동하는 것을 확인할 수 있다.
d) 해결 방법 (2) - 양쪽 객체 모두 사용한다.
- 객체 지향적으로 생각해보면 양쪽 객체 모두에게 값을 삽입하는 것이 가장 올바른 해결책이다.
- 특히, Java만을 이용한 테스트 케이스를 작성할 때에는 양쪽 객체 모두에게 값을 삽입하는 방법이 옳다.
- 다음 예시 코드처럼 말이다.
- 위의 코드처럼, 양쪽 객체 모두에게 값을 설정하는 것이 가장 올바르다.
- 그 이유는 양쪽 객체에서 모두 조회가 가능해야 하기 때문이다.
- 예를 들어, Player 객체만 이용하여 Team을 설정했다고 가정해보자.
- commit()이 완료되기 이전에, 반대로 Team 객체를 통해 Team에 등록되어있는 선수를 조회하려고 한다.
- 다음 코드를 보면 이해가 쉬울 것이다. 이 코드는 올바르게 동작할까?
- 사실 Player 객체만을 이용하여 Team을 설정하는 방법에 잘못된 부분은 없다.
- 관계의 주인을 이용하여 FK에 값을 생성하는 방법이므로, 데이터는 잘 생성된다.
- 그러나 중요한 점은 시점에 있다. 위에서 들은 예는 commit()이 완료되기 이전 시점이다.
- 우선, em.persist(team) 코드가 실행된 시점에서 Team 객체가 영속성 콘텍스트에 저장된다.
- 동시에 동일한 Team 객체가 1차 캐시에 저장된다.
- 1차 캐시에 저장되어 있는 Team 객체는 아직 아무런 정보가 생성되지 않은 상태다.
- 그러므로 findTeam 객체에는 아무런 정보가 들어있지 않다.
- for 문을 수행하더라도 아무 값도 출력되지 않는다. 아래의 결과처럼 말이다.
- 올바르게 동작했다면 빨간 화살표가 가리키는 곳에 for문이 출력되어야 한다.
- 그러나 양쪽 객체 모두에게 설정했다면, 이야기는 달라진다.
- 아래의 코드와 결과를 보자.
e) 해결 방법(3) - flush(), clear()를 사용한다.
- 위의 두 번째 해결책에서 아래의 코드가 문제가 된 이유는 Team 객체의 조회 시점 때문이었다.
- 즉, 1차 캐시에 저장되어 있는 Team 객체의 상태 때문이었다.
- 그렇다면 Team 객체를 조회하기 이전에 Team 객체의 상태를 최신화시킬 수 있다면 어떻게 될까?
- 다음 코드를 살펴보자.
- 1차 캐시에 저장되어 있는 Team 객체의 상태를 최신화시키기 위해서는 DB에서 가져오는 방법이 있다.
- 그러므로 우선 Team 객체를 조회하기 이전에, 변화된 상태를 DB에 저장시켜야 한다.
- 이 과정을 em.flush(); 메서드를 통해 수행한다.
- 그다음, em.clear(); 메서드를 통해 1차 캐시 및 JPA 메모리를 비운다.
- 이와 같은 상태에서 Team 객체를 조회하면, DB로부터 가져온 Team 객체의 최신 정보를 1차 캐시에 저장한다.
- 그러므로 다음과 같이 기대한 결과 값을 출력한다.
- 위의 결과를 보면 선수 이름이 마지막에 잘 출력된 것을 확인할 수 있으며,
- 이전에 SELECT문이 실행되면서 DB로부터 가장 최신 정보를 가져오는 것을 확인할 수 있다.
* 주의하자!
- mappedBy 옵션은 관계의 대상이 되는 Entity가 관계의 주인(= FK)을 명시하기 위해 사용된다.
- 그러므로 mappedBy를 가짜 매핑이라고 부르기도 한다.
- 이처럼 가짜 매핑은 값을 조회(= read)할 수 있지만, 생성(= create)할 수 없다.
f) 해결 방법(4) - 연관 관계 편의 메서드를 사용한다.
- 애초에 연관 관계의 주인이 어떤 객체인지 알고 있다면, 상대 객체의 Setter를 수정하는 방법도 있다.
- 위의 예시를 기반으로 설명하자면, Player Entity의 Setter를 다음과 같이 수정하는 것이다.
- 이처럼 Setter를 미리 수정해놓으면, 양쪽 객체의 값을 모두 설정하는 것과 동일한 기능을 수행한다.
* 주의하자!
- Setter를 이렇게 수정하여 사용하는 방법을 연관관계 편의 메서드라고 부른다.
- 이와 같은 메서드를 작성할 때에는 Setter를 사용하지 않는 것을 권장한다.
- 대신 Setter의 이름을 변경하여 사용하는 것을 권장한다.
g) 결론
- 양방향 연관 관계에서 FK 값을 설정할 때, 순수 객체의 사용을 고려하여 양쪽 객체 모두를 설정한다.
2. toString()에 의한 무한루프
a) 무한루프 발생 과정
- 양방향 매핑을 사용할 때, toString() 메서드를 호출하는 과정에서 무한루프가 자주 발생한다.
- 이는 다음과 같은 상황에서 일어난다. 아래의 코드를 살펴보자.
- 위의 코드는 Team 객체에서 라이브러리를 통해 toString() 메서드를 생성한 것이다.
- 빨간 밑줄을 보면, players라는 Player 객체를 호출하는 것을 확인할 수 있다.
- 이는 결국에 players.toString() 형식으로, 다시 toString()을 호출하는 것을 확인할 수 있다.
- 만약 Player 객체에도 이와 같이 toString() 메서드가 있다면 어떻게 될까?
- 아래의 코드를 살펴보자.
- 위의 코드는 Player 객체에서 라이브러리를 통해 toString() 메서드를 생성한 것이다.
- 빨간 밑줄을 보면, teamId라는 Team 객체를 호출하는 것을 확인할 수 있다.
- 이는 결국 teamId.toString() 형식으로, 다시 toString()을 호출하는 것을 확인할 수 있다.
- 이처럼 서로 다른 객체에서, 계속하여 toString()을 호출하는 무한 루프가 생성된다.
b) 무한루프 발생 원인
- 그렇다면 이렇게 toString()으로 인한 무한루프가 발생하는 원인이 무엇일까?
- 당연히 toString() 메서드를 재정의하여 사용하기 때문이기도 하지만, 또 다른 문제가 있다.
- 바로, JSON 생성 라이브러리에서 toString()을 사용하기 때문이다.
- Spring에서 Entity를 반환 값으로 직접 return을 하는 경우 문제가 발생한다.
- 반환된 Entity를 JSON 형식으로 변환하기 위해서는 JSON 라이브러리 내부적으로 toString()을 호출한다.
- 그러면 위에서 설명한 것처럼 무한 toString() 호출이 발생하게 되는 것이다.
c) 해결 방법
- 사실 이 문제를 해결하는 방법은 생각보다 간단하다.
1. Controller에서 절대로 Entity를 직접 반환하지 않는다.
2. lombok 또는 toString() 라이브러리를 이용하여 toString()을 오버라이드 하지 않는다.
- 이 2가지만 지켜도 toString() 무한 호출로 인한 문제는 발생하지 않는다.
'Back-end > JPA 개념' 카테고리의 다른 글
11. 연관 관계의 종류(일대일, 다대다) (0) | 2022.04.05 |
---|---|
10. 연관 관계의 종류(일대다, 다대일) (0) | 2022.04.04 |
8. 양방향 연관 관계의 기본 개념 (0) | 2022.03.31 |
7. 단방향 연관 관계 (0) | 2022.03.30 |
6. Primary key(기본 키) Mapping (0) | 2022.03.29 |
댓글