1. 연관관계가 필요한 이유
1.1. 객체지향 설계의 목표
객체지향의 선구자 조영호는 그의 저서 '객체지향의 사실과 오해'에서 이렇게 말했다:
"객체지향 설계의 목표는 자율적인 객체들의 협력 공동체를 만드는 것이다."
이 말은 객체들이 서로 협력하면서 문제를 해결할 수 있어야 함을 의미한다. 그러나 테이블 중심의 설계에서는 이 협력 관계를 구현하기 어렵다.
1.2. 예제 시나리오
간단한 비즈니스 도메인을 생각해보자:
- 회원과 팀이 존재
- 회원은 하나의 팀에만 소속 가능
- 회원과 팀은 다대일(N:1) 관계
1.3. 객체를 테이블에 맞추어 모델링 (문제점)
테이블 구조에 맞춘 객체 설계 (연관관계가 없는 객체)

@Entity
public class Member {
@Id
@GeneratedValue
private Long id;
@Column(name = "USERNAME")
private String name;
@Column(name = "TEAM_ID") // 외래키를 직접 관리
private Long teamId; // 객체지향적이지 않음!
}
@Entity
public class Team {
@Id
@GeneratedValue
private Long id;
private String name;
}
이 방식의 문제점
// 팀 저장
Team team = new Team();
team.setName("TeamA");
em.persist(team);
// 회원 저장
Member member = new Member();
member.setName("member1");
member.setTeamId(team.getId()); // 식별자를 직접 설정
em.persist(member);
// 조회 시 문제 발생
Member findMember = em.find(Member.class, member.getId());
// 연관된 팀을 조회하려면 직접 쿼리 실행 필요
Team findTeam = em.find(Team.class, findMember.getTeamId());
1.4. 근본적인 차이
테이블과 객체는 연관관계를 표현하는 방식이 근본적으로 다르다:
| 구분 | 테이블 | 객체 |
| 연관관계 표현 | 외래키와 조인 | 참조(객체 참조) |
| 방향성 | 항상 양방향 | 기본적으로 단방향 |
| 관계 찾기 | 외래키로 조인 | 참조로 탐색 |
이 차이를 이해하지 못하면 객체지향의 장점을 살리지 못하는 설계가 된다.
2. 단방향 연관관계
2.1. 객체지향 모델링 (객체 연관관계 사용)
객체지향적인 설계에서는 외래키 대신 객체 참조를 사용한다.

@Entity
public class Member {
@Id
@GeneratedValue
private Long id;
@Column(name = "USERNAME")
private String name;
private int age;
// 객체 참조 사용
@ManyToOne
@JoinColumn(name = "TEAM_ID") // TEAM_ID 컬럼과 매핑
private Team team; // Team 객체 참조
}
@Entity
public class Team {
@Id
@GeneratedValue
private Long id;
private String name;
}
2.2. 연관관계 매핑 애노테이션
@ManyToOne
- 다대일(N:1) 관계를 표현
- 가장 자주 사용하는 연관관계
- 연관관계의 주인(Owner)이 됨
@JoinColumn
- 외래키를 매핑할 때 사용
- name 속성: 매핑할 외래키 컬럼명 지정
- 생략 가능: 생략시 필드명 + "_" + 참조테이블의 기본키 컬럼명
2.3. 연관관계 사용 예시

저장
// 팀 저장
Team team = new Team();
team.setName("TeamA");
em.persist(team);
// 회원 저장 (객체 참조 사용)
Member member = new Member();
member.setName("member1");
member.setTeam(team); // 객체 참조 설정
em.persist(member);
조회 (객체 그래프 탐색)
// 회원 조회
Member findMember = em.find(Member.class, member.getId());
// 연관된 팀 조회 (객체 그래프 탐색)
Team findTeam = findMember.getTeam(); // 참조를 통한 자연스러운 접근
System.out.println("팀 이름: " + findTeam.getName());
수정
// 새로운 팀 생성
Team teamB = new Team();
teamB.setName("TeamB");
em.persist(teamB);
// 회원의 팀 변경
member.setTeam(teamB); // 참조 변경으로 간단한 수정
2.4. 단방향 연관관계의 장점
- 자연스러운 객체 사용: 객체 그래프 탐색 가능
- 간결한 코드: 참조로 연관관계 설정 및 조회
- 객체지향적 설계: 테이블 구조에 종속되지 않음
- 유연성: 필요시 양방향으로 확장 가능
3. 양방향 연관관계와 연관관계의 주인

