군만두의 IT 공부 일지

[2주차] 내일배움캠프 Spring Java 심화 부트캠프 3기 - JPA 심화 본문

개발일지/스파르타코딩클럽

[2주차] 내일배움캠프 Spring Java 심화 부트캠프 3기 - JPA 심화

mandus 2025. 2. 10. 15:47

 

오늘은 JPA에 대해서 조금 더 심화된 내용을 공부했다. 지난 블로그에서는 JPA가 무엇이고 어떻게 사용할 수 있는지 간략히 정리했는데, 이번에는 엔티티의 연관 관계에 대해서 정리하려고 한다.

1. Entity 연관 관계

주문 APP DB 테이블 설계

  1. 고객(users) 테이블
    id name
    1 Robbie
    2 Robbert
    CREATE TABLE users (
        id BIGINT NOT NULL AUTO_INCREMENT,
        name VARCHAR(255),
        PRIMARY KEY (id)
    );
    
  2. 음식(food) 테이블
    id name price
    1 후라이드 치킨 15000
    2 양념 치킨 20000
    3 고구마 피자 30000
    4 아보카도 피자 50000
    CREATE TABLE food (
        id BIGINT NOT NULL AUTO_INCREMENT,
        name VARCHAR(255),
        price FLOAT NOT NULL,
        PRIMARY KEY (id)
    );
    

DB 테이블 간의 연관 관계

  1. 고객이 음식을 주문 시, 주문 정보는 어느 테이블에 들어가야 할까?
    • 고객이 여러 음식을 주문할 수 있으므로 1:N 관계로 설정
    • 주문 정보는 별도의 orders 테이블로 관리
    • 주문 테이블 생성 및 연관 관계 설정
    CREATE TABLE orders (
        id BIGINT NOT NULL AUTO_INCREMENT,
        user_id BIGINT,
        food_id BIGINT,
        order_date DATE,
        PRIMARY KEY (id)
    );
    
    ALTER TABLE orders
    ADD CONSTRAINT orders_user_fk FOREIGN KEY (user_id) REFERENCES users (id);
    
    ALTER TABLE orders
    ADD CONSTRAINT orders_food_fk FOREIGN KEY (food_id) REFERENCES food (id);
    
    • 데이터 삽입 예시
    INSERT INTO users (name) VALUES ('Robbie'), ('Robbert');
    INSERT INTO food (name, price) VALUES ('후라이드 치킨', 15000), ('양념 치킨', 20000);
    INSERT INTO orders (user_id, food_id, order_date) VALUES (1, 1, SYSDATE());
    
  2. 정리
- DB 테이블에서는 테이블 사이의 연관관계를 FK(외래 키)로 맺을 수 있고 방향 상관없이 조회할 수 있다.
- Entity에서는 상대 Entity를 참조하여 Entity 사이의 연관관계를 맺을 수 있다.
- 하지만 상대 Entity를 참조하지 않고 있다면 상대 Entity를 조회할 수 있는 방법이 없다.
- 따라서 Entity에서는 DB 테이블에는 없는 방향의 개념이 존재한다.

2. 1:1 관계 설정

@OneToOne 애너테이션은 1대 1 관계를 맺어주는 역할을 한다.

  1. @OneToOne 사용 예시
    • 고객과 음식의 1:1 관계를 설정한다. 주문한 음식이 하나의 고객에게만 할당되는 경우를 나타낸다.
  2. 단방향 @OneToOne
    • 고객(User)이 음식(Food)을 참조할 때 설정한다.
    @Entity
    @Table(name = "users")
    public class User {
        @Id
        @GeneratedValue(strategy = GenerationType.IDENTITY)
        private Long id;
        private String name;
    
        @OneToOne
        @JoinColumn(name = "food_id") // 외래 키 지정
        private Food food;
    }
    
  3. 양방향 @OneToOne
    • 음식에서도 고객을 참조할 수 있게 설정한다.
    @Entity
    @Table(name = "food")
    public class Food {
        @Id
        @GeneratedValue(strategy = GenerationType.IDENTITY)
        private Long id;
        private String name;
        private double price;
    
        @OneToOne(mappedBy = "food") // 고객에서 설정한 외래 키의 주인
        private User user;
    }
    

