Spring Boot JPA 적용 2. JpaRepository 사용 심화

최민길(Gale)·2023년 8월 4일
1

Spring Boot 적용기

목록 보기
43/46

안녕하세요 이번 시간에는 JpaRepository 사용법에 대해 조금 더 깊게 알아보는 시간을 갖도록 하겠습니다.

먼저 Jpa 사용 시 FK 설정된 테이블 엔티티를 생성하는 방법에 대해 알아보겠습니다. 아래는 유저 정보와 공지사항 정보를 가지고 있는 UserAnnouncement 테이블의 Entity입니다. 테이블 내의 userID는 User 테이블의 ID와 매핑되며, announcementID는 Announcement 테이블의 ID와 매핑됩니다.

FK 설정을 진행함에 있어서 컬럼의 관계를 정의하는 어노테이션은 @ManyToOne, @OneToMany 등의 어노테이션입니다. 해당 어노테이션은 일대다 혹은 다대일 관계를 정의하며 targetEntity에 정의한 엔티티와의 관계를 나타냅니다. 한 유저가 여러 공지사항을 읽을 수 있으며 한 공지사항을 여러 유저가 읽을 수 있기 때문에 UserAnnouncement 테이블 입장에서는 유저와 공지사항 테이블과 다대일 관계가 성립됩니다. 따라서 해당 컬럼에 @ManyToOne 어노테이션을 추가합니다.

객체를 가져오는 방식을 추가적으로 설정할 수 있습니다. fetch 옵션에서 FetchType.LAZY 또는 FetchType.EAGER를 선택할 수 있습니다.

EAGER 옵션의 경우 디폴트 옵션으로 매핑된 객체가 한번에 로딩됩니다. 쉽게 말해 아래의 엔티티를 findBy를 통해 가져오게 되면 User 테이블과 Announcement 테이블의 정보를 같이 로딩하게 됩니다. 이는 한번에 여러 엔티티 정보를 가져올 수 있어 편리하지만 불필요한 조회 쿼리를 실행시키기 때문에 성능 이슈가 발생할 수 있습니다.

반면 LAZY 옵션의 경우 매핑된 객체가 실제로 사용되는 시점에 조회됩니다. LAZY 옵션의 경우 프록시 객체를 이용하여 구현되며 연결된 객체에 프록시 객체를 넣어 실제 사용될 때까지 로딩을 미루게 됩니다. 아래의 엔티티에서 보면 UserAnnouncement 테이블에서 findBy 메서드를 사용하면 UserAnnouncement 컬럼들만 조회하고 User 및 Announcement 객체에는 프록시 객체가 추가됩니다. 이후 UserAnnouncement 엔티티에서 User 및 Announcement 객체를 조회할 때 데이터베이스이 접근합니다.

@JoinColumn 어노테이션의 경우 현재 테이블의 컬럼과 매핑할 테이블의 컬럼을 선택하는 역할을 담당합니다. 여기서 주의할 점은 엔티티 변수명이 아니라 DB의 테이블의 컬럼명으로 입력해야한다는 점입니다. 아래 예시를 보면 UserAnnouncement 테이블의 userID 컬럼과 User 테이블의 ID 컬럼을 입력하여 DB 레벨에서 매핑이 진행됩니다.

insertable과 updatable 옵션은 데이터 추가 및 변경 가능 여부에 대한 정의입니다. insertable = false의 경우 insert 시점에서 데이터 추가를 막으며 update = false의 경우 update 시점에서 데이터 추가를 막습니다. 이를 통해 데이터 추가 시 자동으로 생성되는 항목이나 변경이 되면 안되는 항목을 해당 옵션을 통해 조절할 수 있습니다.

import jakarta.persistence.*;
import lombok.*;

import java.time.LocalDateTime;

@Entity
@Table(name="UserAnnouncement")
@Getter
@Setter
@ToString
@RequiredArgsConstructor
public class UserAnnouncement {
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    @Column(name="ID", nullable = false, insertable = false, updatable = false)
    private long userAnnouncementID;

    @Column(name="userID", nullable = false)
    private long userID;

