본문 바로가기

Spring Boot

Entity : 연관관계 맵핑

Spring Data JPA

Entity 연관관계를 맵핑하기

 

연관관계 맵핑

애플리케이션의 엔티티 클래스의 관계

메시지 기능(Message)과 채널(Channel)의 관계 : (N 대 1)

 - > 한개의 채널은 여러개의 메시지를 가질 수 있다

회원정보(User)와 회원정보상태(UserStatus)의 관계 : (1 대 1)

 - > 회원 한명은 한개의 회원정보상태를 가질 수 있다

 

이런것도 있을 수 있다 :

주문정보(Order)와 커피 정보의 관계 : (N 대 N)

 - > 하나의 주문은 여러 종류의 커피를 가질 수 있음. 주문 한번에 커피 여러개를 주문 가능하니까

 - > 하나의 커피는 여러 건의 주문에 속할 수 있음. 다른 사람들의 주문에 똑같은 커피가 여러개 들어갈 수 있으니까

 

보통 N 대 N의 관계는 1 대 N, N 대 1의 관계로 재설계 된다고 함. 따라서

주문 정보(Order)주문에 대한 커피 정보(Order_Coffee)의 관계 : 1 대 N - > 하나의 주문에 여러개의 커피주문을 담을 수 있음

주문에 대한 커피 정보(Order_Coffee)커피 정보(Coffee)의 관계 : N 대 1 - > 하나의 커피는 여러개의 커피주문에 담을 수 있음

 

 

Entity 클래스간의 관계에 대해서

 

Member - Order - OrderCoffee - Coffee의 관계

 

1. Member 클래스와 Order 클래스는 1 대 N의 관계,

Member 하나에 Order 여러개를 담을 수 있음

Order를 여러개 담을 수 있다는 것을 List<Order> 형태로 만들어서 Member에 넣을 수 있다.

 - > private List<Order> orders;

 

Order 클래스와 Coffee 클래스는 N 대 N의 관계를 가지고 있다.

주문표 하나에는 여러개의 커피가 들어갈 수 있으며 커피 또한 아메리카노 하나가 여러개의 주문표에 들어갈 수 있다.

만약 아메리카노가 하나의 주문표에만 들어갈 수 있다면

한 사람이 아메리카노를 주문 할 때 다른 사람들은 아메리카노를 주문할 수 없을 것이다.

따라서 Order 클래스와 Coffee 클래스는 N 대 N 의 관계를 가질 수 있다.

 

이 N 대 N의 관계를 각각 N 대 1, 1 대 N으로 나눌 수 있다.

Order - Coffee의 N 대 N 관계의 중간지점에 OrderCoffee 클래스를 따로 만들어서

Order - OrderCoffee - Coffee 의 형태로 만든 다음에

Order - OrderCoffee를 1 대 N의 관계로, OrderCoffee - Coffee를 N 대 1의 관계로 만든다.

 

    ※ Order - OrderCoffee 가 N 대 1의 관계, OrderCoffee - Coffee가 1 대 N이 아닌건가?

생각해보자

OrderCoffee는 커피에 대한 주문정보를 담고 있다.

만약 내가 커피를 아메리카노 하나, 카푸치노 하나를 주문했다.

그럼 이 주문을 우선 하나의 장바구니(OrderCoffee)에 담을 수 있을 것이다.

그리고 주문할 커피를 다 정했으면 이걸 주문(Order) 할 수 있을 것이다.

그런데 만약 Order와 OrderCoffee가 N 대 1의 관계라면, 내가 주문한 커피에 대해 여러번의 주문이 들어갈 수 있을 것이다.

또한 중간에 커피를 또 주문하고 싶어서 OrderCoffee를 다시 호출할 수 없다. 왜냐하면 OrderCoffee는 1의 관계에 있으니까

주문이 여러개가 있고 커피주문정보가 단 1개라면 뭔가 좀 이상하지 않겠는가?

 

학교로 예를 들어보자면, 하나의 반에 여러명의 학생이 들어갈 수 있지

