[Spring In Action] JPA의 동작 원리(Lazy Loading/Proxy)와 Persistence Layer 기술 비교 (JDBC vs MyBatis)

1. 문제 제기 (Introduction & Problem Statement)

  • 관찰 현상 또는 질문:
    데이터베이스 접근 기술을 학습하면서 *"왜 JPA 엔티티에는 기본 생성자(@NoArgsConstructor)가 반드시 필요한가?", *"편리한 Lombok의 @Data를 왜 엔티티에서는 지양해야 하는가?" 와 같은 의문이 생긴다. 또한, 실무에서 JdbcTemplate, MyBatis, JPA 중 어떤 기술을 선택해야 하는지, 그 기준이 단순히 '최신 기술' 여부인지에 대한 고찰이 필요하다.
  • 탐구 목표:
    본 아티클에서는 SerializableOptional 같은 자바 기본 개념부터 시작하여, JdbcTemplateSpring Data JDBC의 차이점을 분석한다. 특히 JPA의 핵심인 리플렉션(Reflection)과 프록시(Proxy) 매커니즘을 통해, 지연 로딩(Lazy Loading) 환경에서 @Data 사용이 초래하는 성능 이슈의 원인을 규명한다.

2. 기술 분석 및 핵심 원리 (Technical Deep Dive)

2-1. 기본 개념: 자바의 안정성과 명시성

데이터를 다루기 전, 자바 언어 차원에서 제공하는 안전 장치들에 대한 이해가 선행되어야 한다.

  • Serializable (직렬화):
  • 정의: 아무런 메서드도 없는 '마커 인터페이스(Marker Interface)'다.
  • 역할: JVM에게 "이 객체는 파일 저장이나 네트워크 전송을 위해 직렬화가 가능하다"는 것을 알려주는 표시(Flag) 역할만 수행한다.
  • Optional ``:
  • 정의: Null이 될 수도 있는 객체를 감싸는 래퍼(Wrapper) 클래스다.
  • 사용 이유:
  1. NPE 방지: NullPointerException을 컴파일 시점이나 개발 단계에서 예방한다.
  2. 명시성: 반환 타입이 Optional이라면, 호출자에게 "이 값은 없을 수도 있으니(Null), 반드시 열어서 확인(ifPresent, orElse)하라"는 강제성을 부여한다. 즉, 단순 null 체크를 넘어 개발자의 의도를 코드로 명시하는 것이다.

2-2. Persistence Layer의 진화 (JdbcTemplate vs MyBatis)

데이터 접근 기술은 반복적인 코드를 줄이고 생산성을 높이는 방향으로 발전했다.

구분 JdbcTemplate MyBatis
특징 JDBC의 반복 코드(연결, 해제, 예외처리)를 캡슐화 SQL을 XML 파일로 분리하여 관리
장점 컴파일 타임 안전성: 자바 코드로 작성되므로 오타 발견 용이 동적 쿼리: 복잡한 통계 쿼리나 조건문 처리에 유리
단점 SQL이 자바 코드에 섞여 있어 가독성이 떨어질 수 있음 런타임 에러 위험: XML 오타는 실행 시점에 발견됨
사용처 대용량 배치 처리, 간단한 조회, 가벼운 구현 복잡한 레거시 DB 매핑, 정교한 쿼리 튜닝 필요 시

Note: Spring Data JDBCJdbcTemplate을 한 단계 더 추상화한 기술이다. JdbcTemplate이 쿼리 수행 과정을 캡슐화했다면, Spring Data JDBC는 @Id와 같은 어노테이션을 분석해 SQL 쿼리 자체를 자동 생성해준다.

2-3. JPA의 내부 동작 원리와 Proxy

JPA는 단순한 SQL 매퍼가 아니라, 객체와 관계형 데이터베이스를 매핑(ORM)하는 기술이다. 이 과정에서 리플렉션과 프록시 기술이 핵심적으로 사용된다.

