본문 바로가기

개발/쉽게 이해하고 사용하는 JPA

JPA Repository 를 이용한 데이타 사용

Entity 에 있는 데이타를 조회하거나 저장과 변경 그리고 삭제를 할때 Spring JPA 에서 제공하는 Repository 라는 인터페이스를 정의해 해당 Entity 의 데이타를 사용 할 수 있습니다. Repository 는 내부적으로 EntityManager 가 직접 대상 Entity의 데이타를 관리하고 있기 때문에 굳이 Repository 인터페이스를 정의하지 않고도 직접 EntityManager 를 사용해 Persistance Layer 를 구현 할 수도 있지만 Spring JPA 에서 Repository 의 내부 구현체를 자동으로 생성시켜 주기 때문에 별도의 구현체를 따로 생성하지 않아도 됩니다.

Repository 는 대게 Entity 와 1:1 로 매칭된다고 볼 수도 있으나 꼭 그런것은 아니며 부모에 종속적으로 사용이 되는 자식 Entity 가 있고 자식에 대한 직접적인 데이타 조작이 필요가 없다면 부모 Entity 에 대해서만 Repository 를 정의하면 됩니다. 자식 Entity 가 독립적으로 사용될 필요가 없다면 부모 Repository 만 생성하고 필요하다면 부모 Entity 에서 지정된 관계를 통해 자식 Entity 를 사용하면 된다는 의미입니다.

Post Entity

먼저 테스트를 위해 여러 관계가 지정되어 있는 Post Entity 를 정의하겠습니다. 앞서 관계를 설명하는 것 보다는 조금더 긴 내용을 담고 있지만 하나씩 살펴 보도록 하겠습니다.

  1. 데이타의 불변성을 보장해 주기 위해 Entity 내부 변수에 대한 Setter 함수는 정의 하지 않고 lombok lib 에서 자동으로 Getter 함수를 생성해 주는 @Getter 어노테이션을 지정해 줍니다. 이는 Setter 함수를 사용하지 않음으로써 어디에서 발생될지 알 수 없는 값의 오염을 방지 하고자 하는 목적입니다. Entity 내부 변수의 값이 변경될때는 오직 특정 행위를 위해 별도로 정의된 개별 함수를 통해서만 변경이 이루어지도록 합니다.
  2. 또한 EntityManager 가 Entity 를 사용하기 위해선 기본 생성자가 반드시 필요한데 프로그램 내에 아무곳에서나 Entity 를 생성하지 않도록 lombok lib 에서 제공하는 @NoArgsConstructor 어노테이션에 access = AccessLevel.PROTECTED 속성을 정의해 줍니다. @NoArgsConstructorAccessLevel.PROTECTED 속성을 정의하면 protected 기본 생성자가 생성이 되어 무분별한 Entity 생성을 막을 수 있습니다.
  3. 내부 변수에는 다양한 형태로 값을 조회하는 환경을 테스트하기 위해서 @OneToOne, @OneToMany, @ManyToOne, @ManyToMany 등 다양한 형태의 관계를 지정 했습니다.
  4. 마지막으로 Entity 의 모든 내부 변수들을 인자로 받는 생성자를 지정합니다. 이때 id key 변수의 값은 자동으로 생성이 되기 때문에 인자로 전달 받지 않습니다.
@Entity(name = "post")
@Getter
@NoArgsConstructor(access = AccessLevel.PROTECTED)
public class Post {

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

    private String title;

    @Column(length = 2000)
    private String content;

    @ManyToOne(fetch = FetchType.LAZY)
    @JoinColumn(name = "category_id")
    private Category category;

    @OneToOne(mappedBy = "post", cascade = CascadeType.ALL, fetch = FetchType.LAZY)
    private Detail detail;

    @OneToMany(mappedBy = "post", cascade = CascadeType.ALL)
    private List<Reply> replyList;

    @ManyToMany
    @JoinTable(
            name = "post_tag",
            joinColumns = @JoinColumn(name = "post_id"),
            inverseJoinColumns = @JoinColumn(name = "tag_name")
    )
    private List<Tag> tagList;

    @Column(name = "created_by", updatable = false)
    private String createdBy;

    @Column(name = "created_date", updatable = false)
    private LocalDateTime createdDate;

