1. 문제 제기 (Introduction & Problem Statement)
- 관찰 현상 또는 질문:
데이터베이스 접근 기술을 학습하면서 *"왜 JPA 엔티티에는 기본 생성자(@NoArgsConstructor)가 반드시 필요한가?", *"편리한 Lombok의@Data를 왜 엔티티에서는 지양해야 하는가?" 와 같은 의문이 생긴다. 또한, 실무에서JdbcTemplate,MyBatis,JPA중 어떤 기술을 선택해야 하는지, 그 기준이 단순히 '최신 기술' 여부인지에 대한 고찰이 필요하다. - 탐구 목표:
본 아티클에서는Serializable과Optional같은 자바 기본 개념부터 시작하여,JdbcTemplate과Spring Data JDBC의 차이점을 분석한다. 특히 JPA의 핵심인 리플렉션(Reflection)과 프록시(Proxy) 매커니즘을 통해, 지연 로딩(Lazy Loading) 환경에서@Data사용이 초래하는 성능 이슈의 원인을 규명한다.
2. 기술 분석 및 핵심 원리 (Technical Deep Dive)
2-1. 기본 개념: 자바의 안정성과 명시성
데이터를 다루기 전, 자바 언어 차원에서 제공하는 안전 장치들에 대한 이해가 선행되어야 한다.
- Serializable (직렬화):
- 정의: 아무런 메서드도 없는 '마커 인터페이스(Marker Interface)'다.
- 역할: JVM에게 "이 객체는 파일 저장이나 네트워크 전송을 위해 직렬화가 가능하다"는 것을 알려주는 표시(Flag) 역할만 수행한다.
- Optional `
`: - 정의:
Null이 될 수도 있는 객체를 감싸는 래퍼(Wrapper) 클래스다. - 사용 이유:
- NPE 방지:
NullPointerException을 컴파일 시점이나 개발 단계에서 예방한다. - 명시성: 반환 타입이
Optional이라면, 호출자에게 "이 값은 없을 수도 있으니(Null), 반드시 열어서 확인(ifPresent,orElse)하라"는 강제성을 부여한다. 즉, 단순null체크를 넘어 개발자의 의도를 코드로 명시하는 것이다.
2-2. Persistence Layer의 진화 (JdbcTemplate vs MyBatis)
데이터 접근 기술은 반복적인 코드를 줄이고 생산성을 높이는 방향으로 발전했다.
| 구분 | JdbcTemplate | MyBatis |
|---|---|---|
| 특징 | JDBC의 반복 코드(연결, 해제, 예외처리)를 캡슐화 | SQL을 XML 파일로 분리하여 관리 |
| 장점 | 컴파일 타임 안전성: 자바 코드로 작성되므로 오타 발견 용이 | 동적 쿼리: 복잡한 통계 쿼리나 조건문 처리에 유리 |
| 단점 | SQL이 자바 코드에 섞여 있어 가독성이 떨어질 수 있음 | 런타임 에러 위험: XML 오타는 실행 시점에 발견됨 |
| 사용처 | 대용량 배치 처리, 간단한 조회, 가벼운 구현 | 복잡한 레거시 DB 매핑, 정교한 쿼리 튜닝 필요 시 |
Note: Spring Data JDBC는
JdbcTemplate을 한 단계 더 추상화한 기술이다.JdbcTemplate이 쿼리 수행 과정을 캡슐화했다면, Spring Data JDBC는@Id와 같은 어노테이션을 분석해 SQL 쿼리 자체를 자동 생성해준다.
2-3. JPA의 내부 동작 원리와 Proxy
JPA는 단순한 SQL 매퍼가 아니라, 객체와 관계형 데이터베이스를 매핑(ORM)하는 기술이다. 이 과정에서 리플렉션과 프록시 기술이 핵심적으로 사용된다.
1. 기본 생성자(@NoArgsConstructor)가 필요한 이유: Reflection
Hibernate(JPA 구현체)가 DB에서 데이터를 가져와 객체에 매핑하는 과정은 다음과 같다.
- SQL 실행 후 결과셋(ResultSet) 획득.
- 매핑할 엔티티 클래스의 기본 생성자를 호출하여 '빈 객체(Empty Instance)' 생성.
- 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의 @Data는 equals(), hashCode(), toString()을 자동으로 생성한다. 여기서 문제가 발생한다.
- 문제 상황:
@Data가 생성한equals()나toString()은 객체의 모든 필드를 조회한다. - 결과: 지연 로딩으로 설정된 연관 관계 필드(예:
List<Order>)까지 모두 건드리는 순간, 프록시가 강제로 초기화되면서 불필요한SELECT쿼리가 무더기로 실행된다. (심지어 양방향 관계에서는 무한 루프에 빠질 수도 있다.) - 해결책: 엔티티에는
@Getter와 필요한 생성자만 사용하고,@ToString등은 제외하거나 필요한 필드만 명시적으로 재정의해야 한다.
3. 결론 및 고찰 (Conclusion & Takeaways)
- 핵심 요약:
Serializable은 직렬화 가능 여부를 알리는 마커이며,Optional은Null처리를 강제하여 코드의 의도를 명확히 하는 도구다.JdbcTemplate은 컴파일 타임 안전성을 보장하므로, XML 오타 위험이 있는 MyBatis보다 유지보수 측면에서 안전할 수 있다.- JPA는 리플렉션을 사용하므로 기본 생성자가 필수이며, 지연 로딩을 위해 프록시 객체를 사용한다.
- 기술적 통찰 및 나의 생각:
이번 학습을 통해 JPA가 단순한 데이터 저장 도구가 아니라, '객체 그래프 탐색'을 지원하는 정교한 프레임워크임을 깨달았다. 특히 *"아직 사용하지 않은 데이터는 쿼리를 날리지 않고 프록시로 감싸둔다"는 개념은 성능 최적화의 핵심이지만,@Data를 무분별하게 사용할 경우 이 메커니즘을 망가뜨려 오히려 성능 저하(N+1 문제 등)를 일으킬 수 있다는 점이 인상 깊었다. *"편리함(Lombok)과 성능(Lazy Loading) 사이의 트레이드오프"**를 이해하고 코드를 작성해야 한다. - 향후 과제:
@ManyToMany는 중간 테이블을 숨기기 때문에 실무에서 권장되지 않는다고 한다. 이를 해결하기 위해 연결 엔티티(Connection Entity)를 승격시켜@OneToMany-@ManyToOne관계로 풀어내는 구체적인 방법은 무엇인가?- 복잡한 동적 쿼리를 처리할 때 JPA만으로는 한계가 있는데, 이를 보완하기 위한 QueryDSL의 설정과 사용법은 어떻게 되는가?