1. 기본 생성자(@NoArgsConstructor)가 필요한 이유: Reflection

Hibernate(JPA 구현체)가 DB에서 데이터를 가져와 객체에 매핑하는 과정은 다음과 같다.

  1. SQL 실행 후 결과셋(ResultSet) 획득.
  2. 매핑할 엔티티 클래스의 기본 생성자를 호출하여 '빈 객체(Empty Instance)' 생성.
  3. Reflection 기능을 사용하여 DB에서 가져온 값을 필드에 주입.

따라서 인자가 있는 생성자만 존재하면 Hibernate가 객체를 생성할 수 없어 에러가 발생한다.

2. 지연 로딩(Lazy Loading)과 프록시(Proxy)

JPA의 핵심 성능 최적화 전략은 '필요할 때 가져온다'는 지연 로딩이다.

// 예시: Member 조회 시 Team은 당장 필요하지 않음
Member member = em.find(Member.class, 1L); 
// 이때 member.getTeam()은 실제 객체가 아닌 '프록시 객체'임
  • Proxy의 동작: 실제 엔티티 클래스를 상속받은 가짜 객체다. 겉모양은 같지만 내부는 비어있으며, 실제 데이터가 필요한 시점(메서드 호출 시)에 DB에 쿼리를 날려 데이터를 채운다(Initialising).

3. @Data와 JPA의 충돌 (성능 이슈)

Lombok의 @Dataequals(), hashCode(), toString()을 자동으로 생성한다. 여기서 문제가 발생한다.

  • 문제 상황: @Data가 생성한 equals()toString()은 객체의 모든 필드를 조회한다.
  • 결과: 지연 로딩으로 설정된 연관 관계 필드(예: List<Order>)까지 모두 건드리는 순간, 프록시가 강제로 초기화되면서 불필요한 SELECT 쿼리가 무더기로 실행된다. (심지어 양방향 관계에서는 무한 루프에 빠질 수도 있다.)
  • 해결책: 엔티티에는 @Getter와 필요한 생성자만 사용하고, @ToString 등은 제외하거나 필요한 필드만 명시적으로 재정의해야 한다.

3. 결론 및 고찰 (Conclusion & Takeaways)

  • 핵심 요약:
  • Serializable은 직렬화 가능 여부를 알리는 마커이며, OptionalNull 처리를 강제하여 코드의 의도를 명확히 하는 도구다.
  • JdbcTemplate은 컴파일 타임 안전성을 보장하므로, XML 오타 위험이 있는 MyBatis보다 유지보수 측면에서 안전할 수 있다.
  • JPA는 리플렉션을 사용하므로 기본 생성자가 필수이며, 지연 로딩을 위해 프록시 객체를 사용한다.
  • 기술적 통찰 및 나의 생각:
    이번 학습을 통해 JPA가 단순한 데이터 저장 도구가 아니라, '객체 그래프 탐색'을 지원하는 정교한 프레임워크임을 깨달았다. 특히 *"아직 사용하지 않은 데이터는 쿼리를 날리지 않고 프록시로 감싸둔다"는 개념은 성능 최적화의 핵심이지만, @Data를 무분별하게 사용할 경우 이 메커니즘을 망가뜨려 오히려 성능 저하(N+1 문제 등)를 일으킬 수 있다는 점이 인상 깊었다. *"편리함(Lombok)과 성능(Lazy Loading) 사이의 트레이드오프"**를 이해하고 코드를 작성해야 한다.
  • 향후 과제:
  • @ManyToMany는 중간 테이블을 숨기기 때문에 실무에서 권장되지 않는다고 한다. 이를 해결하기 위해 연결 엔티티(Connection Entity)를 승격시켜 @OneToMany - @ManyToOne 관계로 풀어내는 구체적인 방법은 무엇인가?
  • 복잡한 동적 쿼리를 처리할 때 JPA만으로는 한계가 있는데, 이를 보완하기 위한 QueryDSL의 설정과 사용법은 어떻게 되는가?