관리 메뉴

The Nirsa Way

[Spring Data JPA] 연관 관계 매핑 (1) : 단방향 매핑 (@ManyToOne, @OneToMany) 본문

Development/Spring Data JPA

[Spring Data JPA] 연관 관계 매핑 (1) : 단방향 매핑 (@ManyToOne, @OneToMany)

KoreaNirsa 2025. 7. 17. 10:41
반응형

 

연관 관계 매핑이란?

엔티티간의 관계를 DB의 FK와 매핑하여 연결하는 형태를 가질 수 있습니다. 즉 객체 간의 참조 방식을 DB의 외래키(FK)와 연결하여 사용할 수 있습니다. 이번에 살펴볼 어노테이션은 크게 2가지 인데, 아래와 같은 의미를 지닙니다.

 

1. @ManyToOne

일반적으로 단방향 매핑에서 가장 권장하는 방식입니다. 두 개의 엔티티를 만들어 관리하고 여러 명인 쪽에서 @ManyToOne과 @JoinColumn을 사용하여 단방향 매핑을 가집니다. fetch는 추후 연관관계 매핑 포스팅이 끝날 때 쯤 작성할 예정입니다.

@Entity
public class Member {
    
    @Id @GeneratedValue
    private Long id;

    private String name;

    @ManyToOne(fetch = FetchType.LAZY)
    @JoinColumn(name = "team_id") // FK 컬럼명
    private Team team;
}
@Entity
public class Team {
    @Id @GeneratedValue
    private Long id;

    private String name;
}

 

2. @OneToMany

 각종 성능이슈로 인해 단방향 매핑 관계에서는 권장하지 않는 방식이며 @OneToMany는 양방향 매핑 관계일 때 주로 사용됩니다. 참고로 OneToMany에서 사용한 cascade = CascadeType.PERSIST는 예제를 확인하기 위해 Team 저장 시 Member도 함께 저장되도록 추가된 형태입니다.

@Entity
public class Member {

    @Id @GeneratedValue
    private Long id;

    private String name;
}
@Entity
public class Team {
    
    @Id @GeneratedValue
    private Long id;

    private String name;

    @OneToMany(cascade = CascadeType.PERSIST)
    @JoinColumn(name = "team_id") // Member 테이블의 team_id 컬럼을 통해 조인
    private List<Member> members = new ArrayList<>();
}

 

아래의 코드는 OneToMany 단방향 매핑 관계일때 개발자가 실수하기 쉬운 케이스 입니다. 

@Service
@RequiredArgsConstructor
public class TeamService {
    private final TeamRepository teamRepository;

    @Transactional
    public void createTeamWithMembers() {
        // 1. 팀 객체 생성
        Team team = new Team();
        team.setName("팀A");

        // 2. 회원 객체 생성
        Member m1 = new Member();
        m1.setName("회원1");

        Member m2 = new Member();
        m2.setName("회원2");

        // 3. 팀에 회원 추가 (단방향 관계이므로 Member에 team 설정은 없음)
        team.getMembers().add(m1);
        team.getMembers().add(m2);

        // 4. 저장 (CascadeType.PERSIST로 인해 Member도 함께 저장됨)
        teamRepository.save(team);
    }
}

 

위의 코드를 실행하면 save() 시점에서 아래의 쿼리가 호출됩니다. casecadeType.PERSIST로 인하여 member도 insert되니 team 1번, member 2번의 쿼리가 실행되는 것은 맞으나 member 테이블에 데이터를 추가할 때 team_id에 null이 들어가며 update는 2회가 추가적으로 실행되는 모습을 확인할 수 있습니다.

1. INSERT INTO TEAM (id, name) VALUES (?, ?)
2. INSERT INTO MEMBER (id, name, team_id) VALUES (?, ?, null)
3. INSERT INTO MEMBER (id, name, team_id) VALUES (?, ?, null)
4. UPDATE MEMBER SET team_id = ? WHERE id = ?
5. UPDATE MEMBER SET team_id = ? WHERE id = ?

 

위와 같이 UPDATE가 2회가 추가적으로 호출되는 이유는 아래와 같습니다.

  1. teamRepository.save(team)이 호출되며 team에 대한 insert 수행
  2. casecadeType.PERSIST에 의해 m1과 m2 엔티티도 persist()되며 각각 insert 수행 (총 2회)
    단 m1, m2 엔티티에는 team에 대한 정보가 없기에 team_id를 null로 저장
  3. flush 시점에  @JoinColumn(name = "team_id")을 확인하여 team → member 간 FK가 일치할 수 있도록 update 수행
  4. UPDATE MEMBER SET team_id = [team 엔티티 id] WHERE id = [member엔티티 id]

참고로 team 엔티티 id가 자동으로 할당되는 이유는 @GenerateValue 어노테이션에 의해 insert 실행 후 DB에 생성된 PK가 team 엔티티의 id로 자동 할당되기 때문입니다. 즉, 1번에서 insert가 수행되며 team 엔티티는 생성되었던 pk를 자동 할당되었으며 해당 값을 4번의 과정에서 update 하는 것 입니다.

반응형