3. N:1 관계 설정

@ManyToOne 애너테이션은 N대 1 관계를 맺어주는 역할을 한다.

  1. @ManyToOne 사용 예시
    • 음식과 고객 간의 N:1 관계를 설정할 때 사용한다. 여러 음식이 한 명의 고객에게 주문되는 경우를 나타낸다.
  2. 단방향 @ManyToOne
    • 음식(Food)에서 고객(User)을 참조할 때 설정한다.
    @Entity
    @Table(name = "food")
    public class Food {
        @Id
        @GeneratedValue(strategy = GenerationType.IDENTITY)
        private Long id;
        private String name;
        private double price;
    
        @ManyToOne
        @JoinColumn(name = "user_id") // 외래 키 지정
        private User user;
    }
    
  3. 양방향 @ManyToOne
    • 음식 엔티티에서 고객을 참조하는 @ManyToOne 관계를 설정한다.
    @Entity
    @Table(name = "food")
    public class Food {
        @Id
        @GeneratedValue(strategy = GenerationType.IDENTITY)
        private Long id;
        private String name;
        private double price;
    
        @ManyToOne
        @JoinColumn(name = "user_id") // 고객 참조를 위한 외래 키 지정
        private User user;
    }
    
    • 고객 엔티티에서 여러 음식을 참조할 수 있도록 @OneToMany 관계를 설정한다. 이때 mappedBy 속성을 사용하여 관계의 소유자가 음식(Food)임을 명시한다.
    @Entity
    @Table(name = "users")
    public class User {
        @Id
        @GeneratedValue(strategy = GenerationType.IDENTITY)
        private Long id;
        private String name;
    
        @OneToMany(mappedBy = "user") // 음식과의 관계에서 고객이 참조되는 방식 지정
        private List<Food> foodList = new ArrayList<>();
    
        // 음식 리스트에 음식 추가하는 편의 메서드
        public void addFood(Food food) {
            foodList.add(food);
            food.setUser(this); // 양방향 관계 설정
        }
    }
    

4. 1:N 관계 설정

@OneToMany 애너테이션은 N대 1 관계를 맺어주는 역할을 한다.

  1. @ManyToOne 사용 예시
    • 고객과 음식 간의 1:N 관계를 설정할 때 사용한다. 한 고객이 여러 음식을 주문하는 경우를 나타낸다.
  2. 단방향 1:N 관계 설정
    • 고객(User) 엔티티에서 여러 음식(Food) 엔티티를 참조할 수 있게 설정한다.
    @Entity
    @Table(name = "users")
    public class User {
        @Id
        @GeneratedValue(strategy = GenerationType.IDENTITY)
        private Long id;
        private String name;
    
        @OneToMany // 기본 Fetch 타입은 LAZY
        @JoinColumn(name = "user_id") // 이 설정은 User 테이블에 food_id 외래 키가 없을 때 사용
        private List<Food> foods = new ArrayList<>();
    
        public void addFood(Food food) {
            foods.add(food);
            food.setUser(this); // 양방향 설정을 위해 사용될 수 있음
        }
    }
    
  3. 양방향 1:N 관계 설정
    • 음식(Food)에서도 고객(User)을 참조하며, 고객 역시 여러 음식을 참조할 수 있도록 설정한다.
    @Entity
    @Table(name = "food")
    public class Food {
        @Id
        @GeneratedValue(strategy = GenerationType.IDENTITY)
        private Long id;
        private String name;
        private double price;
    
        @ManyToOne(fetch = FetchType.LAZY) // 고객 정보는 필요할 때 로딩
        @JoinColumn(name = "user_id") // 외래 키 설정
        private User user;
    }
    
    @Entity
    @Table(name = "users")
    public class User {
        @Id
        @GeneratedValue(strategy = GenerationType.IDENTITY)
        private Long id;
        private String name;
    
        @OneToMany(mappedBy = "user") // mappedBy 속성으로 Food 엔티티의 'user' 필드가 연관 관계의 주인임을 명시
        private List<Food> foods = new ArrayList<>();
    
        public void addFood(Food food) {
            foods.add(food);
            food.setUser(this);
        }
    }
    

