본문 바로가기
Programing/Spring

Spring JPA 관계 이해하기

by 아주노란콩 2025. 2. 7.

1. JPA 란?

JPA는 Java Persistence API 약자로서 , RDBMS와 OOP 객체 사이의 불일치에서 오는 패러다임을 해결하기 위해서 만들어진 ORM(Object Relational Mapping) 기술이다.구현체가 없으므로 ORM 프레임워크를 사용하는데 대중적인 프레임 워크는 Hibernate이다.

 

JPA 에서 가장 중요한것은 객체데이터베이스 테이블 맵핑 방법이다.

데이터베이스 간의 관계를 통해 데이터를 연결하고, 데이터를 조회할 수 있는데 JPA에서도 관계를 정의하고,

객체간 연결을 할 수 있다. 

따라서, JPA 를 잘 다루기 위해서는 연관관계 맵핑 방법에 대해 숙지하고, 익숙해져야 한다.

2. 연관 관계 정의

연관 관계를 맵핑 할 때 크게 생각해 봐야 할 것은 방향, 관계의 주인, 다중성 이다.

  • 방향: 단방향, 양방향
  • 연관 관계 주인: 조인컬럼의 주인
  • 다중성: N:1(다대일) , 1:N(일대다) , N:N (다대다)

3. 1대 1 관계 단방향

음식과 고객 테이블이 있다. 음식 하나 당 고객 한명, 고객 한명 당 음식 하나 라고 가정을 해본다면, 1: 1 관계이다.

1:1 관계에서는 연관 관계의 주인을 개발자 마음대로 설정하면 된다. 외래키의 주인만이 등록,삭제,추가 를 조작할 수 있다. 그래서, 많이 조회 되는 쪽(많이 사용되는 쪽)이 주인이 되면 좋을 것 같다. 

 

Food.java

@Entity
@Table(name = "food")
public class Food {
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;
    private String name;
    private double price;

    @OneToOne
    @JoinColumn(name = "user_id")
    private User user;
}

 

User.java

@Entity
@Table(name = "users")
public class User {
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;
    private String name;
    
    
}

 

위 코드는 음식과 고객은  단방향으로 설정 되어 있다.

 

4. 1 : 1 관계 양방향

user 와 food의 양방향을 연결하기 위해 user 에 Food를 추가했다.

@OneToOne( mappedby ="user" ) 어노테이션은 주인이 user라는 것을 명시한다. mappedBy = "외래키 주인 필드명"

( 주인의 User 필드명 과 같으면 생략 가능하지만, 되도록 쓰는걸 추천하셨다.) 

@Getter
@Setter
@Table(name = "users")
public class User {
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;
    private String name;

    @OneToOne(mappedBy = "user")
    private Food food;

    public void addFood(Food food) {
        this.food = food;
        food.setUser(this);
    }
}

 

외래키가 주인이 아닌 user 에서 의존관계를 주입하게 되면 food의 외래키값에  null 이 들어간다.

    @Test
    @Rollback(value = false)
    @DisplayName("1대1 양방향 테스트 : 외래 키 저장 실패")
    void test2() {
        Food food = new Food();
        food.setName("고구마 피자");
        food.setPrice(30000);

        // 외래 키의 주인이 아닌 User 에서 Food 를 저장해보겠습니다.
        User user = new User();
        user.setName("Robbie");
        user.setFood(food);

        userRepository.save(user);
        foodRepository.save(food);

  }

 

 

오직 외래키의 주인만이 외래키를 조작할 수 있는데, 그럼에도 user을 통해 food 와 관계를 맺고 싶다면 addFood() 를 참고하면 된다. 외래키의 주인인 Food 를 필드로 받아와서 주인과 연결할 수 있다. 즉, 우회해서 사용하는 방법이다. 

    @Test
    @Rollback(value = false)
    @DisplayName("1대1 양방향 테스트 : 외래 키 저장 실패 -> 성공")
    void test3() {
        Food food = new Food();
        food.setName("고구마 피자");
        food.setPrice(30000);

        // 외래 키의 주인이 아닌 User 에서 Food 를 저장하기 위해 addFood() 메서드 추가
        // 외래 키(연관 관계) 설정 food.setUser(this); 추가
        User user = new User();
        user.setName("Robbie");
        user.addFood(food);

        userRepository.save(user);
        foodRepository.save(food);
    }

 

 

5. N : 1 관계

User 와 Food 가 1 : N 관계로, 고객은 여러개의 푸드를 가질 수 있다. 

 

외래키의 주인은 Food 의 코드는 변경하지 않고, User 는 Food를 여러개 가질 수 있으니까 List<Food> 

