Spring Boot
강의

#13 - JPA 실무 가이드 2부 - FK, PK 관리

중년개발자
중년개발자

@loxo

12일 전

17

JPA 실무 가이드 2부: 연관관계와 복합키의 모든 것

Spring Boot 4 & PostgreSQL 환경에서의 실용주의(Pragmatic) 고급 매핑
"왜 객체를 넣으면 ID가 저장될까요? 원리를 파헤칩니다."


1. 연관관계의 핵심: "객체는 외래키(FK)를 모른다"

데이터베이스(DB) 세상에서는 **"외래키(FK)"**를 사용해 테이블을 연결합니다.
하지만 객체 세상에서는 **"참조(Reference)"**를 사용해 연결합니다.

  • DB: SELECT * FROM orders WHERE member_id = 1
  • Java: order.getMember()

JPA의 역할은 이 패러다임의 불일치를 해결하는 것입니다.
우리가 order.setMember(member)라고 하면,
JPA가 알아서 INSERT INTO orders (member_id) VALUES (1)로 통역해 줍니다.


2. @ManyToOne 단방향: 실무 최강의 조합

가장 많이 쓰는 패턴은 **"다대일(N:1) 단방향"**입니다.
주문(Order) 입장에서 생각해보세요. "나는 회원(Member)을 알고 싶어"

java
@Entity public class Order { // DB의 'member_id' 컬럼과 매핑됩니다. // fetch = LAZY: "회원 정보는 진짜 필요할 때만 가져와!" (성능 최적화 필수) @ManyToOne(fetch = FetchType.LAZY) @JoinColumn(name = "member_id") private Member member; }
  • LAZY가 필수인가요?
    • EAGER (즉시 로딩)로 하면 Order 하나 조회할 때마다 Member도 무조건 같이 조회합니다.
    • 만약 주문 목록 100개를 조회한다면? -> 쿼리가 101방(1 + 100) 나갑니다. (이것이 전설의 N+1 문제)
    • LAZY는 **"가짜 객체(Proxy)"**만 넣어두고, 실제 member.getName()을 호출할 때 쿼리를 날립니다.

2-1. 요즘 트렌드: @ManyToOne vs 그냥 ID만 쓰기 (Long memberId)

QueryDSL이 있으니 굳이 연관관계를 맺지 않고 ID만 가져도 되지 않을까?
최근 실무(MSA 전환 과도기)에서 많이 고민하는 주제입니다.

특징Pattern A: @ManyToOne (정석)Pattern B: Long memberId (ID 참조)
장점데이터 무결성(FK 제약), 객체 그래프 탐색(order.getMember().getName())결합도 낮음, MSA 전환 용이, 복잡한 연관관계 없음
단점잘못 쓰면 N+1 문제, 결합도 높음객체 탐색 불가, 조인시 직접 ON절 작성 필요
추천같은 도메인(패키지) 내의 관계다른 도메인(MSA 분리 예정) 간의 관계

💡 가이드:

