모든 연관관계는 지연로딩으로 설정.
- 그렇지 않으면, 한 테이블을 참조할 때 해당 테이블과 연관되어 있는 모든 테이블을 조회해버림.(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 엔티티 매핑
- 모든 연관관계는 지연 로딩(LAZY)으로
- 이유: 즉시 로딩(EAGER)은 예측 불가능한 SQL을 유발하는 N+1 문제의 주범입니다.
- 해결: 기본은 LAZY로 설정하고, 정말 데이터가 함께 필요할 때만 JPQL의 **fetch join**을 사용해 한 번에 효율적으로 가져옵니다.
- Cascade(영속성 전이)는 '소유' 관계에만 신중하게!
- 역할: 부모를 저장/삭제할 때 자식도 함께 처리하는 '도미노' 효과.
- 언제: Order가 OrderItem을 소유하는 것처럼, 생명주기가 완전히 의존적일 때 사용합니다. (CascadeType.ALL, orphanRemoval=true)
- 주의: 자식이 독립적인 생명주기를 가지면(예: OrderItem과 Item) 절대 사용하면 안 됩니다.
- 양방향 관계에는 '연관관계 편의 메서드'를
- 이유: order.setMember(member)만 호출하고 member.getOrders().add(order)를 누락하는 실수를 막기 위함입니다. 객체 세상에서의 데이터 불일치를 방지합니다.
- 방법: 하나의 메서드 안에서 양쪽 객체의 값을 모두 설정해 실수를 원천 차단합니다.
- @Setter 사용은 가급적 피하기
- 이유: 아무 곳에서나 데이터를 변경할 수 있게 열어두면, 나중에 변경 원인을 추적하기 매우 어렵습니다.
- 방법: cancel(), addStock()처럼 의도가 명확한 비즈니스 메서드를 만들어 그 안에서만 데이터를 변경합니다.
- 컬렉션은 필드에서 바로 초기화
- 이유: NullPointerException을 방지하고, 하이버네이트가 제공하는 내장 컬렉션과의 충돌을 막아줍니다.
- 방법: private List<Order> orders = new ArrayList<>(); 처럼 필드를 선언할 때 바로 초기화하는 것이 가장 안전하고 간결합니다.
3. 문제 해결 및 디버깅.
- 테이블이 아예 생성되지 않을 때
- application.yml(또는 properties) 파일에 spring.jpa.hibernate.ddl-auto: create (개발용) 설정이 있는지 가장 먼저 확인합니다.
- 특정 테이블만 생성되지 않을 때
- SQL 예약어 충돌을 가장 먼저 의심해야 합니다. (우리가 겪었던 Order 클래스 문제)
- 해결책 1 (권장): Orders처럼 충돌하지 않는 이름으로 클래스명을 변경하고, 관련된 모든 참조를 수정합니다.
- 해결책 2 (차선책): globally_quoted_identifiers: true 설정을 추가해 모든 테이블/컬럼명에 따옴표를 강제합니다.
- 원인을 알 수 없는 에러가 발생할 때
- 애플리케이션 실행 로그가 최고의 단서입니다.
- 로그를 위에서부터 천천히 읽으며 가장 먼저 나타나는 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));
}'공부 > Spring' 카테고리의 다른 글
| [인프런 김영한] 준영속 엔티티 그리고 변경 감지와 병합 (0) | 2025.11.18 |
|---|---|
| [인프런 김영한] 실전! 스프링 부트와 JPA 활용1 - 2 (0) | 2025.11.07 |
| [스프링 MVC 7 / 인프런 김영한] 웹 페이지 만들기 (0) | 2025.03.31 |
| [스프링 MVC 6 / 인프런 김영한] 스프링 MVC - 기본 기능 (0) | 2025.03.30 |
| [스프링 MVC 5 / 인프런 김영한] 스프링 MVC - 구조 이해 (0) | 2025.03.30 |