여러 반에 한명의 학생이 등록될 순 없을 것이다.

 

따라서 Order와 OrderCoffee는 1 대 N의 관계가 되어야한다.

 

마찬가지로  OrderCoffee와 Coffee의 관계도 N 대 1이 되어야한다. 하나의 커피는 여러개의 커피주문정보에 들어가야지,

여러개의 커피가 하나의 커피주문정보에 들어갈 수 있다면 다른 사람들은 해당 커피를 주문정보에 넣을 수 없을 것이다.

혹은 커피주문정보에 아메리카노 단 하나만 주문할 수 있으며 2개를 넣지 못하는 참사가 발생할 수 있을 것이다.

 

 

2. Order 클래스와 OrderCoffee 클래스는 1 대 N의 관계,

하나의 주문에는 여러개의 커피 주문이 들어갈 수 있음

주문서 하나에 아메리카노 하나만 들어가는게 아니라 카푸치노, 카페라떼, 마끼아또 등도 들어갈 수 있다

따라서 OrderCoffee를 여러개 담을 수 있다는 것을 List<OrderCoffee> 의 형태로 나타내어 Order에 넣어줄 수 있다.

 - > private List<OrderCoffee> orderCoffee;

 

 

3. OrderCoffee 클래스와 Coffee는 N 대 1의 관계,

아메리카노는 A와 B와 C 등 여러 사람의 커피주문정보에 들어갈 수 있다

따라서 Coffee에 List<OrderCoffee>를 필드에 넣을 수 있을 것이다.

 - > private List<OrderCoffee> orderCoffee;

 

 

** 데이터베이스 테이블 간의 관계는 외래키로 맺어지지만, 클래스간의 관계는 객체의 참조를 통해 맺어진다. 매우 중요!

 

 

 

단방향 연관 관계

 

Member와 Order는 1 대 N의 관계이며 Member에 List<Order>가 들어있다.

Member 클래스는 Order 클래스를 참조할 수 있다. 이는 Member가 Order 정보를 알 수 있다는 것을 의미한다.

사람이 주문정보를 알 수 있어야 하잖아요?

 

하지만, Order의 경우 Member 클래스를 참조하고 있지 않기 때문에

Member가 Order를 참조하고 있는지, 애초에 Member의 정보는 어떤것인지 알 수 없다.

 

위 사진의 경우는 Member 클래스에 Order객체가 없고 Order클래스에 Member객체가 있는 경우이다.

Member와 Order는 1 대 N의 관계이기 때문에 Order에는 하나의 Member만 들어올 수 있어서 List<Member>가 아니다.

 

Order클래스는 Member객체를 가지고 있으니 주문정보는 사람의 정보에 대해 알 수 있다.

주문정보에 그 사람의 정보가 적혀있어서 누가 주문했는지 알 수 있다고 생각하면 편하겠다.

 

하지만 Member클래스에는 Order객체가 없기 때문에 사람 입장에서는 내가 뭘 주문했는지 알 길이 없다.

 

이게 단방향 연관 관계임!

 

 

 

양방향 연관 관계

 

Member와 Order는 1 대 N의 관계

두 클래스가 각자 참조함으로 Member는 Order의 정보를 알 수 있고, Order도 Member에 대해 알 수 있는 상태임

양 클래스가 서로의 참조 정보를 가지고 있는 관계를 양방향 연관 관계라고 한다.

 

* JPA는 단향방 연관 관계, 양방향 연관 관계 모두 지원하지만 Spring Data JDBC는 단방향 연관 관계만 지원한다.

순환참조 때문인듯

 

 

 

일대다 단방향 연관 관계

 

Member와 Order는 1 대 N의 관계

1인 Member가 N에 해당하는 Order의 List를 가지고 있는 모습이다.

 

보통 일대다 단방향 맵핑은 잘 사용하지 않는다고 한다.

어째서인가?

 

 

위 사진은 member 테이블과 orders테이블이 1 대 N의 관계를 가지고 있는 모습이다

