데이터베이스 선택은 많은 개발 프로젝트에서 중요한 결정 중 하나이다. 특히, SQL과 NoSQL 사이에서 선택하는 것은 각각의 장단점을 이해하고, 프로젝트의 요구 사항에 가장 잘 맞는 기술을 선택하는 데 중요하다.
이번 프로젝트를 진행하며 기획적으로 어떤 서비스를 진행할지는 결정 되었지만, 아직 종류의 데이터베이스를 사용해야 할지 쉽게 결정되지 않았다. 그렇다고, 개발을 미룰 수는 없기 때문에 SQL과 NoSQL을 둘 다 사용할 수 있도록 개발을 했던 경험을 공유해보려 한다.
특히, Spring의 Dependency Injection(DI) 기능과 추상화를 사용하여 데이터 액세스 계층을 설계하는 과정에 대해 다루고 있다.
Spring Data를 사용해서 개발하였고, SQL을 사용하기 위해 Spring Data JPA, NoSQL을 사용하기 위해 Spring Data MongoDB를 사용해서 데이터 엑세스를 하였다.
선택 기술
- Language
- Java
- Framework
- Spring Boot
- SQL
- Spring Data JPA
- NoSQL
- Spring Data Mongo
배경
회원 기능이 필요하다.
아직 회원 정보에 어떤 정보가 필요한지 다 결정된 상태가 아니다.
회원 정보를 저장할 데이터베이스가 필요로 하며, 아직 어떤 데이터베이스를 활용하는게 좋을지 판단은 안되는 상태이다.
데이터베이스 후보는 RDBMS와 MongoDB이다.
둘 다 사용해보며, 개발을 완료 후 테스트를 통해 하나의 DB를 선택하여 사용할 예정이다.
서비스
UserService
클래스는 데이터 액세스를 추상화하는 데 중요한 역할을 합한다.
이 클래스에서는 ObjectProvider<User>
를 사용하여 IoC Container
로 부터 적절한 User
인스턴스를 동적으로 제공받는다.
이 접근 방식은 Spring DI의 유연성을 활용하여, 애플리케이션의 나머지 부분을 변경하지 않고도 데이터베이스 기술을 쉽게 전환할 수 있게 한다.
UserService.java
@Service
@RequiredArgsConstructor
public class UserService {
private final ObjectProvider<User> userProvider;
private final UserRepository userRepository;
public void addUser(UserAddDto userAddDto) {
User user = userProvider.getObject();
user.setByUserAddDto(userAddDto);
userRepository.save(user);
}
public Optional<User> findUserById(String userId) {
return userRepository.findByUserId(userId);
}
public boolean isUserIdExist(String userId) {
return findUserById(userId).isPresent();
}
}
그럼, 어떻게 적절한 User
인스턴스와 UserRepository
인스턴스를 제공 받을 수 있었는지 알아보자.
도메인
User
- id
- 데이터베이스의 기본 키
- 문자열
- 유일해야함
- userId
- 사용자 로그인 Id
- 문자열
- 유일해야함
- 검색에 빨라야함
- password
- 암호화 되어 저장되어야 함
- 문자열
- name
- 사용자 이름
- 문자열
공용 도메인 User
사실, 굳이 ObjectProvider
를 통해 주입받지 않게 하고, User
객체를 JPA
와 MongoDB
가 둘 다 사용할 수 있도록 공용으로 설계할 수 있다.
Spring Data 공식문서의 4번째 예시 (Using Repositories with Multiple Spring Data Modules) 를 보면 동일한 도메인 유형을 재사용 할 수 있도록 JPA
및 Spring Data MongoDB
주석을 모두 사용한다.
User.java
@Data
@Entity
@Document
public class User {
@jakarta.persistence.Id
@org.springframework.data.annotation.Id
private String id;
private String userId;
private String name;
private String password;
}
벌써부터 불편하지 않은가?
- 동일한 이름의 어노테이션
동일한 이름의@Id
어노테이션이 두개나 필요해서 package명을 명시하며 어노테이션을 적용하고 있다.
지금은@Id
하나뿐이지만, 여기서elasticsearch
도 데이터베이스 후보에 추가된다면@Document
도 이름이 두개나 필요할것이다. - 자료형의 차이
또, 각 데이터베이스에서 지원하는 자료형이 다를 수 있다.MongoDB
는 Json 타입을 지원하는 반면에,JPA
는 Json 타입을 지원하지 않아 해당 자료형을 다루기 위해서는 추가적인 조치가 필요하다.
해결방안을 고안해보자. 공통적인 속성을 추상화 시킨 추상 class를 만들고, 각 데이터베이스에 맞는 구체 class를 활용하자.
추상 User class
코드에서 실제로 어떤 class인지 알 필요 없이 공통적인 필드와, 메서드를 정의해놓은 abstract를 만들자.
User.java
@Data
@MappedSuperclass
public abstract class User {
protected String name;
protected String password;
abstract public String getId();
abstract public void setId(String id);
abstract public String getUserId();
abstract public void setUserId(String userId);
public void setByUserAddDto(UserAddDto userAddDto) {
this.setUserId(userAddDto.getUserId());
this.setName(userAddDto.getName());
this.setPassword(userAddDto.getPassword());
}
}
name
필드와 password
필드는 데이터베이스 별 특별한 설정 없이 공용으로 사용할 필드이며, id
와 userId
는 데이터베이스 별로 다른 설정이 필요하므로 getter
, setter
를 구현하도록 추상 메서드로 남겨 두었다.
이제 각 데이터 베이스에서 사용할 구체 User class를 만들어보자
구체 User class
JPA
jakarta.persistence.Id
어노테이션을id
필드에 적용할 것userId
필드에 인덱스와unique
제약조건을 줄 것- String 형태의 Id를 자동 생성할 것
UserEntity.java
import jakarta.persistence.*;
@Entity
@Getter @Setter
@Table(indexes = {
@Index(name = "idx_user_id", columnList = "userId", unique = true)
})
public class UserEntity extends User{
@Id
private String id;
private String userId;
@PrePersist
public void prePersist() {
if (id == null) {
id = UUID.randomUUID().toString();
}
}
}
Spring Data MongoDB
org.springframework.data.annotation.Id
어노테이션을id
필드에 적용할 것userId
필드에 인덱스와unique
제약조건을 줄 것
UserDoc.java
import org.springframework.data.annotation.Id;
@Document
@Getter @Setter
public class UserDoc extends User{
@Id
private String id;
@Indexed(unique = true)
private String userId;
}
Bean 등록하기
아래 그림과 같이 클래스 다이어그램이 그려진다. 이제, 상황에 따라 하나의 구체 class 만 Bean에 등록되도록 해보자.

