특징
- 객체와 테이블의 매핑, 속성과 컬럼의 매핑
- 클래스와 데이터베이스 테이블, 클래스의 필드와 테이블의 컬럼을 매핑해서 데이터베이스를 객체 지향적으로 다룰 수 있음
- 예) User 클래스가 데이터베이스의 user-tb 테이블에 매핑
- 예) User 클래스의 username 속성이 user_tb 테이블의 username 컬럼에 매핑
- 데이터베이스 독립성
- SQL 문법에 의존하지 않고 ORM이 제공하는 메서드를 통해 작업
- 데이터베이스 변경시 코드 수정 최소화
- 생산성 향상
- 반복적인 SQL 작성을 줄이고 객체지향적인 코드로 데이터베이스를 다룰 수 있어 개발 속도 높임
- 자동화된 SQL 생성
- 기본적인 CRUD(Create, Read, Update, Delete) 쿼리를 자동으로 생성하고 실행
- 개발자는 SQL 쿼리를 직접 작성할 필요 없이 객체를 조작하는 코드만 작성하면 됨
- 트랜잭션 관리
- ORM 프레임워크는 데이터베이스 트랜잭션을 관리하여 데이터의 일관성과 무결성 유지
장점
- 유지보수 용이성
- 코드에서 객체만 수정하면 돼서 데이터베이스 구조 변경 시 영향 최소화
- 효율적인 협업
- 데이터베이스에 익숙하지 않은 개발자도 객체 지향적으로 데이터베이스 사용 가능
- 보안
- SQL Injection 같은 보안 문제를 방지하는데 도움
단점
- 성능 문제
- 복잡한 쿼리나 대량 데이터 처리가 필요한 경우, 직접 작성한 SQL보다 느릴 수 있음
- 학습 곡선
- ORM 도구(JPA, Hibernate 등)의 동작 방식을 이해하고 최적화 하려면 학습 필요
- 제한된 기능
- 데이터베이스 특정 기능을 사용하는 데 제약 있을 수 있음
Java 환경에서의 ORM (Spring Boot)
- Spring Boot에서는 대표적인 ORM 프레임워크로 JPA를 사용하며, 그 구현체로 Hibernate 주로 사용
- JPA : 표준 ORM 스펙(API)
- Hibernate: JPA를 구현한 대표적인 프레임워크
ORM을 구현하기 위해 사용한 기술 참고
Hibernate
특징
- 객체와 테이블 매핑
- Java 클래스와 데이터베이스 테이블 간의 매핑 자동 처리
- 자동 SQL 생성
- 데이터베이스 작업을 위해 필요한 SQL 쿼리 자동 생성하고 실행
- 지연 로딩(Lazy Loading)
- 필요한 시점까지 데이터베이스에서 데이터를 로드하지 않아 성능 최적화 가능
- 캐시 지원
- 1차 캐시(세션 캐시)와 2차 캐시(전역 캐시)를 통해 데이터베이스의 접근 최소화하고 성능 향상시킴
- 트랜잭션 관리
- 데이터의 일관성과 무결성 유지 위해 트랜잭션 관리
익명 블로그에 ORM 활용하기
[User 테이블]
- @Builder
- 풀 생성자 만들고 Builder의 형태 잡음 ⇒ @AllArgsConstructor 어노테이션 지우고 풀 생성자에 @Builder 어노테이션 달기
- @Builder는 디폴트 생성자가 있어야 사용 가능 ⇒ @NoArgsConstructor 어노테이션은 제외하지 않음
- 컬렉션은 빌더 생성 불가 주의 ⇒ 컬렉션 포함되어있으면 빌더에서 제외됨
- @NoArgsConstructor(access = AccessLevel.PROTECTED)
- 외부에서 빈(디폴트) 생성자 생성하지 못하도록 제한(protected)
- @GeneratedValue(strategy = GenerationType.IDENTITY)
- Autoincrement
- @Column(unique = true, nullable = false)
- 컬럼제약조건 unique(index 알아서 만들어짐)와 not null
// Builder 문법
// @Builder는 @AllArgsConstructor(디폴트 생성자)가 있어야 사용 가능
// 컬렉션은 빌더 생성 불가 => 포함돼있으면 빌더에서 제외
// 풀 생성자 생성해놓고 @Builder 달아서 빌더 만듦 => 풀생성자 어노테이션 지우기
@NoArgsConstructor(access = AccessLevel.PROTECTED) // 외부에서 디폴트 생성자 생성하지 못하게 제한(protected)
@Table(name = "user_tb") // 테이블명
@Getter // private에 접근 하려면 getter 필요
@Entity // 엔티티, 이거 달아놔야 테이블 생성 가능
public class User {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY) // Autoincrement
private Integer id; // Null일것도 감안해서 Integer
@Column(unique = true, nullable = false) // 컬럼제약조건 unique(index 알아서 만들어질것), not null
private String username; // 유저 아이디는 username으로 약속되어있음, id 아님
@Column(nullable = false)
private String password;
@Column(nullable = false)
private String email;
@CreationTimestamp
private Timestamp createdAt;
// 풀 생성자 만들고 @Builder 달아서 빌더의 형태 잡음
@Builder
public User(Integer id, String password, String username, String email, Timestamp createdAt) {
this.id = id;
this.password = password;
this.username = username;
this.email = email;
this.createdAt = createdAt;
}
}[Board 클래스]
- @Builder
- 풀 생성자 만들고 Builder의 형태 잡음 ⇒ @AllArgsConstructor 어노테이션 지우고 풀 생성자에 @Builder 어노테이션 달기
- @Builder는 디폴트 생성자가 있어야 사용 가능 ⇒ @NoArgsConstructor 어노테이션은 제외하지 않음
- @NoArgsConstructor(access = AccessLevel.PROTECTED)
- 외부에서 빈(디폴트) 생성자 생성하지 못하도록 제한(protected)
- @GeneratedValue(strategy = GenerationType.IDENTITY)
- AutoIncrement
- 기존 Board 객체의 필드에 User user가 추가됨
- @ManyToOne(fetch = FetchType.LAZY)
- @ManyToOne : JPA에서 객체 간의 관계 설정 시 사용
- 현재 엔티티가 다른 엔티티와 N:1 관계에 있음을 나타냄
- 여러 Board(N)가 하나의 User(1)와 연결되는 관계
- DB에는 User 객체가 들어가지 못하기 떄문에 user_tb의 PK(user_id)를 board_tb의 FK로 설정
- fetch = FetchType.LAZY : 관련된 User 엔티티를 지연 로딩(Lazy Loading)
- 실제로 User 데이터가 필요할 때까지 데이터베이스에서 로드되지 않음
- 밑의 지연 로딩 VS 즉시 로딩 이론 참고
- 연관 관계 관련 Hibernate 문법 규칙
- 연관 관계 설정해놓은 User 객체(user_tb) 자체를 들고오는 것이 아니라 user_tb 테이블의 pk(userId)를 들고옴 ⇒ board_tb 테이블의 fk
@NoArgsConstructor(access = AccessLevel.PROTECTED) // 외부에서 빈 생성자 생성하지 못하게 제한(protected)
@Getter
@Table(name = "board_tb") // 테이블 명
@Entity // Board 객체를 테이블로 생성하며, 데이터베이스 테이블과 매핑하여 데이터베이스 작업(insert, update, delete 등)을 수행
public class Board {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY) // AutoIncrement
private Integer id;
private String title;
private String content;
// 241129 User 필드 추가
// 하이버네이트 문법 규칙
// 연관 관계 설정해놓은 User 객체(user_tb 테이블) 자체를 들고오는 것이 아니라
// user_tb 테이블의 PK (user_id(필드로는 userId))를 들고옴 => board_tb 테이블의 fk
// 연관 관계 설정 (User 쪽이 1이니까 ManyToOne)
@ManyToOne(fetch = FetchType.LAZY) // LAZY와 EAGER일때 콘솔 출력되는 쿼리 다름
private User user;
@CreationTimestamp
private Timestamp createdAt;
// 풀 생성자 만들고 @Builder 달아서 빌더의 형태 잡음
@Builder
public Board(Integer id, String title, String content, User user, Timestamp createdAt) {
this.id = id;
this.title = title;
this.content = content;
this.user = user;
this.createdAt = createdAt;
}
// update(글 수정)를 setter로 만들어서 활용하기로 함
// setter는 명확한 의도가 있는것만 해놓는게 좋기 때문에
public void update(String title, String content) {
this.title = title;
this.content = content;
}
}[지연 로딩(Lazy Loading) vs 즉시 로딩(EAGER Loading) 이론 참고]
지연 로딩(Lazy Loading)
- 관련된 엔티티 데이터를 실제로 사용하기 전까지 로드하지 않음
- Board 엔티티를 조회할 때 관련된 User 엔티티를 즉시 로드하지 않고, User 데이터가 필요할 때 user.getUser() 등을 데이터베이스에서 가져옴
- Board 엔티티가 로드될 떄 User 엔티티는 프록시 객체로 대체 ⇒ getter 가져오면 프록시 객체로 대체해서 select 발생
- User 엔티티의 데이터를 가지지 않지만 User 엔티티에 접근하는 시점에 데이터베이스 조회(select) 발생하여 실제 데이터 로드 ⇒ 프록시 객체는 실제 User 엔티티로 대체
조회할 board_tb 테이블에 user_id가 fk로 존재하기 떄문에 User 객체의 id값은 초기화됨
User 객체의 나머지 값들은 전부 null
장점
- 초기 조회 시 불필요한 데이터를 로드하지 않음으로써 성능 향상
- 필요한 시점에만 데이터를 로드해서 메모리 사용량 감소
단점
- 연관된 데이터를 사용할 때 추가적인 데이터베이스 쿼리가 발생할 수 있음
주의점
- N+1 문제
- Lazy Loading을 사용할 때 가장 흔히 발생하는 문제 ⇒ 하나의 쿼리로 N개의 엔티티를 가져온 후, 각 엔티티의 연관된 데이터를 각각 추가 쿼리로 가져오는 경우가 발생
- 데이터베이스 쿼리가 불필요하게 많이 발생하게 되어 성능 저하될 수 있음
- 프록시 객체 사용
- Lazy Loading은 프록시 객체를 사용하여 연관된 데이터를 필요한 시점에 로드
- 프록시 객체는 실제 데이터가 로드되기 전까지는 원본 객체와 다르게 동작할 수 있음 ⇒ 프록시 객체의 특정 메서드 호출 시 데이터베이스 쿼리가 발생할 수 있음 주의
- 데이터 접근 시점 (트랜잭션 관리)
- Lazy Loading된 데이터를 접근하는 시점에 데이터베이스 세션이 닫혀 있으면(트랜잭션이 종료돼있으면) 예외 발생 ⇒ 서비스 계층에서 필요한 데이터 미리 로드하는것이 좋음
- 필요 시점에 미리 데이터 로딩하는 것이 중요
- 성능 고려
- 성능에 영향을 미칠 수 있어서 대량의 데이터나 연관된 데이터가 많을 때는 주의
- Fetch 전략을 상황에 따라 적절하게 조정하고 필요한 경우 Eager Loading으로 전환하는 것이 좋음
지연 로딩일 때의 쿼리문
- board_tb만 조회하고 user_tb는 board_tb가 들고 있는 fk인 user_id만 조회함 ⇒ 단순한 select문으로만 조회함
- 상황에 따라 불필요한 데이터를 빼는게 좋을 경우에 Lazy를 쓰면 좋음 ⇒ 게시글 목록 출력할 때는 굳이 유저의 정보가 필요 없기 때문에 Lazy Loading 전략 사용