일반적으로 테이블간의 관계에서 1 대 N의 관계인 경우 N의 입장에 있는 테이블에서 1의 PK(기본키)를 FK(외래키)로 갖는다.

따라서 orders 테이블이 member의 기본키인 member_id를 외래키로서 가지고 있는 것을 알 수 있다.

 

다시 그림을 가져오면,

 

이 상태에서는 Order 클래스가 Member 클래스를 참조할 수 있는 필드, 즉 참조값을 가지고 있지 않다.

따라서 Order 클래스는 Member클래스를 참조할 수 없으며 Member의 정보를 알 길이 없다.

이를 테이블간의 관계를 정상적으로 표현하지 못하고 있다~ 라고 말한다.

 

이와 같은 일대다 단방향 연관 관계, 일대다 단방향 맵핑의 형태가 되면 Order클래스의 정보를 테이블에 저장하더라도

외래키에 해당하는 member테이블의 member_id가 데이터베이스에 외래키의 형태로 orders 테이블에 있다고 하더라도

member클래스의  memberId가 없는 채로 값이 저장이 되어버린다.

 

그럼 어떻게 맵핑을 하는게 맞는가????

우선 다대일 단방향 맵핑을 먼저 하고

필요한 경우, 일대다 단방향 맵핑을 추가해서 양방향 연관 관계를 만드는 것이 일반적이라고 합니다.

 

 

다대일 연관 관계

다(N)에 해당하는 클래스가 1에 해당하는 객체를 참조할 수 있는 관계를 말함

이때까지의 예제에서는 1에 해당하는 클래스가 다에 해당하는 객체를 참조하거나 양방향으로 참조했다.

이건 다에 해당하는 클래스가 1의 객체를 참조하는거임

 

1명의 사람은 여러개의 주문을 할 수 있다.

아메리카노 2개, 카푸치노 3개, 마끼아또 1개, 모카 2개를 주문할 수도 있는거 아닌가?

따라서 Member와 Order의 관계는 1 대 N의 관계라고 볼 수 있다.

 

또한 Order만 객체를 참조하고 있으므로 단방향 관계이다. Member에는 Order객체가 없기 때문에 Order의 정보를 모름

따라서 Order는 Member 타입의 member필드를 가지고 있으며, 이는 Foreign Key의 형태가 될 것이다.

 

다대일 단방향 맵핑은 굉장히 자연스러운 맵핑방식이다.

JPA 엔티티에서 가장 기본적으로 사용되는 맵핑방식이라고 함

 

 

코드로 보기

다(N)에 해당하는 Order 클래스

// import 생략

@Entity(name = "orders")
@Getter
@Setter
@NoArgsConstructor
public class Order {
	// 생략
    
    @ManyToOne
    @JoinColumn(name = "member_id")
    private Member member;
    
    public void addMember(Member member) {
    	this.member = member;
    }
}

 

위의 Member와 Order는 1 대 N의 관계로 Order가 N의 입장에 있다.

위의 코드에서 private Member = member 를 적을 때

Member와의 관계에서 주체인 Order 입장을 먼저 생각한다면 @ManyToOne의 다대일 관계를 명시하는게 맞다.

Order - Member 는 ManyToOne이며 Member - Order 는 OneToMany 이니까..

 

이후에 @JoinColumn 애너테이션으로 orders 테이블에서 외래키에 해당하는 열 이름을 적어준다.

@JoinColumn(name = "member_id") 라고 적혀있으니 Order과 관련된 orders 테이블에는 member_id가 FK로 있을것이다.

 

다대일(N : 1) 단방향 연관관계에서는 다(N)쪽에만 설정해주면 맵핑작업이 끝난다.

 

 

다대일 맵핑을 이용한 회원과 주문정보 저장 : 다대일 관계에서는 일에 대한 객체의 정보를 얻을 수 있음

// import CommandLineRunner, Bean, Configuration
// import EntityManager, EntityManagerFactory, EntityTransaction

@Configuration
public class JpaManyToOneUniDirectionConfig {
	private EntityManager em;
    private EntityTransaction tx;
    