  • 회원-주문 처럼 강한 결합이면 @ManyToOne을 쓰세요. (FK 도움 및 JPA 이점 활용)
  • 주문-배송로그 처럼 느슨하거나, 다른 서비스로 쪼개질 것 같으면 Long memberId만 가지세요.
  • QueryDSL: 어차피 조인(join(order.member, member))은 둘 다 문법만 다를 뿐 똑같이 잘 됩니다.

3. INSERT 시점의 고민: "객체를 다 조회해야 하나요?"

Order를 저장하려면 Member가 필요합니다.
그런데 우리는 **회원 ID(PK)**만 알고 있는 경우가 많습니다. (화면에서 넘어온 memberId)

✅ 정석 방법: 조회 후 할당

가장 안전하고 일반적인 방법입니다. 이미 DB에 있는 친구를 데려와서 연결해 주는 것이죠.

java
@Transactional public Long createOrder(Long memberId) { // 1. "누구니?" -> DB에서 찾아옴 (SELECT 쿼리 발생) Member member = memberRepository.findById(memberId) .orElseThrow(() -> new NotFoundException()); // 2. "너랑 짝꿍이야" -> 객체 연결 Order order = Order.builder() .member(member) .build(); // 3. "저장!" -> JPA가 member의 ID를 꺼내서 FK로 저장함 orderRepository.save(order); // 💡 INSERT는 Dirty Checking 대상이 아닙니다! // 처음 저장할 때는 반드시 repository.save()를 호출해야 ID가 생성됩니다. (IDENTITY 전략) }

✅ 최적화 방법 (팁): 프록시(Proxy) 활용

"어차피 ID만 있으면 되는데, 굳이 SELECT 쿼리를 날려야 해?"
이럴 때 쓰는 것이 getReferenceById입니다.

java
@Transactional public Long createOrderFast(Long memberId) { // 1. "이름표만 줘" -> 가짜 객체(Proxy) 생성 (SELECT 안 함!) // 이 객체는 ID값만 가지고 있고, 나머지는 비어있습니다. Member memberProxy = memberRepository.getReferenceById(memberId); // 2. 객체 연결 (ID는 있으니까 문제 없음) Order order = Order.builder() .member(memberProxy) .build(); // 3. 저장! (DB에는 ID만 잘 들어감) orderRepository.save(order); }

주의: 만약 없는 memberId였다면? -> save 하는 시점에 FK 오류가 터집니다. (데이터 정합성은 DB가 지켜줌)


4. 복합키(Composite Key): "주민번호 + 이름이 PK라고요?"

보통은 id(Long) 하나만 PK로 쓰지만, 레거시 DB나 특정 상황에서는 두 개 이상의 컬럼을 묶어서 PK로 써야 할 때가 있습니다. (예: 주문번호 + 상품번호)

JPA에서는 이를 위해 별도의 **"식별자 클래스"**를 만들어야 합니다. (@IdClass)

✅ 실전 코드 (OrderItem: 주문번호 + 순번)

1. 식별자 클래스 (DTO 처럼 생겼지만 PK입니다)

java
@Getter @NoArgsConstructor @EqualsAndHashCode // ⚠️ 필수! (JPA가 이걸로 식별함) public class OrderItemKey implements Serializable { private Long orderId; private Long itemSeq; }

2. 엔티티 적용 (@IdClass)

java
@Entity @Getter @IdClass(OrderItemKey.class) // "나의 PK는 저 클래스야" @NoArgsConstructor(access = AccessLevel.PROTECTED) public class OrderItem { @Id // 복합키 1 @Column(name = "order_id") private Long orderId; @Id // 복합키 2 @Column(name = "item_seq") private Long itemSeq; private int quantity; // ... }

3. 조회 방법 (Repository)

java
// 식별자 객체를 만들어서 찾아야 함 OrderItemKey key = new OrderItemKey(100L, 1L); OrderItem item = repository.findById(key).orElseThrow(...);

실무 팁: 복합키는 설정도 복잡하고 쿼리하기도 귀찮습니다. 가능하면 그냥 Long id (대리키) 하나만 쓰는 게 정신 건강에 좋습니다. 어쩔 수 없을 때만 쓰세요!

💡 Why? (복합키 대신 대리키를 쓰는 이유)

  1. API 설계의 단순함: GET /items/{id} 처럼 깔끔하게 호출할 수 있습니다. 복합키라면 GET /items?orderId=1&itemSeq=2 처럼 지저분해집니다.
  2. 비즈니스 변경에 유연: 나중에 비즈니스 키(주문번호 등)가 변경되어도 PK(Long id)는 불변이므로 DB 구조를 뜯어고칠 필요가 없습니다.
  3. FK 매핑의 편리함: 다른 테이블에서 OrderItem을 참조할 때, 복합키라면 FK 컬럼이 2개가 생겨서 관리가 매우 복잡해집니다.

결론: 새로운 테이블이라면 무조건 Long id (IDENTITY)를 쓰세요. 복합키는 레거시 DB 연동 때만 씁니다.

#JPA#Spring Boot#PostgreSQL#객체지향#연관관계매핑

댓글 0

Ctrl + Enter를 눌러 등록할 수 있습니다
※ AI 다듬기는 내용을 정제하는 보조 기능이며, 최종 내용은 사용자가 확인해야 합니다.