즉시 로딩(Eager Loading)
- 연관된 엔티티 데이터를 즉시 로드함
- Board 엔티티를 조회할 때 관련된 User 엔티티를 함께 로드함
User 객체의 모든 값이 초기화됨
불필요한 select가 일어날 수 있어서 용도에 맞춰서 사용해야함
장점
- 연관된 데이터를 사용할 때 추가적인 데이터 베이스 쿼리가 발생하지 않음
단점
- 초기 조회 시 성능이 저하될 수 있으며 메모리 사용량 증가할 수 있음
즉시 로딩일때의 쿼리문
- 연관된 정보들을 모두 들고오기 위해 join문 사용됨 ⇒ left join 사용해서 board_tb 전부 조회(left join 기준 왼쪽)하고 user_tb도 게시글과 상관있는 회원 정보 전부 다 조회됨

DB에서의 동작
- 지연 로딩(Lazy Loading)
- Board 엔티티를 검색할 떄, 관련된 User 엔티티는 로드되지 않음
- 관련된 User 엔티티에 접근할 때 별도의 쿼리(select)가 발생하여 User 데이터 로드
Board board = boardRepository.findById(1).orElse(null);
User user = board.getUser(); // 이 시점에 User 데이터베이스 조회 쿼리 발생- 즉시 로딩(EAGER Loading)
- Board 엔티티를 검색할 때 관련된 User 엔티티도 함께 로드됨
- Board 엔티티를 조회하는 시점에 User 데이터가 이미 로드되어 있어서 추가 쿼리 발생하지 않음 (join으로 다 가져오니까)
Board board = boardRepository.findById(1).orElse(null);
User user = board.getUser(); // 이 시점에 이미 User 데이터가 로드되어 있음테스트로 지연로딩(Lazy Loading) 실행 시점 확인하기
[BoardRepositoryTest 테스트 클래스의 findById_test 메서드]
- Board는 ManyToOne 연관 관계에 의해 userId만 들고 있고 username은 없는 상태
- username을 달라고 하면 연관 관계 속성이 Lazy여도 username을 select해서 줌 ⇒ 결론은 Lazy Loading은 getter를 달라고할 때 실행됨
@Import(BoardRepository.class) // BoardRepository 클래스를 사용
@DataJpaTest // DB 관련된 자원들을 메모리(IoC)에 올림(JPA 관련 컴포넌트만 초기화)
public class BoardRepositoryTest {
@Autowired
private BoardRepository boardRepository;
@Test
public void findById_test() {
// given
Integer id = 1;
// when
Optional<Board> boardOP = boardRepository.findById(id);
Board board = boardOP.get();
// eye
System.out.println("Lazy Loading 직전");
// Board는 manyToOne 연관 관계에 의해 userId만 들고있고 userName은 없는 상태
// userName을 달라고 하면 연관 관계 속성이 Lazy지만
// userName을 select해서 줌
// 결론은 레이지로딩은 getter를 달라고할때 실행됨
String username = board.getUser().getUsername();
System.out.println("Lazy Loading 직후");
}
}- 테스트 실행 후 출력 확인

