[JPA] Chapter 5. 연관관계 매핑 기초 1 - 단방향 연관관계

들어가기 앞서

이 글은 김영한 님의 저서 「자바 ORM 표준 JPA 프로그래밍」을 학습한 내용을 정리한 글입니다. 모든 출처는 해당 저서에 있습니다.


  • 엔티티는 대부분 다른 엔티티와 연관관계가 존재함
  • 객체의 참조테이블의 외래 키매핑하는 것이 핵심

🔎 핵심 키워드

  • 방향(Direction) : 엔티티의 관계가 어느쪽을 참조하고 있는가
    • 종류
      • 단방향 : 엔티티의 관계가 한 쪽만 참조하는 것
        ex) 회원 → 팀, 팀 → 회원
      • 양방향 : 엔티티의 관계가 양쪽 모두 참조하는 것
        ex) 회원 ↔ 팀
    • 객체 관계에만 존재
    • 테이블 관계는 항상 양방향
  • 다중성(Multiplicity) : 연관 관계가 있는 여러 개의 클래스가 있을 때 실제로 연관을 가지는 객체의 수
    • 다대일(N:1) ex) 회원(N) : 팀(1)
    • 일대다(1:N) ex) 팀(1) : 회원(N)
    • 일대일(1:1)
    • 다대다(N:N)
  • 연관관계의 주인 : 객체가 양방향 연관관계일 때 테이블의 외래키를 관리하는 객체


5.1 단방향 연관관계

  • 회원(Member)과 팀(Team)을 통한 관계 이해

    1. 회원은 하나의 팀에만 소속될 수 있다.
    2. 회원과 팀은 다대일 관계이다.
    • 객체 연관관계

      • 회원 객체는 Member.team 필드(멤버변수)로 팀 객체와 연관관계를 맺음
      • 회원 객체와 팀 객체는 단방향 관계
        → 회원은 Member.team 필드를 통해 팀을 알 수 있지만, 반대로 팀은 회원을 알 수 없음
        ex) memberteam : member.getTeam()
        ex) teammember : x
    • 테이블 연관관계

      • 회원 테이블은 TEAM_ID 외래 키로 팀 테이블과 연관관계를 맺음
      • 회원 테이블과 팀 테이블은 양방향 관계
        → 회원 테이블의 TEAM_ID 외래키를 통해 회원과 팀, 팀과 회원 조인 가능
        • 회원(MEMBER)과 팀(TEAM) 조인
        • 팀(TEAM)과 회원(MEMBER) 조인
    • 객체 연관관계 vs 테이블 연관관계

      • 객체는 참조(주소)로, 테이블은 외래 키로 연관관계를 맺는다.
      • 객체의 연관관계는 단방향(A → B (a.b)), 테이블의 연관관계는 양방향(A JOIN B, B JOIN A)이다.
      • 객체를 양방향으로 참조하려면 단방향 연관관계를 2개 만들어야 한다.
        • A → B (a.b)
        • B → A (b.a)

5.1.1 순수한 객체 연관관계

✅ 클래스 관계

public class Member {

    private String id;
    private String username;
    
    private Team team;  //팀의 참조를 보관
    
    public void setTeam(Team team) {
        this.team = team;
    }
    
    //Getter, Setter ...
    
}

public class Team {

    private String id;
    private String name;
    
    //Getter, Setter ...
    
}

✅ 인스턴스 관계

public static void main(String[] args) {

    //생성자(id, 이름)
    Member member1 = new Member("member1", "회원1");
    Member member2 = new Member("member2", "회원2");
    Team team1 = new Team("team1", "팀1");
    
    //member1과 member2를 team1에 소속
    member1.setTeam(team1);
    member2.setTeam(team1);
    
    //객체 그래프 탐색을 통한 연관관계 탐색
    Team findTeam = member1.getTeam();
    
}

5.1.2 테이블 연관관계

💻 테이블 DDL

CREATE TABLE MEMBER {
    MEMBER_ID VARCHAR(255) NOT NULL,
    TEAM_ID VARCHAR(255),
    USERNAME VARCHAR(255),
    PRIMARY KEY (MEMBER_ID)
}