    @Bean
    public CommandLineRunner testJpaManyToOneRunner(EntityManagerFactory emFactory) {
    	this.em = emFactory.createEntityManager();
        this.tx = em.getTransaction();
        
        return args -> {
        	mappingManyToOneUniDirection();
        }
    }
    
    //맵핑하는 메서드
    public void mappingManyToOneUniDirection() {
    	tx.begin();
        Member member = new Member("example@gmail.com","B1uufer","010-1234-5678");
        
        // 여기서부터 주문정보를 저장하나봄
        em.persist(member);
        
        Order order = new Order();
        order.addMember(member);
        em.persist(order);
        
        tx.commit();
        
        Order findOrder = em.find(Order.class, 1L);
        
        System.out.println("findOrder: " + findOrder.getMember().getMemberId() + 
                            ", " + findOrder.getMember().getEmail());
    }
}

 

밑에 public void mappingManyToOneUniDirection에서 저장한다

1. em.persist(member)를 통해 새로 만든 new Member 회원정보를 저장함

 

2. 저장한 회원정보의 주문정보를 저장하기 위해

새롭게 만든 Order객체 order에다가 addMember(member) 해준다.. Order객체에다가 member객체를 추가해준다

order객체에 추가된 member객체는 외래키의 역할을 한다고 한다.

 

orders 테이블에 insert문을 사용해서 엔티티를 추가할 경우,

member 외래키가 있기 때문에 insert문에 member테이블의 member_id가 추가될 것이다.

 

즉 order.addMember(member)를 통해서 해당 member에 대한 order를 알 수 있게 되었다고 생각하면 될 것이다.

 

3. em.persist(order) 를 통해 주문정보를 저장한다.

여기서 em은 맨 처음에 만든 private EntityManager em 필드이다.

 

4. Order findOrder = em.find(Order.class, 1L); 를 통해 등록한 회원에 해당하는 주문정보를 조회하고 있다.

1L는 예제에서 기본키인 id를 의미하니까 1L에 해당하는 member의 주문정보를 조회한다고 보면 되겠다.

 

5. findOrder에는 1L에 해당하는 member의 주문정보가 저장되게 되었다.

이걸로 findOrder.getMeber().getMemberId() 및 findOrder.getMember().getEmail() 처럼 주문에 해당하는 회원정보를 가져올 수 있게 되었다.

이 상황은 Order객체를 통해 Member객체의 정보를 가져온 셈이 된다.

이렇게 객체를 통해 다른 객체의 정보를 얻을 수 있는 것을 객체 그래프 탐색이라고 한다.

 

 

다대일 맵핑에 일대다 맵핑을 추가하기

내가 카페 주인이다.

일반적으로 주문한 회원의 정보는 뭐.. 알고 싶지 않을 수도 있고 그냥 커피만 만들어서 주면 될 것이다.

만약 주문한 회원이 남자인지 여자인지 확인해서 통계자료를 만들고 싶어하는 사람이라면?

회원정보를 알긴 해야겠지? 그럼 위의 다대일 맵핑을 통해 Order를 통해 Member의 정보를 조회할 수 있게 하면 된다

 

그런데 Member 입장에서는 내가 주문한 주문의 목록을 확인할 수 있어야 하는데, Order의 정보를 알 길이 없으니

Member는 Order객체들을 조회할 수 없다.

내가 주문한 커피의 목록을 조회할 수 없다니 이게 말이 되는 일인가!

 

그럴 때, 다대일 맵핑이 되어있는 상태에서 일대다 맵핑을 추가해주어 양방향 관계를 만들어 줄 수 있다.

// import 생략

@Entity
@Getter
@Setter
@NoArgsConstructor
public class Member {
	// 필드 생략
    
    @OneToMany(mappedBy = "member")
    private List<Order> orders = new ArrayList<>();
    
    // 생성자 생략
    
    public void addOrder(Order order) {
    	orders.add(order);
    }
}

 

Member와 Order는 1 대 N의 관계

N에 해당하는 Order에 Member 객체가 있을것이다.