    @ManyToOne(targetEntity = User.class, fetch = FetchType.LAZY)
    @JoinColumn(name="userID", insertable = false, updatable = false, referencedColumnName="ID")
    private User user;

    @Column(name="announcementID", nullable = false)
    private long announcementID;

    @ManyToOne(targetEntity = Announcement.class, fetch = FetchType.LAZY)
    @JoinColumn(name="announcementID", insertable = false, updatable = false, referencedColumnName="ID")
    private Announcement announcement;

    @Temporal(TemporalType.TIMESTAMP)
    @Column(name="createAt", nullable = false, insertable = false, updatable = false)
    private LocalDateTime createAt;
}

그럼 조회 이외의 다른 기능들을 사용하보겠습니다. 우선 insert와 update는 둘 다 save() 메서드를 이용합니다. 해당 엔티티를 생성한 후 원하는 값을 채워 넣어 save() 메서드를 실행하면 됩니다. 이 때 @Transactional 어노테이션을 이용하여 트랜잭션을 생성하여 모든 로직이 성공했을 때 커밋하여 DB에 안정적으로 정보를 저장합니다.

    @Transactional
    public long createUser(CreateUser createUser, String code){
        User user = new User();
        user.setEmail(createUser.getEmail());
        user.setPassword(createUser.getPassword());
        user.setUserName(createUser.getName());
        user.setUserCode(code);
        User newUser = userRepository.save(user);
        return newUser.getUserID();
    }

하지만 where를 이용한 단순 update문을 실행시킬 경우 save() 메서드를 사용 시 findBy로 관련 정보를 가져온 후 정보를 수정해서 다시 save() 해야 하는데, 이 과정에서 DB에 접근을 2번 하게 되기 때문에 성능 이슈가 발생할 수 있습니다. 이를 위해 JPA에서는 자체적으로 쿼리를 실행시킬 수 있는 @Query 어노테이션을 지원합니다. @Query 어노테이션 내부에 실행시킬 쿼리와 파싱할 변수를 설정하여 아래 코드와 같이 설정할 수 있습니다. 여기서 주의할 점은 @Query 어노테이션 내에 사용되는 쿼리는 DB 기준이 아닌 엔티티 기준으로 작성해야 한다는 점입니다. 따라서 아래의 경우 User 엔티티에서 정의한 항목들로 쿼리를 작성해주셔야 합니다.

@Repository
public interface UserRepository extends JpaRepository<User,Long> {
    ...

    @Modifying
    @Transactional
    @Query("UPDATE User SET appPassword = 99999 WHERE userID = :userID")
    void deleteUser(long userID);
}

JPA에서 내장 함수는 자체적인 메서드로 제공됩니다. 대표적으로 count()의 경우 countBy 메서드 형식으로 제공합니다.

@Repository
public interface UserAnnouncementRepository extends JpaRepository<UserAnnouncement,Long> {
    long countByUserID(long userID);
}

또한 SQL 쿼리에서 limit, 즉 특정 개수의 데이터만을 가져오고 싶을 경우 Pageable 객체를 사용합니다. Pageable 변수를 매개변수로 받고 PageRequest를 이용하여 Pagable 객체를 생성합니다. 이 때 내부 파라미터는 limit와 같습니다, 즉 (시작 인덱스, 가져올 데이터 크기) 형식으로 데이터를 입력받습니다. 이렇게 생성한 Pageable 객체를 넣어주면 데이터를 특정 개수만큼 가져오게 됩니다.

@Repository
public interface AnnouncementRepository extends JpaRepository<Announcement,Long> {
    List<AnnouncementList> findByStatusNotOrderByCreateAtDesc(int status, Pageable pageable);
}
        Pageable pageable = PageRequest.of(start,20);
        List<AnnouncementList> announcementList = announcementRepository.findByStatusNotOrderByCreateAtDesc(0,pageable);
profile
저는 상황에 맞는 최적의 솔루션을 깊고 정확한 개념의 이해를 통한 다양한 방식으로 해결해오면서 지난 3년 동안 신규 서비스를 20만 회원 서비스로 성장시킨 Software Developer 최민길입니다.

0개의 댓글