3.1. 양방향 매핑의 필요성
단방향 연관관계만으로도 충분하지만, 경우에 따라 반대 방향으로도 탐색이 필요한 경우가 있다.
단방향 매핑 유지 (Member 엔티티)
@Entity
public class Member {
@Id
@GeneratedValue
private Long id;
@Column(name = "USERNAME")
private String name;
@ManyToOne
@JoinColumn(name = "TEAM_ID")
private Team team; // Team으로의 참조
}
양방향으로 확장 (Team 엔티티 수정)
@Entity
public class Team {
@Id
@GeneratedValue
private Long id;
private String name;
// 컬렉션 추가
@OneToMany(mappedBy = "team") // Member 엔티티의 team 필드에 매핑됨
private List<Member> members = new ArrayList<>();
}
3.2. 양방향 연관관계 사용
// Team에서 Member 목록 조회
Team findTeam = em.find(Team.class, team.getId());
List<Member> teamMembers = findTeam.getMembers(); // 역방향 조회
for (Member member : teamMembers) {
System.out.println("회원 이름: " + member.getName());
}
3.3. 연관관계의 주인(Owner) 개념
3.3.1. 객체와 테이블의 차이
객체의 양방향 관계
class Member {
Team team; // Member -> Team 참조 (단방향)
}
class Team {
List<Member> members; // Team -> Member 참조 (단방향)
}
// 사실은 서로 다른 단방향 관계 2개
테이블의 양방향 관계
-- MEMBER 테이블
CREATE TABLE MEMBER (
ID BIGINT PRIMARY KEY,
USERNAME VARCHAR(255),
TEAM_ID BIGINT, -- 하나의 외래키로 양방향 관계 표현
FOREIGN KEY (TEAM_ID) REFERENCES TEAM(ID)
);
테이블은 TEAM_ID 하나의 외래키로 양쪽 테이블과의 관계를 모두 표현한다.
3.3.2. 누가 외래키를 관리할 것인가?
양방향 매핑에서 가장 중요한 질문: "두 객체 참조 중 어느 쪽이 외래키를 관리할 것인가?"
양방향 매핑 규칙
- 객체의 두 관계 중 하나를 연관관계의 주인으로 지정
- 주인만이 외래키를 관리(등록, 수정, 삭제)
- 주인이 아닌 쪽은 읽기만 가능
- 주인은 mappedBy 속성 사용하지 않음
- 주인이 아니면 mappedBy 속성으로 주인을 지정
3.3.3. 연관관계 주인 결정 기준
외래키가 있는 곳을 주인으로 정하라

- 다대일/일대다 관계에서 "다"쪽이 외래키를 가짐
- 따라서 "다"쪽이 연관관계의 주인이 됨
- 예: Member(N) : Team(1) → Member가 주인
@Entity
public class Member {
@ManyToOne
@JoinColumn(name = "TEAM_ID") // 주인: 외래키를 관리
private Team team;
}
@Entity
public class Team {
@OneToMany(mappedBy = "team") // 주인 아님: mappedBy 사용
private List<Member> members;
}
3.4. 흔히 하는 실수와 해결책
3.4.1. 주인에 값을 입력하지 않는 실수
잘못된 예시
Team team = new Team();
team.setName("TeamA");
em.persist(team);
Member member = new Member();
member.setName("member1");
// 주인이 아닌 쪽에만 값 설정
team.getMembers().add(member); // 무시됨!
em.persist(member);
// 결과: MEMBER 테이블의 TEAM_ID는 null

올바른 예시
Team team = new Team();
team.setName("TeamA");
em.persist(team);
Member member = new Member();
member.setName("member1");
// 주인에 값 설정 (필수)
member.setTeam(team); // 외래키 설정
// 객체의 일관성을 위해 양쪽 모두 설정 (권장)
team.getMembers().add(member);
em.persist(member);
// 결과: MEMBER 테이블의 TEAM_ID가 정상적으로 설정됨