    public Post(Category category, String title, String content, String description, List<Tag> tagList, String createdBy) {
        this.category = category;
        this.title = title;
        this.content = content;
        this.detail = new Detail(this, description);
        this.tagList = tagList;
        this.replyList = new ArrayList<>();
        this.createdBy = createdBy;
        this.createdDate = LocalDateTime.now();
    }

    public void addReply(String reply) {
        this.replyList.add(new Reply(this, reply));
    }

    public void change(Category category, String title, String content, String description, List<Tag> tagList) {
        this.category = category;
        this.title = title;
        this.content = content;
        this.detail.change(description);
        this.tagList = tagList;
    }

    public String getDescription() {
        if (this.detail != null) {
            return this.detail.getDescription();
        } else {
            return "";
        }
    }
}

나머지 Entity 에 대해서는 따로 기술하지 않았습니다. 동작되는 모든 내용을 확인하고 싶으시면 아래 소스코드를 확인해 주세요.

 

PostRepository

앞서 정의한 Post Entity 를 위한 Repository 를 생성합니다. 이야기 한것과 같이 Post Entity 와 관계를 맺고 있고 하위에 위치하고 있는 Detail, Reply Entity 에 대해선 별도의 Repository 를 생성하지 않았습니다. Detail, Reply 는 Post 와의 관계를 벗어나 단독으로 사용될 경우가 없기 때문입니다.

Repository 의 정의는 간단히 JpaRepository 인터페이스를 상속만 하면 끝입니다. 이중에 Creteria 를 위한 인터페이스, QueryDSL 을 위한 인터페이스등을 추가로 상속받아 확장 시킬 수 있으며 이때 EntityManager 가 알아서 해당 기능의 구현체를 정의해 줍니다. 다른 인터페이스에 대해선 나중에 더 살펴 보도록 하겠습니다.

JpaRepository 는 기본적인 CRUD 를 처리할 수 있는 getOne, findById, findAll, save, delete 등의 함수가 정의 되어 있어 간단한 내용을 데이타페이스로 부터 처리 할 수 있게 해줍니다.

public interface PostRepository extends JpaRepository<Post, Long> {
}

 

Entity 데이타 조회

Entity 에는 데이타를 구분하기 위한 식별자 id를 기본적으로 가진다고 이야기 드렸습니다. Repository 는 이 식별자를 기준으로 데이타를 조회 하며 이 식별자에 해당하는 단일 데이타를 조회하는 방법으로 getOnefindById 함수가 제공 됩니다. 두가지 방법의 차이는 findById 의 경우 java Optional 인스턴스가 반환이 되며 getOne 의 경우 Entity 인스턴스 자체가 반환이 된다는 점 입니다. 또한 findAll 함수를 이용해 Entity 의 데이타 전부를 가지고 올 수도 있습니다.

Optional 은 java 8 에서 처음 도입이 되었으며 java 에서 값이 없음을 표현하기 위한 null 값을 그대로 사용하지 않고 Optional 인스턴스로 대체하여 값이 없음에 대한 예기치 못한 에러 발생으로 부터 안전한 값의 처리를 지원한다는 점이 특징이라고 할 수 있습니다.

Optional<Post> optionalPost = postRepository.findById(id);
if (optionalPost.isPresent()) {
    Post post = optionalPost.get()
}

List<Post> postList = postRepository.findAll();

 

Entity 데이타 조회 조건

JPA 는 미리 정의된 키워드를 통해 보다 정확한 데이타를 가지고 올 수 있도록 추가 조회 조건이 정의된 함수를 만들 수 있는 방법을 제공해 줍니다. 이것은 마치 데이타베이스에 Query 를 질의 하는 방식과 동일하다고 할 수 있습니다. 키워드 종류를 구분해 보자면 값을 비교하는 조건키워드, 특정 조건들을 And, Or 로 서로 연결하는 연결키워드, 그리고 정렬할 기준을 정의할 수 있는 정렬키워드 로 구분할 수 있습니다. 중요한 것은 데이타베이스에 Query 를 직접 작성 하지 않고 키워드들을 합쳐 놓은 조회용 함수를 분석해 EntityManager 가 알아서 Query 를 생성한다는 점입니다.

조회용 함수를 지정하는 방법은 간단합니다. 단일건을 조회 할지 여러건을 조회 할지에 따라 findBy, findAllBy 로 시작이 되며 Entity 에 미리 정의되어 있는 내부 변수명을 연결하여 사용할 조건 키워드와 함께 함수를 정의합니다.