설계는 둘 다 해두었지만, 결국 하나만 적용할 것 이므로 아래와 같이 패키지별로 특정 Repository를 활성화 시키는 방법 말고, Spring Boot의 Profiles 기능을 사용하자.
참고
Annotation-driven configuration of base packages
@EnableJpaRepositories(basePackages = "com.acme.repositories.jpa")
@EnableMongoRepositories(basePackages = "com.acme.repositories.mongo")
class Configuration { … }
Spring Profiles
Spring Profiles provide a way to segregate parts of your application configuration and make it be available only in certain environments. - Spring Profile
Spring Profile를 사용하면 특정 환경에서만 사용할 수 있도록 도와준다. 따라서, profile값을 이용해서 mongo
, jpa
profile을 상황에 맞게 각각 활상화 시켜 특정 구체 class 만 Bean에 등록되도록 하자.
1. profile 활성화하기application.properties
에 아래 예시와 같이 활성화 할 수 있다.
spring.profiles.active=mongo
# or
spring.profiles.active=jpa
2. config 파일 구성하기
profire값에 따른 하나의 Bean만 등록되도록 하는 config파일을 만들자.
User class는 싱글턴이 아니라 필요할때 마다 새롭게 생되어야 하므로 @Scope("prototype")
어노테이션으로 프로토타입 빈으로 등록해야한다.
UserConfig.java
@Configuration
public class UserConfig {
@Bean
@Profile("jpa")
@Scope("prototype")
public User userJpa() {
return new UserEntity();
}
@Bean
@Profile("mongo")
@Scope("prototype")
public User userMongo() {
return new UserDoc();
}
}
이렇게 작성되면 jpa
profile이 활성화 되어있을때는 UserEntity
를 User
Bean으로 등록하고,mongo
profile이 활성화 되어있을때는 UserDoc
이 User
Bean으로 등록된다.
따라서, ObjectProvider<User>
를 통해서 상황에 맞는 User
class를 제공받을 수 있다.
Repository
Repository 적용
목표는 Spring을 사용하여 SQL과 NoSQL 데이터베이스를 모두 지원하는 유연한 구조를 만드는 것이다. 이를 위해, 우리는 User
도메인 객체를 공통 인터페이스로 추상화하고, 각각의 데이터베이스 기술에 맞는 구체적인 구현체를 만들었다. 이제, 이 도메인 모델을 어떻게 저장소에 저장하고 조회할 수 있도록 하는지, Repository
의 적용 과정을 살펴보자.
공통 Repository 인터페이스
먼저, 모든 유형의 사용자 저장소에 공통적인 작업을 정의한 UserRepository
인터페이스를 만들었다. 이 인터페이스는 제네릭 타입 T
를 사용하여, JPA용 UserEntity
와 Spring Data MongoDB용 UserDoc
모두를 처리할 수 있도록 유연성을 제공한다.
UserRepository.java
public interface UserRepository<T extends User> {
T save(T user);
Optional<T> findById(String id);
Optional<T> findByUserId(String userId);
}
JPA Repository
UserJpaRepository
인터페이스는 SQL 데이터베이스를 사용할 때의 저장소 작업을 정의한다. 이 인터페이스는 Spring Data JPA의 JpaRepository
를 확장하여, JPA Entity 관리 및 CRUD 작업에 필요한 메서드를 자동으로 제공받습니다. 또한, @Profile("jpa")
어노테이션을 사용하여, 이 저장소 구현이 jpa
profile이 활성화될 때만 사용되도록 설정했다.
UserJpaRepository.java
@Profile("jpa")
public interface UserJpaRepository extends JpaRepository<UserEntity, String>, UserRepository<UserEntity> {
}
MongoDB Repository
UserMongoRepository
인터페이스는 NoSQL 데이터베이스를 사용할 때의 저장소 작업을 정의한다. 이 인터페이스는 Spring Data MongoDB의 MongoRepository
를 확장하여, MongoDB Document 관리 및 CRUD 작업에 필요한 메서드를 자동으로 제공받는다. 마찬가지로, @Profile("mongo")
어노테이션을 사용하여, 이 저장소 구현이 mongo
profile이 활성화될 때만 사용되도록 설정했다.
UserMongoRepository.java
@Profile("mongo")
public interface UserMongoRepository extends MongoRepository<UserDoc, String>, UserRepository<UserDoc> {
}
프로필 기반의 구성

