안녕하세요, 저는 이번에 Spring JPA와 Flyway를 주제로 기술 세미나를 진행했습니다.

프로젝트에서 협업을 하며 데이터베이스 버전을 관리해야할 때가 있는데요, 변경할 내용이 있을 때마다 직접 문서화를 해줄 경우 번거롭고, 실수할 수도 있습니다. 이런 문제를 해결하기 위해서 어떻게 협업을 할 수 있는지, 이 때 주의해야할 것들이 있는지 등 프로젝트를 겪으며 알게된 내용을 공유하고자 합니다. 더 나아가 SQL과 Spring JPA의 연관관계를 맺는 방법도 비교해보겠습니다 !

1. Spring JPA란?

스프링 JPA(Java Persistence API)는 자바 ORM(Object-Relational Mapping) 표준으로, 객체와 데이터베이스 테이블 간의 매핑을 쉽게 처리할 수 있게 도와줍니다. 스프링 JPA는 기본적으로 JPA를 사용하면서 스프링에서 제공하는 추가적인 기능을 사용할 수 있게 해줍니다.

스프링 JPA의 주요 특징

  • 개발 편의성 향상: 스프링 JPA는 개발자가 직접 SQL 쿼리를 작성하는 대신, 메소드 이름만으로 데이터베이스의 CRUD(Create, Read, Update, Delete) 연산을 수행할 수 있는 기능을 제공합니다. 예를 들어, findByName(String name)과 같은 메소드를 작성하면, 스프링 JPA가 이를 해석하여 적절한 SQL 쿼리를 생성하고 실행합니다.
  • 데이터 접근 추상화와 자동화: 스프링 JPA는 데이터베이스 엔진에 대한 종속성을 줄이고, 데이터 접근 계층을 객체지향적으로 다루게 도와줍니다. 이는 다양한 데이터베이스 엔진에 대한 코드 변경을 최소화하고, 개발자가 비즈니스 로직에 더 집중할 수 있게 합니다.
  • 성능 최적화 기능: 스프링 JPA는 JPA가 제공하는 다양한 성능 최적화 기능, 예를 들어 1차 캐시, 지연 로딩(Lazy Loading), 쓰기 지연(Write Behind) 등을 사용할 수 있습니다.
  • 트랜잭션 관리: 스프링 프레임워크의 @Transactional 어노테이션을 이용하여 선언적인 트랜잭션 관리를 지원합니다. 이를 통해 트랜잭션의 시작, 커밋, 롤백 등을 개발자가 직접 관리하지 않아도 됩니다.

따라서 스프링 JPA를 사용하면, 데이터베이스 애플리케이션을 더 쉽고 효율적으로 개발할 수 있습니다.

2. Flyway란?

Flyway는 데이터베이스 마이그레이션을 도와주는 오픈 소스 도구입니다. 데이터베이스 마이그레이션은 데이터베이스의 스키마 버전을 관리하고 변경하는 과정을 말합니다. Flyway는 버전 관리 시스템과 비슷한 방식으로 데이터베이스 스키마의 버전을 관리합니다.

Flyway의 주요 특징

  • 버전 관리: Flyway는 각 마이그레이션 스크립트에 버전 번호를 부여합니다. 이를 통해 어떤 버전이 적용되었는지, 어떤 버전이 적용되어야 하는지를 명확하게 관리할 수 있습니다.
  • 마이그레이션 스크립트 실행: Flyway는 SQL 스크립트를 실행하여 데이터베이스 스키마를 변경합니다. 이 스크립트는 데이터베이스 테이블 생성, 데이터 입력, 테이블 구조 변경 등의 작업을 수행합니다.
  • 초기화 및 복구: Flyway는 데이터베이스를 특정 버전으로 초기화하거나, 이전 상태로 복구하는 기능을 제공합니다.
  • 플랫폼 독립성: Flyway는 다양한 데이터베이스 시스템을 지원합니다. 따라서 데이터베이스 종류에 상관없이 일관된 방식으로 데이터베이스 마이그레이션을 관리할 수 있습니다.

따라서 Flyway를 사용하면, 데이터베이스 스키마 변경을 안전하고 효율적으로 관리할 수 있습니다. 이는 특히 팀에서 함께 작업하거나, 프로덕션 환경에서 데이터베이스 변경을 관리해야 할 때 유용합니다.

아래와 같이 SQL 스크립트를 작성하여 FLyway를 사용합니다. image

3. Flyway와 Spring JPA 함께 사용하기

1. 만약 Spring JPA만 사용한다면?

  • Spring JPA는 spring.jpa.hibernate.ddl-auto 설정에 따라 데이터베이스 스키마를 자동으로 생성하거나 업데이트해줄 수 있습니다. 예를 들어 이 값을 create로 설정하면, 애플리케이션 시작 시점에 JPA가 데이터베이스 스키마를 생성합니다.
  • 그러나 이 방식은 개발 과정에서 편리하긴 하지만, 데이터베이스 마이그레이션을 정밀하게 제어하기 어렵다는 단점이 있습니다. 또한, 실제 운영 환경에서는 이런 방식으로 데이터베이스 스키마를 자동으로 변경하는 것은 위험할 수 있습니다.

2. Spring JPA + Flyway 사용

  • Flyway가 데이터베이스 스키마의 생성과 변경을 담당합니다.
  • Spring JPA는 이후의 데이터베이스 작업(조회, 삽입, 수정, 삭제 등)을 담당하게 됩니다. 이렇게 하면 데이터베이스 스키마 변경을 안전하게 관리하면서, 동시에 객체지향적인 데이터베이스 작업을 수행할 수 있습니다.

