[Spring] Association Mapping

배창민·2025년 10월 23일
post-thumbnail

JPA Association Mapping


프로젝트 기본 설정

build.gradle

dependencies {
    implementation 'org.springframework.boot:spring-boot-starter-data-jpa'
    runtimeOnly 'com.mysql:mysql-connector-j'
    // 또는 MariaDB 사용 시
    // runtimeOnly 'org.mariadb.jdbc:mariadb-java-client'
}

application.yml

spring:
  datasource:
    driver-class-name: org.mariadb.jdbc.Driver
    url: jdbc:mariadb://localhost:3306/menudb
    username: swcamp
    password: swcamp
  jpa:
    show-sql: true
    hibernate:
      ddl-auto: none
      naming:
        physical-strategy: org.hibernate.boot.model.naming.CamelCaseToUnderscoresNamingStrategy
    properties:
      hibernate:
        format_sql: true
  • CamelCaseToUnderscoresNamingStrategy: myFieldName → my_field_name 으로 물리 이름 변환
  • ddl-auto: 운영 DB에는 none 권장(마이그레이션 도구 사용)

주의: 의존성의 DB 드라이버와 driver-class-name은 일치시켜야 한다. (MySQL 커넥터 사용 시 com.mysql.cj.jdbc.Driver)


연관관계 분류

  • 다중성: N:1(ManyToOne), 1:N(OneToMany), 1:1, N:N
  • 방향성: 단방향 / 양방향
    (객체 세계에서 양방향은 단방향 2개를 구성 + 주인(owner) 개념 필요)

ManyToOne (N:1)

하나의 Category에 여러 Menu가 속하는 관계. 외래키는 Menu(다 쪽)가 가진다.

엔티티

@Entity(name = "Section01Category")
@Table(name = "tbl_category")
public class Category {
    @Id private int categoryCode;
    private String categoryName;
    private Integer refCategoryCode;
    protected Category() {}
}

@Entity(name = "menu_and_category")
@Table(name = "tbl_menu")
public class Menu {
    @Id private int menuCode;
    private String menuName;
    private int menuPrice;

    @ManyToOne(cascade = CascadeType.PERSIST) // Menu 저장 시 Category도 함께 저장
    @JoinColumn(name = "categoryCode")       // FK 컬럼명
    private Category category;

    private String orderableStatus;
    protected Menu() {}
}

@JoinColumn / @ManyToOne 주요 속성 요약

어노테이션핵심 속성요약
@JoinColumnname, referencedColumnName, nullable, unique, insertable, updatable, columnDefinition, foreignKeyFK 컬럼 제어
@ManyToOnecascade, fetch, optional전이/로딩/필수 여부

JPQL 조회 예

String jpql =
  "SELECT c.categoryName " +
  "FROM menu_and_category m JOIN m.category c " +
  "WHERE m.menuCode = :menuCode";
  • FROM에는 테이블명이 아닌 엔티티명을 사용

저장 흐름(cascade=PERSIST)

Menu.persist() → FK가 가리키는 Category도 함께 persist → tbl_category insert → tbl_menu insert


OneToMany (1:N, 단방향)

Category → Menu 리스트를 직접 참조. 단방향 1:N은 조인 컬럼을 부모 쪽에서 관리해야 하므로 insert 이후에 update가 한 번 더 발생할 수 있다.

엔티티

@Entity(name = "Section02Menu")
@Table(name = "tbl_menu")
public class Menu {
    @Id private int menuCode;
    private String menuName;
    private int menuPrice;
    private int categoryCode;     // FK 컬럼
    private String orderableStatus;
    protected Menu() {}
}

@Entity(name = "category_and_menu")
@Table(name = "tbl_category")
public class Category {
    @Id private int categoryCode;
    private String categoryName;
    private Integer refCategoryCode;

    @JoinColumn(name = "categoryCode")        // 자식 테이블 FK
    @OneToMany(cascade = CascadeType.PERSIST) // 전이 저장
    private List<Menu> menuList;

    protected Category() {}
}

저장 흐름

  1. tbl_category insert
  2. tbl_menu insert
  3. FK 동기화를 위한 tbl_menu update(케이스에 따라 발생)

실전에서는 양방향(N:1 주인, 1:N mappedBy) 또는 다대일 단방향을 더 권장


양방향 매핑 (Bi-direction)

외래키를 가진 Menu(@ManyToOne) 가 연관관계의 주인.
반대편 CategorymappedBy = "category" 로 읽기 전용 뷰.

엔티티

@Entity(name="bidirection_menu")
@Table(name = "tbl_menu")
public class Menu {
    @Id private int menuCode;
    private String menuName;
    private int menuPrice;

    @JoinColumn(name = "categoryCode")
    @ManyToOne
    private Category category;

    private String orderableStatus;
    protected Menu() {}
}

@Entity(name = "bidirection_category")
@Table(name = "tbl_category")
public class Category {
    @Id private int categoryCode;
    private String categoryName;
    private Integer refCategoryCode;

    @OneToMany(mappedBy = "category") // 주인: Menu.category
    private List<Menu> menuList;

    protected Category() {}
}

조회/저장 특징

  • 주인(Menu)로 조회 시: 처음부터 조인으로 가져오는 케이스 확인 가능
  • 비주인(Category)로 조회 시: 사용 시점에 컬렉션 로딩(지연 로딩 기본값일 때)
  • 저장은 항상 주인 쪽(FK 보유) 에서 관계를 세팅해야 확실

실습에서 유의할 점

  • ManyToOne 기본 fetch는 EAGER, OneToMany는 LAZY
    N+1을 피하려면 fetch 전략, JPQL fetch join, 엔티티 그래프 등을 고려
  • 단방향 1:N은 추가 update가 발생할 수 있어 비권장. 가능하면 양방향(N:1 주인) 사용
  • JPQL FROM에 엔티티명 사용, 파라미터 바인딩 습관화
  • cascade는 신중히(특히 REMOVE). 연쇄 삭제 위험
  • 네이밍 전략을 쓰면 컬럼/테이블 실명이 바뀌므로 DDL과 일치해야 함

요약

  • N:1 주인이 FK를 가진다 → 연관관계의 변경/저장은 주인쪽에서
  • 1:N 단방향은 편하지만 추가 update·관리 비용이 생길 수 있음
  • 양방향은 탐색이 편하지만 주인/비주인 정확히 구분하고 주인에서 값 세팅
  • JPQL은 엔티티 중심 쿼리 → 조인도 객체 그래프 기준으로 작성
profile
개발자 희망자

0개의 댓글