Contents

Spring Data JPA findById 안티패턴

Spring Data JPA는 가장 편리하게 자바 객체와 데이터베이스 간 ORM을 구성할 수 있게 해주는 편리한 방법 중 하나이다.

그런데, Spring Data JPA의 findById를 사용하는 것은 라지 스케일 어플리케이션에서 성능 이슈를 일으킬 수 있다.

https://medium.com/javarevisited/spring-data-jpa-findbyid-anti-pattern-aefbf045fc44

1. 불필요한 데이터베이스 쿼리

데이터베이스에서 여러 개의 엔티티를 PK 값으로 가져와야 하는 상황을 가정해보자.

이 때 각 엔티티에 대해 findById를 사용한다면 하나의 엔티티를 가져오기 위해 하나의 쿼리를 사용하게 된다. 그리고 이는 성능에 있어서 빠른 병목 지점이 될수 있다.

해결 방법

findAllById 메서드를 대신 사용하는 방법이 있다. 이 메서드는 하나의 데이터 베이스에 대해 PK의 컬렉션을 전달함으로써 다수의 엔티티를 가져올 수 있게 한다.

@Service
public class UserService {

    @Autowired
    private UserRepository userRepository;

    public List<User> getUsersByIds(List<Long> ids) {
        return userRepository.findAllById(ids);
    }
}

상기 코드는 아래와 같은 쿼리를 수행한다.

SELECT * FROM User user WHERE user.id IN :ids

findById를 사용하면 단일 SQL 쿼리를 사용해서 다수의 사용자를 가져오게 된다. 이는 데이터베이스로의 접근 횟수를 줄이고, 따라서 어플리케이션의 성능을 향상시킨다.

2. 불필요한 객체 생성

findById를 수행할 때 마다 Spring Data JPA는 이미 해당 엔티티가 퍼시스턴스 컨텍스트에 존재해도 새로운 엔티티 객체를 생성한다. 이는 메모리 사용을 꽤 증가시키고, GC에 따른 오버헤드를 증가시킨다.

@Entity
@Table(name = "departments")
public class Department {

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

    private String name;

    @OneToMany(mappedBy = "department", fetch = FetchType.LAZY)
    private List<Employee> employees = new ArrayList<>();

    // getters and setters
}

@Entity
@Table(name = "employees")
public class Employee {

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

    private String name;

    @ManyToOne(fetch = FetchType.LAZY)
    @JoinColumn(name = "department_id")
    private Department department;

    // getters and setters
}

위의 예시에서는 엔티티 간 일대다 관계를 설정해놓았다. 엔티티를 id로 조회한다면 Department 객체와 관련된 Employee 객체를 fetch 하기 위해 하이버네이트는 SQL 쿼리를 생성할 것이다.

SELECT * FROM departments WHERE id = ?

SELECT * FROM employees WHERE department_id = ?

해결 방법

대신 getOne 메서드를 사용해야 한다. getOne 메서드는 실제로 데이터베이스로부터 가져오는 것이 아니라 엔티티에 대한 레퍼런스를 리턴한다.

이 동작은 엔티티의 PK나 엔티티 속성의 하위 집합에만 접근이 필요할 때 유용하다.

@Service
public class DepartmentService {

    @Autowired
    private DepartmentRepository departmentRepository;

    public Department getDepartmentReferenceById(Long id) {
        return departmentRepository.getOne(id);
    }
}

id 값으로 상기 메서드를 호출할 때, 하이버네이트는 Department 엔티티를 표현하는 프록시 객체를 fetch하기 위한 HQL 쿼리를 생성할 것이다.

SELECT department FROM Department department WHERE department.id = ?

관계된 Employee 객체를 fetch하기 위한 실제 SQL 쿼리는 Department 객체의 employees 속성에 접근하기 전까지 실행되지 않는다.

JPA를 사용할 때에는 lazy하게 쿼리를 수행하는 전략을 적절히 사용해야 성능을 향상시킬 수 있을 것 같다.