Java 개체와 관계 관계는 DB 테이블 간에 어떻게 다릅니까?
그리고 JPA는 이러한 불일치를 어떻게 완화합니까?
ORM 기술은 DB의 테이블 지향 패러다임과 Java의 객체 지향 패러다임 간의 불일치를 해결하기 위해 도입되었습니다.
Infran 김영한의 강의를 듣던 중 JPA를 만나 프로젝트를 진행하게 되었다. 각 연결 및 해당 설정이 실제 코드에 반영되는 방식과 테이블 구조가 적용되는 방식 궁금해서 이번 기회에 직접 코드를 작성하고 DB 상태를 확인했습니다.
협회
ORM 기술은 객체와 테이블 중심의 다양한 패러다임에서 발생하는 문제를 해결하기 위해 등장했으며, 이를 통해 개발자는 서비스 로직을 구축하면서 객체에 온전히 집중하여 개발할 수 있으며 DB의 테이블에 대한 걱정을 최소화할 수 있습니다.
패러다임은 다르지만 둘 다 연관관계존재합니다. OOP에서 연관 관계는 객체 간의 협력을 목적으로 하며 DB 테이블의 데이터를 효율적으로 로드/관리하기 위해 사용됩니다.
팀에 여러 명의 구성원이 있을 수 있는 경우 팀과 구성원 간의 관계는 1:N(일대다)이라고 할 수 있습니다. 각 위치에서 이러한 관계의 구현은 다음과 같습니다.
테이블 세계의 관계
- 멤버 테이블
| 회원 번호 | 별명 | TEAM_ID |
| 0 | 남자 | 10 |
- 팀 테이블
| TEAM_ID | 성 |
| 10 | 알파 |
각 테이블에서 TEAM_ID라는 외래 키(FK)를 기준으로 JOIN하여 관련 테이블을 찾습니다.
객체 세계의 협회
public class Member {
private Long id;
private String nickname;
// ...
}
public class Team {
private Long id;
private String name;
Member() members;
// ...
}
객체는 참조관련 개체를 찾습니다.
예인 경우 JPA와의 연관 매핑

@Entity
public class Member {
@Id @GeneratedValue
private Long memberId;
private String name;
@ManyToOne
@JoinColumn(name = "TEAM_ID")
private Team team; // Team 객체
}
@Entity
public class Team {
@Id @GeneratedValue
private Long teamId;
private String nickname;
}
Team team = new Team();
team.setName("Alpha");
em.persist(team);
Member member = new Member();
member.setUserName("John");
member.setTeam(team); // Member에 직접 Team객체 주입
em.persist(member);
Member findMember = em.find(Member.class, member.getId());
//Team findTeam = em.find(Team.class, findTeamId);
Team findTeam = findMember.getTeam();
@ManyToOne, @JoinColumn 관계를 통해.
단, 이때 멤버는 팀에 접근할 수 있지만 팀은 자신이 소유한 멤버에 접근할 수 없다.
한쪽만 참조/액세스할 수 있는 관계를 단방향 연결이라고 합니다.
위의 코드가 있는 테이블 상황
- 멤버 테이블
| 회원 번호 | 나이 | 별명 | TEAM_ID |
| 하나 | 20 | 남자 | 하나 |
- 팀 테이블
| TEAM_ID | 성 |
| 하나 | 알파 |
양방향 연결 매핑
@Entity
public class Member {
@Id @GeneratedValue
private Long id;
private String nickname;
@ManyToOne // Member의 입장에서 Many가 된다.
@JoinColumn(name = "TEAM_ID")
private Team team; // Team 객체
}
@Entity
public class Team {
@Id @GeneratedValue
@Column(name = "TEAM_ID")
private Long teamId;
@Column(name = "USERNAME")
private String name;
@OneToMany(mappedBy = "team")
private List<Member> members = new ArrayList<>(); // 추가된 필드
}
1:N 관계에서 1에 해당하는 팀에게 @OneToMany구성원 개체(테이블)는 에 의해 매핑됩니다.mappedBy속성을 사용하여 외부 개체의 필드 이름을 지정합니다.
Team team = new Team();
team.setName("Alpha");
em.persist(team);
Member member = new Member();
member.setUsername("John");
member.setTeam(team);
em.persist(member);
em.flush();
em.clear();
Member findMember = em.find(Member.class, member.getId());
List<Member> members = findMember.getTeam().getMembers(); // 해당 팀에 속한 모든 멤버 로드
위 예제의 데이터베이스 상황
- 멤버 테이블
| 회원 번호 | 나이 | 별명 | TEAM_ID |
| 하나 | 20 | 남자 | 하나 |
- 팀 테이블
| TEAM_ID | 성 |
| 하나 | 알파 |
위의 샘플 코드와 다른 점은 팀에 구성원이 추가되었다는 점입니다.
회원용으로 직접 추가한건 아니지만 찾아보니 mappedBy옵션은 실제 쿼리 중에 검색됩니다.
select
m1_0.member_id,
m1_0.nickname,
t1_0.team_id,
t1_0.name
from
member m1_0
left join
team t1_0
on t1_0.team_id=m1_0.team_id
where
m1_0.member_id=?
위의 코드는 팀 구성원을 검색하는 SQL 쿼리입니다.
소유자 및 매핑협회에 의해
개체의 양방향 연결은 실제로 두 개의 단방향 연결에 의해 구현됩니다.
class A {
B b;
}
class B {
A a;
}
A a = new A();
B b = new B();
a.b = b;
b.a = a;
반면에 테이블 구조에서는 양방향 연결이 외래 키에 의해 즉시 구현됩니다.
SELECT *
FROM MEMBER M
JOIN TEAM T ON M.TEAM_ID = T.TEAM_ID
SELCT *
FROM TEAM T
JOIN MEMBER M ON T.TEAM_ID = M.TEAM_ID
외래 키 관리
- 테이블에서
멤버의 TEAM_ID(FK)만 변경하면 됩니다. - 개체에서
A멤버가 A팀에 소속되어 있다면 A멤버를 B팀으로 변경하기 위해서는 오브젝트 내에서 팀 리스트 박스나 멤버 A의 팀 필드를 변경해야 합니다.
이러한 차이점이 존재합니다.
DB 관점에서 외래 키가 존재하는 페이지는 반드시 Many이고, 이를 참조하는 페이지는 One일 가능성이 높습니다. 따라서 외래 키를 관리하는 소유자는 Many(위 예에서는 Member)인 것이 좋습니다.

