모든 연관관계는 지연로딩으로 설정.

  • 그렇지 않으면, 한 테이블을 참조할 때 해당 테이블과 연관되어 있는 모든 테이블을 조회해버림.(N + 1 문제) 결과적으로 성능이 매우 나빠짐. 따라서 지연로딩을 통해 필요한 것만 먼저 일단 가져오고 나머지는 진짜 필요할 때 요청하면 그때 가져오게 하는 것. 프록시 객체를 넣어뒀다가 필요하면 가져옴.
  • ~ToMany는 디폴트가 FetchType.LAZY 지만, ~ToOne은 FetchType.EAGER이다. 그래서 항상 FetchType을 LAZY로 명시적으로 설정해주어야 한다.

 

컬렉션은 필드에서 바로 초기화해줄 것.

List<Order> orders = new ArrayList<>(); // 생성자에서 초기화하거나 그러지 말고, 필드에서 바로 초기화 해주자.

그리고 이 컬렉션 타입을 바꾸지 말자. Hibernate가 관리하는데, 컬렉션을 바꿔버리면 Hibernate가 제대로 관리하지 못하게 될 수 있다. 그래서 필드에서 초기화하고 밖으로 꺼내지 말고 못 바꾸게 냅두자.

 

Cascade 전략

// Cascade가 있을 때 (ALL: 모든 작업(PERSIST, REMOVE, MERGE)
OrderItem orderItem1 = new OrderItem();
OrderItem orderItem2 = new OrderItem();
Order order = new Order();

order.addOrderItem(orderItem1);
order.addOrderItem(orderItem2);

// 부모만 저장하면 자식들(orderItem1, orderItem2)도 알아서 저장된다!
em.persist(order);

 

 

연관관계 편의 메서드

    //==연관관계 메서드==
    public void setMember(Member member) {
        this.member = member;
        member.getOrders().add(this);
    }

    public void addOrderItem(OrderItem orderItem) {
        orderItems.add(orderItem);
        orderItem.setOrder(this);
    }

    public void setDelivery(Delivery delivery) {
        this.delivery = delivery;
        delivery.setOrder(this);
    }

비즈니스 로직에서 별도로 set 해주지 않고, 엔티티 내에 이러한 메서드를 정의해놓음으로써, 원자적으로 양쪽을 모두 설정하는 것. 실수로 한 쪽을 설정을 안해버리는 경우를 예방할 수 있다. 이러한 로직은 외래 키를 관리하는 연관관계의 주인 또는 비즈니스 로직상 더 중요하고 핵심이 되는 엔티티에 만드는 것이 좋다.

 

 

 

1. 데이터베이스 관계 설계 핵심 원칙

1. 일대다 (One-to-Many): '명찰 붙이기'

  • 관계: 한 부서에 여러 직원, 한 직원은 한 부서. (한쪽만 '다'수)
  • 비유: 누구에게 명찰을 붙여줄까? → 직원(다)에게 "개발팀"이라는 명찰을 붙여준다.
  • 결론: 외래 키는 항상 '다(Many)' 쪽에 생긴다. (직원 테이블에 부서_ID)

2. 다대다 (Many-to-Many): '참석자 명단 만들기'

  • 관계: 한 학생은 여러 과목, 한 과목에 여러 학생. (양쪽 다 '다'수)
  • 비유: 학생증이나 과목 칠판에 모든 정보를 적을 수 없으니, **별도의 '수강신청서'(참석자 명단)**를 만든다.
  • 결론: 양쪽의 ID를 모두 외래 키로 갖는 중간 연결 테이블이 생긴다. (수강신청 테이블)

3. 일대일 (One-to-One): '개인 사물함'

  • 관계: 한 회원에 하나의 상세정보, 한 상세정보는 한 회원에게. (1:1 짝꿍)
  • 비유: **더 중요하고 자주 조회하는 쪽(회원)**이 **종속적인 쪽(상세정보)**을 관리한다.
  • 결론: 비즈니스적으로 더 중요하거나 자주 접근하는 쪽이 외래 키를 갖고 연관관계의 주인이 된다. (보통 주문이 배송의 외래 키를 가짐)

