ORM
ORM(Object-Relational Mapping)이란 객체 지향 프로그래밍 언어를 사용하는 개발자가 관계형 데이터베이스를 사용할 때, 객체와 데이터베이스 간의 상호 변환을 자동화하는 기술 또는 개념입니다. 이는 객체 지향 프로그래밍과 관계형 데이터베이스 간의 불일치를 해결하기 위해 사용됩니다.
주요 개념
- 객체와 테이블의 매핑:
- 객체 지향 프로그래밍의 객체를 데이터베이스의 테이블에 매핑합니다. 예를 들어,
User
클래스가 데이터베이스의users
테이블에 매핑됩니다.
- 속성과 컬럼의 매핑:
- 클래스의 속성을 테이블의 컬럼에 매핑합니다. 예를 들어,
User
클래스의username
속성이users
테이블의username
컬럼에 매핑됩니다.
- 자동화된 SQL 생성:
- 데이터베이스 작업(삽입, 업데이트, 삭제, 조회)을 위해 필요한 SQL 쿼리를 자동으로 생성합니다. 개발자는 SQL 쿼리를 직접 작성할 필요 없이, 객체를 조작하는 코드만 작성하면 됩니다.
- 트랜잭션 관리:
- ORM 프레임워크는 데이터베이스 트랜잭션을 관리하여 데이터의 일관성과 무결성을 유지합니다.
ORM을 구현하기 위해 사용한 기술 :
Hibernate는 Java 언어를 위한 ORM(Object-Relational Mapping) 프레임워크로, 객체 지향 프로그래밍에서 데이터베이스와 상호작용할 수 있도록 돕습니다. 주로 데이터베이스의 테이블과 Java 객체 간의 매핑을 자동으로 처리합니다. 이로 인해 개발자는 SQL 쿼리를 직접 작성하지 않고도 데이터베이스 작업을 수행할 수 있습니다.
Hibernate의 주요 기능 및 특징
- 객체와 테이블 매핑:
- Java 클래스와 데이터베이스 테이블 간의 매핑을 자동으로 처리합니다.
- 자동 SQL 생성:
- 데이터베이스 작업을 위해 필요한 SQL 쿼리를 자동으로 생성하고 실행합니다.
- 지연 로딩(Lazy Loading):
- 필요한 시점까지 데이터베이스에서 데이터를 로드하지 않아 성능을 최적화할 수 있습니다.
- 캐시 지원:
- 1차 캐시(세션 캐시)와 2차 캐시(전역 캐시)를 통해 데이터베이스 접근을 최소화하고 성능을 향상시킵니다.
- 트랜잭션 관리:
- 데이터의 일관성과 무결성을 유지하기 위해 트랜잭션을 관리합니다.
예시
※ 기존 Board 객체의 필드 값에 User user 가 추가된 클래스 파일 내용입니다.
@NoArgsConstructor(access = AccessLevel.PROTECTED)
@Getter
@Table(name = "board_tb")
@Entity // Board 객체를 테이블로 생성하며, 데이터베이스 테이블과 매핑하여 데이터베이스 작업(insert, update, delete 등)을 수행할 수 있습니다.
public class Board {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY) // Auto increment 와 동일
private Integer id;
private String title;
private String content;
// 여기부터 ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
@ManyToOne(fetch = FetchType.LAZY)
private User user;
// 여기까지 ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
@CreationTimestamp
private Timestamp createdAt;
@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;
}
public void update(String title, String content) {
this.title = title;
this.content = content;
}
}
@ManyToOne
어노테이션은 JPA에서 객체 간의 관계를 설정할 때 사용됩니다. 이 어노테이션은 현재 엔티티가 다른 엔티티와 다대일 관계(여러 Board
가 하나의 User
와 연결되는 관계)에 있음을 나타냅니다.@ManyToOne
어노테이션 설명
- 기본 설정:
@ManyToOne
: 이 어노테이션은Board
엔티티가User
엔티티와 다대일 관계에 있음을 명시합니다.fetch = FetchType.LAZY
: 이 속성은 관련된User
엔티티를 지연 로딩(Lazy Loading)할 것임을 나타냅니다. 즉, 실제로User
데이터가 필요할 때까지 데이터베이스에서 로드되지 않습니다.
지연 로딩(FetchType.LAZY) vs 즉시 로딩(FetchType.EAGER)
- 지연 로딩 (Lazy Loading):
- 지연 로딩은 관련된 엔티티 데이터를 실제로 사용하기 전까지 로드하지 않습니다.
- 예를 들어,
Board
엔티티를 조회할 때 관련된User
엔티티를 즉시 로드하지 않고,User
데이터가 필요할 때(user.getUsername()
등) 데이터베이스에서 가져옵니다. - 장점: 초기 조회 시 성능이 향상될 수 있으며, 메모리 사용량이 감소합니다.
- 단점: 연관된 데이터를 사용할 때 추가적인 데이터베이스 쿼리가 발생할 수 있습니다.
예제에서는 조회할 board_tb 테이블에 user_id가 참조키로 존재하기 때문에 User 객체의 id 값은 초기화됩니다.
User 객체의 나머지 값들은 전부 null 입니다.
지연 로딩일 때의 쿼리문 예시 :