public interface PostRepository extends JpaRepository<Post, Long> {
    List<Post> findAllByTitleLike(String title);

    List<Post> findAllByCategory(Category category);

    List<Post> findAllByCreatedDateGreaterThanEqualOrderByIdDesc(LocalDateTime localDateTime);

    List<Post> findAllByTagListInOrderByCreatedDateAsc(List<Tag> tagList);
}

 

사용할 수 있는 키워드의 종류는 아래와 같습니다.

  • 조건키워드
    • Is, Equals, Between, Like, NotLike, StartingWith, EndingWith, Containing, Not, In, NotIn, True, False, IgnoreCase
    • LessThan, LessThanEqual, GreaterThan, GreaterThanEqual, After, Before
    • IsNull, IsNotNull, NotNull
  • 연결키워드
    • AND, OR
  • 정렬키워드
    • OrderBy

 

@Query

JPA 에 정의된 키워드를 조합하면 특정조건에 해당하는 데이타를 원하는 형태대로 가지고 올 수 있습니다. 하지만 데이타베이스에 종속적인 문법을 사용해야 할때나 Entity 간의 명시적으로 들어나지 않는 관계간의 조인, 데이타 조회 속도 향상등의 목적으로 직접 쿼리를 작성할 수 있는 방법을 제공하고 있습니다. @Query 속성중에 nativeQuery 속성을 true 로 설정하지 않았다면 기본적으로 JPQL 문법으로 동작이 됩니다.

JPQL 문법은 JPA 에서 사용 되는 언어이며 쿼리 구문과 유사하나 Table 이 아닌 Entity 를 기준으로 데이타를 조회한다는 것이 다릅니다. 하지만 만약 쿼리 문법이 더 익숙하다면 nativeQuery 속성을 통해 직접 쿼리를 작성할 수도 있습니다.

public interface PostRepository extends JpaRepository<Post, Long> {
    @Query("select p from Post p where p.title like ?1")
    List<Post> findAllByTitleLike(String title);

    @Query(value = "select p.* from post p where p.category_id = ?1", nativeQuery = true)
    List<Post> findAllByCategoryId(Long categoryId);
}

 

Entity 의 영속화

JPA 는 내부적으로 데이타를 임시로 담기 위한 공간을 가지는데 이것을 @Transactional 단위로 묶어서 관리합니다. 임시로 담긴 데이타는 commit 혹은 rollback 이 되는 순간까지 변경 상태가 유지가 됩니다. 만약 조회함수로 Post Entity 의 @Id key'1' 인 인스턴스를 조회해 왔다고 가정해 봅시다. 이때 동일한 @Transactional 단위에서 동일한 Post Entity 의 @Id Key'1' 인 인스턴스를 조회하면 해당 Entity 인스턴스가 새롭게 데이타베이스상에서 조회가 되는 것이 아니라 메모리에 있는 Entity 인스턴스가 반환이 됩니다.

이것은 Entity 의 저장에 대해서도 동일하게 사용이 되는데 이를 위해 JpaRepository 인터페이스의 내부에는 save, saveAndFlush 함수가 제공됩니다. 두가지 함수 모두 Entity 인스턴스의 영속화 즉 데이타베이스에 데이타를 저장하는 함수 이지만 saveAndFlush 함수는 저장 즉시 데이타를 데이타베이스로 전송하고 save 함수는 @Transactional 범위가 종료되면 commit 이 되는 순간 데이타베이스에 저장이 되는 차이점이 있습니다.

만약 new Post() 를 통해 새로운 Post Entity 인스턴스를 생성 했다고 가정 했을때 명시적으로 PostRepository.save() 함수를 통해 저장을 한다는 코드를 작성할 필요없이 @Transactional 이 종료 되는 순간 새롭게 생성된 Post Entity 내용은 즉시 데이타베이스에 반영이 된다는 점이 특이점이라고 할 수 있습니다. 이것은 값의 변경에도 동일하게 적용이 되기 때문에 만일 Entity 인스턴스의 내부 변수 값이 변경이 되었다면 명시적으로 save() 함수를 호출하지 않더라도 데이타베이스에 영속화 된다는 점을 주의해야 합니다.

 

소스코드

앞서 설명한 JPA 환경 구성과 사용 예제에 대한 소스 코드는 아래 주소에서 확인 하실 수 있습니다.

https://github.com/jogeum/hellojpa