JPA 도입과 적용 – 이론과 실습 정리

기본 개념부터 실무 적용까지 JPA를 단계적으로 학습하고 프로젝트에 활용해봅니다.


글 요약

  • 목표: JPA의 영속성 관리와 객체 중심 설계 학습
  • 문제: 기존 SQL Mapper(MyBatis) 방식의 생산성 한계
  • 해결: JPA 적용, Entity-DTO 분리, ModelMapper 활용
  • 배운 점: DB 쿼리 최소화 + 유지보수성 향상 경험

프로젝트 배경 및 목적

취업 준비 과정에서 여러 채용 공고에 JPA 경험이 필수 또는 우대 조건으로 명시된 것을 접하며, 단순한 기능 구현을 넘어 JPA가 제공하는 영속성 관리, 객체 중심 설계, 그리고 효율적인 데이터베이스 접근 방법을 실무에서 직접 경험하는 것이 필요하다고 판단했습니다.

특히, 이번 Auction Server 프로젝트에서는 복잡한 입찰 내역 조회낙찰자 선정 같은 비즈니스 로직을 객체 지향적으로 모델링하고, JPA의 영속성 컨텍스트1차 캐시 기능을 활용해 트랜잭션 내에서 발생하는 불필요한 데이터베이스 쿼리 호출을 최소화하는 것을 중점 과제로 삼았습니다.


Auction Server JPA 도입 과정

1. Entity 설계

User Entity를 설계할 때, MySQL 예약어인 user와 충돌하지 않도록 테이블명을 'user'로 지정했습니다.

코드 예시

@Entity
@Table(name="`user`")
public class User {
  @Id
  @GeneratedValue(strategy = GenerationType.IDENTITY)
  private Long id;

  private String username;

  // 생략...
}

회고
단순 Entity 정의에 그치지 않고, DB 예약어 충돌 문제를 사전에 고려해 설계했습니다.


2. DTO 분리와 ModelMapper 적용

회원 정보 조회 시, Entity 전체를 클라이언트에 반환하는 것은 보안상 적절하지 않았습니다.
예를 들어, DB의 PK, 마지막 로그인 정보 등은 클라이언트에 노출할 필요가 없기 때문에 DTO를 별도로 설계했습니다.

이를 위해 ModelMapper 라이브러리를 도입해 Entity ↔ DTO 간 매핑을 쉽게 처리했습니다. (참고 : 아래의 토글을 참고 해주세요.)

코드 예시

ModelMapper modelMapper = new ModelMapper();
UserDto userDto = modelMapper.map(userEntity, UserDto.class);
ModelMapper와 Builder 패턴 매핑 코드 비교 보기 / 닫기

1. UserEntity 예시

public class UserEntity {
    private Long id;
    private String username;
    private String email;
    private String role;
    private String phoneNumber;
    // Getter, Setter, Constructor 생략
}
    

2. UserDto 예시

public class UserDto {
    private String username;
    private String email;
    private String phoneNumber;

    // Builder 패턴
    @Builder
    public UserDto(String username, String email, String phoneNumber) {
        this.username = username;
        this.email = email;
        this.phoneNumber = phoneNumber;
    }
}
    

3. ModelMapper 사용 시

ModelMapper modelMapper = new ModelMapper();
UserDto userDto = modelMapper.map(userEntity, UserDto.class);
    
  • 특징: 설정만 하면 한 줄 코드로 Entity → DTO 변환 가능
  • 필드명이 동일해야 함
  • 커스텀 매핑 정책이 필요할 경우 ModelMapper 설정 추가 필요

4. Builder 직접 매핑 시

UserDto userDto = UserDto.builder()
    .username(userEntity.getUsername())
    .email(userEntity.getEmail())
    .phoneNumber(userEntity.getPhoneNumber())
    .build();
    
  • 특징: 명시적 매핑으로 가독성, 안전성이 높음
  • 필드명이 다르거나 비즈니스 로직 반영해 변환할 때 적합
  • 필드가 많으면 코드가 길어짐

요약

구분 ModelMapper Builder 직접 매핑
장점 한 줄 코드, 빠른 개발 명시적, 안전, 로직 추가 용이
단점 필드명 다르면 설정 필요 필드 많으면 반복 코드 증가
추천 케이스 빠른 CRUD 개발 비즈니스 로직 반영, 필드명이 다를 때

3. 기존 SQL Mapper vs JPA 코드 비교