- ‘김영한의 강의’의 양방향 매핑 규칙
- 개체의 두 관계 중 하나를 연결 소유자로 지정
- 지도의 소유자는 외래 키의 위치를 기반으로 합니다. -> 많이 가진 자가 주인이다.
- 협회의 오너만이 외래키 관리(등록 및 수정)
- 비소유자 읽기 및 보기
- 소유자는 mappedBy 속성 X를 사용합니다.
- 소유자가 아닌 경우 mappedBy 속성으로 소유자를 지정합니다.
나쁜 습관
Team team = new Team();
team.setName("Alpha");
em.persist(team);
Member member = new Member();
member.setName("John");
// 역방향(주인이 아닌 방향)만 연관관계 설정
team.getMembers().add(member);
em.persist(member);
위 코드에 대한 DB 상황
- 멤버 테이블
| 회원 번호 | 별명 | TEAM_ID |
| 하나 | 남자 |
- 팀 테이블
| TEAM_ID | 성 |
| 하나 | 알파 |
위 샘플 코드를 실행하면 실제 DB의 멤버 테이블에 TEAM_ID(FK)가 null로 입력된다.
멤버가 팀을 지정하지 않았기 때문이다.
추천
Team team = new Team();
team.setName("Alpha");
em.persist(team);
Member member = new Member();
member.setName("John");
member.setTeam(team); // Member-Team간의 연관관계를 맺도록 하는 코드 부분
em.persist(member);
team.getMembers().add(member); // 연관관계 매핑에 대해 명시적으로 표현하기 위함
// in Team 클래스
// 메소드에서 양방향 지정하는 것이 좋다.
public void addMember(Member member) {
member.setTeam(this);
this.members.add(member);
}
Java(JPA) 레벨에서는 단방향 매핑이더라도 DB에서 FK를 통해 연관이 이미 매핑되어 있고 관계가 설정되어 있습니다.
양방향 매핑과의 차이점은 Java 수준에서 그래프 검색 기능을 사용하여 반대 방향(Team->Member)으로 검색할 수도 있다는 것입니다.
따라서 반대 방향의 검색이 필요할 때 양방향 매핑을 적용할 수 있으며 테이블에는 영향을 미치지 않습니다.
가능하면 양방향 매핑을 피하십시오.
- 양방향 매핑으로 인해 순환이 발생할 수 있습니다.
- 양방향으로 설정하면 엔티티 간의 관계가 복잡해질 수 있습니다.
아 나 이해 했어
직접 코드를 작성하고 DB를 보면서 막연한 상황을 좀 더 명확하게 이해할 수 있었다.
영한선생이 늘 말하던 100문장은 역시나 날아갔다.
Spring Deep Dive 연구를 진행하면서 재구성하여 직접 샘플 코드를 생성하였습니다.
꽤 오랜 시간이 걸렸지만 아직 “my”로 완전히 모양을 잡을 수 있는 시간이 있었습니다.
다음으로 연구에서 제기된 질문에 대한 분석에 대한 기사를 작성하겠습니다.