09a 영속성 컨텍스트·더티체킹 📚 전체 맵 09c 지연로딩·N+1·해결
Series 09 · JPA 완전 이해 · 2/3

ONE TO
MANY
RELATION

@ManyToOne · @OneToMany · 연관관계 주인 · 양방향 vs 단방향
DB의 FK와 Java 객체 참조를 JPA가 어떻게 연결하는가

09a 영속성 컨텍스트·생명주기·더티체킹 09b @OneToMany·연관관계 매핑 09c 지연로딩·N+1·해결
01 Java 02 Spring 03 웹/통신 04~08 프로젝트·심화·DI 09 JPA 완전 이해 10 Bean·어노테이션 11 Docker
이 파일의 학습 목적
09b — @ManyToOne · @OneToMany · 연관관계 주인

board.getUser().getNickname()처럼 DB의 FK를 Java 객체로 접근할 수 있는 것은 @ManyToOne 연관관계 덕분이다. 09b는 DB FK ↔ Java 객체 참조를 JPA가 어떻게 연결하는지, 연관관계 주인이 누구인지를 정확하게 이해하는 것이 목표다.

이 파일에서 배울 것
  • 연관관계 개념 — DB FK vs Java 객체 참조
  • @ManyToOne — Board.user + @JoinColumn
  • @OneToMany + mappedBy — User.boards
  • 연관관계 주인 — FK 보유자만 DB 반영
  • 단방향 vs 양방향 — 설계 기준
프로젝트 코드 연결
  • · Board.java — @ManyToOne User user
  • · board.getUser().getNickname() — toDto()
  • · board.setUser(loginUser) — write()에서
  • · deleteByUser(User user) — 회원 탈퇴
시리즈 순서: 09a 영속성 컨텍스트 → 09b ← 지금 여기 → 09c 지연로딩·N+1
01 — 연관관계란?
DB의 FK(외래키)를 Java 객체 참조로 표현하는 방법
🔗 DB FK ↔ Java 객체 참조 Association
📝 비유 — 게시글과 작성자
DB에서는 board 테이블에 user_id 컬럼(FK)으로 연결
Java에서는 Board 객체가 User 객체를 직접 참조
board.getUserId() 대신 board.getUser().getNickname() 바로 접근 가능
User
id: Long
username: String
nickname: String
N : 1
@ManyToOne
user_id (FK)
Board
id: Long
title: String
user: User ← FK 보유
❌ FK만 저장 (불편)
public class Board {
private Long userId; // FK만
}

// 닉네임 가져오려면?
Long userId = board.getUserId();
User user = userRepo.findById(userId).get();
String nickname = user.getNickname();
// 2단계 조회 필요
✅ @ManyToOne 연관관계
public class Board {
@ManyToOne
@JoinColumn(name="user_id")
private User user; // 객체 참조
}

// 닉네임 가져오려면?
String nickname =
board.getUser().getNickname();
// JPA가 JOIN 자동 처리
02 — @ManyToOne
다대일 관계 — 가장 많이 쓰는 연관관계 · FK를 가진 쪽에 선언
🔢 Many(게시글) → One(유저) @ManyToOne
Java — 프로젝트 Board Entity @ManyToOne 전체
@Entity
public class Board {

@Id @GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;

private String title;
private String content;

@ManyToOne(fetch = FetchType.LAZY)
// Many(Board) → One(User)
// fetch = LAZY: getUser() 호출 시에만 DB 조회 (권장)

@JoinColumn(name = "user_id")
// DB 컬럼명 지정 → board.user_id 컬럼 생성
// 이 컬럼이 연관관계의 주인 (FK 보유)

private User user;

private LocalDateTime createdAt;
}

// 사용 예
Board board = boardRepository.findById(1L).get();
String nickname = board.getUser().getNickname();
// → JPA가 자동으로 JOIN 또는 추가 쿼리 실행
// → SELECT * FROM users WHERE id = board.user_id
Java — Comment Entity — ManyToOne 두 개
@Entity
public class Comment {

@Id @GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;

private String content;

@ManyToOne(fetch = FetchType.LAZY)
@JoinColumn(name = "board_id") // 댓글 N : 게시글 1
private Board board;

@ManyToOne(fetch = FetchType.LAZY)
@JoinColumn(name = "user_id") // 댓글 N : 유저 1
private User user;
}
💡 @JoinColumn(name = "user_id")
name 속성 = DB에 실제 생성될 컬럼명
생략하면 JPA가 자동으로 user_id로 만들어주지만 — 명시적으로 쓰는 게 관례
03 — @OneToMany
일대다 관계 — 단방향보다 양방향이 실용적 · mappedBy 필수
📋 One(유저) → Many(게시글 목록) @OneToMany
📚 비유 — 유저 입장에서 게시글 목록
유저 1명이 게시글을 여러 개 가짐
user.getBoards() → 이 유저가 쓴 글 목록 전체
→ DB에 새 컬럼은 만들지 않음 — Board 쪽 user_id가 이미 있으니까
Java — User Entity @OneToMany — 양방향 설정
@Entity
public class User {

@Id @GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
private String username;

@OneToMany(mappedBy = "user")
// mappedBy = "user" → Board.user 필드가 연관관계 주인
// "user_id FK는 Board 쪽에 있어, 난 조회만 할게"
// DB에 새 컬럼 안 만들고 Board.user_id 참조
private List<Board> boards = new ArrayList<>();
}