이 접근법의 핵심은 Spring의 프로필 기능을 사용하여, 애플리케이션 구성 시점에 사용할 데이터베이스 기술을 결정하는 것이다. jpa
또는 mongo
프로필을 활성화함으로써, 애플리케이션을 다시 컴파일하거나 코드를 변경하지 않고도 데이터베이스 기술을 전환할 수 있다.
결론
물론, SQL과 NoSQL 사이에서의 선택은 어려울 수 있지만, 이 글에서 소개된 Spring Data와 추상화를 활용한 접근 방식은 두 데이터베이스 기술을 모두 실험해볼 수 있는 유연한 구조를 제공한다. 이 구조의 또 다른 이점은, 새로운 데이터베이스 기술을 프로젝트에 추가하고 싶을 때도 유사한 방식으로 각각의 구현체를 만들어 등록하기만 하면 된다는 점이다.
즉, 이 구조는 데이터베이스 기술의 추가나 변경에 있어서 높은 확장성을 제공한다. 만약 프로젝트의 요구사항이 변경되거나, 새로운 데이터베이스 기술을 시험해보고 싶을 때, 기존의 코드 구조를 대대적으로 수정할 필요 없이, 새로운 구현체를 만들어서 등록하기만 하면 된다. 이러한 방식으로, 다양한 데이터베이스 옵션을 손쉽게 탐색하고 테스트할 수 있고, 프로젝트의 발전에 따라 최적의 데이터베이스를 선택할 수 있게된다.
데이터베이스 선택은 많은 개발 프로젝트에서 중요한 결정 중 하나이다. 특히, SQL과 NoSQL 사이에서 선택하는 것은 각각의 장단점을 이해하고, 프로젝트의 요구 사항에 가장 잘 맞는 기술을 선택하는 데 중요하다.
이번 프로젝트를 진행하며 기획적으로 어떤 서비스를 진행할지는 결정 되었지만, 아직 종류의 데이터베이스를 사용해야 할지 쉽게 결정되지 않았다. 그렇다고, 개발을 미룰 수는 없기 때문에 SQL과 NoSQL을 둘 다 사용할 수 있도록 개발을 했던 경험을 공유해보려 한다.
특히, Spring의 Dependency Injection(DI) 기능과 추상화를 사용하여 데이터 액세스 계층을 설계하는 과정에 대해 다루고 있다.
Spring Data를 사용해서 개발하였고, SQL을 사용하기 위해 Spring Data JPA, NoSQL을 사용하기 위해 Spring Data MongoDB를 사용해서 데이터 엑세스를 하였다.
선택 기술
- Language
- Java
- Framework
- Spring Boot
- SQL
- Spring Data JPA
- NoSQL
- Spring Data Mongo
배경
회원 기능이 필요하다.
아직 회원 정보에 어떤 정보가 필요한지 다 결정된 상태가 아니다.
회원 정보를 저장할 데이터베이스가 필요로 하며, 아직 어떤 데이터베이스를 활용하는게 좋을지 판단은 안되는 상태이다.
데이터베이스 후보는 RDBMS와 MongoDB이다.
둘 다 사용해보며, 개발을 완료 후 테스트를 통해 하나의 DB를 선택하여 사용할 예정이다.
서비스
UserService
클래스는 데이터 액세스를 추상화하는 데 중요한 역할을 합한다.
이 클래스에서는 ObjectProvider<User>
를 사용하여 IoC Container
로 부터 적절한 User
인스턴스를 동적으로 제공받는다.
이 접근 방식은 Spring DI의 유연성을 활용하여, 애플리케이션의 나머지 부분을 변경하지 않고도 데이터베이스 기술을 쉽게 전환할 수 있게 한다.
UserService.java
@Service
@RequiredArgsConstructor
public class UserService {
private final ObjectProvider<User> userProvider;
private final UserRepository userRepository;
public void addUser(UserAddDto userAddDto) {
User user = userProvider.getObject();
user.setByUserAddDto(userAddDto);
userRepository.save(user);
}
public Optional<User> findUserById(String userId) {
return userRepository.findByUserId(userId);
}
public boolean isUserIdExist(String userId) {
return findUserById(userId).isPresent();
}
}
그럼, 어떻게 적절한 User
인스턴스와 UserRepository
인스턴스를 제공 받을 수 있었는지 알아보자.
도메인
User
- id
- 데이터베이스의 기본 키
- 문자열
- 유일해야함
- userId
- 사용자 로그인 Id
- 문자열
- 유일해야함
- 검색에 빨라야함
- password
- 암호화 되어 저장되어야 함
- 문자열
- name
- 사용자 이름
- 문자열
공용 도메인 User
사실, 굳이 ObjectProvider
를 통해 주입받지 않게 하고, User
객체를 JPA
와 MongoDB
가 둘 다 사용할 수 있도록 공용으로 설계할 수 있다.
Spring Data 공식문서의 4번째 예시 (Using Repositories with Multiple Spring Data Modules) 를 보면 동일한 도메인 유형을 재사용 할 수 있도록 JPA
및 Spring Data MongoDB
주석을 모두 사용한다.
User.java
@Data
@Entity
@Document
public class User {
@jakarta.persistence.Id
@org.springframework.data.annotation.Id
private String id;
private String userId;
private String name;
private String password;
}
벌써부터 불편하지 않은가?
- 동일한 이름의 어노테이션
동일한 이름의@Id
어노테이션이 두개나 필요해서 package명을 명시하며 어노테이션을 적용하고 있다.
지금은@Id
하나뿐이지만, 여기서elasticsearch
도 데이터베이스 후보에 추가된다면@Document
도 이름이 두개나 필요할것이다. - 자료형의 차이
또, 각 데이터베이스에서 지원하는 자료형이 다를 수 있다.MongoDB
는 Json 타입을 지원하는 반면에,JPA
는 Json 타입을 지원하지 않아 해당 자료형을 다루기 위해서는 추가적인 조치가 필요하다.
해결방안을 고안해보자. 공통적인 속성을 추상화 시킨 추상 class를 만들고, 각 데이터베이스에 맞는 구체 class를 활용하자.
추상 User class
코드에서 실제로 어떤 class인지 알 필요 없이 공통적인 필드와, 메서드를 정의해놓은 abstract를 만들자.
User.java
@Data
@MappedSuperclass
public abstract class User {
protected String name;
protected String password;
abstract public String getId();
abstract public void setId(String id);
abstract public String getUserId();
abstract public void setUserId(String userId);
public void setByUserAddDto(UserAddDto userAddDto) {
this.setUserId(userAddDto.getUserId());
this.setName(userAddDto.getName());
this.setPassword(userAddDto.getPassword());
}
}
name
필드와 password
필드는 데이터베이스 별 특별한 설정 없이 공용으로 사용할 필드이며, id
와 userId
는 데이터베이스 별로 다른 설정이 필요하므로 getter
, setter
를 구현하도록 추상 메서드로 남겨 두었다.
이제 각 데이터 베이스에서 사용할 구체 User class를 만들어보자
구체 User class
JPA
jakarta.persistence.Id
어노테이션을id
필드에 적용할 것userId
필드에 인덱스와unique
제약조건을 줄 것- String 형태의 Id를 자동 생성할 것
UserEntity.java
import jakarta.persistence.*;
@Entity
@Getter @Setter
@Table(indexes = {
@Index(name = "idx_user_id", columnList = "userId", unique = true)
})
public class UserEntity extends User{
@Id
private String id;
private String userId;
@PrePersist
public void prePersist() {
if (id == null) {
id = UUID.randomUUID().toString();
}
}
}
Spring Data MongoDB
org.springframework.data.annotation.Id
어노테이션을id
필드에 적용할 것userId
필드에 인덱스와unique
제약조건을 줄 것
UserDoc.java
import org.springframework.data.annotation.Id;
@Document
@Getter @Setter
public class UserDoc extends User{
@Id
private String id;
@Indexed(unique = true)
private String userId;
}
Bean 등록하기
아래 그림과 같이 클래스 다이어그램이 그려진다. 이제, 상황에 따라 하나의 구체 class 만 Bean에 등록되도록 해보자.

