JPA를 활용한 개발 생산성 증대하기 - boostcampwm-2022/web17-waglewagle GitHub Wiki

JPA를 활용한 개발 생산성 증대하기

객체 중심의 자바에서 기존의 JDBC를 통한 쿼리문 작성은 복잡하고 반복적인 쿼리문DAO의 작성을 강제해왔다. 거기에 더불어 자바 코드 자체를 객체지향적 코드가 아닌, 데이터지향적 코드로 변질시켜버리는 문제점까지 존재한다.

자바진영 측에서 객체-관계 매핑, ORM을 정의한 스펙 JPA는 위와 같은 문제점들을 해결해주어 자바 개발자들이 편하게 객체지향에 집중할 수 있도록 하고자 등장했다.

이번 프로젝트에 JPA를 도입하는 과정에서 JPQL, QueryDSL 그리고 Spring Data JPA까지 객체지향 쿼리와 여러 관련 기술들을 사용해보았고, 각 기술 별로 어떤 불편함을 어떻게 해결하였는 지를 경험하게 되어 해당 내용을 기록하게 되었다.

JDBC + Native Query(+ DAO)

JDBC와 Native Query를 사용하여 회원을 조회하려고 하면…”

  1. 회원 객체 정의
public class Member {
	
	private String memberId;
	private String name;
}
  1. 조회용 DAO 작성
public class MemberDAO {

	public Member find(String memberId) {...}
}
  1. 쿼리 작성
SELECT MEMBER_ID, NAME FROM MEMBER M WHERE MEMBER_ID = ?
  1. JDBC API를 통한 SQL 실행
ResultSet rs = stmt.executeQuery(sql);
  1. 조회 결과를 객체로 매핑
public Member find(String memberId) {
	//쿼리 작성
	//JDBC API를 통한 SQL 실행

	//조회 결과 매핑
	String memberId = rs.getString("MEMBER_ID");
	String name = rs.getString("NAME");
	
	Member member = new Member();
	member.setMemberId(memberId);
	member.setName(name);

	return member;
}

문제점

데이터베이스로부터 단순한 조회를 위해서도 위와 같은 복잡한 과정을 거쳐야한다.(반복적인 DB Connection 요청과 반납 과정은 덤) 단순히 복잡하고 번거롭기만 하다면 다행일지도 모른다. JDBC와 NativeQuery문의 조합은 아래와 같은 문제점들을 내재하고 있다.

  • 애플리케이션 레이어와 데이터베이스 레이어간 계층 분할이 제대로 이뤄지지 않는다.

  • SQL에 의존적인 개발을 진행하게 된다.

    Member 클래스와 Member 테이블에 새로운 멤버 변수(칼럼)가 추가 되었다면,

    1. 해당 변수에 관련된 쿼리문들을 전부 새로 작성해줘야한다.
    2. 기존의 많은 쿼리문들에 수정이 발생한다.
    3. 각 쿼리문별로 조회하는 변수와 조회하지 않는 변수들이 서로 달라 비지니스 로직을 어지럽게 한다.
  • 엔티티 객체를 신뢰할 수 없다.

    Member 클래스에 소속된 팀에대한 정보, Team 객체가 멤버 변수로 추가 되었을 때, 아래와 같은 객체 그래프 탐색 코드는 NPE문제가 발생할 위험이 있다.

    Team team = member.getTeam(); //NullPointException 발생 가능!

    위의 코드에서 Team클래스가 null이 아님을 확인하기 위해서는 Member를 조회하는 쿼리문과 DAO를 추가적으로 직접 확인해 주어야만 한다.

    SELECT M.MEMBER_ID, M.NAME, T.TEAM_ID, T.TEAM_NAME
    FROM MEMBER M
    JOIN TEAM T
    	ON M.TEAM_ID = T.TEAM_ID
    ResultSet rs = stmt.executeQuery(sql);
    
    String memberId = rs.getString("MEMBER_ID");
    String memberName = rs.getString("MEMBER_NAME");
    String teamId = rs.getString("TEAM_ID");
    String teamName = rs.getString("TEAM_MEMBER");
    
    Team team = new Team();
    team.setTeamId(teamId);
    team.setTeamName(teamName);
    Member member = new Member();
    member.setMemberId(memberId);
    member.setName(name);
    member.setTeam(team); //***
    ...

한 문장으로 정리해보면, “객체지향 언어와 관계형 데이터베이스 간의 패러다임 불일치 문제가 발생 한다.”

JPA + JPQL

무엇을, 어떻게 해결하였는가 : 패러다임 불일치 문제

JPA_save.png

JPA

JPA는 개발자가 직접 구현해야 했던 아래의 항목들을 대신 해줌으로써 패러다임 불일치 문제를 해결해준다.

  1. 쿼리 결과를 일일이 객체에 매핑

    데이터베이스 테이블과 자동으로 매핑되는 ‘엔티티’ 객체를 지원.

  2. 데이터지향적으로 작성되던 쿼리문들

    엔티티와 엔티티의 멤버 변수를 기반으로 JPA가 NativeQuery를 대신 작성 및 수행

이와 같은 JPA의 지원으로, 자바 개발자는 데이터에 대한 접근 및 조회에 대한 코드를 “객체지향 언어, 자바 스타일로” 작성할 수 있다.

entityManager.persist(member); //저장

Member member = entityManager.findById(memberId); //조회

