계좌가 많아질 경우, 어떻게 설계하면 좋을까?🤔
N+1은 왜 발생할까?🤔(Fetch Join 적용 전후 성능 비교)
N+1은 왜 JPA에서 발생할까?🤔(JPQL)
N+1은 메모리에 어떤 영향을 미칠까?🤔(JVM)
영속성 컨텍스트는 N+1과 어떤 관계가 있을까?🤔(Hibernate)
Fetch Join은 영속성 컨텍스트에서 어떻게 동작할까?🤔
Hibernate 내부에서 영속성 컨텍스트는 어떻게 동작할까?🤔
N+1은 GC를 왜 자주 발생시킬까?🤔
블로그 스터디를 진행하던 중 “계좌가 많아질 경우, 어떻게 설계하는 게 좋을까요?”라는 질문을 받았다.
이에 일단 다음 두 가지 경우를 생각해냈다. 예를 들어, 현재 DB에 총 100개의 계좌가 존재한다고 가정해보자.
물론 이는 단순화한 경우의 수이며, 실제 서비스에서는 이 두 상황이 섞여 더 다양한 패턴이 존재할 수 있다. 하지만 “계좌가 많아진다”는 말을 조금 더 구체적으로 바라보는 출발점으로는 충분하다고 느꼈다.
그러나 답변하며 상황을 더 구체적으로 나눠보니 교착상태에 빠지게 되었다. 고민이 시작됐다. 계좌 존재 여부 확인에는 HashSet이 좋아 보이고 사용자 기준 접근에는 HashMap이 더 적합해 보인다.
그렇다면, 이 두 가지를 하나의 자료구조로 해결할 수는 없을까? 아니면, 역할에 따라 자료구조를 분리하는 것이 맞을까? 하나의 책임에 하나의 구조를 두는 게 더 나은 선택일까? 이러한 고민이 이어지면서 점점 미궁으로 빠져들었다.
유럽에서는 법적으로 Open Banking 규격이 도입되면서, 계좌 정보를 API를 통해 안전하게 공유하는 구조가 보편화되었다. 이 과정에서 자연스럽게 등장한 것이 바로 ‘계정 집계(Account Aggregation)’라는 아키텍처 패턴이다. 계정 집계 서비스는 여러 금융기관에 흩어져 있는 계좌 정보를 한 곳에서 조회·관리할 수 있도록 하며, 이를 위해 표준화된 인터페이스와 데이터 모델을 적극적으로 활용한다. 단순히 계좌를 많이 다루기 위한 구조라기보다는, 계좌 수가 늘어나도 복잡도가 급격히 증가하지 않도록 하기 위한 설계 선택에 가깝다.
다음과 같은 구조로 중앙 허브 기관이 없고 각 은행이 직접 API(Open Banking API)를 제공한다. 계정 집계는 핀테크 서비스가 직접 수행하여 책임과 복잡성이 각 서비스로 분산된다.

이 패턴의 핵심은 다음과 같다.
이러한 접근 덕분에 유럽의 금융 시스템에서는 계좌 수의 증가를 ‘자료구조 문제’가 아닌 ‘설계와 책임 분리의 문제’로 다룬다. 결과적으로 계좌가 많아질수록 복잡해지는 것이 아니라, 오히려 확장에 유리한 구조를 유지할 수 있다.
은행 A ─┐
은행 B ─┼→ Account Aggregator (핀테크) → 사용자
은행 C ─┘
다음과 같은 이유로 계정 집계는 개념만 표준화되고, 구현은 분산되었다.
특정 기관에 데이터가 집중되는 것을 피한다. 핀테크가 은행과 직접 연결해 차별화된 서비스를 만들 수 있도록 허용한다.
EU는 다국가·다은행이므로 환경 하나의 중앙 허브를 만들고 운영하기가 현실적으로 어렵다.
규제 당국은 “API를 열어라”까지만 강제하고 누가 집계할지는 시장에 맡긴다.
유럽(Open Banking, PSD2)에서는 서드파티가 반드시 존재한다.
해시맵이나 해시셋과 같은 자료구조는 메모리 기반에서 매우 빠르지만, 계좌 정보처럼 영속적으로 저장되어야 하고 지속적으로 조회되는 데이터를 다루기에는 한계가 있다. 계좌 수와 트래픽이 증가하는 상황을 고려한다면, 결국 RDBMS를 중심으로 인덱스와 캐시 계층을 함께 설계하는 구조가 필수적이다.
이러한 구조에서는 도메인 주도 설계(DDD)가 특히 효과적이다. User, Account, Transaction과 같은 핵심 개념의 경계를 명확히 나누고, 각 도메인 서비스가 자신의 책임에만 집중하도록 하면, 계좌 수가 늘어나더라도 도메인 모델이 불필요하게 비대해지는 것을 막을 수 있다. 또한 계좌 정보를 외부에 제공하거나 내부 서비스 간에 조회해야 한다면, API 설계 역시 표준화된 방식을 따르는 것이 중요하다.
/users/{userId}/accounts/{accountId}와 같은 RESTful한 리소스 경로를 기준으로, 페이지네이션, 필터링, 버저닝을 함께 고려하면 계좌 수가 증가해도 안정적인 조회와 확장이 가능해진다.
| 요구사항 | 구조적 대응 |
|---|---|
| 적은 유저 × 많은 계좌 | 사용자당 계좌 리스트 → 분산/파티셔닝 고려 |
| 많은 유저 × 계좌 소수 | 유저-계좌 인덱스 기반 접근 |
| 계좌 정보 API 제공 | RESTful + Pagination + 필터링 |
| 표준 준수 필요 | ISO 20022 / Open Banking API 적용 |
Account는 User를 참조하는 단방향 연관관계를 가진다. Account 내부에 User 필드가 존재하며, User는 Account를 전혀 알지 못한다. 또한 여러 Account가 하나의 User를 참조할 수 있으므로, 이 관계는 교과서적인 단방향 Many-to-One 연관관계에 해당한다.