2. JPA 엔티티 매핑

  1. 모든 연관관계는 지연 로딩(LAZY)으로
    • 이유: 즉시 로딩(EAGER)은 예측 불가능한 SQL을 유발하는 N+1 문제의 주범입니다.
    • 해결: 기본은 LAZY로 설정하고, 정말 데이터가 함께 필요할 때만 JPQL의 **fetch join**을 사용해 한 번에 효율적으로 가져옵니다.
  2. Cascade(영속성 전이)는 '소유' 관계에만 신중하게!
    • 역할: 부모를 저장/삭제할 때 자식도 함께 처리하는 '도미노' 효과.
    • 언제: Order가 OrderItem을 소유하는 것처럼, 생명주기가 완전히 의존적일 때 사용합니다. (CascadeType.ALL, orphanRemoval=true)
    • 주의: 자식이 독립적인 생명주기를 가지면(예: OrderItem과 Item) 절대 사용하면 안 됩니다.
  3. 양방향 관계에는 '연관관계 편의 메서드'를
    • 이유: order.setMember(member)만 호출하고 member.getOrders().add(order)를 누락하는 실수를 막기 위함입니다. 객체 세상에서의 데이터 불일치를 방지합니다.
    • 방법: 하나의 메서드 안에서 양쪽 객체의 값을 모두 설정해 실수를 원천 차단합니다.
  4. @Setter 사용은 가급적 피하기
    • 이유: 아무 곳에서나 데이터를 변경할 수 있게 열어두면, 나중에 변경 원인을 추적하기 매우 어렵습니다.
    • 방법: cancel(), addStock()처럼 의도가 명확한 비즈니스 메서드를 만들어 그 안에서만 데이터를 변경합니다.
  5. 컬렉션은 필드에서 바로 초기화
    • 이유: NullPointerException을 방지하고, 하이버네이트가 제공하는 내장 컬렉션과의 충돌을 막아줍니다.
    • 방법: private List<Order> orders = new ArrayList<>(); 처럼 필드를 선언할 때 바로 초기화하는 것이 가장 안전하고 간결합니다.

3. 문제 해결 및 디버깅.

  1. 테이블이 아예 생성되지 않을 때
    • application.yml(또는 properties) 파일에 spring.jpa.hibernate.ddl-auto: create (개발용) 설정이 있는지 가장 먼저 확인합니다.
  2. 특정 테이블만 생성되지 않을 때
    • SQL 예약어 충돌을 가장 먼저 의심해야 합니다. (우리가 겪었던 Order 클래스 문제)
    • 해결책 1 (권장): Orders처럼 충돌하지 않는 이름으로 클래스명을 변경하고, 관련된 모든 참조를 수정합니다.
    • 해결책 2 (차선책): globally_quoted_identifiers: true 설정을 추가해 모든 테이블/컬럼명에 따옴표를 강제합니다.
  3. 원인을 알 수 없는 에러가 발생할 때
    • 애플리케이션 실행 로그가 최고의 단서입니다.
    • 로그를 위에서부터 천천히 읽으며 가장 먼저 나타나는 ERROR 또는 WARN 메시지를 찾으세요. 그 뒤에 나오는 에러들은 대부분 첫 에러로 인한 연쇄 반응일 뿐입니다.
    • (우리가 발견한 status enum() 오류처럼, 로그를 자세히 보면 Hibernate가 만든 SQL 구문 자체의 오류를 찾을 수 있습니다.)

 

@RequiredArgsConstructor는 final이 붙은 필드에 대해서만 생성자를 만들어준다.

테스트 코드에 @Transactional 어노테이션이 있으면, 롤백을 해준다 기본적으로

@SpringBootTest는 스프링부트를 띄우고 테스트를 진행하겠다는 것

test 패키지에 resources/application.yml을 둠으로써 테스트에만 설정을 적용시킬 수 있다. 덕분에 테스트를 할 때 in-memory db를 이용해서 테스트를 진행할 수 있다. 

 

    @Test
    public void 중복_회원_예외() throws Exception {
        // given
        Member member1 = new Member();
        member1.setName("kim");

        Member member2 = new Member();
        member2.setName("kim");

        // when
        memberService.join(member1);

        // then
        assertThrows(IllegalStateException.class, () -> memberService.join(member2));
    }