5. N:M 관계 설정

@ManyToMany 애너테이션은 N대 M 관계를 맺어주는 역할을 한다.

  1. @ManyToMany 사용 예시
    • 음식과 고객 간의 N:M 관계를 설정할 때 사용한다. 여러 고객이 동일한 음식을 주문할 수 있고, 한 고객이 여러 음식을 주문할 수 있는 경우를 나타낸다.
  2. 단방향 @ManyToMany
    • 고객(User)에서 음식(Food)을 참조한다. 중간 테이블(orders)이 자동으로 생성된다.
    @Entity
    @Table(name = "users")
    public class User {
        @Id
        @GeneratedValue(strategy = GenerationType.IDENTITY)
        private Long id;
        private String name;
    
        @ManyToMany
        @JoinTable(
            name = "orders", // 중간 테이블 이름
            joinColumns = @JoinColumn(name = "user_id"), // 현재 Entity의 참조 키
            inverseJoinColumns = @JoinColumn(name = "food_id") // 반대쪽 Entity의 참조 키
        )
        private List<Food> foodList = new ArrayList<>();
    }
    
  3. 양방향 @ManyToMany
    • 음식(Food)에서도 고객(User)을 참조할 수 있게 설정한다.
    @Entity
    @Table(name = "food")
    public class Food {
        @Id
        @GeneratedValue(strategy = GenerationType.IDENTITY)
        private Long id;
        private String name;
        private double price;
    
        @ManyToMany(mappedBy = "foodList")
        private List<User> userList = new ArrayList<>();
    }
    

6. 관계 유형별 방향성의 장단점

관계 유형 방향성 장점 단점
1:1 단방향 - 단순하고 명확함.
- 성능 최적화 용이.
- 연관된 객체에 접근하기 위해 항상 소유자를 통해야 함.
1:1 양방향 - 두 엔티티 간 직접 접근 가능.
- 관계의 이해가 용이.
- 관리 복잡도 증가.
- 순환 참조 가능성.
N:1 단방향 - 구현이 간단함.
- 자주 사용되는 패턴.
- 반대 방향에서 접근 불가능.
N:1 양방향 - 반대 방향에서도 접근 가능.
- 관계를 양쪽에서 관리.
- 관리 복잡도 증가.
- 순환 참조 가능성.
1:N 단방향 - 자연스러운 모델링.
- 구현 용이.
- N 측에서 1 측에 접근하기 어려움.
1:N 양방향 - 양쪽에서 자유롭게 접근 가능.
- 데이터 일관성 유지 용이.
- 성능 문제 (EAGER 로딩).
- 복잡한 관리 요구.
N:M 단방향 - 구현이 간단하고 직관적임. - 반대 방향에서의 접근이 어려움.
N:M 양방향 - 양쪽에서 자유롭게 접근 가능.
- 더 유연한 데이터 관리.
- 매핑 복잡도 증가.
- 성능 저하 가능성.
  • 단방향 관계는 일반적으로 구현이 간단하며, 엔티티 간의 관계가 명확할 때 사용된다. 엔티티 간의 상호 작용이 필요할 때 제약이 따를 수 있다.
  • 양방향 관계는 두 엔티티 간에 자유로운 상호 작용을 가능하지만, 엔티티 간의 관계를 설정하고 유지하는 데 많은 주의가 필요하다. 잘못 구현된 양방향 관계는 성능 저하와 같은 문제를 초래할 수 있다.

