[JAVA] JPA : 간단한 개념과 예제 코드로 이해하기

dakcoh·2024년 8월 7일


이번 포스트에서는 백엔드 개발에서 자주 사용하는 JPA(Java Persistence API)에 대해 간단하게 설명하고, Spring과 함께 어떻게 사용할 수 있는지 알아보겠습니다.

JPA의 핵심 기능도 함께 다루며, 예제 코드를 통해 공부해보려고 합니다.
먼저 ORM의 간단한 개요부터 알아보겠습니다.


ORM이란?

ORM(Object-Relational Mapping)은 객체와 관계형 DB 사이의 데이터를 매핑해주는 기술입니다.

쉽게 말해, 객체를 통해 DB 테이블을 조작할 수 있게 해줍니다.

ORM의 동작 방식

일반적으로 자바 프로그램에서는 데이터를 객체로 다루고, DB 테이블과 SQL을 사용해 데이터를 관리합니다.

그런데, 이 두 가지는 다소 다른 개념이죠. ORM은 이 간극을 메워줍니다.
자바 객체와 DB 테이블을 연결해주고, 우리가 객체를 조작하면 ORM이 알아서 SQL로 변환해 DB에 반영합니다.

ORM의 장점

  • 생산성: SQL을 직접 작성할 필요 없이, 객체를 통해 DB 작업을 할 수 있어 개발이 쉬워집니다.
  • 유지보수 용이: 객체 지향적 코딩으로 DB 작업을 하기 때문에 코드가 직관적이고, 유지보수가 쉬워집니다.
  • 이식성: 특정 DB에 종속되지 않아, 코드 변경 없이도 다양한 DBMS로 전환이 가능합니다.

JPA란?

JPA(Java Persistence API)는 자바에서 ORM을 위한 표준 인터페이스입니다.

즉, JPA는 자바 애플리케이션에서 DB와 객체 간의 매핑을 쉽게 할 수 있도록 도와주는 ORM 기술의 표준입니다.

대표적인 구현체로는 Hibernate가 있습니다.

JPA의 장점

  • 생산성 향상: SQL을 직접 작성하지 않고, 객체 지향적 프로그래밍을 유지하면서 DB 작업이 가능합니다.
  • 자동화된 CRUD 기능: JPA는 기본적인 CRUD 작업을 쉽게 처리해줍니다.
  • 변경 감지 및 영속성 컨텍스트: 객체의 변경 사항을 자동으로 감지하여 DB에 반영합니다.
  • 지연 로딩(LAZY Loading): 필요한 데이터를 나중에 불러오는 기능을 통해 성능 최적화를 할 수 있습니다.

JPA의 단점

  • 성능 문제: 잘못된 사용이나 설정으로 성능 저하가 발생할 수 있습니다.
  • 복잡한 학습곡선: 처음 사용할 때 많은 개념과 설정을 익히는 데 시간이 걸립니다.
  • 복잡한 쿼리 처리 한계: 단순한 CRUD는 간편하지만, 복잡한 쿼리 처리는 JPQL로 한계가 있을 수 있습니다.

JPA의 핵심 기능

JPA는 단순한 CRUD 기능 외에도 몇 가지 중요한 핵심 기능을 제공합니다.

이를 잘 이해해야 JPA를 제대로 활용할 수 있습니다.

1. 영속성 컨텍스트 (Persistence Context)

영속성 컨텍스트는 JPA에서 엔티티 객체를 관리하는 일종의 '저장소'입니다.
엔티티가 영속성 컨텍스트에 포함되면, JPA는 이 객체의 상태를 추적하며, 필요할 때 DB에 자동으로 반영합니다.

User user = new User();
user.setName("dakcoh");
user.setEmail("dakcoh@example.com");

entityManager.persist(user); // 영속성 컨텍스트에 저장

1차 캐시
영속성 컨텍스트는 1차 캐시를 제공하여 동일한 트랜잭션 안에서는 같은 객체를 여러 번 조회할 때 DB에 다시 접근하지 않고 캐시에서 가져옵니다. 이를 통해 성능이 향상됩니다.

2. 변경 감지 (Dirty Checking)

JPA는 엔티티의 변경 사항을 자동으로 감지해 트랜잭션이 끝날 때 DB에 반영해줍니다.
즉, 객체의 값을 수정하고 별도의 SQL을 작성하지 않아도, 트랜잭션이 커밋될 때 자동으로 변경 사항이 DB에 저장됩니다.

User user = entityManager.find(User.class, 1L);
user.setEmail("new_email@example.com"); // DB에 반영됨

성능 최적화
변경 감지 기능을 통해 불필요한 업데이트를 줄일 수 있지만, 너무 많은 엔티티가 관리될 경우 성능 문제가 발생할 수 있으니 주의해야 합니다.

3. 지연 로딩 (Lazy Loading)과 즉시 로딩 (Eager Loading)

