Spring에서는 @Entity
어노테이션을 이용하여 실제 데이터베이스의 테이블과 매핑되는 클래스를 정의할 수 있습니다. 이를 통해 데이터베이스의 테이블과 CRUD 작업을 쉽게 수행하고 관리할 수 있습니다. Entity는 데이터베이스와 직접적으로 연결되기 때문에 테이블 구조와 일치해야 하며, Spring Boot와 JPA는 다음과 같은 과정으로 @Entity
클래스와 데이터베이스 테이블을 매핑합니다.
기본 JPA 구성 요소 설정
스프링 부트는 @SpringBootApplication
내부의 @EnableAutoConfiguration
과 spring-data-jpa
종속성을 이용해
EntityManagerFactory
, DataSource
, TransactionManager
등의 JPA 기본 구성 요소를 자동 설정합니다.
Entity 클래스 스캔 및 매핑 정보 생성
@Entity
가 붙은 클래스를 스캔하여, JPA 프로바이더(Hibernate 등)는 데이터베이스 테이블과 Entity 클래스 간의
매핑 정보(필드, 컬럼, 기본키, 제약 조건)를 생성합니다.
테이블 생성 및 업데이트
spring.jpa.hibernate.ddl-auto
설정에 따라 데이터베이스 테이블을 생성하거나 업데이트합니다.
JpaRepository 인터페이스 스캔 및 프록시 생성
@EnableJpaRepositories
를 통해 JpaRepository
를 확장한 인터페이스들을 스캔하고, 리포지토리 프록시를 생성하여 JPA 기능을 제공합니다.
EntityManagerFactory와 EntityManager 생성
실제 CRUD 작업 시 EntityManagerFactory
가 트랜잭션 내에서 EntityManager
를 생성하고,
리포지토리 프록시가 이를 이용해 작업을 수행합니다.
리포지토리 프록시를 통한 CRUD 수행
리포지토리 프록시는 메서드 호출 시 이를 가로채서 EntityManager
를 통해 CRUD 작업을 수행합니다.
트랜잭션 종료 및 커밋
트랜잭션이 종료되면 변경사항을 커밋하고, EntityManager
와 리포지토리의 연결이 끊어집니다.
참고: @Entity
객체는 영속성 컨텍스트에 등록되어야 실제 CRUD 작업이 가능합니다. 이때부터 EntityManager
관리 하에 놓여 데이터베이스와의 동기화가 이루어집니다.
@Entity
@Getter
@AllArgsConstructor
@NoArgsConstructor(access = AccessLevel.PROTECTED)
public class User {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
@NonNull
private String phoneNumber;
@Column(name = "nick_name")
private String nickName;
@Column(name = "is_curtaincall_on_and_off")
private boolean isCurtainCallOnAndOff;
@Column(name = "user_role")
@ColumnDefault("'USER'")
private String userRole;
@Builder
public User(@NotNull String phoneNumber, String nickName, boolean isCurtainCallOnAndOff) {
this.phoneNumber = phoneNumber;
this.nickName = nickName;
this.isCurtainCallOnAndOff = isCurtainCallOnAndOff;
}
}
@Entity
: 해당 클래스가 데이터베이스의 테이블과 매핑됨을 나타냅니다.@AllArgsConstructor
: 모든 필드의 값을 받는 생성자를 생성하여 초기화합니다.@NoArgsConstructor(access = AccessLevel.PROTECTED)
: 기본 생성자를 protected로 정의하여 객체 생성 시 리플렉션을 통한 접근을 제한합니다.@Id
: 해당 필드를 데이터베이스의 기본키(PK)로 매핑합니다.@GeneratedValue(strategy = GenerationType.IDENTITY)
: ID 값 생성 전략을 지정합니다.@GeneratedValue(strategy = GenerationType.IDENTITY)
AUTO_INCREMENT
기능을 사용해 ID 값을 생성하는 전략입니다.persist
호출 시 바로 INSERT
가 실행됩니다.INSERT
가 발생합니다.@Transactional
public void addUsers(List<User> users) {
for (User user : users) {
em.persist(user);
}
}
@GeneratedValue(strategy = GenerationType.SEQUENCE)
persist
호출 전부터 ID를 관리할 수 있습니다.allocationSize
를 통해 한 번에 여러 개의 ID 값을 가져와 성능을 최적화할 수 있습니다.@GeneratedValue(strategy = GenerationType.TABLE)
@TableGenerator
로 설정이 필요하며, 모든 데이터베이스에서 사용 가능하지만 성능 이슈가 발생할 수 있습니다.Pessimistic Lock
이나 Optimistic Lock
을 통해 동시성 문제를 제어해야 합니다.@GeneratedValue(strategy = GenerationType.AUTO)
NULL
값이 허용되지 않습니다.다량의 데이터 삽입 시 각 전략에 따라 성능이 다르게 나타납니다. 예를 들어, SEQUENCE
와 TABLE
전략은 쓰기 지연을 통해 트랜잭션 커밋 시 한꺼번에 INSERT
를 처리할 수 있지만, IDENTITY
는 AUTO_INCREMENT
특성상 persist
호출마다 즉시 INSERT
를 실행합니다.
SEQUENCE
,'Table' 전략의 장단점SEQUENCE
와 TABLE
전략은 쓰기 지연을 통해 트랜잭션이 커밋될 때 한꺼번에 INSERT
가 실행됩니다.TABLE
전략은 시퀀스 값을 테이블로 관리하기 때문에, 여러 트랜잭션이 동시에 접근할 경우 동시성 문제가 발생할 수 있습니다. 이를 해결하기 위해 비관적 락(Pessimistic Lock)과 낙관적 락(Optimistic Lock)을 사용할 수 있습니다.잠금 방식 | 원리 | 장점 | 단점 | 적합한 상황 |
---|---|---|---|---|
비관적 락 | 다른 트랜잭션이 접근하지 못하도록 미리 잠금을 설정 | 동시성 문제 예방, 데이터 일관성 보장 | 대기 시간 증가, 성능 저하 가능 | 충돌이 빈번한 경우 |
낙관적 락 | 충돌 시 다시 시도, 충돌 발생 시 감지 후 처리 | 경합 없이 접근 가능, 충돌이 드문 환경에서 성능 우수 | 충돌 발생 시 재시도로 성능 저하 가능 | 충돌이 드문 경우 |
TABLE
전략은 ACID를 보장하기 위해서는 잠금이 필요하며, 이러한 이유로 성능 저하가 발생할 수 있습니다. 또한, Hibernate에서 제공하는 TABLE
전략은 별도로 잠금을 설정할 수 없기 때문에, IDENTITY
와 SEQUENCE
전략이 많이 사용됩니다.
예를 들어, 두 개의 트랜잭션이 각각 두 자원(A와 B)을 비관적 락으로 잠그려고 하는 상황을 가정해 보겠습니다.
1.트랜잭션 1이 자원 A에 비관적 락을 겁니다.
2.트랜잭션 2가 자원 B에 비관적 락을 겁니다.
3.트랜잭션 1이 자원 B를 잠그려고 시도하지만,
4.이미 트랜잭션 2가 잠금 중이므로 대기 상태에 들어갑니다.
5.트랜잭션 2도 자원 A를 잠그려고 하지만,
6.트랜잭션 1이 잠금 중이므로 대기 상태에 들어갑니다.
IDENTITY
전략은 AUTO_INCREMENT
방식으로, 즉시 INSERT
가 발생하여 다량의 데이터 삽입 시 성능이 떨어질 수 있습니다.SEQUENCE
전략은 시퀀스를 지원하는 데이터베이스에서 빠르고 효율적인 방식으로 ID를 생성하며, 쓰기 지연을 활용할 수 있습니다. 또한 트랜잭션과 무관한 객체로 활용되므로, ACID의 보장이 안될수 있습니다.TABLE
전략은 모든 데이터베이스에서 사용할 수 있지만 동시성 문제로 인해 성능 저하가 발생할 수 있습니다.IDENTITY
전략이 가장 많이 사용되며, 호환성과 안정성 측면에서 우수합니다.