언어/JAVA

Spring/JPA 데이터베이스 관련 어노테이션 완벽 가이드

shaprimanDev 2025. 7. 14. 12:50
반응형

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의 데이터베이스 관련 어노테이션들은 강력하고 편리한 기능을 제공합니다. 각 어노테이션의 특성과 용도를 정확히 이해하고 적절히 활용하면 효율적이고 안정적인 데이터 액세스 계층을 구축할 수 있습니다. 특히 성능 최적화와 관련된 어노테이션들은 실제 운영 환경에서의 성능 측정을 통해 효과를 검증하는 것이 중요합니다.

이 가이드가 Spring/JPA 개발에 도움이 되었기를 바랍니다.

반응형