단순한 select 문으로 조회가 된 것을 확인할 수 있습니다.
- 즉시 로딩 (Eager Loading):
- 즉시 로딩은 연관된 엔티티 데이터를 즉시 로드합니다.
- 예를 들어,
Board
엔티티를 조회할 때 관련된User
엔티티를 함께 로드합니다. - 장점: 연관된 데이터를 사용할 때 추가적인 데이터베이스 쿼리가 발생하지 않습니다.
- 단점: 초기 조회 시 성능이 저하될 수 있으며, 메모리 사용량이 증가할 수 있습니다.
Eager 로딩을 할 시 User 객체의 모든 값이 초기화됩니다. 다만 해당 방식은 불필요한 select가 일어날 수 있기 때문에 용도에 맞추어 사용해야합니다.
예제에서 게시글 목록을 출력할 때는 굳이 유저의 정보가 필요 없기 때문에 lazy 로딩 전략을 사용하였습니다.
즉시 로딩일 때의 쿼리문 예시 :

User의 정보들을 모두 들고오기 위해 join문이 사용된 것을 확인 할 수 있습니다.
DB에서의 동작
- 지연 로딩:
Board
엔티티를 검색할 때, 관련된User
엔티티는 로드되지 않습니다.- 관련된
User
엔티티에 접근할 때 별도의 쿼리가 발생하여User
데이터를 로드합니다.
Board board = boardRepository.findById(1).orElse(null);
User user = board.getUser(); // 이 시점에 User 데이터베이스 조회 쿼리 발생
- 즉시 로딩:
Board
엔티티를 검색할 때, 관련된User
엔티티도 함께 로드됩니다.Board
엔티티를 조회하는 시점에User
데이터가 이미 로드되어 있어 추가 쿼리가 발생하지 않습니다.
Board board = boardRepository.findById(1).orElse(null);
User user = board.getUser(); // 이 시점에 이미 User 데이터가 로드되어 있음
Lazy 로딩 전략의 주의점
- N+1 문제:
- Lazy 로딩을 사용할 때 가장 흔히 발생하는 문제는 N+1 문제입니다.
- 예를 들어, 하나의 쿼리로 N개의 엔티티를 가져온 후, 각 엔티티의 연관된 데이터를 각각 추가 쿼리로 가져오는 경우가 발생합니다.
- 이로 인해 데이터베이스 쿼리가 불필요하게 많이 발생하게 되어 성능 저하를 초래할 수 있습니다.
인 쿼리 또는 properties 파일 설정을 통해 해결 가능
- 프록시 객체 사용:
- Lazy 로딩은 프록시 객체를 사용하여 연관된 데이터를 필요한 시점에 로드합니다.
- 프록시 객체는 실제 데이터가 로드되기 전까지는 원본 객체와 다르게 동작할 수 있습니다.
- 프록시 객체의 특정 메서드 호출 시 데이터베이스 쿼리가 발생할 수 있음을 염두에 두어야 합니다.
- 데이터 접근 시점:
- 지연 로딩된 데이터를 접근하는 시점에 데이터베이스 세션이 닫혀 있으면 예외가 발생합니다.
- 따라서, 서비스 계층에서 필요한 데이터를 미리 로드하는 것이 좋습니다.
- 필요 시점에 미리 데이터 로딩을 수행하는 것이 중요합니다.
- 성능 고려:
- Lazy 로딩은 성능에 영향을 미칠 수 있으므로, 대량의 데이터나 연관된 데이터가 많을 때는 주의해야 합니다.
- Fetch 전략을 상황에 따라 적절하게 조정하고, 필요한 경우 즉시 로딩(Eager Loading)으로 전환하는 것이 좋습니다.
DTO 대신 Model 그대로 반환하였을 때
컨트롤러에서 반환 받은 모델을 그대로 return 할 때 문제가 발생할 수 있습니다.
jackson 라이브러리가 전달 받은 객체를 json으로 변환을 시도할 때, username이 없는 상태라면 username을 다시 조회하여 들고 오기 전에 데이터 파싱을 시도하여 예외를 발생 시킬 수 있습니다.
위 문제를 막기 위해 OSIV 설정을 false로 하게 되면 Service 객체를 기준으로 데이터베이스 세션이 열리고 닫힙니다.

당연히 데이터 베이스 세션이 닫힌 이후에는 getter를 이용하여 조회할 수 없습니다.
EX : 위 설정을 false로 두고, 컨트롤러에서 getter를 사용하는 경우 예외 발생.
결과적으로 Service 객체에서 DTO로 변환하여 컨트롤러로 전달하게 되면 위와 같은 문제를 미리 방지할 수 있습니다.
Share article