목차
객체가 자기를 저장한다
주문을 하나 만들어 저장하는 코드다. 저장소를 부르는 줄이 없다.
order = Order.new(product_id: 7, quantity: 2)
order.save # 객체가 자기를 저장한다
order.quantity = 3
order.save # 고치고 다시 저장
Order.find(1) # 찾는 것도 클래스가 안다
Order.where(status: 'PAID')Ruby on Rails 코드다. Python의 Django도 거의 같다.
order = Order(product_id=7, quantity=2)
order.save()
Order.objects.get(id=1)
Order.objects.filter(status='PAID')문법은 낯설어도 그림은 분명하다. Order라는 객체가 자기를 DB에 넣고 빼는 방법을 스스로 알고 있다. 저장소도, 매퍼도, 별도의 인터페이스도 없다.
이 방식에 이름이 있다. Active Record다.
행 하나가 객체 하나다
Active Record는 이름이 곧 정의다. 테이블의 행(record) 하나를 객체 하나로 놓고, 그 객체에 저장·조회 능력을 붙인 것이다.
객체 하나가 두 가지를 겸한다. 행의 데이터를 담고, 그 행을 다루는 SQL을 안다. 둘이 한 클래스에 있다는 게 이 패턴의 전부다.
그래서 별도의 매핑 설정이 거의 없다. 테이블 이름이 orders면 클래스는 Order, 칼럼이 quantity면 속성도 quantity다. 규칙을 정해두고 이름으로 맞춘다.
짧다, 그리고 빠르다
이 방식이 널리 쓰이는 이유는 분명하다. 쓸 코드가 거의 없다.
같은 일을 우리가 아는 방식으로 하면 최소 이렇게 된다.
// 인터페이스
public interface OrderRepository {
Order findById(Long id);
void save(Order order);
}
// 구현 (또는 Spring Data가 대신 만들어준다)
// 엔티티 매핑
@Entity
public class Order { ... }
// 서비스에서 주입받아 사용
public class OrderService {
private final OrderRepository orders;
}클래스가 셋이고, 그 셋을 이어주는 설정이 붙는다. Active Record는 이게 클래스 하나다.
얻는 게 셋이다.
- 처음 화면이 빨리 나온다. 테이블 만들고 클래스 하나 쓰면 CRUD가 끝난다
- 읽을 코드가 적다.
Order.find(1)이 어디로 가는지 따라갈 필요가 없다 - 배우기 쉽다. 계층 구조를 몰라도 쓸 수 있다
스타트업이나 관리 도구가 이 방식으로 출발하는 게 이상한 일이 아니다. 만들 것이 많고 도메인 규칙이 아직 적을 때, 이만한 속도를 내는 방식이 드물다.
Repository와 정확히 반대다
이 패턴의 성격은 Repository와 나란히 놓으면 가장 잘 보인다. 같은 문제에 정반대 답을 낸다.
문제는 하나다. “저장하는 방법을 어디에 둘 것인가.”
| Active Record | Repository | |
|---|---|---|
| 저장 방법이 있는 곳 | 객체 안 | 저장소 뒤 |
| 도메인 객체가 아는 것 | 자기 데이터 + 테이블 | 자기 데이터만 |
| 클래스 수 | 하나 | 둘 이상 |
| 저장 기술을 바꾸려면 | 객체를 고친다 | 구현만 갈아끼운다 |
| DB 없이 테스트 | 어렵다 | 쉽다 |
Repository 편의 핵심 문장이 “서비스가 어떻게 저장하는지를 잊어버리게 만드는 것”이었다. Active Record는 정확히 그 반대다. 객체가 자기 저장 방법을 똑똑히 안다.
여기서 오해하기 쉬운 게 하나 있다. Active Record가 “규칙을 객체에 넣는” 도메인 모델과 같은 얘기로 보인다는 것이다. 다르다.
- 도메인 모델 - 객체에 업무 규칙을 넣는다 (
order.cancel()) - Active Record - 객체에 저장 방법까지 넣는다 (
order.save())
앞은 “취소 가능한 주문인가”를 아는 것이고, 뒤는 “어느 테이블 어느 칼럼에 넣는가”를 아는 것이다. 성격이 전혀 다른 두 지식이 한 클래스에 같이 사는 게 Active Record다.
테이블이 곧 모델이 된다
편리함에는 값이 붙어 있다. 그 값이 가장 먼저 드러나는 곳은 스키마와 도메인이 붙어버리는 것이다.
Active Record에서 클래스는 테이블을 그대로 비춘다. 칼럼을 추가하면 속성이 생기고, 칼럼 이름을 바꾸면 코드가 따라 바뀐다. 처음에는 이게 장점이다. 맞출 게 없으니까.
문제는 도메인이 테이블과 다른 모양이 되고 싶을 때 시작된다.
예를 들어 “주문은 항목 여러 개를 묶은 하나의 덩어리”라고 보고 싶다고 하자(Aggregate에서 본 그것이다). 그런데 테이블은 orders와 order_items 둘이다. Active Record에서는 클래스도 자연히 둘이 되고, 바깥에서 OrderItem.find(...)로 항목만 따로 꺼내는 길이 열려 있다.
경계를 지키고 싶어도 지킬 도구가 없다. 항목 클래스가 스스로 저장할 줄 아는 이상, 루트를 통해서만 만지자는 약속은 약속으로만 남는다.
여러 테이블에 흩어진 데이터를 도메인 개념 하나로 묶고 싶을 때도 같은 벽을 만난다. 테이블이 곧 클래스라서, 테이블에 없는 모양은 표현할 자리가 마땅치 않다.
테스트가 DB를 부른다
두 번째 값은 테스트에서 돌아온다.
“비활성 사용자를 거부하는가”만 확인하고 싶다고 하자. Repository였다면 메모리 구현을 끼워 DB 없이 검증할 수 있었다. Active Record에서는 그럴 자리가 없다. 저장 방법이 객체 안에 박혀 있어서 갈아끼울 이음매가 없기 때문이다.
그래서 테스트가 이런 모양이 된다.
- 테스트용 DB를 띄운다
- 테이블을 만들고 데이터를 넣는다
- 검증한다
- 정리한다
느리고, 준비할 게 많고, 테스트끼리 서로의 데이터에 영향을 준다. 업무 규칙 하나 확인하는 데 인프라가 따라붙는다.
요즘은 컨테이너로 실제 DB를 띄우는 테스트가 흔해져서, 이 부담이 예전만큼 크진 않다. 다만 **“DB를 안 띄우고 규칙만 확인하는 선택지가 사라진다”**는 사실은 남는다. 빠른 피드백이 필요한 자리에서 이게 아프다.
객체가 두 가지 이유로 커진다
세 번째 값은 시간이 지나야 보인다.
도메인이 복잡해지면 Order에 업무 규칙이 쌓인다. 취소 가능 여부, 할인 계산, 상태 전이. 그런데 이 클래스에는 이미 저장 관련 코드가 살고 있다. 조회 조건, 연관 로딩, 저장 순서.
한 클래스가 두 가지 이유로 부푼다.
- 업무 규칙이 늘어서
- 저장 방식이 복잡해져서
둘은 바뀌는 이유도 다르고 바뀌는 시점도 다르다. 그런데 같은 파일에 있으니 한쪽을 고치다 다른 쪽을 건드린다. DTO 글에서 “다른 이유로 바뀌는 것을 한 객체가 겸하면 한쪽 사정이 다른 쪽을 흔든다”고 했던 그 이야기가 여기서도 나온다.
그래서 규칙이 많은 도메인일수록 이 방식이 버거워진다. 반대로 규칙이 적고 테이블 모양 그대로 다루면 되는 영역에서는 끝까지 편하다.
실무: 무엇을 보고 고르나
“어느 쪽이 옳은가”는 답이 없는 질문이다. 대신 무엇을 보고 정하는지는 말할 수 있다.
| 이런 상황 | 맞는 쪽 |
|---|---|
| 도메인 규칙이 적고 CRUD가 대부분 | Active Record |
| 빨리 만들어 검증해야 한다 | Active Record |
| 관리자 화면·내부 도구 | Active Record |
| 상태 전이·정산처럼 규칙이 얽힌다 | Repository |
| DB 없이 로직을 검증하고 싶다 | Repository |
| 도메인 모양이 테이블과 다르다 | Repository |
읽어보면 로직을 어디에 둘 것인가에서 본 기준과 거의 같다. 도메인이 얼마나 복잡한가가 다시 갈림길이 된다.
그리고 한 시스템 안에서도 영역마다 다르게 갈 수 있다. 공지사항이나 코드 테이블은 Active Record 스타일로 두고, 주문·정산만 저장소 뒤로 숨기는 식이다. 전부를 한 방식으로 통일해야 한다는 규칙은 없다.
자바·Spring을 쓴다면 이 패턴을 그대로 만나는 일은 드물다. JPA와 Spring Data가 Repository 쪽을 기본값으로 깔고 있어서다. 다만 엔티티에 정적 조회 메서드를 붙이거나, 엔티티가 저장까지 책임지게 만드는 코드를 보게 되면 그게 Active Record 스타일이라는 걸 알아두면 된다. 나쁘다는 뜻이 아니라, 그 선택에 무엇이 딸려 오는지를 알고 하자는 것이다.
정리
| Active Record란 | 행 하나를 객체 하나로 놓고, 그 객체에 저장·조회를 붙인 패턴 |
| 왜 쓰나 | 클래스 하나로 끝난다. 처음 만드는 속도가 빠르다 |
| Repository와 | 같은 문제에 정반대 답. 저장 방법이 객체 안이냐 저장소 뒤냐 |
| 도메인 모델과 | 업무 규칙을 객체에 넣는 것과 다르다. 이건 저장 방법까지 넣는다 |
| 대가 | 테이블이 곧 모델 · 테스트에 DB 필요 · 객체가 두 이유로 커진다 |
| 고르는 기준 | 도메인 규칙이 적으면 Active Record, 얽히면 Repository |
객체가 자기 저장을 아는 것. 그게 Active Record이고, 그 앎이 편할 때와 발목을 잡을 때가 갈린다.