Team team = member.getTeam();
team.getTeamName(); //team을 직접적으로 사용하는 시점에 "추가적인 쿼리를 날려준다."

개발자는 좀 더 객체지향에 가까운 JPQL을 사용하여 JPA에게 데이터 접근, 조작을 부탁하고, NativeQuery는 JPA가 대신하여 처리해주는 구조인 것이다.

JPQL

JPA에게 데이터 접근, 조작을 부탁하는 쿼리 언어인 JPQL은 다음과 같이 사용할 수 있다.

Member findById(Long memberId) {
	return entityManager.createQuery(
		"SELECT m FROM Member m WHERE m.id = :_memberId, Member.class
	)
	.setParameter("_memberId", memberId)
	.getSingleResult();
}

한계점

하지만 JPQL도 여전히 자바 코드가 아닌, 문자열을 통해서 쿼리문을 작성하기 때문에 쿼리문의 문법적 오류를 컴파일 단계가아닌 런타임 시점에서 밖에 캐치할 수 없고, 다양한 조건의 쿼리문, 즉 동적 쿼리(ex. 복잡하고 다양한 조건의 주문조회)를 작성함에 있어 조건별로 쿼리문을 전부 작성해주어야 하는 불편함이 존재한다. (추가적으로 역시, 문자열로 작성된다는 이유로 IDE의 자동완성 기능, Intellisense의 도움을 받을 수 없다는 점도 단점으로 작용한다.)

QueryDSL

무엇을, 어떻게 해결하였는가 : 컴파일 타임의 문법 검사, 동적 쿼리

QueryDSL은 문자열이 아닌 자바 코드를 통해 쿼리문을 작성할 수 있게 해주어 문법 오류를 런타임이 아닌 컴파일 시점에서 확인할 수 있게 해주고, 동적 쿼리를 편하게 작성할 수 있도록 지원하여 개발자의 생산성을 증가시켜 준다.

public Optinal<Member> findById(Long memberId) {
	return jpqlQueryFactory
					.selectFrom(QMember.member)
					.where(QMember.member.id.eq(memberId))
					.fetchOne();
}
@Getter
@Builder
public class OrderSearchCondition {
	private LocalDate from;
	private LocalDate to;

	//1. null이 아닌 조건 변수에 대해서만 쿼리문의 조건으로 포함시킨다.
	//2. 모든 조건 변수가(from, to) null로 채워져 있다면, 모든 레코드가 조회된다.
	public Predicate makePredicate() {
		BooleanBuilder booleanBuilder = new BooleanBuilder();

		if (from != null) {
			booleanBuilder.and(QOrder.order.orderDate.after(from));
		}
		if (to != null) {
			booleanBuilder.and(QOrder.order.orderDate.before.eq(to));
		}

		return booleanBuilder;
	}
}
...
public List<Order> searchByCondition(OrderSearchCondition searchCondition) {
	return jpaQueryFactory
					.selectFrom(QOrder.order)
					.where(searchCondition.makePredicate())
					.fetch();
}

Spring Data JPA

무엇을, 어떻게 해결하였는가 : 반복되는 기본적인 CRUD

프로젝트를 진행하면 여러 도메인이 생기게 되고, 각각의 도메인들은 그 구조와 역할이 비슷한 CRUD 쿼리문들을 가지게 된다. 이때 Spring Data JPA를 사용하면, 개발자는 기본적인 CRUD 쿼리문 작성의 지루함을 피하면서도, 깔끔한 코드를 통해 더욱 중요한 도메인 쿼리문에 집중할 수 있는 이점을 얻을 수 있다.

  • Spring Data JPA가 만들어놓은 인터페이스를 확장하는 것만으로 기본적인 CRUD 쿼리문을 지원받을 수 있다.

    public interface MemberRepository extends JpaRepository<Member, Long> {
    }
    public boolean isExist(Long memberId) {
    	return memberRepository.findById(memberId); //...Spring Data JPA가 자동으로 만들어준 구현 메서드
    }
  • Spring Data JPA는 또한, 인터페이스에 정의한 메소드의 이름을 기반으로 쿼리문을 자동으로 생성 및 지원해주기 때문에, 기본적인 CRUD이외로 확장하여 사용하기에도 편리하다.

    public interface MemberRepository extends JpaRepository<Member, Long> {
    	List<Member> findByGroupId(Long groupId);
    }

위와 같은 사용법 외로, 직접 JPQL 쿼리문을 작성할 수 있도록 해주는 @Query 을 지원하기도 하며, QueryDSL과 함께 쓰는것이 가능하여(Spring Data JPA의 커스텀 인터페이스 확장 기능), Spring Data JPA의 편리함을 누리면서도 복잡한 쿼리문까지도 쉽게 작성할 수 있는 개발환경을 조성할 수 있다.

결론

JPQL부터 시작해서 QueryDSL, 그리고 Spring Data JPA까지, 순서대로 이번 프로젝트에 적용해 보았는데, 처음부터 남들이 으레 쓴다고 하는, 좋다고 하는 기술들을 무턱대고 적용하지 않고 차례로 적용을 해보았기에 각 단계의 해당 기술들이 ‘어떤 불편함을 어떻게 해소하고자 했는지’, ‘어떤 장점이 있어 어느 상황에 적재적소로 사용해야하는지’ 더 크게 느낄 수 있는 프로젝트 경험이 될 수 있었다.

⚠️ **GitHub.com Fallback** ⚠️