설계는 둘 다 해두었지만, 결국 하나만 적용할 것 이므로 아래와 같이 패키지별로 특정 Repository를 활성화 시키는 방법 말고, Spring Boot의 Profiles 기능을 사용하자.
참고
Annotation-driven configuration of base packages
@EnableJpaRepositories(basePackages = "com.acme.repositories.jpa")
@EnableMongoRepositories(basePackages = "com.acme.repositories.mongo")
class Configuration { … }
Spring Profiles
Spring Profiles provide a way to segregate parts of your application configuration and make it be available only in certain environments. - Spring Profile
Spring Profile를 사용하면 특정 환경에서만 사용할 수 있도록 도와준다. 따라서, profile값을 이용해서 mongo
, jpa
profile을 상황에 맞게 각각 활상화 시켜 특정 구체 class 만 Bean에 등록되도록 하자.
1. profile 활성화하기application.properties
에 아래 예시와 같이 활성화 할 수 있다.
spring.profiles.active=mongo
# or
spring.profiles.active=jpa
2. config 파일 구성하기
profire값에 따른 하나의 Bean만 등록되도록 하는 config파일을 만들자.
User class는 싱글턴이 아니라 필요할때 마다 새롭게 생되어야 하므로 @Scope("prototype")
어노테이션으로 프로토타입 빈으로 등록해야한다.
UserConfig.java
@Configuration
public class UserConfig {
@Bean
@Profile("jpa")
@Scope("prototype")
public User userJpa() {
return new UserEntity();
}
@Bean
@Profile("mongo")
@Scope("prototype")
public User userMongo() {
return new UserDoc();
}
}
이렇게 작성되면 jpa
profile이 활성화 되어있을때는 UserEntity
를 User
Bean으로 등록하고,mongo
profile이 활성화 되어있을때는 UserDoc
이 User
Bean으로 등록된다.
따라서, ObjectProvider<User>
를 통해서 상황에 맞는 User
class를 제공받을 수 있다.
Repository
Repository 적용
목표는 Spring을 사용하여 SQL과 NoSQL 데이터베이스를 모두 지원하는 유연한 구조를 만드는 것이다. 이를 위해, 우리는 User
도메인 객체를 공통 인터페이스로 추상화하고, 각각의 데이터베이스 기술에 맞는 구체적인 구현체를 만들었다. 이제, 이 도메인 모델을 어떻게 저장소에 저장하고 조회할 수 있도록 하는지, Repository
의 적용 과정을 살펴보자.
공통 Repository 인터페이스
먼저, 모든 유형의 사용자 저장소에 공통적인 작업을 정의한 UserRepository
인터페이스를 만들었다. 이 인터페이스는 제네릭 타입 T
를 사용하여, JPA용 UserEntity
와 Spring Data MongoDB용 UserDoc
모두를 처리할 수 있도록 유연성을 제공한다.
UserRepository.java
public interface UserRepository<T extends User> {
T save(T user);
Optional<T> findById(String id);
Optional<T> findByUserId(String userId);
}
JPA Repository
UserJpaRepository
인터페이스는 SQL 데이터베이스를 사용할 때의 저장소 작업을 정의한다. 이 인터페이스는 Spring Data JPA의 JpaRepository
를 확장하여, JPA Entity 관리 및 CRUD 작업에 필요한 메서드를 자동으로 제공받습니다. 또한, @Profile("jpa")
어노테이션을 사용하여, 이 저장소 구현이 jpa
profile이 활성화될 때만 사용되도록 설정했다.
UserJpaRepository.java
@Profile("jpa")
public interface UserJpaRepository extends JpaRepository<UserEntity, String>, UserRepository<UserEntity> {
}
MongoDB Repository
UserMongoRepository
인터페이스는 NoSQL 데이터베이스를 사용할 때의 저장소 작업을 정의한다. 이 인터페이스는 Spring Data MongoDB의 MongoRepository
를 확장하여, MongoDB Document 관리 및 CRUD 작업에 필요한 메서드를 자동으로 제공받는다. 마찬가지로, @Profile("mongo")
어노테이션을 사용하여, 이 저장소 구현이 mongo
profile이 활성화될 때만 사용되도록 설정했다.
UserMongoRepository.java
@Profile("mongo")
public interface UserMongoRepository extends MongoRepository<UserDoc, String>, UserRepository<UserDoc> {
}
프로필 기반의 구성

