01. Table (Entity)

이번 포스팅에서는 Django 에서 테이블을 만드는방법과, Spring 에서 테이블을 만드는 방법에 대해 비교형식으로 공부를 해보고자 한다. 둘다 코드로 테이블을 관리한다는점은 동일하고, Django 와 Spring 에서 같은역할을 하지만 코드로 어떻게 다른지 공부해보자.

  Django Spring
1: 1 models.OneToOneField() @OneToOne
1: N models.ForeignKey() @OneToMany
@ManyToOne
M: N models.ManyToManyField() @ManyToMany

Spring에서 @OneToMany@ManyToOne 애노테이션의 fetch 방식이 다르기 때문에 잘 이해하고 사용하는 것이 중요하다. 김영한님 강의에서 보면 모두 Lazy 방식으로 변경하라고 권장한다. ( N + 1 ) 문제 연관되어있음

  • @OneToMany
    • default: Lazy
  • @ManyToOne
    • default: Eager

Django ORM 에서는 기본적으로 Lazy 로딩 방식이며, 객체가 실제로 평가되기 전까진 DB 에 SQL 을 호출하지 않는다.
조인이 필요한 경우

  • 정참조
  • selected_related()
  • 역참조
    • prefetch_related()

그리고 Django에서는 역참조 이름을 related_name 옵션을 주어서 설정할 수 있으며, 따로 지정하지않으면 xxx_set 와 같은 형식으로 디폴트로 지정된다. 실무에서는 역참조 이름이 중복될수도 있으니 네이밍 규칙을 정해서 “일관성” 있게 사용하는것이 좋다.

또한 김영한님의 스프링부트 강의에서는 ManyToMany 옵션 사용을 지양하고 있는데, 그 이유는 RDBMS 에서는 M: N 관계를 표현하기 위해 중간테이블을 두게 되고, 각각 1:N, M:1 방식으로 연결해서 중간테이블을 만들기 때문이다.

Django 에서도 ManyToManyField() 로 M: N 관계를 설정할 수는 있지만, through 옵션을 따로 주지않으면 장고가 내부적으로 fk 컬럼만 있는 테이블을 자동으로 생성시키게 된다.

실무에서는 fk 정보외에도 다른 다양한 데이터들을 더 담아야 할 일이 생기므로 가급적 ManyToMany 사용시 중간테이블을 꼭. 직접 만들어서 사용하는것이 좋다.

  • Django
    • ManyToManyField() 사용시 through 옵션 무조건 줘서, 직접 중간테이블을 관리하자.
  • Spring
    • @ManyToMany 어노테이션 사용을 지양하고, OneToMany, ManyToOne 어노테이션을 사용해서 직접 중간테이블을 만들자.

Django

from django.db import models
from django.conf import settings

class Genre(models.Model):
    name = models.CharField(max_length=50)

class Movie(models.Model):
    title = models.CharField(max_length=200)
    title_en = models.CharField(max_length=200)
    overview = models.TextField()
    release_date = models.CharField(max_length=50, null=True)
    poster_path = models.URLField(max_length=1000, blank=True)
    backdrop_path = models.URLField(max_length=1000, blank=True)
    adult = models.BooleanField()
    vote_average = models.IntegerField()

    like_users = models.ManyToManyField(
      settings.AUTH_USER_MODEL,
      related_name='like_movies'
    )
    genre = models.ManyToManyField(Genre, related_name='movies')


class Review(models.Model):
    title = models.CharField(max_length=100)
    content = models.TextField()
    created_at = models.DateTimeField(auto_now_add=True)
    updated_at = models.DateTimeField(auto_now=True)

    movie = models.ForeignKey(Movie,
                              on_delete=models.CASCADE,
                              related_name='reviews'
    )
    user = models.ForeignKey(settings.AUTH_USER_MODEL,
                             on_delete=models.CASCADE, # 유저가 삭제되면 리뷰도 삭제됨.
                             related_name='reviews'
    )
    like_users = models.ManyToManyField(settings.AUTH_USER_MODEL, related_name='like_reviews') # related : 유저가 좋아요한 리뷰들


class Comment(models.Model):
    content = models.TextField()
    created_at = models.DateTimeField(auto_now_add=True)
    updated_at = models.DateTimeField(auto_now=True)

    review = models.ForeignKey(Review, on_delete=models.CASCADE, related_name='comments')
    user = models.ForeignKey(settings.AUTH_USER_MODEL, on_delete=models.CASCADE, related_name='comments')

Spring

package jpabook.jpashop.domain;

import jakarta.persistence.*;
import lombok.Getter;
import lombok.Setter;

import java.util.ArrayList;
import java.util.List;

import jpabook.jpashop.domain.Address;
import jpabook.jpashop.domain.Order;


@Entity
@Getter @Setter
public class Member {

    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    @Column(name = "member_id")
    private Long id;

    private String name;

    @Embedded
    private Address address;

    @OneToMany(mappedBy = "member")  // order Table(Entity) 에 있는 member field 에 의해 매핑이 되었다. (읽기 전용)
    private List<Order> orders = new ArrayList<>();

}



@Entity
@Getter @Setter
@Table(name = "orders")
public class Order {

    @Id @GeneratedValue(strategy = GenerationType.IDENTITY)
    @Column(name = "order_id")
    private Long id;

    @ManyToOne
    @JoinColumn(name = "member_id")
    private Member member;

    @OneToMany(mappedBy = "order")
    private List<OrderItem> orderItems = new ArrayList<>();

    @OneToOne
    @JoinColumn(name = "delivery_id")
    private Delivery delivery;

    private LocalDateTime orderDate;

    @Enumerated(value = EnumType.STRING)
    private OrderStatus status;  // 주문상태 (ORDER, CANCEL)
}

느낀점

  • 개인적으로 장고로 커리어를 시작하고, 스프링을 뒤늦게 배우는 입장인지라, Django 코드가 더 눈에 잘들어옵니다.
  • 스프링이나 장고나 하는 역할은 비슷한데, 스프링 진영에서는 Annotation 으로 옵션을 지정해줘야 하는곳이 너무 많은것 같습니다.

댓글남기기