JPA

JPA를 활용한 연관관계 데이터 저장: getReferenceById()를 쓰는 이유

khds 2024. 7. 29. 17:10

 

왜 findById() 대신 getReferenceById()를 쓰는가

 

Spring boot와 JPA를 통해 프로젝트를 진행하는 중, Banner 엔티티와 Comment 엔티티, Member 엔티티가 있고 Comment 엔티티가 Banner, Member 엔티티와 다대일 관계를 맺고 있다고 하자.  Comment를 저장하기 위해서는 Banner, Member 엔티티를 가져와야 한다. 

@Entity
@Getter
@NoArgsConstructor(access = AccessLevel.PROTECTED)
public class BannerComment {

    @Id
    @Column(name="banner_comment_id")
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;

    @ManyToOne(fetch = FetchType.LAZY)
    @JoinColumn(name = "banner_id")
    private Banner banner;

    @ManyToOne(fetch = FetchType.LAZY)
    @JoinColumn(name = "member_id")
    private Member member;

    @Column(nullable = false)
    private String content;

    public BannerComment(Banner banner, Member member, String content) {
        this.banner = banner;
        this.member = member;
        this.content = content;
    }
}

 
현재 Member, Banner는 Id만 알고 있고, 실제 엔티티는 데이터베이스에서 조회를 해야 한다. 우리가 흔히 알고 있는 findById()를 통해 조회해 보겠다. 아래는 예시 코드이다.

System.out.println("start");
Member member = memberRepository.findById(memberId).orElseThrow(() -> new RuntimeException("존재하지 않는 회원"));
Banner banner = bannerRepository.findById(bannerId).orElseThrow(() -> new RuntimeException("존재하지 않는 배너"));
BannerComment bannerComment = new BannerComment(banner, member, "작성할 댓글 내용");
System.out.println("====================================");
bannerCommentRepository.save(bannerComment);
System.out.println("end");

 
아래는 출력 화면이다.

 
나는 단순히 BannerComment 객체를 데이터베이스에 저장하고 싶을 뿐인데 Member 객체와 Comment 객체를 조회하는 쿼리가 실행이 되었다..!
이러면 성능상 좋지 않을 것이므로, 다른 방식을 사용해보고자 한다. 
바로 getReferenceById()이다.
 
findById() 대신 getReferenceById()를 사용하여 아래와 같이 코드를 작성하였다.

System.out.println("start");
Member member = memberRepository.getReferenceById(memberId);
Banner banner = bannerRepository.getReferenceById(bannerId);
BannerComment bannerComment = new BannerComment(banner, member, "작성할 댓글 내용");
System.out.println("====================================");
bannerCommentRepository.save(bannerComment);
System.out.println("end");

 
아래는 출력 화면이다.

 
아주 깔끔하게 insert 쿼리만 실행된 것을 확인할 수 있다. 
그렇다면 도대체 getReferenceById()는 findById()와 어떻게 다르길래 위와 같이 효율적으로 데이터를 저장할 수 있을까? 한번 알아보자.
 
 

getReferenceById는 무엇인가?

 

getReferenceById() 메서드는 JPA에서 사용되는 메서드로, 주어진 기본 키(ID)에 해당하는 엔티티의 레퍼런스를 가져온다. 이 메서드는 실제로 데이터베이스에서 해당 엔티티를 즉시 로드하지 않고, JPA의 프록시를 반환한다. 즉, 이 메서드를 호출할 때는 해당 엔티티 객체가 영속성 컨텍스트에 로드되지 않으며, 실제 데이터에 접근할 때 데이터베이스 쿼리가 발생하면서 로드된다.
즉, 지연 로딩과 같다고 할 수 있다.
조회되는 프록시 객체는 진짜 객체가 아니기에 오직 Id(식별자) 값만을 가지고 있으며, 다른 데이터를 가지고 있지 않기에, 다른 데이터를 조회하고자 할 시 DB에 접근하여 실제 객체를 가져오는 것이다.
아래의 코드를 봐보자.

System.out.println("start");
Member member = memberRepository.getReferenceById(memberId);
Banner banner = bannerRepository.getReferenceById(bannerId);
System.out.println(member.getId());
System.out.println(banner.getId());
System.out.println("end");

 
단순히 Id만 조회하기에 DB에 접근하지 않았다.

 
하지만 Id 외에 다른 데이터를 조회한다면 DB에 접근하게 된다. 

System.out.println("start");
Member member = memberRepository.getReferenceById(memberId);
Banner banner = bannerRepository.getReferenceById(bannerId);
System.out.println(member.getEmail());
System.out.println(banner.getAddress1());
System.out.println(member.getOauthProviderType());
System.out.println(banner.getTitle());
System.out.println("end");

 

 
위와 같이 Id 외에 다른 데이터를 조회할 시 select문을 통해 실제 데이터를 조회하는 것을 확인할 수 있다. 
그리고 member.getEmail()과 banner.getAddress1()을 통해 실제 데이터를 영속성 컨텍스트에 가져왔기 때문에 이후 member.getOauthProviderType(),. banner.getTitle()를 호출해도 DB에 더 이상의 접근이 없다.
이를 프록시의 초기화라고 한다. 프록시는 처음 실제 객체를 조회할 일이 생길 때에 실제 객체를 영속성 컨텍스트에 가져오는 것이다.
 

gertReferenceById 사용 시 발생할 수 있는 예외

 
이처럼 getReferenceById()를 통해 실제 객체가 아닌 프록시 객체를 가져와서 효율적으로 연관관계에 있는 데이터를 저장할 수가 있다. 
하지만 findById()와 다르게 문제가 하나 있다. 바로 getReferenceById()를 호출한 데이터가 실제로 DB에 저장되어 있는 데이터인지 확인하지 않는다는 것이다. 
장점인 동시에 단점이 될 수도 있는 것이다.
id외 다른 데이터를 조회하거나 연관엔티티의 필드로서 save()를 호출할 때, 실제 DB에 저장되어 있는지 확인한다. 만약 DB에 저장되어 있지 않다면 예외를 발생시킨다.
 
아래와 같이 memberId를 전혀 다른 값을 넣어서 코드를 실행해 보았다.

System.out.println("start");
Member member = memberRepository.getReferenceById(200L);
Banner banner = bannerRepository.getReferenceById(bannerId);
System.out.println("===================");
System.out.println(member.getEmail());
System.out.println(banner.getAddress1());
System.out.println("end");

 
아래와 같이 'EntityNotFoundException' 예외가 발생한 것을 알 수 있다.

 
이번엔 연관관계에 있는 객체를 저장할 때이다. 

System.out.println("start");
Member member = memberRepository.getReferenceById(200L);
Banner banner = bannerRepository.getReferenceById(bannerId);
BannerComment bannerComment = new BannerComment(banner, member, "작성할 댓글 내용");
System.out.println("====================================");
bannerCommentRepository.save(bannerComment);
System.out.println("end");

 
아래와 같은 에러 메시지와 함께 'DataIntegrityViolationException'가 발생하였다. 

 
DataIntegrityViolationException은 Spring Framework에서 제공하는 예외 클래스 중 하나로, 데이터베이스의 무결성 제약 조건이 위반되었을 때 발생한다. 유일성 제약 조건, Foreign Key 제약 조건, NOT NULL 제약 조건, 체크 제약 조건 등을 위반하였을 시 발생하는 예외다.
Comment를 저장할 때 연관관계에 있는 member가 실제로 DB에 저장되어 있지 않아서 생기는 문제이다.