이는 단방향 맵핑이며 다대일 맵핑을 해준것이다. N에 해당하는 Order에 1에 해당하는 Member의 객체가 있으니까.

 

이제 1에 해당하는 Member 클래스에 Order의 객체를 넣어줄 수 있다.

 

그래서 추가해주었다!

@OneToMany(mappedBy = "member")

private List<Order> orders = new ArrayList<>();

 

여기에서 중요한건 @OneToMany(mappedBy = "member")이다.

1 대 N, 일대다 단방향 맵핑의 경우 mappedBy 애트리뷰트의 값이 필요하지 않다.

mappedBy는 참조할 대상이 필요하며, 일대다 단방향 맵핑의 경우 참조할 대상이 없기 때문이다.

 

우리는 members 테이블과 orders 테이블의 관계에서,

orders의 테이블에다가 member 테이블에 있는 기본키인 member_id를 외래키로 지정해주었다.

따라서 Order 클래스에서 외래키의 역할을 하는 필드는 member 필드가 될 것이다.

private Member member; 얘 말이다..

 

따라서,

private List<Order> orders가 mapped 하고 있는 대상은 "member" 이라고 @OneToMany에서 명시해주고 있는 것이다.

 

 

다대일 맵핑에 일대다 맵핑을 추가하여 주문정보를 조회하기

// import 생략

@Configuration
public class JpaManyToOneBiDirectionConfig {
	private EntityManager em;
    private EntityTransaction tx;
    
    @Bean
    public CommandLineRunner testJpaManyToOneRunner(EntityManagerFactory emFactory) {
        this.em = emFactory.createEntityManager();
        this.tx = em.getTransaction();

        return args -> {
            mappingManyToOneBiDirection();
        };
        // 동일한 필드에 동일한 메서드
    }


    private void mappingManyToOneBiDirection() {
        tx.begin();
        Member member = new Member("example@gmail.com", "B1uffer", "010-1234-5678");
        Order = new Order();

        member.addOrder(order);
        order.addMember(member);

        em.persist(member);
        em.persist(order);

        tx.commit();

        Member findMember = em.find(Member.class, 1L);

        findMember.getOrders()
                  .stream()
                  .forEach(findOrder -> {
                           System.out.println("findOrder: " + findOrder.getOrderId() + 
                           ", " + findOrder.getOrderStatus());
                  }); // stream()끝
                  
    } // 메서드 끝
} // class 끝

 

mappingManyToOneBiDirection 메서드에 뭔가가 많이 추가되었다.

1. member.addOrder(order)를 통해 member객체에 order객체를 추가해줌

 - > member객체에 order 객체를 주가해주지 않아도 테이블에는 member, order정보가 정상적으로 저장이 되지만,

      이걸 해주지 않으면 find()로 조회할 수 없다..

 

2. order.addMember(member)를 통해 order객체에 member객체를 추가해줌

 

3. em.persist(member), em.persist(order)를 통해 회원정보와 주문정보를 저장한다

 

4. Member findMember = em.find(Member.class, 1L) 를 통해 회원정보를 조회한다

일대다 양방향 관계를 맵핑하지 않았을땐 Order타입이었다..

 

일대다 양방향 관계를 맵핑했기 때문에 findMember.getOrders().stream().forEach() 와 같이 find() 메서드로 조회한 member로부터 객체 그래프 탐색을 통해 List<Order> 정보에 접근할 수 있게 된다.

 

 

연관관계의 주인(Owner of the Relationship) 이해

이게 매우 어렵게 다가와서 이때까지 적어왔다.

 

연관관계의 주인

양방향 연관관계에서는 어느쪽에서 외래키(FK)를 관리하는지 반드시 명시해야한다.

외래키를 관리하는쪽을 연관관계의 주인이라고 한다.

 - 외래키를 가진 엔티티, 외래키와 관련된 필드를 가진 클래스

 - 연관관계의 주인만이 외래키를 변경할 수 있으며, DB반영은 주인을 기준으로 한다

 