연관된 엔티티를 언제 로드할지 결정하는 방법입니다.
기본적으로 지연 로딩(LAZY)을 사용해 필요할 때만 데이터를 불러오는 것이 성능에 유리합니다.

@ManyToOne(fetch = FetchType.LAZY)
private User user; // 지연 로딩 설정

성능 고려
즉시 로딩(EAGER)은 관련된 모든 데이터를 한 번에 가져오지만, 불필요한 데이터를 함께 로드할 수 있어 성능 저하가 발생할 수 있습니다. 가능하면 지연 로딩(LAZY)을 사용하시면 좋습니다.

4. JPQL (Java Persistence Query Language)

JPA는 SQL 대신 JPQL이라는 객체 지향 쿼리 언어를 사용합니다.
테이블이 아닌 객체를 대상으로 쿼리를 작성할 수 있어 객체 지향적인 개발이 가능합니다.

String jpql = "SELECT u FROM User u WHERE u.name = :name";
List<User> users = entityManager.createQuery(jpql, User.class)
                                .setParameter("name", "dakcoh")
                                .getResultList();

복잡한 쿼리는 네이티브 SQL 사용
JPQL은 객체 지향적으로 쿼리를 작성할 수 있다는 장점이 있지만, 복잡한 쿼리가 필요할 때는 네이티브 SQL을 사용하는 것도 좋은 방법입니다.


JPA로 간단한 CRUD 만들기

이제 Spring에서 JPA를 사용해 간단한 CRUD(Create, Read, Update, Delete)를 구현해보겠습니다.

1. User 엔티티

@Entity
public class User {

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

    private String name;
    private String email;

    // Getters and Setters
}

@Entity는 이 클래스가 DB 테이블과 연결된다는 것을 의미하고, @Id는 기본 키를 나타냅니다. @GeneratedValue는 기본 키를 자동 생성하는 전략을 설정합니다.

2. UserRepository 인터페이스

public interface UserRepository extends JpaRepository<User, Long> {
    List<User> findByName(String name);  // 이름으로 사용자 찾기
}

JpaRepository를 상속하면 기본적인 CRUD 메서드를 사용할 수 있으며, 이름으로 사용자를 검색하는 findByName 메서드를 추가했습니다.

3. 사용자 생성하기 (Create)

@RestController
@RequestMapping("/users")
public class UserController {

    private final UserRepository userRepository;

    public UserController(UserRepository userRepository) {
        this.userRepository = userRepository;
    }

    @PostMapping
    public User createUser(@RequestBody User user) {
        return userRepository.save(user);  // 사용자 저장
    }
}

POST 요청으로 들어온 User 객체를 DB에 저장하는 코드입니다. save() 메서드를 통해 사용자가 저장됩니다.

4. 사용자 조회하기 (Read)

@GetMapping("/{id}")
public ResponseEntity<User> getUser(@PathVariable Long id) {
    return userRepository.findById(id)
            .map(user -> ResponseEntity.ok(user))
            .orElse(ResponseEntity.notFound().build());
}

ID로 사용자를 조회하는 코드입니다.
findById() 메서드로 DB에서 해당 ID를 가진 사용자를 찾습니다.

5. 사용자 업데이트하기 (Update)

@PutMapping("/{id}")
public ResponseEntity<User> updateUser(@PathVariable Long id, @RequestBody User updatedUser) {
    return userRepository.findById(id)
        .map(user -> {
            user.setName(updatedUser.getName());
            user.setEmail(updatedUser.getEmail());
            userRepository.save(user);  // 변경된 사용자 저장
            return ResponseEntity.ok(user);
        })
        .orElse(ResponseEntity.notFound().build());
}

기존 사용자의 정보를 수정하는 코드입니다.
findById()로 사용자를 찾고, 변경된 내용을 저장합니다.

6. 사용자 삭제하기 (Delete)

@DeleteMapping("/{id}")
public ResponseEntity<Void> deleteUser(@PathVariable Long id) {
    return userRepository.findById(id)
        .map(user -> {
            userRepository.delete(user);  // 사용자 삭제
            return ResponseEntity.noContent().build();
        })
        .orElse(ResponseEntity.notFound().build());
}

ID로 사용자를 찾아 삭제하는 코드입니다. delete() 메서드를 사용해 사용자를 삭제합니다.


마무리

이번 포스트에서는 ORM과 JPA의 개념, 그리고 JPA의 핵심 기능을 알아보았습니다.
Spring과 함께 JPA를 사용해 간단한 CRUD 기능을 구현하는 방법도 함께 살펴봤습니다.

JPA는 객체 지향적인 방식으로 DB와 상호작용할 수 있게 해주어 개발 생산성을 높이는 강력한 도구이지만, 성능 문제를 유발할 수 있는 지점도 있으니 주의가 필요합니다.

다음 포스트에서는 JPA 성능 최적화 방법에 대해 다뤄보겠습니다.

profile
포기하기 금지

0개의 댓글