@Entity
@Getter
@Setter
@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 addFoodList(Food food) {
        this.foodList.add(food);
        food.setUser(this); // 외래 키(연관 관계) 설정
    }
}

 

addFoodList 는 Food를 파라미터로 받아 List 에 하나씩 추가하는 메소드다.  외래키의 주인이 아닌 User에서 Food를 등록하게 되면 외래키가 Null 로 나온다. 

   @Test
    @Rollback(value = false)
    @DisplayName("N대1 양방향 테스트 : 외래 키 저장 실패 -> 성공")
    void test3() {

        Food food = new Food();
        food.setName("후라이드 치킨");
        food.setPrice(15000);

        Food food2 = new Food();
        food2.setName("양념 치킨");
        food2.setPrice(20000);

        // 외래 키의 주인이 아닌 User 에서 Food 를 쉽게 저장하기 위해 addFoodList() 메서드 생성하고
        // 해당 메서드에 외래 키(연관 관계) 설정 food.setUser(this); 추가
        User user = new User();
        user.setName("Robbie");
        user.addFoodList(food);
        user.addFoodList(food2);

        userRepository.save(user);
        foodRepository.save(food);
        foodRepository.save(food2);
    }

 

User 를 먼저 생성하고, Food에 User를 Set 하면 외래키가 정상적으로 들어간다.

 @Test
    @Rollback(value = false)
    @DisplayName("N대1 양방향 테스트")
    void test4() {
        User user = new User();
        user.setName("Robbert");

        Food food = new Food();
        food.setName("고구마 피자");
        food.setPrice(30000);
        food.setUser(user); // 외래 키(연관 관계) 설정

        Food food2 = new Food();
        food2.setName("아보카도 피자");
        food2.setPrice(50000);
        food2.setUser(user); // 외래 키(연관 관계) 설정

        userRepository.save(user);
        foodRepository.save(food);
        foodRepository.save(food2);
    }

 

 

6. 1 : N 관계

데이터베이스 상으로 User 에 외래키가 있다고 가정하고 JPA 에서 외래키 주인인 케이스 이다. 해당 방법은 일반적인 상황은 아니지만 이미 데이터베이스가 정해진 상태에서 JPA 를 사용하게 된다면, 알아둬야 한다.

 

외래 키를 Food Entity가 직접 가질 수 있다면 INSERT 발생 시 한번에 처리할 수 있지만 실제 DB에서 외래 키를 User 테이블이 가지고 있기 때문에 추가적인 UPDATE가 발생된다는 단점이 있다 => 비효율적

 

Food.java

@Entity
@Table(name = "food")
public class Food {
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;
    private String name;
    private double price;

    @OneToMany
    @JoinColumn(name = "food_id") 
    private List<User> userList = new ArrayList<>();
}

 

User.java

@Entity
@Table(name = "users")
public class User {
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;
    private String name;

	@ManyToOne
	@JoinColumn(name = "food_id", insertable = false, updatable = false)
	private Food food;
}

 

MayToOne은 mappedBy 옵션을 지원하지 않는다.

N 관계의 Entity인 User Entity에서 @JoinColum의 insertable 과 updatable 옵션을 false로 설정하여 양쪽으로 JOIN 설정을 하면 양방향처럼 설정할 수는 있다.

    @Test
    @Rollback(value = false)
    @DisplayName("1대N 단방향 테스트")
    void test1() {
        User user = new User();
        user.setName("Robbie");

        User user2 = new User();
        user2.setName("Robbert");

        Food food = new Food();
        food.setName("후라이드 치킨");
        food.setPrice(15000);
        food.getUserList().add(user); // 외래 키(연관 관계) 설정
        food.getUserList().add(user2); // 외래 키(연관 관계) 설정

        userRepository.save(user);
        userRepository.save(user2);
        foodRepository.save(food);

        // 추가적인 UPDATE 쿼리 발생을 확인할 수 있습니다.
    }

 

6.  N : M 관계

 

N:M 관계에서 Order 테이블은 중간 테이블로 관계를 풀어내기 위해 사용한다.

중간테이블은 직접 테이블을 생성하는게 아니라 관리하기가 어렵다.

 

Food.java

@Entity
@Table(name = "food")
public class Food {
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;
    private String name;
    private double price;

    @ManyToMany
    @JoinTable(name = "orders", // 중간 테이블 생성
    joinColumns = @JoinColumn(name = "food_id"), // 현재 위치인 Food Entity 에서 중간 테이블로 조인할 컬럼 설정
    inverseJoinColumns = @JoinColumn(name = "user_id")) // 반대 위치인 User Entity 에서 중간 테이블로 조인할 컬럼 설정
    private List<User> userList = new ArrayList<>();
    
    
     public void addUserList(User user) {
        this.userList.add(user); // 외래 키(연관 관계) 설정
        user.getFoodList().add(this);
    }
}

 

