반응형
Spring/JPA 데이터베이스 관련 어노테이션 완벽 가이드
목차
1. 트랜잭션 관련 어노테이션
@Transactional
목적: 메서드나 클래스에 트랜잭션 경계를 설정하여 데이터베이스 작업의 원자성을 보장합니다.
@Transactional(
isolation = Isolation.READ_COMMITTED,
propagation = Propagation.REQUIRED,
rollbackFor = Exception.class,
noRollbackFor = IllegalArgumentException.class,
timeout = 30,
readOnly = false
)
public void updateUser(User user) {
userRepository.save(user);
}
주요 속성:
isolation: 트랜잭션 격리 수준 (DEFAULT, READ_UNCOMMITTED, READ_COMMITTED, REPEATABLE_READ, SERIALIZABLE)
propagation: 트랜잭션 전파 방식 (REQUIRED, REQUIRES_NEW, NESTED, SUPPORTS 등)
rollbackFor: 롤백을 수행할 예외 클래스 지정
timeout: 트랜잭션 타임아웃 시간 (초)
readOnly: 읽기 전용 트랜잭션 여부
@EnableTransactionManagement
목적: Spring의 트랜잭션 관리 기능을 활성화합니다.
@Configuration
@EnableTransactionManagement
public class DatabaseConfig {
@Bean
public PlatformTransactionManager transactionManager(EntityManagerFactory emf) {
return new JpaTransactionManager(emf);
}
}
Configuration 클래스에 추가하여 @Transactional 어노테이션이 동작하도록 합니다.
2. JPA 엔티티 관련 어노테이션
기본 엔티티 어노테이션
@Entity
목적: 클래스를 JPA 엔티티로 지정하여 데이터베이스 테이블과 매핑합니다.
@Entity
@Table(name = "users")
public class User {
// 필드와 메서드
}
주요 속성:
name: 엔티티 이름 (기본값: 클래스명)
@Table
목적: 엔티티와 매핑될 테이블의 상세 정보를 지정합니다.
@Table(
name = "users",
schema = "public",
indexes = {
@Index(name = "idx_email", columnList = "email"),
@Index(name = "idx_name_status", columnList = "name, status")
},
uniqueConstraints = @UniqueConstraint(columnNames = {"email"})
)
public class User { ... }
주요 속성:
name: 테이블 이름
schema: 데이터베이스 스키마
indexes: 인덱스 정의
uniqueConstraints: 유니크 제약 조건
@Id
목적: 엔티티의 기본 키(Primary Key)를 지정합니다.
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
모든 엔티티는 반드시 @Id 어노테이션이 지정된 필드가 하나 이상 있어야 합니다.
@GeneratedValue
목적: 기본 키 값의 생성 전략을 지정합니다.
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
@GeneratedValue(strategy = GenerationType.SEQUENCE, generator = "user_seq")
@SequenceGenerator(name = "user_seq", sequenceName = "user_sequence", allocationSize = 1)
private Long id;
주요 전략:
AUTO: JPA 구현체가 자동으로 선택
IDENTITY: 데이터베이스의 AUTO_INCREMENT 사용
SEQUENCE: 데이터베이스 시퀀스 사용
TABLE: 별도 테이블을 사용한 키 생성
@Column
목적: 엔티티 필드와 데이터베이스 컬럼의 매핑 정보를 지정합니다.
@Column(
name = "email",
nullable = false,
unique = true,
length = 100,
columnDefinition = "VARCHAR(100) NOT NULL"
)
private String email;
주요 속성:
name: 컬럼 이름
nullable: NULL 허용 여부
unique: 유니크 제약 조건
length: 문자열 길이
columnDefinition: 컬럼 정의 SQL
@Temporal
목적: 날짜/시간 타입 필드의 데이터베이스 매핑 방식을 지정합니다.
@Temporal(TemporalType.TIMESTAMP)
private Date createdAt;
@Temporal(TemporalType.DATE)
private Date birthDate;
타입:
DATE: 날짜만 (YYYY-MM-DD)
TIME: 시간만 (HH:MM:SS)
TIMESTAMP: 날짜와 시간 (YYYY-MM-DD HH:MM:SS)
@Enumerated
목적: Enum 타입 필드의 데이터베이스 저장 방식을 지정합니다.
@Enumerated(EnumType.STRING)
private UserStatus status;
@Enumerated(EnumType.ORDINAL)
private Priority priority;
타입:
STRING: Enum 이름을 문자열로 저장 (권장)
ORDINAL: Enum 순서를 숫자로 저장
ORDINAL 사용 시 Enum 순서가 변경되면 데이터 무결성 문제가 발생할 수 있습니다.
@Lob
목적: 대용량 데이터(Large Object)를 저장하는 필드를 지정합니다.
@Lob
private String content; // CLOB
@Lob
private byte[] image; // BLOB
문자열 타입은 CLOB, 바이트 배열은 BLOB으로 매핑됩니다.
연관관계 어노테이션
@OneToOne
목적: 일대일 관계를 매핑합니다.
@OneToOne(fetch = FetchType.LAZY, cascade = CascadeType.ALL)
@JoinColumn(name = "profile_id")
private UserProfile profile;
주요 속성:
fetch: 로딩 전략 (LAZY, EAGER)
cascade: 영속성 전이 설정
mappedBy: 연관관계의 주인이 아닌 쪽에서 사용
@OneToMany
목적: 일대다 관계를 매핑합니다.
@OneToMany(mappedBy = "user", cascade = CascadeType.ALL, orphanRemoval = true)
private List<Order> orders = new ArrayList<>();
주요 속성:
mappedBy: 연관관계의 주인 지정
orphanRemoval: 고아 객체 제거 여부
@ManyToOne
목적: 다대일 관계를 매핑합니다.
@ManyToOne(fetch = FetchType.LAZY)
@JoinColumn(name = "user_id")
private User user;
다대일 관계에서는 '다' 쪽이 연관관계의 주인이 됩니다.
@ManyToMany
목적: 다대다 관계를 매핑합니다.
@ManyToMany
@JoinTable(
name = "user_roles",
joinColumns = @JoinColumn(name = "user_id"),
inverseJoinColumns = @JoinColumn(name = "role_id")
)
private Set<Role> roles = new HashSet<>();
다대다 관계는 중간 테이블을 생성하며, 복잡한 비즈니스 로직이 필요한 경우 중간 엔티티를 직접 생성하는 것이 좋습니다.
@JoinColumn
목적: 연관관계 매핑 시 외래 키 컬럼을 지정합니다.
@JoinColumn(
name = "user_id",
nullable = false,
foreignKey = @ForeignKey(name = "FK_order_user")
)
private User user;
@JoinTable
목적: 다대다 관계에서 중간 테이블을 지정합니다.
@JoinTable(
name = "user_roles",
joinColumns = @JoinColumn(name = "user_id"),
inverseJoinColumns = @JoinColumn(name = "role_id")
)
3. Repository 관련 어노테이션
@Repository
목적: 데이터 접근 계층을 나타내는 스테레오타입 어노테이션입니다.
@Repository
public class UserRepositoryImpl {
@PersistenceContext
private EntityManager entityManager;
}
Spring의 예외 변환 기능을 제공하여 데이터베이스 예외를 Spring의 DataAccessException으로 변환합니다.
@Query
목적: Repository 메서드에 JPQL 또는 네이티브 SQL 쿼리를 지정합니다.
@Query("SELECT u FROM User u WHERE u.email = :email")
Optional<User> findByEmail(@Param("email") String email);
@Query(value = "SELECT * FROM users WHERE created_at > :date", nativeQuery = true)
List<User> findUsersCreatedAfter(@Param("date") Date date);
주요 속성:
value: JPQL 또는 SQL 쿼리
nativeQuery: 네이티브 SQL 사용 여부
@Modifying
목적: 데이터 변경 쿼리(UPDATE, DELETE)임을 나타냅니다.
@Modifying
@Query("UPDATE User u SET u.status = :status WHERE u.id = :id")
int updateUserStatus(@Param("id") Long id, @Param("status") UserStatus status);
@Modifying 쿼리 실행 후에는 영속성 컨텍스트를 수동으로 clear해야 할 수 있습니다.
@Param
목적: 쿼리 파라미터의 이름을 지정합니다.
@Query("SELECT u FROM User u WHERE u.name = :name AND u.age > :age")
List<User> findByNameAndAge(@Param("name") String name, @Param("age") int age);
@PersistenceContext
목적: JPA EntityManager를 주입합니다.
@PersistenceContext
private EntityManager entityManager;
@PersistenceUnit
목적: JPA EntityManagerFactory를 주입합니다.
@PersistenceUnit
private EntityManagerFactory entityManagerFactory;
4. 검증 관련 어노테이션
@NotNull
목적: 필드가 null이 아님을 검증합니다.
@NotNull(message = "이메일은 필수입니다")
private String email;
@NotEmpty
목적: 컬렉션이나 문자열이 null이 아니고 비어있지 않음을 검증합니다.
@NotEmpty(message = "이름은 필수입니다")
private String name;
@NotBlank
목적: 문자열이 null이 아니고 공백이 아님을 검증합니다.
@NotBlank(message = "제목은 필수입니다")
private String title;
@Size
목적: 문자열, 컬렉션, 배열의 크기를 검증합니다.
@Size(min = 2, max = 50, message = "이름은 2-50자 사이여야 합니다")
private String name;
@Min / @Max
목적: 숫자 값의 최소/최대 값을 검증합니다.
@Min(value = 0, message = "나이는 0 이상이어야 합니다")
@Max(value = 150, message = "나이는 150 이하여야 합니다")
private Integer age;
@Email
목적: 이메일 형식을 검증합니다.
@Email(message = "올바른 이메일 형식이 아닙니다")
private String email;
@Pattern
목적: 정규표현식을 이용한 패턴 검증을 수행합니다.
@Pattern(regexp = "^[0-9]{10,11}$", message = "올바른 전화번호 형식이 아닙니다")
private String phoneNumber;
5. 캐싱 관련 어노테이션
@Cacheable
목적: 메서드 실행 결과를 캐시에 저장합니다.
@Cacheable(value = "users", key = "#id")
public User findById(Long id) {
return userRepository.findById(id).orElse(null);
}
주요 속성:
value: 캐시 이름
key: 캐시 키 (SpEL 표현식)
condition: 캐시 조건
@CacheEvict
목적: 캐시에서 데이터를 제거합니다.
@CacheEvict(value = "users", key = "#user.id")
public User updateUser(User user) {
return userRepository.save(user);
}
@CacheEvict(value = "users", allEntries = true)
public void clearAllUsers() {
// 모든 사용자 캐시 삭제
}
주요 속성:
allEntries: 모든 캐시 엔트리 삭제 여부
beforeInvocation: 메서드 실행 전 캐시 삭제 여부
@CachePut
목적: 메서드를 항상 실행하고 결과를 캐시에 저장합니다.
@CachePut(value = "users", key = "#user.id")
public User updateUser(User user) {
return userRepository.save(user);
}
@Cacheable과 달리 메서드를 항상 실행하고 캐시를 업데이트합니다.
@EnableCaching
목적: Spring의 캐시 기능을 활성화합니다.
@Configuration
@EnableCaching
public class CacheConfig {
@Bean
public CacheManager cacheManager() {
return new ConcurrentMapCacheManager("users", "orders");
}
}
6. 성능 최적화 어노테이션
@NamedQuery
목적: 엔티티에 명명된 쿼리를 정의하여 재사용합니다.
@Entity
@NamedQuery(
name = "User.findByEmailAndStatus",
query = "SELECT u FROM User u WHERE u.email = :email AND u.status = :status"
)
public class User { ... }
애플리케이션 시작 시 쿼리 검증이 이루어져 런타임 오류를 방지할 수 있습니다.
@NamedEntityGraph
목적: 엔티티 그래프를 정의하여 N+1 문제를 해결합니다.
@Entity
@NamedEntityGraph(
name = "User.withOrders",
attributeNodes = {
@NamedAttributeNode("orders"),
@NamedAttributeNode(value = "profile", subgraph = "profile.details")
},
subgraphs = @NamedSubgraph(
name = "profile.details",
attributeNodes = @NamedAttributeNode("address")
)
)
public class User { ... }
@EntityGraph
목적: Repository 메서드에서 엔티티 그래프를 사용합니다.
@Repository
public interface UserRepository extends JpaRepository<User, Long> {
@EntityGraph(value = "User.withOrders", type = EntityGraph.EntityGraphType.FETCH)
List<User> findByStatus(UserStatus status);
@EntityGraph(attributePaths = {"orders", "profile"})
Optional<User> findById(Long id);
}
주요 속성:
value: @NamedEntityGraph 이름
attributePaths: 직접 로딩할 속성 경로
type: FETCH(즉시 로딩) 또는 LOAD(기본값 유지)
@BatchSize
목적: 컬렉션이나 프록시를 일괄 로딩할 때의 배치 크기를 지정합니다.
@OneToMany(mappedBy = "user")
@BatchSize(size = 10)
private List<Order> orders;
N+1 문제를 완화하기 위해 지정된 크기만큼 한 번에 로딩합니다.
@Fetch
목적: Hibernate의 fetch 전략을 세밀하게 제어합니다.
@OneToMany(mappedBy = "user")
@Fetch(FetchMode.SUBSELECT)
private List<Order> orders;
FetchMode:
SELECT: 기본값, 별도 SELECT 쿼리
JOIN: JOIN을 사용한 즉시 로딩
SUBSELECT: 서브쿼리를 사용한 로딩
7. 테스트 관련 어노테이션
@DataJpaTest
목적: JPA Repository 계층만을 테스트하기 위한 슬라이스 테스트를 제공합니다.
@DataJpaTest
@AutoConfigureTestDatabase(replace = AutoConfigureTestDatabase.Replace.NONE)
public class UserRepositoryTest {
@Autowired
private TestEntityManager entityManager;
@Autowired
private UserRepository userRepository;
}
내장 데이터베이스를 사용하고 @Entity 클래스들과 Repository만 스캔합니다.
@AutoConfigureTestDatabase
목적: 테스트용 데이터베이스 설정을 제어합니다.
@AutoConfigureTestDatabase(replace = AutoConfigureTestDatabase.Replace.NONE)
public class IntegrationTest { ... }
Replace 옵션:
ANY: 내장 데이터베이스로 교체 (기본값)
AUTO_CONFIGURED: 자동 설정된 DataSource만 교체
NONE: 교체하지 않음
@Sql
목적: 테스트 실행 전후에 SQL 스크립트를 실행합니다.
@Test
@Sql("/test-data.sql")
@Sql(scripts = "/cleanup.sql", executionPhase = Sql.ExecutionPhase.AFTER_TEST_METHOD)
public void testFindByEmail() { ... }
주요 속성:
scripts: 실행할 SQL 파일 경로
executionPhase: 실행 시점 (BEFORE_TEST_METHOD, AFTER_TEST_METHOD)
@Rollback
목적: 테스트 메서드 실행 후 트랜잭션 롤백 여부를 지정합니다.
@Test
@Rollback(false)
public void testCreateUser() {
// 이 테스트는 롤백되지 않음
}
@TestPropertySource
목적: 테스트용 프로퍼티 소스를 지정합니다.
@TestPropertySource(
locations = "classpath:application-test.properties",
properties = {
"spring.jpa.hibernate.ddl-auto=create-drop",
"logging.level.org.hibernate.SQL=DEBUG"
}
)
public class DatabaseTest { ... }
8. 감사(Auditing) 관련 어노테이션
@CreatedDate
목적: 엔티티가 생성된 날짜를 자동으로 설정합니다.
@Entity
@EntityListeners(AuditingEntityListener.class)
public class User {
@CreatedDate
private LocalDateTime createdAt;
}
@LastModifiedDate
목적: 엔티티가 마지막으로 수정된 날짜를 자동으로 설정합니다.
@LastModifiedDate
private LocalDateTime updatedAt;
@CreatedBy
목적: 엔티티를 생성한 사용자를 자동으로 설정합니다.
@CreatedBy
private String createdBy;
@LastModifiedBy
목적: 엔티티를 마지막으로 수정한 사용자를 자동으로 설정합니다.
@LastModifiedBy
private String lastModifiedBy;
@EnableJpaAuditing
목적: JPA Auditing 기능을 활성화합니다.
@Configuration
@EnableJpaAuditing
public class AuditConfig {
@Bean
public AuditorAware<String> auditorProvider() {
return () -> {
// 현재 사용자 정보 반환
return Optional.of("system");
};
}
}
@EntityListeners
목적: 엔티티 이벤트 리스너를 지정합니다.
@Entity
@EntityListeners(AuditingEntityListener.class)
public class User { ... }
9. 이벤트 관련 어노테이션
@PrePersist
목적: 엔티티가 영속화되기 전에 실행됩니다.
@Entity
public class User {
@PrePersist
public void prePersist() {
this.createdAt = new Date();
this.updatedAt = new Date();
}
}
@PostPersist
목적: 엔티티가 영속화된 후에 실행됩니다.
@PostPersist
public void postPersist() {
log.info("새로운 사용자가 생성되었습니다: {}", this.id);
}
@PreUpdate
목적: 엔티티가 업데이트되기 전에 실행됩니다.
@PreUpdate
public void preUpdate() {
this.updatedAt = new Date();
}
@PostUpdate
목적: 엔티티가 업데이트된 후에 실행됩니다.
@PostUpdate
public void postUpdate() {
log.info("사용자 정보가 업데이트되었습니다: {}", this.id);
}
@PreRemove
목적: 엔티티가 삭제되기 전에 실행됩니다.
@PreRemove
public void preRemove() {
log.warn("사용자가 삭제됩니다: {}", this.id);
}
@PostRemove
목적: 엔티티가 삭제된 후에 실행됩니다.
@PostRemove
public void postRemove() {
log.info("사용자가 삭제되었습니다: {}", this.id);
}
@PostLoad
목적: 엔티티가 데이터베이스에서 로드된 후에 실행됩니다.
@PostLoad
public void postLoad() {
// 엔티티 로드 후 추가 초기화 작업
this.displayName = this.firstName + " " + this.lastName;
}
10. 기타 유용한 어노테이션
@DynamicInsert / @DynamicUpdate
목적: Hibernate가 동적으로 INSERT/UPDATE SQL을 생성하도록 합니다.
@Entity
@DynamicInsert
@DynamicUpdate
public class User {
// null이 아닌 필드만 INSERT/UPDATE에 포함
}
성능 최적화에 도움이 되지만 SQL 캐싱 효과가 감소할 수 있습니다.
@Where
목적: 엔티티 조회 시 추가 조건을 자동으로 적용합니다.
@Entity
@Where(clause = "deleted = false")
public class User {
@Column(name = "deleted")
private Boolean deleted = false;
}
@Formula
목적: 계산된 필드를 정의합니다.
@Formula("(SELECT COUNT(*) FROM orders o WHERE o.user_id = id)")
private int orderCount;
@Version
목적: 낙관적 락을 위한 버전 필드를 지정합니다.
@Version
private Long version;
동시성 제어를 위해 사용되며, 엔티티 수정 시 자동으로 증가합니다.
주의사항:
- 어노테이션을 과도하게 사용하면 코드 복잡성이 증가할 수 있습니다
- 성능 관련 어노테이션은 실제 측정을 통해 효과를 검증해야 합니다
- 데이터베이스 독립성을 고려하여 어노테이션을 선택해야 합니다
- 테스트 환경과 운영 환경의 설정이 다를 수 있으므로 주의가 필요합니다
결론: Spring과 JPA의 데이터베이스 관련 어노테이션들은 강력하고 편리한 기능을 제공합니다. 각 어노테이션의 특성과 용도를 정확히 이해하고 적절히 활용하면 효율적이고 안정적인 데이터 액세스 계층을 구축할 수 있습니다. 특히 성능 최적화와 관련된 어노테이션들은 실제 운영 환경에서의 성능 측정을 통해 효과를 검증하는 것이 중요합니다.
반응형