Spring & Spring boot

[Spring Boot] JPA - 3. Entity & Repository

jh4dev 2024. 7. 26. 10:59

지난 포스팅에서 Entity / Entity Manager / Persistence Context 의 역할과 관계에 대해 알아보았다.

이번 포스팅에서는 실제 Entity 클래스를 생성하는 방법과, Spring Data JPA 를 사용하기에, Entity Manager 를 사용하는 Repository 에 대해 알아보자.

 

** 포스팅 내, 샘플 소스는 롬복(Lombok) 을 사용하여 작성되어있으니 유의 바랍니다. **

롬복 관련해서는 별도의 포스팅을 작성할 예정입니다.

 

<목차>

1. Entity
2. Repository
3. Repository & Entity Manager

 

[Entity]

JPA에서 Entity 는 DB 테이블에 매핑된 클래스를 의미한다.

Entity 클래스에서는 어노테이션을 활용해 DB에 쓰일 테이블과 컬럼을 정의한다.

 

예제는, 참고 도서를 활용하겠다.

생성할 테이블의 구조는 다음과 같다.

 

이제 Product 라는 이름의 Entity 클래스를 생성해보자.

    //Lombok
    @Builder
    @Getter
    @Setter
    @AllArgsConstructor
    @NoArgsConstructor
    @EqualsAndHashCode(of = "number")
    //Entity 관련
    @Entity
    @Table(name="product")
    public class Product {

        @Id
        @GeneratedValue(strategy = GenerationType.IDENTITY)
        private Long number;

        @Column(nullable = false)
        private String name;

        @Column(nullable = false)
        private Integer price;

        @Column(nullable = false)
        private Integer stock;

        private LocalDateTime createdAt;

        private LocalDateTime updatedAt;
    }

 

위 소스에서는, @Entity / @Table / @Id / @GeneratedValue / @Column 와 같은 것들이 Entity 클래스에서 사용하는 어노테이션이다.

작성된 어노테이션 외, Entity 클래스에서 사용하는 기본 어노테이션에서 알아보자.

  • 기본 어노테이션
    • @Entity
      • 클래스가 JPA Entity 임을 나타내며, DB테이블과 매핑된다.
      • 해당 클래스의 인스턴스는 매핑되는 테이블에서 하나의 Row를 의미한다.
    • @Table
      • 기본적으로 클래스명과 동일한 테이블이 생성되며, 별도로 name 속성을 활용해 테이블 명을 지정할 수 있다.
      • Java에서 클래스 명의 첫 글자는 대문자로 시작하기에, "Product" 와 같은 형태로 생성되며, 이를 방지하기 위해 name 속성을 활용한다.
      • EX) @Table(name="product")
    • @Id
      • 엔티티 클래스의 필드 중, PK로 사용할 필드를 지정하는 어노테이션
      • 모든 엔티티에 필수로 있어야 함!
    • @GeneratedValue
      • 일반적으로 @Id 어노테이션과 함께 사용된다.
      • 해당 필드의 값이 자동으로 생성되는 방법을 정의한다.
        • 미사용 : 비즈니스 로직에서 자체적으로 고유한 기본 값을 생성하여 사용하는 경우
        • AUTO : Default 설정으로, DB에 맞게 자동 생성한다.
        • IDENTITY : DB에 기본값 생성을 위임하는 방식으로, AUTO_INCREMENT 를 사용해 생성된다.
        • SEQUENCE : DB 시퀀스를 사용한다.
        • TABLE : 별도의 테이블을 사용하여 기본값을 생성한다.
    • @Column
      • 엔티티 클래스의 필드는 자동으로 테이블 컬럼으로 매핑된다.
      • 따라서, 별도의 설정을 할 내용이 없다면, 어노테이션을 명시하지 않아도 된다.
      • 사용할 수 있는 속성은 아래와 같은 것들이 있다.
        • name : 컬럼명을 지정하는 속성으로, 명시하지 않으면 필드명과 동일하게 생성된다.
        • nullable : 해당 컬럼이 null 을 허용할 지 명시하는 속성 (true / false)
        • length : 해당 컬럼의 길이를 지정한다.
        • unique : 해당 컬럼을 Unique 로 설정한다.
    • @ Transient
      • 데이터베이스에 매핑되지 않는 필드를 지정할 때 사용하며, JPA는 이 필드를 데이터베이스에 저장하거나 로드하지 않는다.

엔티티 간의 관계에 대한 어노테이션은 추후 정리 예정.


[Repository]

Spring Data JPA 를 사용하면, Repository 를 사용하여 DB에 접근한다.

 

이제 실제 Repository 인터페이스를 생성하는 방법에 대해 알아보자.

Spring Data JPA 를 사용한다면, 매우 간단하다.

 

위 생성한 Product Entity 에 대한 Repository 를 생성해보자.


<PrudctRepository>

    public interface ProductRepository extends JpaRepository<Product, Long> {
    }

 

이처럼, JpaRepository 인터페이스를 상속받아 생성하지만, 해당 인터페이스에는 비교적 기본적인 CRUD 메서드만 존재한다.

 