[data.sql 파일]
- 더미데이터 추가
- user_tb 추가하고 user_id를 board_tb의 fk로 둠
insert into user_tb(username, password, email) values ('ssar', '1234', 'ssar@nate.com');
insert into user_tb(username, password, email) values ('cos', '1234', 'cos@nate.com');
-- User의 userId가 fk
insert into board_tb(title, content, user_id, created_at) values('제목1', '내용1', 1, now());
insert into board_tb(title, content, user_id, created_at) values('제목2', '내용2', 1, now());
insert into board_tb(title, content, user_id, created_at) values('제목3', '내용3', 1, now());
insert into board_tb(title, content, user_id, created_at) values('제목4', '내용4', 2, now());
insert into board_tb(title, content, user_id, created_at) values('제목5', '내용5', 2, now());[application.properties 파일]
- OSIV 설정을 false
- Service 객체를 기준으로 데이터베이스 세션이 열리고 닫힘
- 컨트롤러에서 반환 받은 Model을 그대로 return 할 때의 문제 발생 방지
- jackson 라이브러리가 전달 받은 객체를 json으로 변환을 시도할 때, username이 없는 상태라면 username을 다시 조회하여 들고 오기 전에 데이터 파싱을 시도하여 예외 발생 할 수 있기 때문
- Service 객체에서 DTO로 변환하여 컨트롤러로 전달하게 되면 문제 발생 방지 가능
- 데이터베이스 세션이 닫힌 이후에는 getter를 이용한 조회 불가 주의
- OSIV 설정을 false로 두고 컨트롤러에서 getter를 사용하는 경우 예외 발생