// 사용 예
User user = userRepository.findById(1L).get();
List<Board> boards = user.getBoards();
// → SELECT * FROM board WHERE user_id = 1
⚠️ mappedBy를 빠뜨리면?
JPA가 중간 테이블을 새로 만들어버림 → user_boards 같은 불필요한 테이블 생성
→ 반드시 mappedBy로 "FK는 저쪽이 가지고 있어" 명시해야 함
어노테이션선언 위치FK 보유mappedBy
@ManyToOneBoard (많은 쪽)✅ Board.user_id불필요
@OneToManyUser (하나인 쪽)❌ FK 없음필수 — "user" 지정
04 — 연관관계 주인
FK를 가진 쪽이 주인 — 주인만 INSERT/UPDATE 시 DB 반영됨
👑 연관관계 주인 = FK 보유자 Owner
🗝️비유 — 열쇠를 가진 사람
집(DB)을 관리하는 열쇠(FK)는 한 명만 가질 수 있음
Board.user = 열쇠 보유자 (연관관계 주인) → INSERT/UPDATE 시 user_id 반영
User.boards = 조회 전용 (mappedBy) → 여기서 추가해도 DB에 반영 안 됨!
❌ 주인 아닌 쪽에서 설정 (DB 반영 안 됨)
User user = userRepo.findById(1L).get();
Board board = new Board();
board.setTitle("새 글");

// 주인 아닌 쪽에서만 설정
user.getBoards().add(board); // ❌
boardRepo.save(board);
// → board.user_id = null !
// User.boards는 mappedBy → DB 반영 안 됨
✅ 주인 쪽에서 설정 (DB 반영됨)
User user = userRepo.findById(1L).get();
Board board = new Board();
board.setTitle("새 글");

// 주인 쪽(Board.user)에서 설정
board.setUser(user); // ✅
boardRepo.save(board);
// → board.user_id = 1
// INSERT INTO board (user_id...) VALUES (1...)
💡 양방향 설정 시 편의 메서드 권장
주인 쪽(Board)과 역방향(User.boards) 양쪽 모두 설정해두면 → 객체 그래프 탐색 시 일관성 유지
실제 DB 반영은 Board.user만 하지만, 같은 트랜잭션 내 User.boards도 동기화되어야 버그 없음
Java — 우리 프로젝트 편의 메서드 패턴
// Board에 편의 메서드 추가
public void setUser(User user) {
this.user = user; // 주인 쪽 설정 (DB 반영)
user.getBoards().add(this); // 역방향도 동기화 (객체 일관성)
}

// Service에서
@Transactional
public void write(BoardDto dto, User loginUser) {
Board board = new Board();
board.setTitle(dto.getTitle());
board.setContent(dto.getContent());
board.setUser(loginUser); // 편의 메서드 호출
boardRepository.save(board);
}
05 — 단방향 vs 양방향
우리 프로젝트는 어떤 방향으로 설계했나
↔️ 방향 선택 기준 설계
구분단방향양방향
설명한 쪽만 참조 (Board → User)양쪽 모두 참조 (Board ↔ User)
장점단순, 관리 쉬움user.getBoards() 편리
단점역방향 탐색 불가mappedBy 관리, 무한루프 주의
권장기본 선택 ✅역방향 조회가 자주 필요할 때
⚠️ 양방향 연관관계 — 무한루프 주의
Board → User → boards → Board → ... JSON 직렬화 시 무한루프 발생
해결책: @JsonIgnore 또는 DTO 변환 (권장)
→ 우리 프로젝트는 DTO로 변환해서 반환 → 무한루프 방지
09a 영속성 컨텍스트·더티체킹 📚 전체 맵 09c 지연로딩·N+1·해결