연관관계 매핑 (1)

dereck·2025년 2월 18일

JPA

목록 보기
7/13
post-thumbnail

김영한님의 JPA 강의를 보고 정리한 내용입니다.

연관관계 매핑 1

연관관계 정의 규칙

연관관계를 매핑할 때, 생각해야 할 것은 크게 3가지가 있다.

  1. 방향 - 단/양방향
    • 무조건 양방향 연관관계를 가지면 안될까?
    • 양방향 매핑 정리
  2. 연관관계의 주인 - 연관관계에서 관리 주체
    • 왜 연관관계의 주인을 지정해야 할까
    • 연관관계 매핑에서 자주하는 실수
  3. 다중성
    • 일대일(1:1)
    • 일대다(1:N)
    • 다대일(N:1)
    • 다대다(N:M)

"연관관계 매핑 1"에서는 방향과 연관관계의 주인에 대한 내용이 있음

방향

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

하지만 객체는 참조용 필드가 있는 객체만 다른 객체를 참조하는 것이 가능하다. 그렇기 때문에 두 객체 사이에 하나의 객체만 참조용 필드를 갖고 참조하면 단방향 관계, 두 객체 모두가 각각 참조용 필드를 갖고 참조하면 양방향 관계라고 한다.

엄밀하게는 양방향 관계는 없고, 두 객체가 서로 단방향 참조를 가져서 양방향 관계처럼 사용하고 있다고 말하는 것이다.

JPA를 사용하여 데이터베이스와 패러다임을 맞추기 위해서는 객체는 어떤 방향으로 연관관계를 가질 지 선택해야 한다. 이때 선택은 비즈니스 로직에서 어떤 객체가 참조가 필요한 지 생각해보면 된다.

  • MemberTeam이 있을 때
    - member.getTeam(): MemberTeam을 참조
    - team.getMember: TeamMember를 참조

이렇게 비즈니스 로직에 맞게 선택 했는데 두 객체가 서로 단방향 참조를 했다면 양방향 연관관계가 되는 것이다.

무조건 양방향 연관관계를 가지면 안될까?

객체 관점에서 양방향 연관관계를 가지면 오히려 복잡해질 수 있다.

예시로 일반적인 비즈니스 애플리케이션에서 사용자(User) 엔티티는 많은 테이블과 연관관계를 맺게 된다. 이런 경우에 모든 엔티티를 양방향으로 설계하게 되면 사용자 엔티티는 엄청나게 복잡해질 것이다.

그리고 다른 엔티티들도 불필요한 연관관계 매핑으로 인해 복잡성이 증가할 수 있다.

양방향 매핑 정리

  • 단방향 매핑만으로도 이미 연관관계 매핑은 완료되어야 한다.
    - 설계라는 관점에서만 봤을 때 단방향으로 끝내라는 말
  • 양방향 매핑은 반대 방향으로 조회 기능이 추가된 것일 뿐이다.
    - 조회 기능 = 객체 그래프 탐색
  • JPQL에서 역방향으로 탐색할 일이 많다.
    - (하지만) 단방향 매핑을 한 뒤에 필요할 때 양방향을 추가해도 된다.
    - 어차피 테이블에 영향을 주지 않음
    - 연관관계 편의 메서드 예시를 활용해서 추가하도록 한다.

연관관계 편의 메서드 예시

class Member {
	...
	@ManyToOne
	@JoinColumn(name = "team_id")
	private Team team;

	public void changeTeam(Team team) {
		this.team = team;
		team.getMembers().add(this);  // 해당 메서드 호출 시 자동으로 동기화
	}
}

비즈니스 로직이 정말 복잡하면 값을 체크하는 로직을 포함하도록 하자.

  • null 체크 로직
  • 기존 값 변경 시 필요한 로직

연관관계의 주인

두 객체가 양방향(단방향 2개) 관계를 맺을 때, 연관관계의 주인을 지정해야 한다. 연관관계의 주인을 지정하는 것은 두 단방향 관계 중, 제어의 권한을 갖는 실질적인 관계가 어떤 것인지 JPA에게 알려준다고 생각하면 된다.

  • 제어의 권한: 외래 키를 비롯한 테이블 레코드를 조회, 저장, 수정, 삭제 처리하는 것

연관관계의 주인이 아닌 쪽에 mappedBy 속성으로 주인을 지정한다. 쉽게 말하면 외래 키가 존재하는 곳이 주인이고, 외래 키가 없는 쪽이 주인이 아니기 때문에 외래 키가 없는 쪽에 mappedBy로 주인을 지정해주면 된다.