[BuilderTest 테스트 클래스]
- 테스트 파일에서는 롬복 적용 안됨
- 테스트 클래스나 메서드는 @Test 어노테이션을 달고 이름뒤에 test를 붙이는것이 약속
- Builder를 이용한 테스트
- 특정 필드 한 개 또는 몇 개만 필요한 경우 그에 따른 생성자를 또 만들어야되거나 풀 생성자를 그대로 가져오면 불필요한 정보도 가져야함
- 빌더를 쓰면 필요한 것만 뽑아 쓸 수 있음
// 테스트 파일에서는 롬복 적용 안됨
import org.junit.jupiter.api.Test;
class Member {
private Integer id;
private String name;
private String addr;
private Member() {}
// builder로 빈(디폴트)생성자 만들기
public static Member builder() {
return new Member();
}
public Member id(Integer id) {
this.id = id;
return this; // 자기자신 반환, 나중에 new되면 이걸로 되는것
}
public Member name(String name) {
this.name = name;
return this;
}
public Member addr(String addr) {
this.addr = addr;
return this;
}
}
public class BuilderTest {
@Test
public void new_test() {
// 풀 생성자 안 쓰고 빌더 쓰는 이유?
// 전체 필드가 필요한게 아니라 특정만 필요하거나 몇 개만 필요한 경우
// 그에 따른 생성자를 또 만들어야되거나 풀 생성자 가져와서 불필요한 정보도 가져야하기 때문
// 빌더 쓰면 필요한것만 뽑아 쓸 수 있음
// private Member() {}로 막아놔서 그냥 new는 안되고
// builder로 빈생성자 만들어놓은거 이용하면 됨
Member m = Member.builder()
.id(1)
.name("이름")
.addr("주소");
}
}Share article