[IT 개발자를 위한 필독서 SSAFYdia] 자바 개발자가 쉽게 이해하는 JPA 개념 정리
개발하면서 SQL을 직접 쓰는 게 익숙해질 때쯤 사람들은 이런 생각을 하곤 합니다.
“이걸 꼭 일일이 다 써야 해? 더 편하게 하는 방법은 없을까?”
사실 단순한 CRUD 작업, 예를 들면 회원 정보를 저장하거나 불러오는 일은 언제나 비슷비슷합니다. 그런데 매번 SQL을 직접 쓰자니 번거롭고 객체로 변환하는 코드도 반복되는 게 많죠.
그래서 등장한 게 바로 JPA(Java Persistence API)입니다.
JPA는 자바 개발자가 SQL에 매달리지 않아도 객체 중심으로 코드를 짤 수 있게 도와주는 기술이에요.
저도 프로젝트를 진행하면서 JPA를 활용해 데이터베이스 연동 기능을 직접 구현해본 경험이 있어서 이번 기사에서는 JPA를 처음 접하시는 분들도 쉽게 이해할 수 있도록 기초 개념부터 차근차근 정리해보려 합니다.
JPA는 왜 필요할까요?
전통적인 방식에서는 JDBC를 이용해 직접 SQL을 작성하고 그 결과를 자바 객체로 일일이 변환해야 했습니다. 따라서 작업이 복잡하거나 테이블이 많아질수록 SQL 코드도 늘어나고 유지보수가 어려웠습니다.
그런데 JPA를 사용하면 이런 흐름이 바뀝니다.
개발자는 자바 객체만 다루면 JPA가 내부적으로 적절한 SQL을 만들어 실행해 주기 때문입니다.
예를 들어 회원을 저장하는 코드도 이렇게 간단해집니다.
Member member = new Member("철수");
memberRepository.save(member);
SQL을 직접 쓰지 않아도 `INSERT INTO member ...` 쿼리가 자동으로 실행됩니다.
결국 더 이상 SQL에 집중하지 않고 비즈니스 로직에 집중할 수 있는 환경을 갖게 됩니다.
객체와 테이블을 어떻게 연결할까요?
JPA의 핵심은 객체와 테이블 간의 연결입니다.
이를 위해 `@Entity`라는 어노테이션을 사용해서 자바 클래스를 “엔티티”로 지정합니다.
엔티티는 말 그대로 "데이터베이스 테이블과 연결되는 자바 객체"라고 이해하시면 됩니다.
예를 들어 이런 코드가 있다고 해볼게요:
@Entity
public class Member {
@Id
@GeneratedValue
private Long id;
private String name;
}
이렇게만 적어두면 JPA는 이 클래스를 보고 “이건 member라는 테이블과 연결된 클래스구나”라고 판단합니다. 그리고 데이터를 저장하거나 조회할 때 자동으로 SQL을 생성하고 실행합니다.
JPA는 객체를 어떻게 관리할까요? – 영속성 컨텍스트
JPA는 단순히 데이터를 넣고 빼는 것뿐 아니라 객체의 상태를 추적하고 관리하는 기능도 갖고 있습니다.
이걸 가능하게 해주는 게 바로 영속성 컨텍스트라는 개념입니다.
이해를 돕기 위해 이렇게 생각해 보세요.
JPA는 데이터베이스와 직접 이야기하기 전에 중간에 메모장 같은 걸 하나 들고 있습니다.
그 안에 “얘는 DB에서 가져온 데이터야”, “얘는 새로 만든 객체야” 같은 식으로 상태를 기록해 두는 거라고 생각하면 됩니다.
예를 들어 다음과 같이 코드를 짠다고 해볼게요:
Member member = em.find(Member.class, 1L);
member.setName("영희");
DB에서 회원 정보를 불러와 이름을 바꿨지만 별도의 저장 단계를 거치지 않아도 DB에 반영이 됩니다.
왜냐하면 JPA가 이미 그 객체를 “영속” 상태로 관리하고 있기 때문이에요.
JPA가 member 객체를 영속성 컨텍스트에 담아두고 값이 바뀌었는지 감시하고 있다가
트랜잭션이 끝날 때 바뀐 부분만 자동으로 감지해서 UPDATE 쿼리를 실행해 줍니다.
이걸 변경 감지(Dirty Checking)라고 해요.
객체 간의 관계도 표현할 수 있을까요?
현실 세계에서 객체들은 관계를 맺으며 존재합니다.
예를 들어 한 명의 회원은 여러 개의 주문을 할 수 있고 각 주문은 하나의 회원에 속하겠죠.
JPA에서도 이런 관계를 연관관계 매핑으로 자연스럽게 표현할 수 있어요.
다음은 회원과 주문 간의 1:N 관계를 나타내는 코드입니다.
@Entity
public class Member {
@OneToMany(mappedBy = "member")
private List<Order> orders = new ArrayList<>();
}
이렇게 작성하면 JPA는 Member와 Order 간의 관계를 인식하고 연관된 데이터를 가져오거나 저장할 때 함께 처리해 줍니다.
또한 `mappedBy` 속성은 연관관계의 주인을 지정하는 역할을 합니다.
JPA에서는 어느 쪽이 외래 키를 관리하는지를 명확히 해야 하는데 그 기준이 되는 게 바로 이 설정입니다.
데이터를 언제 불러와야 할까요? – Fetch 전략
객체 간의 관계가 있을 때 관련 데이터를 언제 가져올지는 꽤 중요한 문제입니다.
JPA는 기본적으로 두 가지 전략을 제공합니다.
- 즉시 로딩(EAGER): 연관된 객체를 함께 즉시 가져옵니다. ▶ 지금 당장 다 가져옴
- 지연 로딩(LAZY): 실제로 객체가 사용될 때 쿼리를 실행합니다. ▶ 진짜 필요할 때 가져옴
실제로는 대부분 지연 로딩을 기본값으로 사용합니다.
왜냐하면 모든 연관 데이터를 한꺼번에 가져오면 성능에 부담이 되기 때문이죠.
예를 들어 회원 목록만 보고 싶을 때 주문 내역까지 같이 가져올 필요는 없는 것처럼 말입니다.
하지만 지연 로딩을 사용하다 보면 N+1 문제라는 골칫거리를 만나게 됩니다.
예를 들어 회원 10명을 불러오고 각 회원의 주문을 지연 로딩하면 총 11번의 쿼리가 실행됩니다.
- 총 쿼리 수 = 1 (회원 10명 불러오기) + 10 (각 회원의 주문 불러오기) = 11번
이런 문제를 해결하기 위해선 `join fetch`와 같은 기능을 활용하게 됩니다.
@Query("SELECT m FROM Member m JOIN FETCH m.orders")
List<Member> findAllWithOrders();
이렇게 하면 회원과 주문을 모두 한 번의 쿼리로 가져오게 되어서 N+1 문제가 사라집니다.
연관 객체까지 함께 저장하거나 삭제하고 싶을 땐?
JPA에서는 연관된 객체를 함께 저장하거나 삭제할 수 있도록 Cascade라는 기능을 제공합니다.
예를 들어 회원 가입할 때 주소도 같이 저장하고 싶다면 이런 설정을 사용합니다.
@OneToOne(cascade = CascadeType.ALL)
private Address address;
또한 부모 객체에서 자식 객체를 제거했을 때 DB에서도 삭제되기를 원한다면 `orphanRemoval = true` 옵션을 사용합니다.
@OneToMany(orphanRemoval = true)
단 이 기능은 매우 강력하기 때문에 잘못 사용하면 의도치 않은 삭제가 발생할 수 있습니다.
실수로 데이터가 삭제되지 않도록 꼭 필요한 경우에만 사용해야 해요.
트랜잭션, 꼭 필요한가요?
JPA는 트랜잭션 안에서만 제대로 동작합니다.
앞에서 이야기한 변경 감지나 지연 로딩도 모두 트랜잭션이 있어야 작동해요.
예를 들어 이름을 바꾸는 메서드는 이렇게 구성됩니다.
@Transactional
public void updateName(Long id) {
Member member = memberRepository.findById(id).get();
member.setName("영희");
}
이 코드에서 따로 저장하지 않아도 메서드가 끝나면서 트랜잭션이 커밋되고 변경된 이름이 DB에 반영(Update 실행)됩니다.
반복되는 CRUD, 더 간단하게 – Spring Data JPA
CRUD 작업은 어디서나 비슷하죠.
그래서 JPA를 더 편리하게 쓰도록 도와주는 도구가 바로 Spring Data JPA입니다.
예를 들어 다음과 같이 `Repository` 인터페이스만 만들어두면:
public interface MemberRepository extends JpaRepository<Member, Long> {
List<Member> findByName(String name);
}
이제 SQL을 직접 작성하지 않아도 `findByName()`이라는 메서드 하나로 이름 검색이 가능합니다.
이런 식으로 메서드 이름만 잘 지으면 알아서 쿼리를 만들어줘요.
CRUD는 물론 조건 검색도 쉽게 할 수 있어서 활용하기 너무 간편한 것 같습니다.
JPA는 처음엔 낯설고 복잡하게 느껴졌지만 하나씩 개념을 익혀가다 보니 반복적인 SQL 작업에서 벗어날 수 있었고, 코드도 훨씬 객체지향적으로 바뀌는 걸 직접 경험할 수 있었습니다.
처음에는 간단한 CRUD부터 시작해서 연관관계 설정이나 성능 최적화 같은 부분도 점차 넓혀가면 좋을 것 같습니다. 무엇보다 중요한 건 단순히 사용하는 데 그치지 않고 원리를 이해하며 활용하는 것이라고 생각하기 때문입니다.
그 과정에서 이번 기사가 도움이 되셨으면 좋겠습니다!
⭐ SSAFY의 다양한 소식을 확인해보세요!
삼성 청년 SW 아카데미
삼성 청년 SW 아카데미| 소프트웨어 교육, 취업 지원, 코딩 교육
www.ssafy.com