현재 진행중인 프로젝트에서 JPA를 사용하면서 느낌 점들과 추가적으로 더 학습한 내용을 정리해보려고 합니다.
JPA가 무엇인지 설명하기 전에 ORM이 뭔지 먼저 설명을 해보겠습니다.
0. ORM이란?
ORM(Object-Relational Mapping)은 객체와 관계형 데이터베이스를 매핑해주는 기술입니다. 개발자가 직접 SQL을 작성하지 않고도 객체를 통해 데이터베이스를 조작할 수 있게 해줍니다.
예를 들어서 자바의 User 클래스가 있다면 데이터베이스의 users 테이블과 자동으로 매핑됩니다.
- User 객체를 생성하면 → users 테이블에 새로운 행이 추가됨
- User 객체의 속성을 변경하면 → users 테이블의 해당 데이터가 수정됨
- User 객체를 삭제하면 → users 테이블의 해당 행이 삭제됨
1. JPA란 무엇인가?
JPA는 Java Persistence API의 약자로 자바 애플리케이션에서 관계형 데이터베이스를 사용하는 방식을 정의한 인터페이스입니다.
쉽게 말해, 자바 객체와 데이터베이스 테이블을 매핑해주는 도구라고 할 수 있습니다.
1.1 JPA 이전의 데이터베이스 작업
JPA를 사용하기 전에는 JDBC를 통해 직접 SQL을 작성하고 결과를 매핑해야 했습니다.
이는 반복적이고 오류가 발생하기 쉬운 작업이었습니다.
// JDBC를 사용한 전통적인 방식
public User findUser(Long id) {
String sql = "SELECT * FROM users WHERE id = ?";
PreparedStatement pstmt = connection.prepareStatement(sql);
pstmt.setLong(1, id);
ResultSet rs = pstmt.executeQuery();
User user = new User();
if (rs.next()) {
user.setId(rs.getLong("id"));
user.setUsername(rs.getString("username"));
user.setEmail(rs.getString("email"));
}
return user;
}
1.2 JPA를 사용한 방식
JPA를 사용하면 개발자는 SQL을 직접 작성하지 않고도 데이터베이스 작업을 수행할 수 있습니다.
// JPA를 사용한 방식
public User findUser(Long id) {
return entityManager.find(User.class, id);
}
2. 왜 JPA를 사용해야 할까?
2.1 생산성 향상
- SQL 쿼리를 직접 작성할 필요가 없어집니다.
- 데이터베이스 설계에서 객체 설계에 집중할 수 있습니다.
- 반복적인 CRUD 작업을 자동화할 수 있습니다.
2.2 유지보수성
예를 들어, User 엔티티에 필드를 하나 추가한다고 가정해보겠습니다.
// 기존 User 엔티티
public class User {
private Long id;
private String username;
private String email;
}
// 필드 추가 후
public class User {
private Long id;
private String username;
private String email;
private String phoneNumber; // 새로운 필드 추가
}
JPA를 사용하지 않았다면 INSERT SQL 수정, UPDATE SQL 수정, SELECT SQL 수정, 관련된 모든 쿼리 하나 하나 수정해야 하지만JPA를 사용하면 엔티티 클래스에 필드만 추가하면 됩니다. JPA가 자동으로 새 필드를 데이터베이스 테이블에 매핑합니다. 기존 쿼리들은 수정 없이 그대로 사용할 수 있습니다.
JPA에서 매핑이란 자바 객체와 관계형 데이터베이스의 테이블을 연결하는 과정을 의미합니다.
3. JPA 핵심 개념
3.1 엔티티(Entity)
엔티티는 데이터베이스 테이블과 매핑되는 자바 클래스입니다.
@Entity
@Table(name = "users")
public class User {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
@Column(nullable = false, length = 50)
private String username;
@Email
@Column(unique = true)
private String email;
@OneToMany(mappedBy = "user")
private List<Post> posts = new ArrayList<>();
}
3.2 영속성 컨텍스트(Persistence Context)
영속성 컨텍스트는 엔티티를 영구 저장하는 환경입니다. 이는 엔티티의 생명주기를 관리하고, 1차 캐시, 지연 로딩, 변경 감지 등의 기능을 제공합니다.
// 영속성 컨텍스트 예시
@Service
@Transactional
public class UserService {
@PersistenceContext
private EntityManager em;
public void saveUser(User user) {
// 영속성 컨텍스트에 저장
em.persist(user);
// 이 시점에서는 아직 DB에 저장되지 않음
// 트랜잭션 종료 시점에 DB에 실제로 저장됨
}
}
3.3 연관관계 매핑
JPA는 객체 간의 관계를 데이터베이스 관계로 매핑할 수 있습니다. 이를 통해 객체 지향적인 설계를 데이터베이스에 반영할 수 있습니다.
// 양방향 연관관계 예시
@Entity
public class Post {
@Id @GeneratedValue
private Long id;
private String title;
@ManyToOne(fetch = FetchType.LAZY)
@JoinColumn(name = "user_id")
private User user; // 다대일 관계
}
4. Spring Data JPA 활용
Spring Data JPA는 JPA를 더 쉽게 사용할 수 있게 해주는 프레임워크입니다.
4.1 리포지토리 인터페이스
public interface UserRepository extends JpaRepository<User, Long> {
// 메서드 이름으로 쿼리 생성
Optional<User> findByEmail(String email);
// 나이가 특정 값 이상인 사용자 찾기
List<User> findByAgeGreaterThan(int age);
// JPQL 쿼리 직접 작성
@Query("SELECT u FROM User u WHERE u.username LIKE %:keyword%")
List<User> searchByUsername(@Param("keyword") String keyword);
}
4.2 실제 서비스 구현
@Service
@Transactional(readOnly = true)
public class UserService {
private final UserRepository userRepository;
public UserService(UserRepository userRepository) {
this.userRepository = userRepository;
}
@Transactional
public User createUser(UserDto userDto) {
// 중복 이메일 체크
if (userRepository.existsByEmail(userDto.getEmail())) {
throw new DuplicateEmailException();
}
User user = new User();
user.setUsername(userDto.getUsername());
user.setEmail(userDto.getEmail());
return userRepository.save(user);
}
}
5. 주의할 점
5.1 N+1 문제
N+1 문제는 JPA에서 연관 엔티티를 조회할 때 발생하는 성능 이슈입니다. 이는 하나의 쿼리로 N개의 엔티티를 조회한 후 각 엔티티의 연관 엔티티를 조회하기 위해 N번의 추가 쿼리가 발생하는 현상을 말합니다. 이는 성능 저하의 주요 원인이 될 수 있으므로 주의가 필요합니다.
// N+1 문제가 발생하는 코드
List<User> users = userRepository.findAll(); // 1번 쿼리
for (User user : users) {
List<Post> posts = user.getPosts(); // N번의 추가 쿼리 발생
}
// 해결 방법: fetch join 사용
@Query("SELECT u FROM User u JOIN FETCH u.posts")
List<User> findAllWithPosts();
5.2 성능 최적화
- 지연 로딩(Lazy Loading) 활용
- 연관 엔티티를 실제 사용 시점에 로딩하여 불필요한 데이터 조회를 방지합니다.
// 지연 로딩 활용 예시
@OneToMany(mappedBy = "user", fetch = FetchType.LAZY)
private List<Post> posts;
- 적절한 인덱스 설정
- 자주 조회되는 컬럼에 인덱스를 설정하여 검색 속도를 향상시킵니다.
// 적절한 인덱스 설정 예시
@Table(indexes = @Index(name = "idx_user_email", columnList = "email"))
public class User { ... }
- 페이징 처리
- 대량의 데이터를 조회할 때 페이징을 사용하여 메모리 사용량을 줄이고 응답 시간을 개선합니다.
// 페이징 처리 예시
@GetMapping("/users")
public Page<UserResponse> getUsers(
@PageableDefault(size = 10, sort = "createdAt", direction = Sort.Direction.DESC)
Pageable pageable
) {
return userRepository.findAll(pageable)
.map(UserResponse::from);
}
결론
JPA는 현대 자바 웹 개발에서 필수적인 기술입니다. 처음에는 어려울 수도 있지만 익숙해지면 생산성과 유지보수가 크게 향상됩니다. 특히 Spring Data JPA와 함께 사용하면 더욱 효율적인 개발이 가능합니다. 이러한 이점들 때문에 많은 기업들이 JPA를 표준 기술로 채택하고 있으며 자바 개발자라면 반드시 알아야 할 기술이라고 생각합니다.
'개발 이야기 > Spring' 카테고리의 다른 글
[Spring] REST API란 ? (1) | 2025.02.07 |
---|---|
[Spring Boot / JWT] Spring Boot에서 JWT를 활용한 인증 시스템 구현 (0) | 2025.01.11 |
[Spring Boot] Maven과 Gradle의 차이점 (0) | 2024.12.22 |
[spring/ flutter] flutter를 활용해 Spring Boot 서버 화면을 웹뷰로 띄우기 [2/2] (5) | 2024.12.12 |
[spring/ flutter] flutter를 활용해 Spring Boot 서버 화면을 웹뷰로 띄우기 [1/2] (3) | 2024.11.11 |