이 접근법의 핵심은 Spring의 프로필 기능을 사용하여, 애플리케이션 구성 시점에 사용할 데이터베이스 기술을 결정하는 것이다. jpa
또는 mongo
프로필을 활성화함으로써, 애플리케이션을 다시 컴파일하거나 코드를 변경하지 않고도 데이터베이스 기술을 전환할 수 있다.
결론
물론, SQL과 NoSQL 사이에서의 선택은 어려울 수 있지만, 이 글에서 소개된 Spring Data와 추상화를 활용한 접근 방식은 두 데이터베이스 기술을 모두 실험해볼 수 있는 유연한 구조를 제공한다. 이 구조의 또 다른 이점은, 새로운 데이터베이스 기술을 프로젝트에 추가하고 싶을 때도 유사한 방식으로 각각의 구현체를 만들어 등록하기만 하면 된다는 점이다.
즉, 이 구조는 데이터베이스 기술의 추가나 변경에 있어서 높은 확장성을 제공한다. 만약 프로젝트의 요구사항이 변경되거나, 새로운 데이터베이스 기술을 시험해보고 싶을 때, 기존의 코드 구조를 대대적으로 수정할 필요 없이, 새로운 구현체를 만들어서 등록하기만 하면 된다. 이러한 방식으로, 다양한 데이터베이스 옵션을 손쉽게 탐색하고 테스트할 수 있고, 프로젝트의 발전에 따라 최적의 데이터베이스를 선택할 수 있게된다.