- Communit Server에서 MyBatis을 사용하였으나 단점을 느꼈으며 이를 해결하기 위해 JPA을 사용하고자 합니다.
- 복잡한 쿼리 처리가 필요한 경우 MyBatis를 사용하면 쿼리를 직접 작성해야 했으며, 오탈자나 오류가 발생할 경우 런타임 시에 확인되며, 컴파일 시에 오류를 확인할 수 없습니다. 이러한 단점을 느껴 JPA을 사용하고자 하였습니다.
- Model Mapper 라이브러리을 활용하여 DTO로 매핑하여 반환하도록 설계하였습니다.
- JPA를 활용한 Auction 서버 프로젝트 : DB모델링과 엔티티 매핑 을 토대로 JPA을 프로젝트에 적용하고자 하였습니다.
build.gradle
dependencies {
implementation 'org.springframework.boot:spring-boot-starter-data-jpa'
}
JPA 의존성을 추가해 주었습니다.
JPA를 사용하기 위해서는 데이터베이스와의 통신 및 매핑을 위해 Entity를 등록해야 합니다. 이를 위해 User 객체를 생성했습니다. 그러나 MySQL에서 user라는 예약어가 있으므로 이를 고려하여 Entity 이름을 "\"user\""로 설정하여 테이블 생성 시 충돌을 방지할 수 있습니다. Entity 객체에는 클라이언트에게 노출되지 않거나 전송되지 않아야 하는 컬럼이 있습니다. 예를 들어, 회원 등록 시 User Entity에는 사용자의 기본키와 마지막 로그인 시간이 포함되어 있습니다. 이 정보는 클라이언트에게는 필요하지 않으므로, 클라이언트에 반환할 때 필요한 컬럼만을 포함하는 DTO 객체를 사용했습니다. 따라서 Entity와 DTO 간의 매핑을 수행하기 위해 ModelMapper 라이브러리를 도입했습니다. ModelMapper는 자바에서 객체 간의 매핑을 쉽게 처리할 수 있는 라이브러리로, 다양한 매칭 전략을 설정할 수 있습니다.
JPA 설정에 필요한 정보는 Spring Boot 애플리케이션의 application.properties에 설정합니다. 그 중 주요한 내용으로는 spring.jpa.hibernate.ddl-auto 속성과 spring.jpa.show-sql 등이 있습니다.
spring.jpa.hibernate.ddl-auto 속성은 Hibernate의 DDL(데이터 정의 언어) 자동 생성을 제어합니다. 이 값에는 none, validate, update, create, create-drop 등이 있습니다. none은 DDL을 생성하지 않고, validate는 데이터베이스 스키마의 유효성을 검사합니다. update와 create는 런타임 시 데이터베이스 스키마를 생성하는데, update는 변경 사항을 기존 스키마에 적용하고 create는 새로운 스키마를 매번 생성합니다. 마지막으로 create-drop은 애플리케이션을 실행할 때 스키마를 생성하며, 애플리케이션 종료 시 스키마를 삭제합니다. 기본적으로는 none으로 설정되어 있으며, 개발자는 자신의 선호나 프로젝트의 환경에 따라 적절한 값을 선택하여 사용할 수 있습니다.
다음으로, spring.jpa.show-sql 속성은 실행하는 SQL 쿼리를 콘솔 또는 로그 파일에 출력할지를 결정합니다. 이 설정을 활성화하면 Hibernate이 실행하는 SQL 쿼리를 확인할 수 있어 디버깅 및 성능 튜닝에 도움이 됩니다.
JPA(Java Persistence API)는 SQL 쿼리를 명시적으로 작성하지 않고도 자바 객체와 데이터베이스 간의 매핑을 처리하는 특징을 가지고 있습니다. JPA를 사용하면 메서드를 통해 CRUD(Create, Read, Update, Delete) 작업을 수행할 수 있습니다. 이때 메서드 이름에는 일반적으로 findBy, findAllBy, deleteBy와 같은 접두사가 포함되며, 이러한 메서드는 자동으로 쿼리를 생성하는 데 사용됩니다. 또한, 이러한 메서드에 엔티티의 필드 이름을 전달하면 JPA가 해당 필드를 기준으로 쿼리를 생성하여 처리합니다. 예를 들어, existsByEmail 메서드는 특정 이메일 값이 데이터베이스에 존재하는지 여부를 확인하고, findByUserIdAndPassword 메서드는 주어진 사용자 ID와 비밀번호가 일치하는 레코드를 찾는 데 사용될 수 있습니다.
JPA를 사용하면 기본적으로 1차 캐시가 활성화됩니다. 이는 동일한 쿼리의 결과를 캐시하여 반복적인 데이터베이스 액세스를 최적화하는 역할을 합니다. 그러나 여러 서버를 운영할 때(예: 여러 DB 서버 또는 여러 애플리케이션 서버) 1차 캐시만으로는 부족한 경우가 있습니다. 이에 따라 2차 캐시를 고려할 필요가 있습니다. 2차 캐시는 데이터베이스 쿼리 결과나 객체를 메모리에 캐시하여 데이터베이스 액세스를 줄이고 성능을 향상시킵니다. 따라서 여러 서버를 운영하는 환경에서는 2차 캐시를 활용하여 성능을 최적화할 수 있습니다.
위의 대한 내용을 기반으로 2차 캐시가 필요한 상황을 시나리오로 작성해보았습니다. 여러 애플리케이션을 사용하는 상황을 예시로 2대의 서버와 2명의 클라이언트를 각 A, B로 가정하여 동작 과정을 작성하였습니다.
- 클라이언트 A가 서버 A에 요청을 합니다.
- 서버 A는 요청을 받고 해당 쿼리를 먼저 1차 캐시에서 확인합니다. 1차 캐시에 캐시된 데이터가 없으면 2차 캐시에 해당 쿼리 결과를 조회합니다.
- 만약 2차 캐시에도 캐시된 데이터가 없으면 실제 데이터베이스에서 쿼리를 실행하여 값을 가져옵니다.
- 가져온 결과를 1차 캐시와 2차 캐시에 저장합니다.
- 이후에 클라이언트 B가 동일한 결과를 요청하면 서버 B는 동일한 쿼리를 1차 캐시에서 먼저 확인합니다.
- 1차 캐시에 캐시된 데이터가 없으면 2차 캐시에서 해당 쿼리 결과를 조회합니다. 이때, 2차 캐시에 동일한 쿼리에 대한 결과값이 있으므로 2차 캐시에서 해당 결과를 가져옵니다.
- 가져온 결과를 1차 캐시에 저장합니다.
이렇게 하면 캐시된 값을 가져와 디스크에서 가져오는 결과보다 상대적으로 빠른 응답과 네트워크 비용을 감소시킬 수 있습니다. 다만, 데이터베이스의 내용이 업데이트되거나 삽입되면 결과를 다시 캐시에 갱신해야 합니다.
2차 캐시를 활성화하기 위해서는 일반적으로 하이버네이트(Hibernate)를 사용합니다. JPA 구현체 중 하나로 내장된 캐시 기능을 제공합니다. 이를 적용하는 과정을 설명하도록 하겠습니다.
build.gradle
dependencies {
implementation 'org.hibernate:hibernate-ehcache'
}
Ehcache는 자바 기반의 오픈 소스 인메모리 캐시 프레임워크입니다. 이를 통해 애플리케이션은 데이터를 메모리에 캐시하여 빠르고 효율적으로 액세스할 수 있습니다. Hibernate는 Ehcache와 함께 사용되어 쿼리 결과 및 엔티티를 캐시합니다. 이는 데이터베이스 액세스를 최적화하고 응답 시간을 단축하여 애플리케이션의 성능을 향상시킵니다.
application.properties
# Hibernate 2차 캐시 활성화
spring.jpa.properties.hibernate.cache.use_second_level_cache=true
# 2차 캐시 구현체 선택 (Ehcache를 사용하는 경우)
spring.jpa.properties.hibernate.cache.region.factory_class=org.hibernate.cache.ehcache.EhCacheRegionFactory
Entity에서 @Cache 어노테이션을 사용하여 2차 캐시의 전략을 설정할 수 있습니다. 사용 가능한 전략으로는 READ_ONLY, READ_WRITE, NONSTRICT_READ_WRITE, TRANSACTIONAL이 있습니다. READ_ONLY는 읽기 전용 캐시로, 데이터가 변경되지 않는 경우에만 캐시된 데이터를 사용합니다. READ_WRITE는 읽기 및 쓰기 캐시이며, NONSTRICT_READ_WRITE는 비엄격한 읽기 및 쓰기 캐시로, 트랜잭션 외부에서 데이터가 변경될 수 있지만 트랜잭션 내에서는 캐시된 데이터를 사용합니다. 마지막으로 TRANSACTIONAL은 트랜잭션 캐시로, 트랜잭션 범위 내에서만 캐시가 유효합니다. 이러한 전략을 상황에 맞게 사용하여 성능을 최적화할 수 있습니다.
READ_ONLY
- 예시) 정적 데이터나 기준 데이터(주소, 국가 코드 등)를 읽을 때 사용됩니다.
- 변경되지 않는 데이터에 대해 반복적으로 데이터베이스에서 조회하는 부하를 줄이기 위해 이 전략을 사용합니다.
- 이는 트랜잭션 격리 수준인 READ_COMMITTED와 비슷합니다.
- 장점
- 읽기 작업이 많고 데이터가 변경되지 않는 경우에 효율적입니다.
- 캐시된 데이터를 빠르게 제공하여 데이터베이스 액세스 부하를 줄일 수 있습니다.
- 단점
- 데이터의 변경을 감지하지 못하므로, 변경된 데이터가 반영되지 않을 수 있습니다.
- 캐시된 데이터가 고정되어 있기 때문에 동적인 데이터에는 적합하지 않을 수 있습니다.
READ_WRITE
- 예시) 주문 또는 재고 관리와 같은 도메인에서 사용됩니다.
- 읽기와 쓰기가 모두 빈번하게 발생하는 환경에서 사용합니다.
- 이는 트랜잭션 격리 수준인 REPEATABLE_READ와 비슷합니다.
- 장점
- 읽기 및 쓰기 작업을 모두 지원하여 다양한 상황에 유용합니다.
- 캐시를 통해 읽기 및 쓰기 작업을 최적화할 수 있습니다.
- 단점
- 데이터의 무결성을 보장하기 위해 쓰기 작업 시 캐시를 업데이트해야 하므로 일부 오버헤드가 발생할 수 있습니다.
- 쓰기 작업이 빈번하게 발생하는 경우 캐시의 일관성 유지가 어려울 수 있습니다.
NONSTRICT_READ_WRITE
- 예시) 블로그나 포럼에서 사용자의 게시글을 읽을 때는 쓰기 작업보다 읽기 작업이 더 많을 때 사용됩니다.
- 일반적으로 읽기 작업이 많고 쓰기 작업은 상대적으로 적은 환경에서 사용됩니다.
- 이는 트랜잭션 격리 수준인 READ_COMMITTED와 비슷합니다.
- 장점
- 비엄격한 일관성으로 읽기 작업을 최적화하면서 쓰기 작업의 일관성을 상대적으로 유지할 수 있습니다.
- 쓰기 작업의 일관성을 보장하기 위한 엄격한 동기화가 필요하지 않습니다.
- 단점
- 동일한 데이터에 대한 동시 쓰기 작업이 발생할 경우 일관성 문제가 발생할 수 있습니다.
- READ_WRITE와 마찬가지로 쓰기 작업이 빈번하게 발생하는 경우 일관성 유지가 어려울 수 있습니다.
TRANSACTIONAL
- 예시) 금융 거래 시스템에서는 트랜잭션 내에서 발생하는 모든 데이터 액세스에 대해 일관성을 유지해야할 때 사용됩니다.
- 트랜잭션 내에서 데이터베이스 엔티티의 일관성을 유지해야 하는 환경에서 사용됩니다.
- 이는 트랜잭션 격리 수준인 SERIALIZABLE과 비슷합니다.
- 장점
- 트랜잭션 범위 내에서 캐시의 일관성을 보장하여 데이터의 일관성을 유지할 수 있습니다.
- 데이터베이스의 트랜잭션과 일관성을 유지하여 데이터 무결성을 보장합니다.
- 단점
- 트랜잭션 범위가 제한되므로 트랜잭션 외부에서의 데이터 변경을 캐시에 반영하지 못할 수 있습니다.
- 트랜잭션 범위가 너무 커질 경우 캐시의 일관성을 유지하기 어려울 수 있습니다.
이와 같이 해당하는 캐시 전략들은 트랜잭션의 격리 수준과 비슷한 과정을 거칩니다. 트랜잭션 격리수준은 ACID 원칙과 트랜잭션 관리 방법 블로그 글의 격리 수준을 참고 해주세요.
이를 통해 2차 캐시를 활용하면 데이터베이스 액세스 부하를 줄여 전체 시스템 성능을 향상시키고, 데이터베이스 서버로의 요청을 줄여 네트워크 비용을 절감할 수 있습니다. 그러나 2차 캐시를 사용할 때 주의해야 할 점이 있습니다. 캐시에 저장되는 데이터의 동시성 문제가 발생할 수 있으며, 또한, 2차 캐시는 메모리를 사용하여 데이터를 캐싱하므로 대용량 트래픽 상황을 고려하여 메모리 사용에 주의하면서 캐시의 사이즈를 조절해야합니다. 이러한 문제를 해결하기 위한 과정은 추후에 정리할 예정입니다.
- 이러한 작업을 통해 JPA를 이론적으로 이해하고 실제 프로젝트에 적용한 결과 불필요한 데이터베이스 요청을 줄이고 시스템 응답 시간을 단축할 수 있었으며, 필요한 정보만을 제공하며 데이터 노출을 최소화할 수 있었습니다.
- 이러한 경험을 바탕으로 다음 프로젝트에서도 JPA를 효과적으로 활용할 수 있을 것 같습니다.
느낀점
- 추후 서버 증설을 고려하여 2차 캐시를 도입하여 성능 테스트를 진행하고자 합니다.
- 또한, 2차 캐시를 사용할 때 발생할 수 있는 동시성 문제와 메모리 문제에 대해 블로그에 작성할 계획입니다.
출처
GitHub - gamsayeon/Auction-Server
Contribute to gamsayeon/Auction-Server development by creating an account on GitHub.
github.com
- https://velog.io/@rainmaker007/Jpa-EntityManager-%EC%84%A4%EB%AA%85-%EC%98%81%EC%86%8D%EC%84%B1-%EC%BB%A8%ED%85%8D%EC%8A%A4%ED%8A%B8
- https://junghyungil.tistory.com/203
- https://junghyungil.tistory.com/140?category=943340
- https://kihwan95.tistory.com/4
- https://keencho.github.io/posts/JPA-cache/
- https://velog.io/@goseungwon/InnoDB%EB%8A%94-Repeatable-Read%EC%97%90%EC%84%9C-%EC%96%B4%EB%96%BB%EA%B2%8C-Phantom-Read%EB%A5%BC-%EC%98%88%EB%B0%A9%ED%95%A0%EA%B9%8C
'Toy Project > Auction-Server' 카테고리의 다른 글
Auction-Server 성능 테스트: 응답 시간 최적화 방법 (0) | 2024.03.27 |
---|---|
Auction-Server에 Docker 적용하기: 개발과 배포의 효율적인 관리 (0) | 2024.03.11 |
경매 서버 성능 최적화: 경매 서버의 Elasticsearch 도입 (0) | 2024.01.08 |
RabbitMQ와 Spring Boot로 구현하는 Auction Server (0) | 2023.11.22 |