← Posts

Active Record · Design Pattern · ORM

Active Record

객체가 자기를 저장할 줄 안다. Repository와 정반대 선택이고, 틀린 선택이 아니라 다른 선택이다.

목차
  1. 객체가 자기를 저장한다
  2. 행 하나가 객체 하나다
  3. 짧다, 그리고 빠르다
  4. Repository와 정확히 반대다
  5. 테이블이 곧 모델이 된다
  6. 테스트가 DB를 부른다
  7. 객체가 두 가지 이유로 커진다
  8. 실무: 무엇을 보고 고르나
  9. 정리

객체가 자기를 저장한다

주문을 하나 만들어 저장하는 코드다. 저장소를 부르는 줄이 없다.

ruby
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도 거의 같다.

python
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) 하나를 객체 하나로 놓고, 그 객체에 저장·조회 능력을 붙인 것이다.

diagramdiagram

객체 하나가 두 가지를 겸한다. 행의 데이터를 담고, 그 행을 다루는 SQL을 안다. 둘이 한 클래스에 있다는 게 이 패턴의 전부다.

그래서 별도의 매핑 설정이 거의 없다. 테이블 이름이 orders면 클래스는 Order, 칼럼이 quantity면 속성도 quantity다. 규칙을 정해두고 이름으로 맞춘다.

짧다, 그리고 빠르다

이 방식이 널리 쓰이는 이유는 분명하다. 쓸 코드가 거의 없다.

같은 일을 우리가 아는 방식으로 하면 최소 이렇게 된다.

java
// 인터페이스
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와 나란히 놓으면 가장 잘 보인다. 같은 문제에 정반대 답을 낸다.

문제는 하나다. “저장하는 방법을 어디에 둘 것인가.”

diagramdiagram
Active RecordRepository
저장 방법이 있는 곳객체 안저장소 뒤
도메인 객체가 아는 것자기 데이터 + 테이블자기 데이터만
클래스 수하나둘 이상
저장 기술을 바꾸려면객체를 고친다구현만 갈아끼운다
DB 없이 테스트어렵다쉽다

Repository 편의 핵심 문장이 “서비스가 어떻게 저장하는지를 잊어버리게 만드는 것”이었다. Active Record는 정확히 그 반대다. 객체가 자기 저장 방법을 똑똑히 안다.

여기서 오해하기 쉬운 게 하나 있다. Active Record가 “규칙을 객체에 넣는” 도메인 모델과 같은 얘기로 보인다는 것이다. 다르다.

  • 도메인 모델 - 객체에 업무 규칙을 넣는다 (order.cancel())
  • Active Record - 객체에 저장 방법까지 넣는다 (order.save())

앞은 “취소 가능한 주문인가”를 아는 것이고, 뒤는 “어느 테이블 어느 칼럼에 넣는가”를 아는 것이다. 성격이 전혀 다른 두 지식이 한 클래스에 같이 사는 게 Active Record다.

테이블이 곧 모델이 된다

편리함에는 값이 붙어 있다. 그 값이 가장 먼저 드러나는 곳은 스키마와 도메인이 붙어버리는 것이다.

Active Record에서 클래스는 테이블을 그대로 비춘다. 칼럼을 추가하면 속성이 생기고, 칼럼 이름을 바꾸면 코드가 따라 바뀐다. 처음에는 이게 장점이다. 맞출 게 없으니까.

문제는 도메인이 테이블과 다른 모양이 되고 싶을 때 시작된다.

예를 들어 “주문은 항목 여러 개를 묶은 하나의 덩어리”라고 보고 싶다고 하자(Aggregate에서 본 그것이다). 그런데 테이블은 ordersorder_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이고, 그 앎이 편할 때와 발목을 잡을 때가 갈린다.

관련 글