기존 MyBatis XML Mapper 방식과 비교했을 때,
JPA Repository Interface는 코드량을 줄이고 가독성을 높일 수 있었습니다.

코드 비교

기존 MyBatis xml예시

<!-- 1. 사용자 조회 -->
<select id="selectUserById" parameterType="long" resultType="User">
  SELECT * FROM user WHERE id = #{id}
</select>

<!-- 2. 이메일 존재 여부 확인 -->
<select id="existsByEmail" parameterType="string" resultType="boolean">
  SELECT COUNT(1) > 0 FROM user WHERE email = #{email}
</select>

<!-- 3. 사용자 삭제 -->
<delete id="deleteUserById" parameterType="long">
  DELETE FROM user WHERE id = #{id}
</delete>

위 MyBatis 코드는 XML Mapper를 별도로 관리해야 하며, SQL 변경 시 Mapper 수정이 필수적입니다.

 

JPA Repository 예시

public interface UserRepository extends JpaRepository<User, Long> {
    // 1. 사용자 조회
    User findById(Long id);

    // 2. 이메일 존재 여부 확인
    boolean existsByEmail(String email);

    // 3. 사용자 삭제
    void deleteById(Long id);
}

JPA는 메서드 명명 규칙으로 쿼리가 자동 생성되어 유지보수가 용이해집니다.


4. 영속성 컨텍스트와 1차 캐시

JPA를 사용하면서 가장 흥미로웠던 부분은 영속성 컨텍스트의 1차 캐시입니다.

예를 들어, 같은 트랜잭션 내에서 동일한 Entity를 조회하면, 두 번째 조회부터는 DB를 조회하지 않고 캐시된 엔티티를 반환합니다.

 

1차 캐시 동작 예시

1차 캐시는 영속성 컨텍스트 내부에 존재하며, 동일 트랜잭션 내 동일한 Entity 조회 시 DB를 다시 조회하지 않고 캐시된 엔티티를 반환합니다.

장점
DB 호출 비용 감소 → 네트워크 비용 감소 → 성능 최적화


ERD (Entity Relationship Diagram)

Auction Server의 도메인 구조를 ERD로 표현했습니다.

Auction Server의 ERD 일부

실습 이후 코드 재검토 및 부족한 점 탐색

  • Service Interface의 불필요한 추상화 여부 고민
    • 구현체가 하나뿐이라 인터페이스를 분리한 의미가 크지 않은 경우가 있었고, 의미 있는 추상화의 기준을 다시 생각하게 되었습니다. 확장에 예정이 없다면 인터페이스로 분리하지 않아도 됨을 알게 되었습니다.
  • 무분별한 FetchType.LAZY 설정으로 인한 N+1 문제 발생 가능성 인지
    • 초기 설계 시 모든 연관관계를 FetchType.LAZY로 설정하여 네트워크 비용과 리소스 절감을 목표로 했습니다.
      그러나 실제 테스트와 코드 리뷰 중, LAZY 설정만으로는 N+1 문제가 발생할 수 있음을 알게 되었고, 이를 정확히 구분하기 위해 쿼리 로그와 트랜잭션 내 쿼리 실행 패턴을 분석하는 실험을 진행했습니다.
    • 처음에는 1차 캐시와 DB 버퍼풀의 차이를 혼동하는 부분도 있었으나, 로그 분석을 통해 트랜잭션 단위에서 실제로 불필요한 중복 쿼리가 실행되는 지점을 찾아내고, N+1 문제의 원인을 명확히 파악할 수 있었습니다.
  • 코드 유지보수성과 확장성을 위해 리팩토링 방향 탐색
    재사용성, 테스트 편의성, 명확한 역할 분리를 위해 앞으로 코드 구조 개선도 필요함을 알게 되었습니다.

이번 실습은 단순한 기능 구현을 넘어서 “왜 JPA를 사용하고, 어떻게 최적화할지”를 고민하는 계기가 되었으며, 앞으로도 이러한 고민과 학습을 계속해 나갈 계획입니다.


참고

https://github.com/gamsayeon/Auction-Server

 

GitHub - gamsayeon/Auction-Server: 대용량 트래픽을 목표로한 중고 경매 서버

대용량 트래픽을 목표로한 중고 경매 서버. Contribute to gamsayeon/Auction-Server development by creating an account on GitHub.

github.com

+ Recent posts