ABOUT ME

-

Today
-
Yesterday
-
Total
-
  • Entity annotation, Listener
    백엔드 2023. 8. 19. 16:11
    728x90
    반응형
    SMALL

    @Entity

    @Entity 어노테이션은 해당 객체가 JPA에서 관리하고 있는 Entity객체임을 선언합니다.

    엔티티 객체에는 레코드를 유일하게 판별할 수 있는 PK값이 반드시 존재해야합니다. 해당 PK컬럼에 @Id 어노테이션을 통해 지정할 수 있습니다.

     

     

    @GeneratedValue

    PK값으로 사용되는 id의 경우 사용자가 직접 값을 넣어주는 것이 아닌 생성된 값을 사용하게 됩니다.

    @GenerateValue를 통해 자동 생성되는 값을 PK에 넣어 줄 수 있습니다.

    @Target({METHOD, FIELD})
    @Retention(RUNTIME)
    
    public @interface GeneratedValue {
    
        /**
         * (Optional) The primary key generation strategy
         * that the persistence provider must use to
         * generate the annotated entity primary key.
         */
        GenerationType strategy() default AUTO;
    
        /**
         * (Optional) The name of the primary key generator
         * to use as specified in the {@link SequenceGenerator} 
         * or {@link TableGenerator} annotation.
         * <p> Defaults to the id generator supplied by persistence provider.
         */
        String generator() default "";
    }

    strategy 속성에 따라 생성되는 값이 달라집니다.

    • IDENTITY : mysql에서 많이 사용하는 전략입니다. db의 auto increment를 사용해 값을 생성해줍니다. 실제 사용을 하게 되면 트랜잭션 종료 전에 insert문이 동작해서 id값을 사전에 받아오게 됩니다. 실제로 커밋되지 않고 로직이 종료되어도 db의 id값은 증가된 상태가 되서 특정 id가 비게 되는 현상이 발생합니다.
    • SEQUENCE : sequence라는 특별한 함수를 제공하는 oracle이나 PostgreSQL, h2 db에서 사용합니다.
    • TABLE : db의 종류에 상관없이 id값을 관리하는 별도의 테이블을 만들어 이 테이블로 id값을 추출해서 사용합니다.
    • AUTO (default) : 각 db에 적합한 값을 넣어주게 됩니다.

     

     

    @Table

    테이블의 여러 정보들을 설정할 수 있도록 해줍니다.

    @Target(TYPE) 
    @Retention(RUNTIME)
    public @interface Table {
        String name() default "";
    
        String catalog() default "";
    
        String schema() default "";
    
        UniqueConstraint[] uniqueConstraints() default {};
    
        Index[] indexes() default {};
    }

    @Table 어노테이션에서 설정한 index나 제약사항과 같은 경우 실제 db에 적용된 것과 다를 수 있습니다. JPA Entity를 활용해서 db ddl을 생성하는 경우에는 적용이 실제로 되지만, crud 쿼리를 실행할 때는 아무런 효력을 주지 않습니다. 

    즉, 실제 db에 인덱스 설정이 되어 있지 않는데 JPA에 인덱스 설정이 되어 있다고 해서 인덱스를 활용한 쿼리가 동작을 하지는 않습니다.

    인덱스나 제약 사항들은 db에 설정을 해주는 것이 좋을 것 같습니다.

     

     

    @Column

    각 컬럼의 속성을 지정하는 필드 스콥의 어노테이션입니다.

    @Target({METHOD, FIELD}) 
    @Retention(RUNTIME)
    public @interface Column {
        String name() default "";
    
        boolean unique() default false;
    
        boolean nullable() default true;
    
        boolean insertable() default true;
    
        boolean updatable() default true;
    
        String columnDefinition() default "";
    
        String table() default "";
    
        int length() default 255;
    
        int precision() default 0;
    
        int scale() default 0;
    }

    다른 값들은 ddl을 할 때 사용하는 값이라 많이 사용하지는 않습니다. 하지만, insertable과 updatable은 다른 속성들과 달리 ddl이 아닌 일반적인 dml 쿼리에도 영향을 끼칩니다.

     

    @Column(nullable=false) vs @NotNull

    두 어노테이션 모두 해당 컬럼에 Not null ddl이 생성됩니다.

    두 어노테이션의 차이점으로는

    • @Column(nullable=false)일 경우 해당 필드의 값이 null이어도 동작을 하다가 db에 쿼리가 도착을 해야지만 예외가 발생합니다.
    • @NotNull의 경우 해당 필드 값에 Null로 채워지게 되면 바로 예외를 던지게 됩니다.

    @NotNull이 좀 더 빠르게 예외를 던져주게 됩니다.

     

     

     

    @Transient

    @Entity객체는 db에 레코드를 그대로 반영하기도 하지만, 객체로써의 역할을 할 때도 있습니다. 그렇기 때문에 db에 저장하지 않을 데이터도 존재할 수 있습니다. 이 때 @Transient를 선언해 사용할 수 있습니다.

    @Transient가 붙은 필드는 영속성 처리에서 제외가 되기 때문에 db데이터에 반영되지 않고 해당 객체와 생명주기를 같이하는 값이 됩니다.

    @Entity
    public class User {
    	@Id
        @GeneratedValue
        private Long id;
        
        private String name;
        
        private String email;
        
        @Transient
        private String sample;
    }

     

     

    enum 처리

    @Entity 객체의 필드 중 enum 타입을 가진 컬럼이 존재할 수 있습니다. 해당 컬럼에 @Enumerated를 선언해서 사용하면 됩니다.

    하지만, 유의해서 사용하지 않으면 다음과 같은 문제가 발생할 수 있습니다.

    enum Gender {
        MALE,
        FEMALE
    }
    
    @NoArgsConstructor
    @Data
    @Entity
    public class User{
        @Id
        @GeneratedValue
        private Long id;
        
        private String name;
        
        private String email;
        
        @Enumerated
        private Gender gender;
    }
    
    public interface UserRepository extends JpaRepository<User, Long> {
    	...
        @Query("select * from user limit 1;", nativeQuery=true)
        Map<String, Object> findByRecord();
    }
    
    @SpringBooTest
    class UserRepositoryTest{
        @Autowired
        UserRepository userRepository;
        
        @Test
        void test(){
            User user = new User();
            user.setName("user1");
            user.setEmail("email");
            user.setGender(Gender.MALE);
            
            userRepository.save(user);
            
            System.out.println(userRepository.findByRecord().get("gender"));
        }
    }

    위와 같은 예시의 경우 Gender값이 enum값이므로 0이 출력되게 됩니다. 이와 같은 경우 만약 enum의 MALE 앞에 다른 값이 추가될 경우 전체 Gender enum을 사용하는 필드의 값들이 의도치 않게 바뀔 수 있습니다.

    그러므로 @Enumerated 어노테이션에 value 속성을 다음과 같이 바꿔서 사용하면 해당 enum의 값이 그대로 db에 저장되게 됩니다.

    public class User{
        ...
        @Enumerated(value = EnumType.STRING)
        private Gender gender;
    }

    위의 테스트를 다시 실행하면 MALE이 출력되는 것을 확인할 수 있습니다.

     

     

     

    Entity Listener

    특정 이벤트가 발생하면 어떠한 동작이 실행될 수 있도록 정의할 수 있습니다.

    public class User {
        @PrePersist // insert 이전
        @PreUpdate // update 이전
        @PreRemove // remove 이전
        @PostPersist // insert 이후
        @PostUpdate // update 이후
        @PostRemove // remove 이후
        @PostLoad // select 이후
    }

    위와 같이 7개의 어노테이션이 존재합니다.

    원하는 이벤트에 동작할 메서드위에 어노테이션을 선언하면 해당 동작과 함께 실행되게 됩니다.

     

    예시)

    createdAt, updatedAt의 경우 모든 insert, update문에서 반복되므로 쿼리 문마다 따로 set을 해주기 보다 @PrePersist, @PreUpdate 메서드 상에서 값을 set해준다면 반복을 없앨 수 있습니다.

    public class User {
    	...
        
        private LocalDateTime createdAt;
        
        private LocalDateTime updatedAt;
        
        @PrePersist
        public void prePersist(){
            this.createdAt = LocalDateTime.now();
            this.updatedAt = LocalDateTime.now();
        }
        
        @PreUpdate
        public void prePersist(){
            this.updatedAt = LocalDateTime.now();
        }
    }

    위와 같이 설정해줄 경우, 각 이벤트가 발생하기 전에 해당 메서드를 실행하게 됨으로써 따로 set해주지 않아도 되게 됩니다.

     

     

    @EntityListeners

    위와 같이 어노테이션으로 설정할 경우 모든 Entity에서 해당 메서드들을 선언하는 것을 반복해야 될 것입니다. 이를 방지하기 위해 나타난 것이 @EntityListeners입니다.

    public class MyEntityListener{
        @PrePersist
        public void prePersist(Object o){
            if(o instanceof Auditable){
                ((Auditable) o).setCreatedAt(LocalDateTime.now());
                ((Auditable) o).setUpdatedAt(LocalDateTime.now());
            }
        }
        
        @PreUpdate
        public void preUpdate(Object o){
            if(o instanceof Auditable){
                ((Auditable) o).setUpdatedAt(LocalDateTime.now());
            }
        }
    }
    
    public interface Auditable {
        LocalDateTime getCreatedAt();
        LocalCateTime getUpdatedAt();
        
        void setCreatedAt(LocalDateTime createdAt);
        void setUpdatedAt(LocalDateTime updatedAt);
    }
    
    @Entity
    @EntityListener(value = MyEntityListener.class)
    public class user implements Auditable{
        ...
    }

    @Entity 객체에 클래스 스콥에 @EntityListener를 선언해주고 value값에 EntityListner클래스를 넣어줍니다.

     

    EntityListener 클래스에서 사용할 메서드를 정의한 Auditable 인터페이스를 선언해주고 이를 User클래스에서 구현하도록 합니다.(User클래스의 경우 @Data 어노테이션에 @Getter, @Setter가 선언되어 있으므로 자동 구현이 되어 있는 상태입니다.)

     

    EntityListener에서는 @PrePersist, @PreUpdate 메서드를 선언해줍니다. 원래, 해당 어노테이션으로 선언된 메서드들은 Object 타입의 인자를 하나 갖습니다. 이 인자는 실제 query문에서 사용하는 객체 값이 전달이 됩니다.

    이 Object의 타입이 Auditable일 경우 set메서드를 통해 값을 넣어주면 됩니다.

     

    예시)

    public class UserEntityListener {
        @PrePersist
        @PreUpdate
        public void prePeristAndPreUpdate(Object o){
            UserHistoryRepository userHistoryRepository = BeanUtils.getBean(UserHistoryRepository.class);
            
            User user = (User) o;
            
            UserHistory userHistory = new UserHistory();
            userHistory.setUserId(user.getId());
            userHistory.setName(user.getName());
            userHistory.setEmail(user.getEmail());
            
            userHistoryRepository.save(userHistory);
        }
    }
    
    @EntityListeners(value = {MyEntityListener.class, UserEntityListener.class })
    public class User implements Auditable {
        ...
    }

    EntityListener의 경우 여러개를 받아올 수 있습니다. UserEntityListener를 User 클래스에 추가해줍니다.

    insert와 update가 발생하기 전에 UserHistory클래스에 새로운 데이터를 추가해주는 작업을 해줄 수 있습니다.

     

    (위의 BeanUtils의 경우 EntityListener의 경우 Spring Bean을 주입 받지 못하기 때문에 Spring Bean을 가져오는 역할입니다. 아래와 같은 설정을 해주면 사용 가능합니다.)

    @Component
    public class BeanUtils implements ApplicationContextAware{
        private static ApplicationContext applicationContext;
        
        @Override
        public void setApplicationContext(ApplicationContext applicationContext) throws BeansException {
            BeanUtils.applicationContext = applicationContext;
        }
        
        public static <T> T getBean(Class<T> clazz) {
            return applicationContext.getBean(clazz);
        }
    }

    getBean 메서드를 통해 클래스를 지정해주면 해당 클래스에 맞는 bean을 반환해줍니다.

     

     

    AuditingEntityListener

    @SpringBootApplication
    @EnableJpaAuditing
    public class Main {
        ...
    }
    
    @Entity
    @EntityListeners(value = { AuditingEntityListener.class })
    public class user{
        ...
        
        @CreatedDate
        private LocalDateTime createdAt;
        
        @LastModifiedDate
        private LocalDateTime updatedAt;
    }

    createdAt, updatedAt과 같이 많이 사용되고 있는 값에 대해서는 쿼리마다 시간 설정을 도와주는 어노테이션을 이미 제공해주고 있습니다.

    @EnableJpaAuditing 어노테이션을 메인 클래스에 선언해준 뒤, createdAt에는 @CreatedDateupdatedAt에는 @LastModifiedDate를 선언해주면 이전의 MyEntityListener에서 만들었던 @PrePersist, @PreUpdate 메서드와 똑같이 동작하게 됩니다.

     

    AuditingEntityListener는 감시를 통해 어떤 이벤트에 필요한 동작을 할 수 있도록 하는 어노테이션을 다양하게 제공해줍니다.

    LastModifedBy의 경우 생성, 수정한 사람의 정보를 함께 저장하도록 할 수도 있습니다. (이는 Spring Security를 사용하는 내용으로 추후에 다루어보도록 하겠습니다)

     

     

    + 리팩토링

    위와 같이 어노테이션의 추가를 통해서 EntityListener를 명시할 수 있었습니다. 이미 많이 편리하게 사용할 수 있지만, createdAt, updatedAt과 같은 경우 여러 엔티티에서 공통적으로 사용하고 있으므로 상위 객체로 빼보도록 하겠습니다

    @Data
    @MappedSuperClass
    @EntityListeners(value = AuditingEntityListener.class)
    public class BaseEntity {
        @CreatedDate
        private LocalDateTime createdAt;
        
        @LastModifiedDate
        private LocalDateTime updatedAt;
    }
    
    @ToString(callSuper = true)
    @EqualsAndHashCode(callSuper = true)
    @Entity
    public class User extends BaseEntity implements Auditable {
        ...
    }

    createdAt과 updatedAt이 gettersetter가 필요하기 때문에 @Data 어노테이션을 선언해주었습니다.

    BaseEntity의 필드를 상속받는 엔티티 클래스에 컬럼으로 포함시켜주기 위해 @MappedSuperClass 어노테이션을 선언해주었습니다.

     

    상속 받는 클래스에서 toString, equals, hashCode 메서드 상에서 상위 클래스의 필드까지 포함할 수 있도록 @ToString, @EqualsAndHashCode 어노테이션을 선언해주고 callSuper 속성을 true로 해줍니다.

     

     

     

     

     

     

     

    오늘은 Entity 생애 주기 안에서 발생하는 이벤트에 따라 추가적으로 동작할 수 있는 EntityListener에 대해 알아보았습니다. Entity 클래스 내부에 선언하거나, 외부에 클래스로 선언하고 @EntityListener의 value로 받는 것, 그리고 Spring Data Jpa에서 제공하는 AuditingEventListener를 사용하는 방법에 대해 알아보았습니다.

    상황에 따라서 필요한 메서드나 클래스를 선언해서 유연하게 사용하는 것이 중요할 것 같습니다.

     

    반응형
    LIST

    '백엔드' 카테고리의 다른 글

    Jpa 연관관계 살펴보기 (N:N)  (0) 2023.08.23
    JPA 연관관계 살펴보기 (1:1, 1:N, N:1)  (4) 2023.08.21
    JPA 쿼리메서드  (0) 2023.08.18
    JPA 살펴보기  (0) 2023.08.17
    Spring boot todo list 만들기  (2) 2023.07.21

    댓글

Designed by Tistory.