<JpaRepository>

    @NoRepositoryBean
    public interface JpaRepository<T, ID> extends PagingAndSortingRepository<T, ID>, QueryByExampleExecutor<T> {
        List<T> findAll();

        List<T> findAll(Sort sort);

        List<T> findAllById(Iterable<ID> ids);

        <S extends T> List<S> saveAll(Iterable<S> entities);

        void flush();

        <S extends T> S saveAndFlush(S entity);

        <S extends T> List<S> saveAllAndFlush(Iterable<S> entities);

        /** @deprecated */
        @Deprecated
        default void deleteInBatch(Iterable<T> entities) {
            this.deleteAllInBatch(entities);
        }

        void deleteAllInBatch(Iterable<T> entities);

        void deleteAllByIdInBatch(Iterable<ID> ids);

        void deleteAllInBatch();

        /** @deprecated */
        @Deprecated
        T getOne(ID id);

        /** @deprecated */
        @Deprecated
        T getById(ID id);

        T getReferenceById(ID id);

        <S extends T> List<S> findAll(Example<S> example);

        <S extends T> List<S> findAll(Example<S> example, Sort sort);
    }

 

이 기본 메서드 외 필요한 부분이 있다면 다음과 같은 규칙을 따라 메서드를 만들어 줄 수 있다.

Entity를 저장 / 갱신 / 삭제 시에는 별도의 규칙이 필요한 경우가 없기 때문에, 주로 Read 부분에 해당하는 Select 쿼리를 생성하는 메서드를 만들 수 있다.

  • findBy
    • SQL 문의 WHERE 절 역할을 수행하는 구문으로, findBy 뒤에 Entity의 필드명을 입력해서 사용한다.
  • And / Or
    • AND, OR 조건 여러 개 설정하기 위해 사용한다.
  • Like / NotLike
    • SQL 문의 LIKE / NOT LIKE 와 동일한 기능을 수행한다.
  • StartWith / StartingWith
    • 특정 키워드로 시작하는 문자열 조건을 설정한다.
  • EndWith / EndingWith
    • 특정 키워드로 끝나는 문자열 조건을 설정한다.
  • IsNull / IsNotNull
    • 레코드 값이 Null 이거나 Null 이 아닌 값을 검색한다.
  • True / False
    • Boolean 타입의 레코드를 검색할 때 사용한다.
  • Before / After
    • 시간을 기준으로 값을 검색한다.
  • LessThan / GreaterThan
    • 특정 값을 기준으로 대소 비교를 할 때 사용한다.
  • Between
    • 두 값 사이의 데이터를 조회한다.
  • OrderBy
    • SQL 문에서 ORDER BY 절과 같은 기능을 수행한다.
    • Asc / Desc 를 붙여 오름차순 / 내림차순을 설정할 수 있다.
  • CountBy
    • SQL 문의 COUNT와 동일한 기능을 수행한다.

[Repository & EntityManager]

앞선 포스팅에서, Entity 는 Entity Manager에 의해 관리되며, Entity 와 DB 간의 상호작용을 처리한다 하였으니, Repository 의 실제 구현체인 SimpleJpaRepository 를 살펴보자.

    @Repository
    @Transactional(
        readOnly = true
    )
    public class SimpleJpaRepository<T, ID> implements JpaRepositoryImplementation<T, ID> {
        private static final String ID_MUST_NOT_BE_NULL = "The given id must not be null!";
        private final JpaEntityInformation<T, ?> entityInformation;
        private final EntityManager em;
        private final PersistenceProvider provider;
        @Nullable
        private CrudMethodMetadata metadata;
        private EscapeCharacter escapeCharacter;

        public SimpleJpaRepository(JpaEntityInformation<T, ?> entityInformation, EntityManager entityManager) {
            this.escapeCharacter = EscapeCharacter.DEFAULT;
            Assert.notNull(entityInformation, "JpaEntityInformation must not be null!");
            Assert.notNull(entityManager, "EntityManager must not be null!");
            this.entityInformation = entityInformation;
            this.em = entityManager; //!!엔티티 매니저!!
            this.provider = PersistenceProvider.fromEntityManager(entityManager);
        }

        public SimpleJpaRepository(Class<T> domainClass, EntityManager em) {
            this(JpaEntityInformationSupport.getEntityInformation(domainClass, em), em);
        }
        ... 중략 ...

        public Optional<T> findById(ID id) {
            Assert.notNull(id, "The given id must not be null!");
            Class<T> domainType = this.getDomainClass();
            if (this.metadata == null) {
                return Optional.ofNullable(this.em.find(domainType, id));
            } else {
                LockModeType type = this.metadata.getLockModeType();
                Map<String, Object> hints = new HashMap();
                this.getQueryHints().withFetchGraphs(this.em).forEach(hints::put);
                return Optional.ofNullable(type == null ? this.em.find(domainType, id, hints) : this.em.find(domainType, id, type, hints));
            }
        }
        ... 이하 생략 ...

 

위와 같이, SimpleRepositoryEntityManager 의존성을 주입받아 생성되며, 주입받은 EntityManagerEntity 를 관리하는데 사용된다.

 

따라서, Spring Data JPA 에서 Repository 사용에 앞서 Extend / Implements 구조를 이해하고 있으면 도움이 된다.

JpaRepository