@JoinTabe  어노테이션으로 외래키의 주인인 Food에서 중간 테이블을 생성하고 외래키를 설정한다.

joinColums = @JoinColumn 외래키의 주인인 Food Entity 에서 중간 테이블과 Join 할 외래키 설정

inverseJoinColums = @JoinColumn 반대편 User Entity와 중 테입블 Join할 외래키 설정

 

User.java

@Entity
@Table(name = "users")
public class User {
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;
    private String name;

    @ManyToMany(mappedBy = "userList")
    private List<Food> foodList = new ArrayList<>();
    
     public void addFoodList(Food food) {
        this.foodList.add(food);
        food.getUserList().add(this); // 외래 키(연관 관계) 설정
    }
}

 

mappdeBy에 의해 외래키 주인인 Food의 userList로 맵핑된다.

 

 @Test
    @Rollback(value = false)
    @DisplayName("N대M 양방향 테스트 : 객체와 양방향의 장점 활용")
    void test5() {

        User user = new User();
        user.setName("Robbie");

        User user2 = new User();
        user2.setName("Robbert");


        // addUserList() 메서드를 생성해 user 정보를 추가하고
        // 해당 메서드에 객체 활용을 위해 user 객체에 food 정보를 추가하는 코드를 추가합니다. user.getFoodList().add(this);
        Food food = new Food();
        food.setName("아보카도 피자");
        food.setPrice(50000);
        food.addUserList(user);
        food.addUserList(user2);

        Food food2 = new Food();
        food2.setName("고구마 피자");
        food2.setPrice(30000);
        food2.addUserList(user);


        userRepository.save(user);
        userRepository.save(user2);
        foodRepository.save(food);
        foodRepository.save(food2);

        // User 를 통해 food 의 정보 조회
        System.out.println("user.getName() = " + user.getName());

        List<Food> foodList = user.getFoodList();
        for (Food f : foodList) {
            System.out.println("f.getName() = " + f.getName());
            System.out.println("f.getPrice() = " + f.getPrice());
        }
    }

 

아보카도피자는 user , Food 서로 연관관계 주입을 했다면 고구마 피자는 food만 연관관계를 맺지 않았다.

하지만, addUserList() 함수 내에 user의 외래키를 맺는 코드가 포함되어 있기 때문에 양방향으로 설정된다.

 

 

6.  N : M 관계를 1 : N 관계로 풀기

음식 과 고객을 N:M 으로 연관관계를 맺게 되면 order 는 중간테이블이 자동으로 생성되는데 order 객체가 없기 때문에 관리하기 어렵다. 그래서, Food : Order  , User : Food 로 연관관계를 풀어줄 것 이다.

 

@Entity
@Getter
@Setter
@Table(name = "food")
public class Food {
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;
    private String name;
    private double price;

    @OneToMany(mappedBy = "food")
    private List<Order> orderList = new ArrayList<>();
}
@Entity
@Getter
@Setter
@Table(name = "users")
public class User {
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;
    private String name;

    @OneToMany(mappedBy = "user")
    private List<Order> orderList = new ArrayList<>();
}
@Entity
@Getter
@Setter
@Table(name = "orders")
public class Order {
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;

    @ManyToOne
    @JoinColumn(name = "food_id")
    private Food food;

    @ManyToOne
    @JoinColumn(name = "user_id")
    private User user;
}

 

order entity 를 추가하여 연관관계를 풀수 있다. order entity가 외래키의 주인이다. 

 

  @Test
    @Rollback(value = false)
    @DisplayName("중간 테이블 Order Entity 테스트")
    void test1() {

        User user = new User();
        user.setName("Robbie");

        Food food = new Food();
        food.setName("후라이드 치킨");
        food.setPrice(15000);

        // 주문 저장
        Order order = new Order();
        order.setUser(user); // 외래 키(연관 관계) 설정
        order.setFood(food); // 외래 키(연관 관계) 설정

        userRepository.save(user);
        foodRepository.save(food);
        orderRepository.save(order);
    }

 

User, Food 를 생성해서 Order 에 연관 관계를 설정해 준다.

 

마무리


JPA 를 학습할때 마다 ManyToOne OneToMany 순서가 헷갈린다.. 내 자신이 앞에 반대쪽은 뒤! 앞으로는 헷갈리지 말고 학습하기! 

연관관계 맺을때 무족권 양방향으로 걸지 말고, 필요한 쪽만 관계를 걸어주는게 효율적이다! Entity 만들 때 잘 생각하고 고민하면서 만들어야 겠다.