Spring Data JPA를 활용하여 AI Chat Memory를 데이터베이스에 저장하고 관리하는 기능을 구현하였다. 이전에는 MyBatis를 사용하여 SQL을 직접 작성하고 Mapper를 통해 데이터베이스를 제어하였다면, 이번에는 객체(Entity)를 중심으로 데이터를 관리하는 ORM(Object Relational Mapping) 방식을 적용하였다.
JPA를 활용하면서 SQL을 직접 작성하지 않고도 Repository를 통해 데이터를 조회·저장·삭제할 수 있었으며, 엔티티(Entity)를 영속성 컨텍스트(Persistence Context)가 관리하는 과정을 직접 실습하였다. 또한 Spring Data JPA가 제공하는 기본 CRUD 기능과 JPQL, 트랜잭션 관리, 영속성 컨텍스트의 Dirty Checking 등 JPA의 핵심 개념을 학습하였다.
이를 통해 데이터베이스의 테이블을 직접 다루는 것이 아니라 객체를 중심으로 비즈니스 로직을 구현하는 개발 방식을 이해할 수 있었다.
이번 프로젝트를 진행하면서 가장 먼저 학습한 내용은 ORM(Object Relational Mapping)의 개념이었다.
기존의 JDBC나 MyBatis는 SQL을 직접 작성하여 데이터베이스의 테이블을 조작하는 방식이었다. 개발자가 SELECT, INSERT, UPDATE, DELETE 문을 작성하고, 조회된 결과를 다시 Java 객체로 변환하는 과정을 직접 구현해야 했다.
반면 JPA는 데이터베이스의 테이블을 하나의 객체(Entity)로 매핑하여 관리한다.
예를 들어 Chat Message 테이블은 다음과 같은 Entity로 표현하였다.
@Entity
@Table(name = "chat_message")
public class ChatMessageJPA {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
private String conversationId;
private String messageType;
private String content;
private int seq;
}
@Entity는 해당 클래스가 데이터베이스 테이블과 매핑되는 객체임을 의미한다.
@Table은 어떤 테이블과 연결되는지를 지정하며, @Id는 기본 키(Primary Key)를 나타낸다.
@GeneratedValue를 사용하여 기본 키 값을 데이터베이스가 자동으로 생성하도록 설정하였다.
이를 통해 SQL을 직접 작성하지 않고도 객체를 저장하거나 조회할 수 있는 JPA의 기본 구조를 이해할 수 있었다.
이번 프로젝트에서는 Spring Data JPA가 제공하는 JpaRepository를 상속받아 Repository를 구현하였다.
public interface ChatMemoryJpaRepository
extends JpaRepository<ChatMessageJPA, Long> {
List<ChatMessageJPA> findAllByConversationId(
String conversationId);
@Query(
"SELECT DISTINCT cm.conversationId FROM ChatMessageJPA cm"
)
List<String> findConversationIds();
void deleteAllByConversationId(
String conversationId);
}
JpaRepository는 CRUD 기능을 기본적으로 제공한다.
따라서 별도의 SQL을 작성하지 않아도
등을 사용할 수 있다.
또한 메서드 이름만으로 SQL이 생성되는 Query Method 기능을 활용하였다.
예를 들어
findAllByConversationId(...)
메서드는 별도의 SQL을 작성하지 않아도
SELECT *
FROM chat_message
WHERE conversation_id = ?
와 동일한 기능을 수행한다.
이를 통해 SQL 작성량을 크게 줄일 수 있다는 점을 학습하였다.
기본 CRUD 외에도 직접 조회 조건이 필요한 경우에는 JPQL을 사용하였다.
@Query(
"SELECT DISTINCT cm.conversationId
FROM ChatMessageJPA cm"
)
JPQL은 SQL과 비슷하지만 테이블이 아닌 Entity 객체를 대상으로 조회한다.
SQL이라면
SELECT DISTINCT conversation_id
FROM chat_message
가 되지만,
JPQL에서는
SELECT cm.conversationId
FROM ChatMessageJPA cm
처럼 Entity 이름과 객체의 필드를 사용한다.
이를 통해 JPA는 객체 중심으로 동작한다는 점을 이해하였다.
Spring AI와 JPA를 연결하기 위해 ChatMemoryRepository를 구현하였다.
@Repository
@RequiredArgsConstructor
public class JpaChatMemoryRepository
implements ChatMemoryRepository
Spring AI는 Chat Memory를 저장하는 방식에 대해 인터페이스만 제공한다.
이번 프로젝트에서는 JPA Repository를 이용하여
기능을 구현하였다.
이를 통해 Spring AI와 Spring Data JPA를 연결하는 구조를 이해할 수 있었다.
Spring AI는 Message 객체를 사용하지만 JPA는 Entity를 저장한다.
따라서 두 객체를 서로 변환하는 과정이 필요하였다.
public static ChatMessageJPA fromMessage(
Message message,
String conversationId,
int seq)
반대로
public Message toMessage()
를 통해 Entity를 Message 객체로 변환하였다.
Entity는 데이터베이스에 저장하기 위한 객체이고,
Message는 Spring AI가 사용하는 객체이다.
따라서
Message
↓
Entity 저장
↓
DB
↓
Entity 조회
↓
Message
과 같은 변환 과정을 직접 구현하였다.
이를 통해 서로 다른 계층에서 사용하는 객체를 변환하는 방법을 학습하였다.
Chat Memory는 saveAll()을 이용하여 저장하였다.
repository.deleteAllByConversationId(
conversationId);
...
repository.saveAll(chatMessages);
새로운 대화가 저장될 때
먼저 기존 대화를 삭제하고,
현재 메모리에 존재하는 대화를 모두 저장하였다.
Spring Data JPA에서는
repository.saveAll(...)
만 호출하면
Entity 개수만큼 INSERT SQL을 자동으로 생성하여 실행한다.
이를 통해 SQL을 직접 작성하지 않고도 여러 데이터를 저장하는 방법을 학습하였다.
이번 프로젝트에서는 저장과 삭제를 하나의 작업으로 처리하기 위해 @Transactional을 사용하였다.
@Transactional
public void saveAll(...)
Chat Memory 저장 과정에서는
두 작업이 하나의 작업 단위로 처리되어야 한다.
만약 삭제는 성공하고 저장에서 오류가 발생하면 대화 기록이 모두 사라지는 문제가 발생한다.
@Transactional을 적용하면 두 작업을 하나의 트랜잭션으로 처리하여 오류 발생 시 전체 작업을 Rollback한다.
이를 통해 데이터의 일관성을 유지하는 트랜잭션의 중요성을 이해할 수 있었다.
이번 프로젝트를 진행하면서 가장 크게 느낀 점은 개발 생산성이었다.
MyBatis에서는
과정을 거쳐야 했다.
반면 JPA에서는
만으로 대부분의 CRUD 기능을 구현할 수 있었다.
또한 Query Method를 활용하면 메서드 이름만으로 SQL이 자동 생성되기 때문에 반복적인 SQL 작성이 크게 줄어드는 것을 경험하였다.
이를 통해 Spring Data JPA가 제공하는 높은 생산성과 유지보수성을 체감할 수 있었다.
이번 프로젝트에서 새롭게 이해한 개념 중 하나는 영속성 컨텍스트(Persistence Context)였다.
JPA에서 조회한 Entity는 단순한 Java 객체가 아니라 영속성 컨텍스트에 의해 관리되는 객체가 된다.
즉, Entity를 조회하면 JPA는 해당 객체를 메모리에서 관리하고 있으며, 트랜잭션이 종료되는 시점까지 객체의 상태 변화를 계속 추적한다.
이러한 기능 덕분에 개발자는 SQL의 UPDATE 문을 직접 작성하지 않고도 객체의 값을 변경하는 것만으로 데이터베이스의 값이 자동으로 수정된다.
비록 이번 Chat Memory 프로젝트에서는 대부분 저장(saveAll)과 삭제(deleteAll)를 중심으로 구현하였지만, 이전 JPA 프로젝트에서 학습한 Dirty Checking 개념과 연결하여 JPA는 객체의 상태를 관리하는 ORM 프레임워크라는 점을 더욱 명확하게 이해할 수 있었다.
| 학습 내용 | 세부 내용 |
|---|---|
| ORM | 객체(Entity)와 테이블을 매핑하여 데이터 관리 |
| Entity | @Entity, @Table, @Id를 이용한 객체 매핑 |
| Spring Data JPA | JpaRepository를 통한 CRUD 자동 제공 |
| Query Method | 메서드 이름만으로 SQL 자동 생성 |
| JPQL | 객체(Entity)를 대상으로 데이터를 조회하는 방법 |
| ChatMemoryRepository | Spring AI와 JPA를 연결하는 저장소 구현 |
| Entity 변환 | Spring AI Message 객체와 Entity 간의 변환 |
| saveAll() | Entity 리스트를 한 번에 저장하는 방법 |
| Transaction | 삭제와 저장을 하나의 작업 단위로 처리하여 데이터 일관성 유지 |
| Persistence Context | Entity의 상태를 관리하고 객체 중심으로 데이터를 처리하는 JPA의 핵심 개념 이해 |
JPA는 단순히 SQL을 자동으로 생성해 주는 기술이 아니라 객체 중심으로 데이터를 관리하는 ORM 프레임워크라는 점을 깊이 이해할 수 있었다. Spring Data JPA의 JpaRepository를 활용하면서 반복적인 CRUD SQL을 작성하지 않아도 대부분의 기능을 구현할 수 있었고, Query Method와 JPQL을 통해 필요한 조회 기능을 객체 중심으로 구현하는 방법도 익힐 수 있었다. 또한 Entity를 영속성 컨텍스트가 관리한다는 개념과 트랜잭션을 통한 데이터 일관성 유지 과정을 학습하면서, JPA가 데이터베이스와 객체 사이의 매핑을 넘어 애플리케이션의 유지보수성과 생산성을 높여 주는 핵심 기술이라는 점을 체감할 수 있었다.