1. ORM (Object-Relational Mapping)
1️⃣ ORM의 개념
ORM은 객체지향 언어의 객체와 관계형 데이터베이스의 테이블을 자동으로 매핑해주는 기술
- 클래스 ↔ 테이블
- 객체 ↔ 행(Row)
- 필드 ↔ 컬럼
- 객체 참조 ↔ 외래키(FK)
SQL 중심이 아니라 객체 중심으로 DB를 다룰 수 있게 해주는 계층
2️⃣ ORM이 필요한 이유
객체지향과 RDB는 근본적으로 사고방식이 다름
- 객체는 참조(reference)를 사용
- DB는 외래키(FK)를 사용
- 상속, 다형성 개념이 DB에는 없음
➡️ ORM은 이 패러다임 불일치를 해결해줌
3️⃣ ORM의 장단점
1) 장점
- 반복적인 CRUD SQL 감소
- 객체 그래프 탐색 가능
- DB 벤더 독립성
- 유지보수성 향상
2) 단점
- 내부 동작을 모르면 성능 이슈 발생
- 복잡한 쿼리 튜닝이 어려울 수 있음
- 잘못 사용 시 쿼리 폭발(N+1 문제)
4️⃣ 실습
package com.example.boardv1.board;
import java.sql.Timestamp;
import org.hibernate.annotations.CreationTimestamp;
import com.example.boardv1.user.User;
import jakarta.persistence.Entity;
import jakarta.persistence.GeneratedValue;
import jakarta.persistence.GenerationType;
import jakarta.persistence.Id;
import jakarta.persistence.ManyToOne;
import jakarta.persistence.Table;
import lombok.Data;
import lombok.NoArgsConstructor;
/**
* 데이터베이스 세상의 테이블을 자바 세상에 모델링한 결과 = 엔티티
*/
@NoArgsConstructor // 디폴트 생성자
@Data // getter, setter, toString
@Entity // 해당 어노테이션을 보고 컴퍼넌트 스캔 후 데이터베이스에 테이블을 생성
@Table(name = "board_tb") // 테이블명 설정
public class Board { // user 1, Board N
@Id // id를 primary key로 설정
@GeneratedValue(strategy = GenerationType.IDENTITY) // auto increment 설정
private Integer id;
private String title;
private String content;
// private Integer userId;
@ManyToOne // FK지정 / Board가 many, User가 one
private User user; // user_id = 1 (select * from user_tb where id = 1)
@CreationTimestamp
private Timestamp createdAt; // import 주의!! (java.sql)
}insert into user_tb (username, password, email, created_at) values ('ssar', '1234', 'ssar@nate.com', now());
insert into user_tb (username, password, email, created_at) values ('cos', '1234', 'cos@nate.com', now());
insert into board_tb (user_id, title, content, created_at) values (1, 'title1', 'content1', now());
insert into board_tb (user_id, title, content, created_at) values (1, 'title2', 'content2', now());
insert into board_tb (user_id, title, content, created_at) values (1, 'title3', 'content3', now());
insert into board_tb (user_id, title, content, created_at) values (2, 'title4', 'content4', now());
insert into board_tb (user_id, title, content, created_at) values (2, 'title5', 'content5', now());
insert into board_tb (user_id, title, content, created_at) values (2, 'title6', 'content6', now());
Board 테이블에는 user_id(FK)만 저장되고,JPA는 그 user_id를 이용해 User 객체를 “필요한 시점에” 조회해서User user필드에 매핑해줌
2. LAZY 전략과 EAGER 전략
1️⃣ LAZY 로딩 전략 (지연 로딩)
1) LAZY의 개념
연관된 엔티티를 실제로 사용할 때 조회하는 전략입니다.
@ManyToOne(fetch = FetchType.LAZY)
private Team team;2) LAZY의 동작 방식
- Member 조회
- Team은 실제 객체가 아닌 프록시 객체로 대체
member.getTeam()호출 시 SQL 실행
SELECT * FROM member WHERE id = 1;
-- team 접근 시
SELECT * FROM team WHERE id = 10;3) LAZY의 장점
- 불필요한 데이터 조회 방지
- 초기 쿼리가 가벼움
- 성능 최적화에 매우 유리
- 실무 기본 전략
4) LAZY의 단점
- 트랜잭션 종료 후 접근 시 예외 발생
LazyInitializationException
- N+1 문제 발생 가능
5) 실습
package com.example.boardv1.board;
import java.sql.Timestamp;
import org.hibernate.annotations.CreationTimestamp;
import com.example.boardv1.user.User;
import jakarta.persistence.Entity;
import jakarta.persistence.FetchType;
import jakarta.persistence.GeneratedValue;
import jakarta.persistence.GenerationType;
import jakarta.persistence.Id;
import jakarta.persistence.ManyToOne;
import jakarta.persistence.Table;
import lombok.Data;
import lombok.NoArgsConstructor;
/**
* 데이터베이스 세상의 테이블을 자바 세상에 모델링한 결과 = 엔티티
*/
@NoArgsConstructor // 디폴트 생성자
@Data // getter, setter, toString
@Entity // 해당 어노테이션을 보고 컴퍼넌트 스캔 후 데이터베이스에 테이블을 생성
@Table(name = "board_tb") // 테이블명 설정
public class Board { // user 1, Board N
@Id // id를 primary key로 설정
@GeneratedValue(strategy = GenerationType.IDENTITY) // auto increment 설정
private Integer id;
private String title;
private String content;
// private Integer userId;
@ManyToOne(fetch = FetchType.LAZY) // FK지정 / Board가 many, User가 one
private User user; // user_id = 1 (select * from user_tb where id = 1)
@CreationTimestamp
private Timestamp createdAt; // import 주의!! (java.sql)
}package com.example.boardv1.board;
import java.util.List;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.data.jpa.test.autoconfigure.DataJpaTest;
import org.springframework.context.annotation.Import;
import jakarta.persistence.EntityManager;
@Import(BoardRepository.class)
@DataJpaTest // EntityManger가 ioc에 등록됨
public class BoardRepositoryTest {
@Autowired // 어노테이션 DI 기법
private BoardRepository boardRepository;
@Autowired
private EntityManager em;
@Test
public void orm_test() {
int id = 1;
Board board = boardRepository.findById(id).get();
System.out.println("board -> user -> id : " + board.getUser().getId());
}
}
package com.example.boardv1.board;
import java.util.List;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.data.jpa.test.autoconfigure.DataJpaTest;
import org.springframework.context.annotation.Import;
import jakarta.persistence.EntityManager;
@Import(BoardRepository.class)
@DataJpaTest // EntityManger가 ioc에 등록됨
public class BoardRepositoryTest {
@Autowired // 어노테이션 DI 기법
private BoardRepository boardRepository;
@Autowired
private EntityManager em;
@Test
public void orm_test() {
int id = 1;
Board board = boardRepository.findById(id).get();
System.out.println("board -> user -> id : " + board.getUser().getId());
System.out.println("----------------------------------------------");
System.out.println("board -> user -> username : " + board.getUser().getUsername());
}
}
2️⃣ EAGER 로딩 전략 (즉시 로딩)
1) EAGER의 개념
엔티티를 조회할 때 연관 엔티티도 즉시 함께 조회하는 전략
@ManyToOne(fetch = FetchType.EAGER)
private Team team;2) EAGER의 동작 방식
Member를 조회하는 순간 Team도 함께 조회
SELECT m.*, t.*
FROM member m
LEFT JOIN team t ON m.team_id = t.id;3) EAGER의 장점
- 연관 엔티티가 항상 보장됨
- 단순한 구조에서는 편리
4) EAGER의 단점
- 필요 없는 JOIN 증가
- 성능 저하 위험
- 쿼리 예측이 어려움
- 튜닝 및 유지보수 난이도 상승
5) 실습
package com.example.boardv1.board;
import java.sql.Timestamp;
import org.hibernate.annotations.CreationTimestamp;
import com.example.boardv1.user.User;
import jakarta.persistence.Entity;
import jakarta.persistence.FetchType;
import jakarta.persistence.GeneratedValue;
import jakarta.persistence.GenerationType;
import jakarta.persistence.Id;
import jakarta.persistence.ManyToOne;
import jakarta.persistence.Table;
import lombok.Data;
import lombok.NoArgsConstructor;
/**
* 데이터베이스 세상의 테이블을 자바 세상에 모델링한 결과 = 엔티티
*/
@NoArgsConstructor // 디폴트 생성자
@Data // getter, setter, toString
@Entity // 해당 어노테이션을 보고 컴퍼넌트 스캔 후 데이터베이스에 테이블을 생성
@Table(name = "board_tb") // 테이블명 설정
public class Board { // user 1, Board N
@Id // id를 primary key로 설정
@GeneratedValue(strategy = GenerationType.IDENTITY) // auto increment 설정
private Integer id;
private String title;
private String content;
// private Integer userId;
@ManyToOne(fetch = FetchType.EAGER) // FK지정 / Board가 many, User가 one
private User user; // user_id = 1 (select * from user_tb where id = 1)
@CreationTimestamp
private Timestamp createdAt; // import 주의!! (java.sql)
}package com.example.boardv1.board;
import java.util.List;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.data.jpa.test.autoconfigure.DataJpaTest;
import org.springframework.context.annotation.Import;
import jakarta.persistence.EntityManager;
@Import(BoardRepository.class)
@DataJpaTest // EntityManger가 ioc에 등록됨
public class BoardRepositoryTest {
@Autowired // 어노테이션 DI 기법
private BoardRepository boardRepository;
@Autowired
private EntityManager em;
@Test
public void orm_test() {
int id = 1;
Board board = boardRepository.findById(id).get();
System.out.println("board -> user -> id : " + board.getUser().getId());
}
}
3️⃣ LAZY vs EAGER 요약 비교
구분 | LAZY | EAGER |
조회 시점 | 접근 시 | 즉시 |
성능 제어 | 좋음 | 나쁨 |
실무 사용 | 매우 많음 | 거의 없음 |
권장 여부 | ✅ | ❌ |
4️⃣ JPA 기본 Fetch 전략
1) 연관관계별 기본값
@ManyToOne→ EAGER
@OneToOne→ EAGER
@OneToMany→ LAZY
@ManyToMany→ LAZY
⚠️ 기본값이 EAGER인 관계도 실무에서는 LAZY로 변경하는 것이 일반적
5️⃣ 실무 권장 전략 (핵심)
1) 기본 원칙
- 모든 연관관계는 LAZY로 설정
- 필요한 경우에만 쿼리에서 제어
2) Fetch Join 사용
SELECT m FROM Member m JOIN FETCH m.team WHERE m.id = :id
- LAZY 전략 유지
- 한 번의 SQL로 연관 엔티티 조회
- 성능 + 제어력 모두 확보
3) 실습
package com.example.boardv1.board;
import java.sql.Timestamp;
import org.hibernate.annotations.CreationTimestamp;
import com.example.boardv1.user.User;
import jakarta.persistence.Entity;
import jakarta.persistence.FetchType;
import jakarta.persistence.GeneratedValue;
import jakarta.persistence.GenerationType;
import jakarta.persistence.Id;
import jakarta.persistence.ManyToOne;
import jakarta.persistence.Table;
import lombok.Getter;
import lombok.NoArgsConstructor;
import lombok.Setter;
/**
* 데이터베이스 세상의 테이블을 자바 세상에 모델링한 결과 = 엔티티
*/
@NoArgsConstructor // 디폴트 생성자
@Getter
@Setter
@Entity // 해당 어노테이션을 보고 컴퍼넌트 스캔 후 데이터베이스에 테이블을 생성
@Table(name = "board_tb") // 테이블명 설정
public class Board { // user 1, Board N
@Id // id를 primary key로 설정
@GeneratedValue(strategy = GenerationType.IDENTITY) // auto increment 설정
private Integer id;
private String title;
private String content;
// private Integer userId;
@ManyToOne(fetch = FetchType.LAZY) // FK지정 / Board가 many, User가 one
private User user; // user_id = 1 (select * from user_tb where id = 1)
@CreationTimestamp
private Timestamp createdAt; // import 주의!! (java.sql)
@Override
public String toString() {
return "Board [id=" + id + ", title=" + title + ", content=" + content + ", user=" + user + ", createdAt="
+ createdAt + "]";
}
}
package com.example.boardv1.board;
import java.util.List;
import java.util.Optional;
import org.springframework.stereotype.Repository;
import jakarta.persistence.EntityManager;
import jakarta.persistence.Query;
import lombok.RequiredArgsConstructor;
/**
* 하이버네이트 기술
*/
@RequiredArgsConstructor // final이 붙어 있는 모든 필드를 초기화하는 생성자를 만들어줌.
@Repository
public class BoardRepository {
private final EntityManager em;
// DI = 의존성 주입 (의존하고 있는게 IoC에 떠있어야됨)
// public BoardRepository(EntityManager em) {
// this.em = em;
// }
public Optional<Board> findByIdJoinUser(int id) {
Query query = em.createQuery("select b from Board b join fetch b.user u where b.id = :id", Board.class);
query.setParameter("id", id);
try {
Board board = (Board) query.getSingleResult();
return Optional.of(board);
} catch (Exception e) {
return Optional.ofNullable(null);
}
}
}6️⃣ 전략별 비교 실습
1) EAGER 전략 - 게시글 목록 보기

