전통적인 DB 설계 방식에서는 테이블간 관계를 외래키(FK) 로 연결한다.
예를 들어 Order테이블에 member_id컬럼이 존재한다면, 이 컬럼을 통해 Member테이블과 Order테이블 간의 연관관계가 있다는걸 알 수 있다.
하지만 JPA에서는 이렇게 외래 키만을 필드로 두는 것보다 실제 Java 객체 간의 참조를 기반으로 연관관계를 매핑하는 것이 핵심이다.
즉, order.memberId처럼 단순히 외래 키 값을 보관하는 것이 아니라, order.getMember()처럼 객체 자체를 직접 참조하는 방식이 권장된다. 이것이 바로 객체지향적인 설계와 데이터베이스 설계의 차이이며, JPA가 추구하는 방향이다.
@Entity
@Getter
@Setter
@NoArgsConstructor
@AllArgsConstructor
public class Member {
@Id @GeneratedValue
@Column(name = "MEMBER_ID")
private String id;
@Column(name = "USERNAME")
private String username;
@Column(name = "TEAM_ID")
private Long teamId;
}
@Entity
@Getter
@Setter
public class Team {
@Id @GeneratedValue
@Column(name = "TEAM_ID")
private Long id;
private String name;
}
Member member = new Member();
member.setUsername("member1");
member.setTeamId(team.getId());
em.persist(member);
연관된 객체를 직접 다룰 수 없다.
Team team = findMember.getTeam();
// Member가 Team을 참조하는 필드를 가지고있는게 아니기 때문에 불가능
매번 아래와 같은 방식으로 접근해야 한다.
Long teamId = findMember.getTeamId();
Team team = em.find(Team.class, teamId);
예를 들어, Team 입장에서 그 팀에 소속된 멤버들을 조회하고 싶다면
List< Member > 같은 필드는 사용할 수 없으며, 직접 쿼리를 날려야 한다.
Cascade, orphanRemoval, FetchType.LAZY 같은 강력한 기능들은
객체 간 관계를 통해서만 동작한다. FK만 있으면 아무 의미 없다.
member.setTeam(team);
System.out.println(member.getTeam().getName());
- 직접 FK 설정
-> 객체를 직접 연결하지 않으므로 객체 간 탐색이 불가능- 데이터 중심 사고
-> 설계가 SQL 중심이 되며, 객체지향 언어의 이점을 활용하지 못함- 코드 중복
-> 반복적으로 em.find() 등을 사용해야 함- 기능 제한
-> Cascade, FetchType, orphanRemoval 등이 적용되지 않음- 유지보수 어려움
-> 관계 파악이 어려워지고, 실수도 잦아짐
JPA는 단순히 SQL 대신 자동으로 쿼리를 만들어주는 도구가 아니다.
객체지향적인 코드로 설계한 엔티티들을 DB와 매핑해주는 기술이다.
그래서 우리는 "테이블 설계를 그대로 따라" JPA를 사용하면 안 된다.
객체는 객체답게 설계하고, 관계는
@ManyToOne, @OneToMany로 명확히 매핑해줘야
JPA의 모든 기능과 성능을 제대로 활용할 수 있다.
JPA에서는 객체 간의 관계를 DB 테이블의 외래키를 이용해 매핑한다.
하지만 객체 지향적으로 설계하기 위해선 단순히 외래키만 사용할 게 아니라, 정확한 방향성과 연관관계 매핑 어노테이션을 잘 이해하고 써야 한다.
- 한 엔티티가 다른 엔티티와 1:1로만 연결될 때
예: User ↔ UserProfile
@Entity
public class User {
@Id @GeneratedValue
private Long id;
@OneToOne
@JoinColumn(name = "profile_id) // FK
private UserProfile profile;
}
@Entity
public class UserProfile {
@Id @GeneratedValue
private Long id;
@OneToOne(mappedBy = "profile")
private User user;
}
@Entity
public class Team {
@Id @GeneratedValue
private Long id;
@OneToMany
@JoinColumn(name = "team_id") // FK는 member 테이블에 생김
private List<Member> members = new ArrayList<>();
}
@Entity
public class Member {
@Id @GeneratedValue
private Long id;
@ManyToOne
@JoinColumn(name = "team_id")
private Team team;
}
@Entity
public class Team {
@Id @GeneratedValue
private Long id;
@OneToMany(mappedBy = "team") // 읽기 전용
private List<Member> members = new ArrayList<>();
}
@Entity
public class Member {
@Id @GeneratedValue
private Long id;
@ManyToOne
@JoinColumn(name = "team_id") // FK
private Team team;
}
@Entity
public class Student {
@Id @GeneratedValue
private Long id;
@ManyToMany
@JoinTable(name = "student_course",
joinColumns = @JoinColumn(name = "student_id"),
inverseJoinColumns = @JoinColumn(name = "course_id"))
private List<Course> courses = new ArrayList<>();
}
@Entity
public class Course {
@Id @GeneratedValue
private Long id;
@ManyToMany(mappedBy = "courses")
private List<Student> students = new ArrayList<>();
}
JPA에서는 항상 연관관계의 방향과 주인을 정확히 설계하고,
필요시 mappedBy, JoinColumn, JoinTable을 올바르게 지정해줘야 한다.
연관관계의 주인(Owner)은 DB의 외래키(FK)를 관리하는 엔티티이다.
즉, 어떤 쪽이 외래키 값을 insert/update할 책임이 있는지를 나타낸다.
JPA는 '주인'만 외래키를 관리할 수 있다.
mappedBy는 반대편에서 연관관계의 주인을 지정하는 속성이다.
mappedBy = "team" → 이 말은 "이 연관관계의 관리는 Member.team이 함"이라는 의미이다.
즉, mappedBy가 붙은 쪽은 읽기 전용, insert/update에 관여하지 않는다.
Member N:1 Team (ManyToOne — 외래키 주인)
@Entity
public class Member {
@Id @GeneratedValue
private Long id;
@ManyToOne
@JoinColumn(name = "team_id") // FK 관리 → 이게 '연관관계 주인'
private Team team;
}
Team 1:N Member (OneToMany — 주인이 아님)
@Entity
public class Team {
@Id @GeneratedValue
private Long id;
@OneToMany(mappedBy = "team") // 'team'은 Member 엔티티 안에 있는 필드명
private List<Member> members = new ArrayList<>();
}