CREATE TABLE TEAM {
    TEAM_ID VARCHAR(255) NOT NULL,
    NAME VARCHAR(255),
    PRIMARY KEY (TEAM_ID)
}

//회원 테이블의 TEAM_ID에 외래 키 제약조건 설정
ALTER TABLE MEMBER ADD CONSTRAINT FK_MEMBER_TEAM
    FOREIGN KEY (TEAM_ID)
    REFERENCES TEAM

💻 회원1과 회원2을 팀1에 소속

INSERT INTO TEAM(TEAM_ID, NAME) VALUES('team1', '팀1');
INSERT INTO MEMBER(MEMBER_ID, TEAM_ID, USERNAME)
VALUES('member1', 'team1', '회원1');
INSERT INTO MEMBER(MEMBER_ID, TEAM_ID, USERNAME)
VALUES('member2', 'team1', '회원2');

💻 회원1이 소속된 팀 조회

//조인을 이용한 연관관계 탐색
SELECT T.* FROM MEMBER M JOIN TEAM T ON M.TEAM_ID = T.TEAM_ID
WHERE M.MEMBER_ID = 'member1'

5.1.3 객체 관계 매핑


✅ 연관관계 매핑 코드 분석

💻 매핑한 회원 엔티티

@Entity
public class Member {

    @Id
    @Column(name = "MEMBER_ID")
    prviate String id;
    
    private String username;
    
    //연관관계 매핑
    @ManyToOne
    @JoinColumn(name="TEAM_ID")
    private Team team;
    
    //연관관계 설정
    public void setTeam(Team team) {
        this.team = team;
    }
    
    //Getter, Setter ...
}
  • @ManyToOne
    • 다대일(N:1) 관계를 나타내는 매핑 정보
    • 연관관계 매핑 시 사용 필수
    • 속성
      속성설명기본값
      optionalfalse로 설정 시 연관된 엔티티가 항상 존재해야 함true
      fetch글로벌 패치 전략 설정FetchType.EAGER
      cascade속성 전이 기능 사용
      targetEntity◾ 연관된 엔티티의 타입 정보 설정
      ◾ 거의 사용하지 않음
      ◾ 컬렉션 사용해도 제네릭으로 타입 정보 알 수 있음
         ex) @OneToMany
              private List<Member> members;

  • @JoinColumn
    • 외래 키 매핑 시 사용
    • 생략 가능
    • 속성
      속성설명기본값
      name매핑할 외래 키 이름필드명 + _ + 참조하는 테이블의
      기본 키 컬럼명
      ex) team_TEAM_ID
      referencedColumnName외래 키가 참조하는
      대상 테이블의 컬럼명
      참조하는 테이블의 기본 키 컬럼명
      foreignKey
      (DDL)
      ◾ 외래 키 제약조건 직접 지정
      ◾ 테이블 생성 시에만 사용
      unique,
      nullable,
      insertable,
      columnDefinition,
      table
      @Column의 속성과 같음

💻 매핑한 팀 엔티티

@Entity
public class Team {

    @Id
    @Column(name = "TEAM_ID")
    private String id;
    
    private String name;
    
    //Getter, Setter ...
}


5.2 연관관계 사용

연관관계를 등록, 수정, 조회, 삭제하는 예제를 통해 연관관계 사용 방식에 대해 알아보자.

5.2.1 저장

💡 JPA에서 엔티티를 저장할 때 연관된 모든 엔티티는 영속 상태여야 한다.

💻 회원과 팀을 저장하는 코드

public void testSave() {

    //팀1 저장
    Team team1 = new Team("team1", "팀1");
    em.persist(team1);
    
    //회원1 저장
    Member member1 = new Member("member1", "회원1");
    member1.setTeam(team1);  //연관관계 설정 member1 → team1
    em.persist(member1);
    
    //회원2 저장
    Member member2 = new Member("member2", "회원2");
    member2.setTeam(team1);  //연관관계 설정 member2 → team1
    em.persist(member2);
}