1. 내가 직접 실행한 쿼리
Hibernate:
select
b1_0.id,
b1_0.content,
b1_0.created_at,
b1_0.title,
b1_0.user_id
from
board_tb b1_0
order by
b1_0.id desc
List<Board>(Board Table) 6건 ~~ (id, title, content, user_id, created_at)
Eager전략 - 한 건일 때는 join
여러 건일 때는 user_id를 모아서 em.find로 User클래스를 다 조회함
user_id -> (1, 1, 1, 2, 2, 2)
em.find(User.class, 1);
em.find(User.class, 1);
em.find(User.class, 1);
em.find(User.class, 2);
em.find(User.class, 2);
em.find(User.class, 2);
튜닝전략
(1) Lazy -> select * from board_tb
(2) in query -> select * from user_tb where id in(1,2);
2. 내가 실행한 쿼리 X
Hibernate:
select
u1_0.id,
u1_0.created_at,
u1_0.email,
u1_0.password,
u1_0.username
from
user_tb u1_0
where
u1_0.id=?
Hibernate:
select
u1_0.id,
u1_0.created_at,
u1_0.email,
u1_0.password,
u1_0.username
from
user_tb u1_0
where
u1_0.id=?
캐싱 됐기 때문에 1번 아이디, 2번 아이디 한번씩만 select
(만약에 서로 다른 유저 6명이라면 6번 select될 것!)2) LAZY 전략 - 게시글 목록 보기

3) in query (EAGER 전략 + application.properties에 내용 추가)
spring.jpa.properties.hibernate.default_batch_fetch_size=10

Share article