추가로 둘을 같이 사용하기 위해 Entity에 @Column 어노테이션을 추가로 붙여줘야합니다. image

JPA만 사용했을 때는 데이터베이스 스키마를 JPA가 생성했기 때문에 상관 없지만, 둘을 함꼐 사용했을 때에는 데이터베이스 스키마는 Flyway, 이 후 데이터베이스 작업은 JPA가 하기 때문에, 생성된 데이터베이스의 테이블, 필드들과 엔티티가 동일한 것이라는 표시를 해주어야 JPA가 인식할 수 있습니다.

4. Flyway(SQL)와 Spring JPA의 연관관계

Flyway는 SQL 스크립트를 사용합니다. SQL과 Spring JPA는 연관관계를 매핑하는 방법에 차이가 있는데요, 각각 비교해보겠습니다.

SQL (Flyway)

  • 테이블 간의 연관 관계를 표현하기 위해 외래 키(Foreign Key)를 사용합니다.
  • 외래 키는 한 테이블의 필드가 다른 테이블의 기본 키(Primary Key)를 참조함으로써 두 테이블 사이의 연관 관계를 정의합니다. image

Spring JPA

  • 객체 간의 연관 관계를 표현하기 위해 @ManyToOne, @OneToMany, @OneToOne, @ManyToMany 등의 어노테이션을 사용합니다. 이러한 어노테이션을 사용하면 객체 간의 관계를 객체지향적으로 표현할 수 있습니다. image

앞의 코드로 보여드린 두 개의 엔티티가 있습니다. 각 엔티티는 다른 방식으로 연관관게를 맺어준 상태입니다. 두 엔티티 모두 User를 참조하고 있네요.

  • Gym, User 를 참조하는 Review 엔티티 ( @ManyToOne 사용 )
  • Review, User 를 참조하는 ReviewReaction 엔티티 ( FK 사용 )

앞으로 참조하고 있는 User를 부모, 참조하고 있는 Review와 ReviewReaction 엔티티를 자식 엔티티라고 하겠습니다.

각 자식 엔티티를 조회했을 때, 부모 엔티티가 존재하지 않는다면 어떻게 조회될까요?

1. 각 자식 엔티티를 조회했을 때, 부모 엔티티가 존재하지 않는다면?

기존의 데이터베이스에는, User 테이블에 id가 1인 엔티티가 있습니다. image

  • Review 테이블에는 user_id가 1인 엔티티를 참조하는 review 엔티티가 2개 존재합니다.(JPA로 연관관계 매핑) image

  • review_reaction 테이블에는 user_id가 1인 엔티티를 참조하는 review_reaction 엔티티가 2개 존재합니다. (SQL로 연관 매핑) image

이런 상황에서 User id가 1인 엔티티를 삭제했을 때, 즉 부모 엔티티를 삭제한 후 자식 엔티티를 살펴보겠습니다.

  • 먼저 JPA로 연관관계를 맺어준 review 테이블입니다. image

부모 엔티티가 사라지더라도, review는 그대로 남아있습니다.

  • 다음으로는 FK로 연관관계를 맺어준 reviewReaction 테이블을 보겠습니다. image

부모 엔티티가 사라짐에 따라, 해당 엔티티를 참조하는 엔티티도 사라졌습니다.

어떻게 연관관계를 맺어주었는지에 따라, 부모엔티티가 삭제되었을 때 실제 데이터베이스에서 삭제되는 경우도 있고, 삭제되지 않는 경우도 있네요!

그렇다면 JPA(삭제 X)로 연관관계가 잘 안맺어진 것일까요? 이 상태에서 JPA로 연관관계를 맺어준 리뷰 테이블을 조회(Get) 해보면, 여전히 데이터 베이스에는 리뷰가 남아있음에도, 리뷰는 없다고 조회가 됩니다.

그러면 FK를 사용하지 않고, JPA 로만 부모엔티티 삭제 시 자식 엔티티까지 함께 삭제하게 하는 방법은 무엇이 있을까요?

다양한 방법들이 있겠지만 당장 생각나는 방법으로는 부모 엔티티에도 연관관계를 만들어주고, 함께 삭제될 수 있도록 CascadeType.REMOVE 옵션을 주는 방법이 있습니다. 하지만, 현재 진행중인 프로젝트의 정책 상 해당 부모 엔티티는 자식 엔티티를 필드 값으로 가지고 있지 않습니다. 이런 경우에는 해당 옵션을 사용할 수 없고, 대신 서비스 레이어에 있는 부모 엔티티가 삭제되는 메소드에서, 자식 엔티티까지 함께 삭제될 수 있도록 해야합니다.

그런 이유로, 부모 엔티티 삭제 시 자식 엔티티까지 삭제해야하는 경우, SQL을 사용하여 FK를 맺어줌으로써 별다른 코드 작성 없이도 데이터베이스 레벨에서 제약조건을 관리할 수 있도록 했습니다.

반면에 만약 부모 엔티티가 삭제되어도 자식 엔티티가 삭제되지 않도록 처리하자는 정책이 있다면 JPA로 연관관계를 맺어줘야겠죠? (FK로 연관관계를 맺을 경우, 부모 엔티티가 삭제되면 대댓글까지 함께 삭제되기 때문)

위와 같이 연관관계를 어떻게 맺느냐에 따라 수고가 덜어질 수 있습니다. 만약 팀에서 Flyway를 사용하고 있다면, 정책에 따라서 적절한 방법을 선택하면 좋을 것 같습니다.

감사합니다 :)