객체는 서로 양방향 참조가 가능하지만, DB에서는 외래키가 어디에 있는지 명확하게 알 수 있기 때문에

JPA를 사용한다면 외래키가 어디에 있는지 명확하게 알아야함

public class Member {
	@OneToMany(mappedBy = "member") // Order객체는 member의 외래키를 가지고 있다
    private List<Order> orders = new ArrayList<>();
}

public class Order {
	@ManyToOne
    @JoinColumn(name = "member_id") // 외래키를 가진 주인
	private Member member;
}

 

실제로, Member와 Order의 객체를 생성해서

member.getOrders().add(order)를 하면 member는 외래키가 없으니까 DB상으로는 member에 order이 저장되지 않는다.

그러나 order.getMembers().add(member)를 하면 order는 member의 외래키를 가지고 있으니까

DB상으로 order에는 member가 저장된다.

 

* @ManyToOne이 무조건 주인이다. @JoinColumn으로 DB의 FK를 관리할 수 있다

* @OneToMany(mappedBy = "")는 주인이 아니다.

* DB에서 FK가 있는쪽이 주인이다.

 

 

다대다 연관관계(N 대 N)

주문(Order)과 커피(Coffee)의 관계는 다대다 관계가 될 수 있다.

주문 하나에 커피 여러개가 들어갈 수 있고, 커피 하나는 주문 여러개에 들어갈 수 있으니까!

 

N 대 N의 엔티티 클래스는 어떻게 맵핑해야하나?

 - > 중간 테이블을 하나 추가해서 두개의 일대다 관계를 만들어주는게 일반적이라고 함

orders와 coffee의 관계는 N 대 N의 관계

그렇기 때문에 중간 테이블에 해당하는 order_coffee를 하나 만들어준다.

그리고 order - order_coffee의 관계를 1 대 N 관계로, coffee와 order_coffee의 관계를 1 대 N의 관계로 연결해준다.

그렇게 해주면 order_coffee 테이블 안에는 orders에 해당하는 외래키와 coffee에 해당하는 외래키를 전부 가지고 있게 된다.

 

따라서 orders와 coffee의 N 대 N 연관관계를 구현할 때는 언제나 중간에 order_coffee를 통해 값을 전달해주면 된다.

 

 

 

일대일 연관관계(1 대 1)

일대일 연관관계 맵핑은 다대일 연관관계 맵핑과 동일한 방법으로 하면 된다.

일대일 단방향 맵핑에 양방향 맵핑을 추가하는법도 다대일에 일대다 맵핑을 추가하는 방법과 동일하다.

단, @OneToOne 애너테이션을 사용한다.

 

일대일 연관관계 맵핑도 외래키가 존재하는 테이블에서 먼저 연결한 다음(단방향 맵핑),

추가적인 연결이 필요하다면 반대쪽에서 연결을 진행한다.(양방향 맵핑)

 

 

앤티티간 연관관계 맵핑 권장법

왼쪽이 연관관계의 주인

서로의 상태를 공유하게끔 해주는 맵핑 메서드이다

 

* 일대다 맵핑은 사용하지 않는다 (과제에서 Message와 BinaryContent처럼)

* 먼저 다대일 단방향 맵핑부터 적용시킨다

* 다대일 단방향 맵핑을 통해 객체 그래프 탐색으로 조회할 수 없는 정보가 있을 경우, 양방향 맵핑을 적용한다.

* 연관관계를 이어주는 메서드를 작성한다. (양방향 맵핑을 적용할 경우에만)

 

내 경우 과제에선 어떻게 될지 몰라서 양방향 맵핑을 진행했으며, 연관관계를 이어주는 메서드를 전부 작성해주었다.

'Spring Boot' 카테고리의 다른 글

Spring Data JPA : 영속성 전이(Cascade)  (0) 2025.07.25
Spring Data JPA : Repository 작성  (0) 2025.07.25
Spring Data JPA : 구조 이해  (3) 2025.07.23
Spring Data JPA : 프로젝트 설정  (0) 2025.07.22
Spring Data JPA  (2) 2025.07.22