3.4.2. 연관관계 편의 메서드
양쪽 모두에 값을 설정하는 것을 잊지 않도록 편의 메서드를 제공하는 것이 좋다.
Member 엔티티에 편의 메서드 추가
@Entity
public class Member {
@ManyToOne
@JoinColumn(name = "TEAM_ID")
private Team team;
// 연관관계 설정 편의 메서드
public void setTeam(Team team) {
// 기존 관계 제거
if (this.team != null) {
this.team.getMembers().remove(this);
}
// 새로운 관계 설정
this.team = team;
if (team != null) {
team.getMembers().add(this);
}
}
}
사용 예시
Team team = new Team();
team.setName("TeamA");
em.persist(team);
Member member = new Member();
member.setName("member1");
// 한 번의 호출로 양쪽 관계 모두 설정
member.setTeam(team); // 편의 메서드 사용
em.persist(member);
3.4.3. 무한 루프 주의
양방향 연관관계에서 toString(), lombok, JSON 생성 라이브러리를 사용할 때 무한 루프가 발생할 수 있다.
문제 예시
@Entity
public class Member {
// ...
public String toString() {
return "Member{id=" + id + ", name=" + name + ", team=" + team + "}";
}
}
@Entity
public class Team {
// ...
public String toString() {
return "Team{id=" + id + ", name=" + name + ", members=" + members + "}";
}
}
// 무한 루프 발생!
// Member.toString() -> Team.toString() -> Member.toString() -> ...
해결 방법
1. toString()에서 연관 객체 제외
// 양방향 참조가 있는 필드는 toString()에서 제외
public String toString() {
return "Member{id=" + id + ", name=" + name + "}";
}
2. JSON 직렬화 설정
// Jackson 사용 시
public class Member {
@JsonIgnore // JSON 직렬화 시 제외
@ManyToOne
private Team team;
}
3.5. 양방향 매핑 정리
- 단방향 매핑으로 시작하라
- 단방향으로 먼저 매핑을 완성
- 양방향은 필요할 때 추가 (테이블 구조 변경 없음)
- 양방향 매핑의 장점
- 반대 방향으로 객체 그래프 탐색 가능
- JPQL에서 역방향 쿼리 작성 편리
- 연관관계 주인 선택 기준
- 비즈니스 로직 기준이 아닌 외래키 위치 기준
- 다대일 관계에서 "다"쪽이 주인
- 객체의 일관성 유지
- 편의 메서드를 활용하여 양쪽 관계 일관성 유지
- 무한 루프에 주의
4. 실전 예제: 연관관계 매핑 시작
4.1. 테이블 구조
기존 테이블 구조는 변경 없이, 객체 구조만 참조를 사용하도록 변경한다.
테이블 DDL (변경 없음)
CREATE TABLE MEMBER (
ID BIGINT PRIMARY KEY,
USERNAME VARCHAR(255),
TEAM_ID BIGINT,
FOREIGN KEY (TEAM_ID) REFERENCES TEAM(ID)
);
CREATE TABLE TEAM (
ID BIGINT PRIMARY KEY,
NAME VARCHAR(255)
);
4.2. 객체 구조 개선
개선 전 (테이블 중심)
@Entity
public class Member {
private Long teamId; // 외래키 직접 관리
}
@Entity
public class Team {
// Member에 대한 참조 없음
}
개선 후 (객체지향)
@Entity
public class Member {
@ManyToOne
@JoinColumn(name = "TEAM_ID")
private Team team; // 객체 참조
// 편의 메서드
public void changeTeam(Team team) {
if (this.team != null) {
this.team.getMembers().remove(this);
}
this.team = team;
if (team != null) {
team.getMembers().add(this);
}
}
}
@Entity
public class Team {
@OneToMany(mappedBy = "team")
private List<Member> members = new ArrayList<>();
// 비즈니스 메서드
public void addMember(Member member) {
members.add(member);
member.setTeam(this);
}
}
4.3. 실제 사용 예시
@Service
@Transactional
public class TeamService {
@PersistenceContext
private EntityManager em;
public void createTeamWithMembers() {
// 팀 생성
Team team = new Team();
team.setName("개발팀");
em.persist(team);
// 회원들 생성
Member member1 = new Member("김개발");
Member member2 = new Member("이디자인");
// 팀에 회원 추가
team.addMember(member1);
team.addMember(member2);
em.persist(member1);
em.persist(member2);
}
public List<Member> getTeamMembers(Long teamId) {
Team team = em.find(Team.class, teamId);
return team.getMembers(); // 객체 그래프 탐색
}
}
5. 핵심 정리
5.1. 연관관계 매핑의 핵심 개념
- 방향(Direction)
- 단방향: 한 쪽만 참조 (Member → Team)
- 양방향: 양쪽 모두 참조 (Member ↔ Team)
- 다중성(Multiplicity)
- 다대일(@ManyToOne): N:1 관계
- 일대다(@OneToMany): 1:N 관계
- 일대일(@OneToOne): 1:1 관계
- 다대다(@ManyToMany): N:M 관계 (실무에서 권장하지 않음)
- 연관관계의 주인(Owner)
- 외래키를 관리하는 쪽
- @ManyToOne이 항상 주인
- 주인은 mappedBy 속성 사용하지 않음
5.2. 실무 적용 가이드라인
- 설계 원칙
- 단방향 매핑으로 시작
- 필요할 때만 양방향 추가
- 연관관계 주인은 외래키 위치 기준으로 결정
- 코딩 규칙
- 편의 메서드 구현으로 객체 일관성 유지
- toString(), JSON 직렬화 시 무한 루프 주의
- 양방향 관계 설정 시 양쪽 모두 설정
- 성능 고려사항
- 지연 로딩(LAZY)을 기본으로 사용
- 필요시만 즉시 로딩(EAGER) 사용
- N+1 문제에 주의
객체지향 설계의 진정한 가치는 객체들이 서로 협력하여 복잡한 문제를 해결할 수 있다는 점에 있다. JPA의 연관관계 매핑은 이 협력 관계를 데이터베이스에 저장하면서도 객체지향의 장점을 살릴 수 있게 해준다. 처음에는 개념이 어려울 수 있지만, 실제 프로젝트에 적용해보면 그 강력함을 체감할 수 있을 것이다.
'Spring > JPA' 카테고리의 다른 글
| [Basic-6] 고급 관계 매핑 (0) | 2026.01.06 |
|---|---|
| [Basic-5] 연관관계 매핑 심화 (0) | 2026.01.06 |
| [Basic-3] 엔터티 매핑 (Entity Mapping) (0) | 2026.01.06 |
| [Basic-2] 영속성 컨텍스트 (Persistence Context) (0) | 2026.01.06 |
| [Basic-1] JPA 시작 (0) | 2026.01.05 |