✅ 분석

JPA는 참조한 팀의 식별자(Team.id)를 외래 키로 사용하여 적절한 등록 쿼리를 생성한다.

INSERT INTO TEAM (TEAM_ID, NAME) VALUES ('team1', '팀1')
INSERT INTO MEMBER (MEMBER_ID, NAME, TEAM_ID) VALUES ('member1', '회원1', 'team1')
INSERT INTO MEMBER (MEMBER_ID, NAME, TEAM_ID) VALUES ('member2', '회원2', 'team1')

다음과 같은 SQL문을 입력하여 실행하면 데이터가 잘 입력되었는지 확인할 수 있다.
SELECT M.MEMBER_ID, M.NAME, M.TEAM_ID, T.NAME AS TEAM_NAME
FROM MEMBER M JOIN TEAM T ON M.TEAM_ID = T.TEAM_ID

💻 결과

MEMBER_IDNAMETEAM_IDTEAM_NAME
member1회원1team1팀1
member2회원2team1팀1

5.2.2 조회

✅ 객체 그래프 탐색

  • 객체를 통해 연관된 엔티티 조회
Member member = em.find(Member.class, "member1");
Team team = member.getTeam();  //객체 그래프 탐색
System.out.println("팀 이름 = " + team.getName();

//출력 결과 : 팀 이름 = 팀1

✅ 객체지향 쿼리(JPQL) 사용

  • 예를 들어 팀1에 소속된 회원만 조회하려면 회원과 연관된 팀 엔티티를 검색 조건으로 사용해야 함
  • SQL은 연관된 테이블을 조인하여 검색조건을 사용하며, JPQL도 조인을 지원함

💻 JPQL 조인 검색

private static void queryLogicJoin(EntityManager em) {

    //회원과 팀 간 관계가 존재하는 필드(m.team)를 통해 Member와 Team 조인
    String jpql = "select m from Member m join m.team t where " +
            "t.name=:teamName";  //조인한 t.name을 검색조건으로 사용
            			 //파라미터 바인딩
    
    List<Member> resultList = em.createQuery(jpql, Member.class)
            .setParameter("teamName", "팀1")  //팀1에 속한 회원만 검색
            .getResultList();
            
    for (Member member : resultList) {
        System.out.println("[query] member.username=" +
                member.getUsername());
    }
    
}

//결과 : [query] member.username=회원1
//결과 : [query] member.username=회원2

💻 실행 SQL

SELECT M.* FROM MEMBER MEMBER
INNER JOIN
    TEAM TEAM ON MEMBER.TEAM_ID = TEAM1_.ID
WHERE
    TEAM1_.NAME = '팀1'

5.2.3 수정

참조하는 대상만 변경하면 나머지는 JPA가 자동으로 처리한다.

💻 연관관계 수정

private static void updateRelation(EntityManger em) {

    //새로운 팀2
    Team team2 = new Team("team2", "팀2");
    em.persist(team2);
    
    //회원1에 새로운 팀2 설정
    Member member = em.find(Member.class, "member1");
    member.setTeam(team2);
    
}

💻 실행 SQL

UPDATE MEMBER
SET
    TEAM_ID='team2', ...
WHERE
    ID='member1'

5.2.4 연관관계 제거

💻 연관관계 삭제

private static void deleteRelation(EntityManager em) {

    Member member1 = em.find(Member.class, "member1");
    member1.setTeam(null);  //연관관계 제거
    
}

💻 실행 SQL

UPDATE MEMBER
SET
    TEAM_ID=null, ...
WHERE
    ID='member1'

5.2.5 연관된 엔티티 삭제

  • 기존에 존재하는 연관관계를 먼저 제거 후 삭제
    why? 외래 키 제약조건으로 인해 데이터베이스에서 오류 발생
member1.setTeam(null);  //회원1 연관관계 제거
member2.setTeam(null);  //회원2 연관관계 제거
em.remove(team);	//팀 삭제



📖 참고

좋은 웹페이지 즐겨찾기