7. 추가적인 JPA 설정

  1. FetchType 설정
      • LAZY(지연 로딩): 지연 로딩은 연관된 엔티티를 실제로 사용하는 시점까지 로딩을 지연시키는 방식이다. 예를 들어, 고객 정보를 조회할 때 연관된 주문 정보는 필요할 때만 데이터베이스에서 로드된다. 이 방식은 초기 로딩 시간을 줄이고, 리소스 사용을 최적화할 수 있다.
      • EAGER(즉시 로딩): 즉시 로딩은 엔티티를 조회할 때 연관된 엔티티도 함께 즉시 로드하는 방식이다. 관계된 모든 데이터를 한 번에 로딩하므로, 복잡한 조인 쿼리나 여러 SQL 호출이 필요 없는 상황에서 유용하다. 하지만 불필요한 데이터까지 로드할 수 있어 성능 저하를 일으킬 수 있다.
    @Entity
    @Table(name = "customer")
    public class Customer {
        @Id
        @GeneratedValue(strategy = GenerationType.IDENTITY)
        private Long id;
        private String name;
    
        // LAZY 로딩: 연관된 엔티티(Order)를 실제 사용하는 시점에 로드함.
        @OneToMany(fetch = FetchType.LAZY, mappedBy = "customer")
        private List<Order> orders = new ArrayList<>();
    
        // EAGER 로딩: Customer 조회 시 연관된 Profile도 즉시 로드됨.
        @OneToOne(fetch = FetchType.EAGER)
        @JoinColumn(name = "profile_id")
        private Profile profile;
    }
  2. Cascade 설정
    • 영속성 전이(Cascade): 영속성 전이 설정은 특정 엔티티의 영속 상태 변화를 연관된 엔티티에도 전파하는 것이다. 예를 들어, 부모 엔티티를 저장할 때 자동으로 자식 엔티티도 저장되게 할 수 있다.
        • CascadeType.PERSIST: 부모 엔티티를 저장할 때 연관된 자식 엔티티도 함께 저장한다.
        • CascadeType.REMOVE: 부모 엔티티를 삭제할 때 연관된 자식 엔티티도 함께 삭제한다.
        • CascadeType.MERGE: 부모 엔티티 상태를 병합할 때 연관된 자식 엔티티의 상태도 함께 병합한다.
        • CascadeType.REFRESH: 부모 엔티티를 새로고침할 때 연관된 자식 엔티티도 함께 새로고침한다.
        • CascadeType.DETACH: 부모 엔티티가 영속성 컨텍스트에서 분리될 때 연관된 자식 엔티티도 함께 분리된다.
        • CascadeType.ALL: 위의 모든 영속성 전이 옵션을 적용한다.
      @Entity
      @Table(name = "parent")
      public class Parent {
          @Id
          @GeneratedValue(strategy = GenerationType.IDENTITY)
          private Long id;
          private String name;
      
          // Parent 엔티티의 생명주기 변화가 Child 엔티티에 전파됨. 저장, 삭제, 병합 등의 연산이 자식에도 적용됨.
          @OneToMany(cascade = CascadeType.ALL, mappedBy = "parent")
          private List<Child> children = new ArrayList<>();
      }
  3. 고아 Entity 삭제(orphanRemoval)
      • 고아 객체 삭제: 고아 객체 삭제 설정은 부모 엔티티와의 관계가 끊어진 자식 엔티티를 자동으로 삭제한다. 예를 들어, 한 목록에서 특정 자식을 제거하면, 그 자식 엔티티는 데이터베이스에서도 삭제된다.
      • 연관된 엔티티가 더 이상 참조되지 않을 때 불필요하게 데이터베이스에 남아 있지 않도록 할 수 있다.
    @Entity
    @Table(name = "team")
    public class Team {
        @Id
        @GeneratedValue(strategy = GenerationType.IDENTITY)
        private Long id;
        private String name;
    
        // Team에서 Member가 제거되면, 그 Member는 데이터베이스에서도 삭제됨. 고아 객체를 관리함.
        @OneToMany(mappedBy = "team", orphanRemoval = true)
        private List<Member> members = new ArrayList<>();
    
        public void removeMember(Member member) {
            this.members.remove(member);
            member.setTeam(null); // 연관관계 해제
        }
    }
Comments