[Basic-4] 연관관계 매핑 기초

2026. 1. 6. 11:29·Spring/JPA

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. 단방향 연관관계의 장점

  1. 자연스러운 객체 사용: 객체 그래프 탐색 가능
  2. 간결한 코드: 참조로 연관관계 설정 및 조회
  3. 객체지향적 설계: 테이블 구조에 종속되지 않음
  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. 누가 외래키를 관리할 것인가?

양방향 매핑에서 가장 중요한 질문: "두 객체 참조 중 어느 쪽이 외래키를 관리할 것인가?"

양방향 매핑 규칙

  1. 객체의 두 관계 중 하나를 연관관계의 주인으로 지정
  2. 주인만이 외래키를 관리(등록, 수정, 삭제)
  3. 주인이 아닌 쪽은 읽기만 가능
  4. 주인은 mappedBy 속성 사용하지 않음
  5. 주인이 아니면 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. 양방향 매핑 정리

  1. 단방향 매핑으로 시작하라
    • 단방향으로 먼저 매핑을 완성
    • 양방향은 필요할 때 추가 (테이블 구조 변경 없음)
  2. 양방향 매핑의 장점
    • 반대 방향으로 객체 그래프 탐색 가능
    • JPQL에서 역방향 쿼리 작성 편리
  3. 연관관계 주인 선택 기준
    • 비즈니스 로직 기준이 아닌 외래키 위치 기준
    • 다대일 관계에서 "다"쪽이 주인
  4. 객체의 일관성 유지
    • 편의 메서드를 활용하여 양쪽 관계 일관성 유지
    • 무한 루프에 주의

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. 연관관계 매핑의 핵심 개념

  1. 방향(Direction)
    • 단방향: 한 쪽만 참조 (Member → Team)
    • 양방향: 양쪽 모두 참조 (Member ↔ Team)
  2. 다중성(Multiplicity)
    • 다대일(@ManyToOne): N:1 관계
    • 일대다(@OneToMany): 1:N 관계
    • 일대일(@OneToOne): 1:1 관계
    • 다대다(@ManyToMany): N:M 관계 (실무에서 권장하지 않음)
  3. 연관관계의 주인(Owner)
    • 외래키를 관리하는 쪽
    • @ManyToOne이 항상 주인
    • 주인은 mappedBy 속성 사용하지 않음

5.2. 실무 적용 가이드라인

  1. 설계 원칙
    • 단방향 매핑으로 시작
    • 필요할 때만 양방향 추가
    • 연관관계 주인은 외래키 위치 기준으로 결정
  2. 코딩 규칙
    • 편의 메서드 구현으로 객체 일관성 유지
    • toString(), JSON 직렬화 시 무한 루프 주의
    • 양방향 관계 설정 시 양쪽 모두 설정
  3. 성능 고려사항
    • 지연 로딩(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
'Spring/JPA' 카테고리의 다른 글
  • [Basic-6] 고급 관계 매핑
  • [Basic-5] 연관관계 매핑 심화
  • [Basic-3] 엔터티 매핑 (Entity Mapping)
  • [Basic-2] 영속성 컨텍스트 (Persistence Context)
h6bro
h6bro
백엔드 개발자의 기술 블로그
  • h6bro
    Jun's Tech Blog
    h6bro
  • 전체
    오늘
    어제
    • 분류 전체보기 (250) N
      • Java (18)
        • Core (9)
        • Design Pattern (9)
      • Spring (80)
        • Core (24)
        • MVC (6)
        • DB (10)
        • JPA (26)
        • Monitoring (3)
        • Security (11)
        • WebSocket (0)
      • Database (33)
        • Redis (15)
        • MySQL (18)
      • MSA (25) N
        • MSA 기본 (11)
        • MSA 아키텍처 (14) N
      • Kafka (30) N
        • Core (18) N
        • Connect (12)
      • ElasticSearch (11)
        • Search (11)
        • Logging (0)
      • Test (4)
        • k6 (4)
      • Docker (9)
      • CI&CD (10)
        • GitHub Actions (6)
        • ArgoCD (4)
      • Kubernetes (18)
        • Core (12)
        • Ops (6)
      • Cloud Engineering (4)
        • AWS Infrastructure (3)
        • AWS EKS (1)
        • Terraform (0)
      • Project (8)
        • LinkFolio (1)
        • Secondhand Market (7)
  • 블로그 메뉴

    • 홈
    • 태그
    • 방명록
  • 링크

  • 공지사항

    • Cloud Engineering 포스팅 정리
  • 인기 글

  • 태그

    ㅈ
  • 최근 댓글

  • 최근 글

  • hELLO· Designed By정상우.v4.10.5
h6bro
[Basic-4] 연관관계 매핑 기초
상단으로

티스토리툴바