User가 여러 Account를 가지니까 One-to-Many 아닌가란 생각이 들 수도 있다. 이는 개념적으로 보면 맞는 말이다. 하지만 JPA와 객체 모델에서 연관관계를 정의할 때의 기준은 다르다.
객체 연관관계는 항상 다음 두 가지를 기준으로 판단한다.
계좌 생성 흐름을 보면, 가장 먼저 다음 두 가지를 검증한다.
// User가 DB에 있는지 검증
User user = userRepository.findById(userId).orElseThrow(
() -> new CustomApiException("유저를 찾을 수 없습니다.")
);
// 해당 계좌가 DB에 있는 중복 여부를 체크
Optional<Account> accountOptional = accountRepository.findByNumber(accountSaveReqDto.getNumber());
if (accountOptional.isPresent()) {
throw new CustomApiException("해당 계좌가 이미 존재합니다.");
}
이 검증은 모두 Repository를 통해 DB 조회를 기반으로 수행된다. 즉, ‘존재 여부’는 컬렉션의 문제라기보다는 영속성 계층의 책임이다. 계좌 존재 여부를 빠르게 확인하기 위해 굳이 메모리 자료구조(HashSet)를 도입하지 않아도 DB와 인덱스가 이미 그 역할을 수행하고 있다.
계좌의 주인인지, 비밀번호가 맞는지, 잔액이 충분한지와 같은 검증은 모두 Account 내부에서 수행된다.
public void checkOwner(Long userId) {
if (user.getId().longValue() != userId.longValue()) {
throw new CustomApiException("계좌 소유자가 아닙니다.");
}
}
public void checkSamePassword(Long password) {
if (this.password.longValue() != password.longValue()) {
throw new CustomApiException("계좌 비밀번호 검증에 실패했습니다.");
}
}
public void checkBalance(Long amount) {
if (this.balance < amount) {
throw new CustomApiException("계좌 잔액이 부족합니다.");
}
}
계좌의 상태(balance, owner, password)를 가장 잘 아는 객체는 Account이다. 외부에서 if 문으로 검증하는 대신, 행위와 검증을 함께 둔다. 결과적으로 도메인 로직이 흩어지지 않는다. 즉, “이 계좌로 이 행위를 해도 되는가?”라는 질문의 책임은 항상 Account 자신에게 있다.
사용자의 계좌 목록을 조회하는 로직은 다음과 같다. 여기서 계좌는 List
public AccountListRespDTO findAccountsByUser(Long userId) {
// User가 DB에 있는지 검증
User user = userRepository.findById(userId).orElseThrow(
() -> new CustomApiException("유저를 찾을 수 없습니다.")
);
// 유저의 모든 계좌목록
List<Account> accountList = accountRepository.findByUserId(userId);
return new AccountListRespDTO(user, accountList);
}
사용자 계좌는 목록 조회 대상이다. 순서, 개수, 반복 처리, 페이징이 중요하며 이미 DB에서 중복 여부를 판단하기에 이 시점에서 중복 여부를 판단할 필요는 없다. 따라서 계좌 컬렉션은 ‘존재 여부 검증용’이 아니라 ‘표현과 조회용’이다.
질문을 받았을 당시에는 다음과 같은 고민을 했다. 계좌 수가 많아질수록 존재 여부 확인과 사용자별 접근을 빠르게 처리해야 한다고 생각했기 때문이다.
하지만 코드를 다시 살펴보며 생각이 정리되었다. 이 시스템에서 계좌의 존재 여부와 사용자–계좌 매핑 관계는 이미 DB와 Repository가 책임지고 있었다. 그리고 도메인 계층에서는 역할이 비교적 명확하게 나뉘어 있었다. Account는 자신의 상태와 행위를 책임지고 Service는 전체 흐름을 조율하며 List는 단순히 “조회 결과”를 표현한다. 즉, 현재 프로젝트의 설계 초점은 자료구조의 선택이 아니라 책임을 어디에 둘 것인가에 맞춰져 있었다.
계좌가 많아질 경우를 고민하며 자료구조부터 떠올렸지만, 정작 코드 안에는 이미 어느 정도의 답이 정리되어 있었다.
결국 중요한 것은 어떤 자료구조를 쓰느냐보다, 역할과 책임의 경계를 명확히 나누는 일일지도 모른다.
앞선 구현에서는 계좌 존재 여부는 DB 조회로 검증하고 사용자 계좌 목록은 List
가장 먼저 떠올릴 수 있는 대안은 사용자 기준 접근을 명확히 드러내는 구조다.
Map<String, List<Account>> userAccounts;
그러나 이 구조는 메인 저장소라기보다는 캐시나 읽기 전용 뷰에 가까운 역할에 더 적합해 보인다.
초기에 떠올렸던 고민으로 다시 생각해보자.
Set<String> accountNumbers;
Map<Long, List<Account>> userAccounts;
이 구조는 기능적으로는 가능하다. 하지만 곧 문제가 드러난다.
즉, 자료구조를 합친 것이 아니라 복잡성을 늘린 것에 가깝다.
여기까지 탐색하며 점점 명확해진 사실은 이것이다. 계좌 수가 많아질 때의 문제는 메모리 자료구조의 문제가 아니라 조회 패턴과 책임 분리의 문제에 가깝다.
이 구조는 데이터가 많아질수록 오히려 더 안정적으로 동작한다.
다음과 같은 경우라면, 지금의 선택을 다시 고민해볼 수 있다.
이때는
계정 집계(Account Aggregation) 서비스는 유럽뿐만 아니라 우리나라에서도 이미 사용되고 있는 구조다. 한국에서는 이 역할을 “금융결제원(KFTC, Korea Financial Telecommunications and Clearing Institute)”이 수행하고 있다.
대한민국 금융위원회의 오픈뱅킹(Open Banking) 정의는 은행의 금융결제 시스템을 핀테크 기업에 개방하는 것을 의미한다. 1986년에 설립된 금융결제원(KFTC)은 국내 소액결제망을 운영하며 오픈뱅킹 모델 설계를 담당했다. 이 모델은 금융결제원이 핀테크 기업과 은행을 위한 데이터 집중기관(Data Aggregator) 역할을 수행하는 통합 플랫폼으로 구성된다.
한국의 오픈뱅킹 모델은 금융결제원을 중심으로 한 통합 플랫폼 구조로 구성된다. 금융결제원은 모든 은행의 계좌 정보를 한데 모아 표준화된 API로 제공하는 중앙 허브(Hub) 역할을 수행한다. 핀테크 기업은 개별 은행과 각각 연동할 필요 없이, 금융결제원 한 곳에만 연결하면 모든 은행의 계좌 데이터에 접근할 수 있다. 즉, 금융결제원은 한국형 오픈뱅킹에서 중추적인 데이터 집합소(Centralized Account Aggregator)로 기능한다.
일반적인 계정 집계 모델과 달리, 한국에서는 금융결제원이 중앙에 위치한 구조를 선택했다. 이는 다음과 같은 장점 때문이다.
이 구조는 계좌 집계를 단순한 기능이 아니라, 국가 단위 금융 인프라 문제로 다루었기 때문에 가능한 선택이다.
이 개념을 이해할 때 한 가지 구분해야 할 점이 있다.
Gemini
ChatGPT
RESTful 웹 API 디자인에 대한 모범 사례
Open Banking API: How It Works, Use Cases & Selection Guide
Open Banking APIs: All you need to know
API standards for data-sharing (account aggregator)