연관관계의 주인을 정할 때 비즈니스 로직을 기준으로 주인을 선택하면 안된다. 연관관계의 주인은 외래 키의 위치를 기준으로 정해야 한다.

연관관계의 주인은 연관관계를 갖는 두 객체 사이에서 제어를 할 수 있지만, 연관관계의 주인이 아니면 조회만 가능하다.

왜 연관관계의 주인을 지정해야 할까?

객체에서 양방향으로 매핑을 하게 되면 테이블과 매핑을 담당하는 JPA에 혼란을 주게 된다. 어떤 객체에서 수정을 할 때 외래 키를 수정할 지를 결정하기 어렵기 때문이다.

그래서 두 객체 간 연관관계의 주인을 정해서 명확하게 어떤 객체에서 수정을 할 때만 외래 키를 수정하겠다고 정하는 것이다.

연관관계 매핑에서 자주하는 실수

실수 1. mappedBy 미사용

주인이 아닌 쪽에 mappedBy로 주인을 정하지 않는 실수를 많이 한다.

Team team = new Team();
team.setName("teamA");
em.persist(team);

Member member = new Member();
member.setName("memberA");

// Team class에 List<Member> members = new ArrayList<>(); 가 있다고 가정
team.getMembers().add(member);

위 코드처럼 역방향만 연관관계를 설정했을 경우 member 객체에는 teamid 값이 null로 들어갈 것이다.

실수 2. 순수 객체 상태를 고려하지 않음

데이터베이스에 외래 키가 있는 테이블을 수정하려면 연관관계의 주인만 변경하는 것이 맞지만, 객체의 관점에선 둘 다 변경해주는 것이 좋다. 두 참조를 사용하는 순수한 객체는 데이터 동기화가 필요하기 때문이다.

  • 데이터 동기화 예시는 연관관계 편의 메서드 예시를 참고

양쪽 어느 곳이든지 데이터 동기화를 위한 코드를 작성해도 상관 없지만 양쪽 모두에 동기화 코드를 작성하는 것은 무한 루프에 걸릴 수도 있으니 한 쪽에만 작성하도록 한다. 해당 코드는 방향대로 작성하든지 역방향에서 작성하든지 상관 없다. (상황에 따라 변경)

또한 단순 setXxx 형식의 메서드 네이밍은 해당 로직이 중요하다는 인식을 주지 못하기 때문에 이름을 바꿔 주는 것이 좋다.

  • ex) setTeam(Team team) -> changeTeam(Team team)

실수 3. 양방향 매핑 시 무한 루프

toString()이나 Lombok, JSON 생성 라이브러리 등을 생성했을 때 무한 루프에 걸릴 수 있다.

예시로 toString()을 생성했을 때를 보자.

@Override  
public String toString() {  
    return "Member{" +  
            "id=" + id +  
            ", name='" + name + '\'' +  
            ", team=" + team +  
            '}';  
}
---
@Override  
public String toString() {  
    return "Team{" +  
            "id=" + id +  
            ", name='" + name + '\'' +  
            ", members=" + members +  
            '}';  
}

toString()안에 있는 teammembers는 각 클래스의 toString()을 호출한다는 뜻이다. 결론적으로 양쪽에서 무한으로 toString()을 호출하게 돼서 StackOverflowError가 발생하게 된다.

Lombok이나 JSON 생성 라이브러리 또한 마찬가지로 양방향이 걸려 있으면 엔티티를 JSON으로 변환하는 순간 무한 루프에 빠져버릴 수 있다.

특히 컨트롤러에서 엔티티를 그대로 반환할 때 주의하도록 한다.

  • 컨트롤러에서 엔티티를 그대로 반환했을 때 생길 수 있는 일
    - 무한 루프
    1. "Member JSON으로 반환
    2. 근데 Member 안에 Team이 있네?
    3. Team을 JSON으로 반환
    4. 근데 Team안에 Members가 있네?"
    5. 1 ~ 4 무한 반복
    - 엔티티의 변경
    - 엔티티를 API에 반환해버리면 나중에 그 엔티티를 변경하는 순간 API 스펙이 변경되는 것

해결 방법에는 Lombok 등의 라이브러리 사용 시 toString()은 사용하지 않도록 하고, 컨트롤러에선 엔티티 반환 대신 DTO 객체를 만들어서 반환하도록 하자.

References

0개의 댓글