[unispace] 프로젝트 회고 - 도메인 객체와 영속성 객체

Deeeep Breath·2024년 9월 20일

unispace

목록 보기
10/12

지난 7월부터 8월까지 스프링 프레임워크를 본격적으로 공부하면서 진행한 프로젝트를 회고해 보니, 기능 구현에만 집중하여 아키텍처 설계나 디자인 패턴에 무신경했다는 생각이 들었다.

특히 JPA를 이리저리 사용하면서 데이터 중심으로 사고하며 코드를 짰다는 점이 아쉬웠다.

프로젝트 이후 여러 책과 강의를 통해 객체지향 웹 어플리케이션이 추구하는 정답에 가까운 설계가 무엇인지 공부해보니, 내가 짰던 코드가 더 초라하고 정신없어보였다. (지금이라도 수정해보고 싶었지만 내가 짠 코드인데도 건드릴 엄두가 나지 않았다. 유지보수성을 고려해야 한다는 말을 절실히 깨닫는 순간이었다.)

도메인에 대해서

웹 개발 아키텍처와 객체지향 프로그래밍과 관련된 책을 보면 항상 등장하는 개념이 있었다. 도메인이란 무엇을 의미하는걸까 ?

소프트웨어는 사용자가 원하는 어떤 문제를 해결하기 위해 만들어진다. 이처럼 문제를 해결하기 위해 사용자가 프로그램을 사용하는 분야를 도메인이라고 부른다. (조영호, 오브젝트)

도메인은 사용자들이 겪는 문제 영역을 이야기한다. 이러한 문제 영역이 곧 비즈니스 영역을 의미한다. 요리사가 '배고프다'는 고객의 문제를 '음식'을 제공해 주어서 해결하듯이, 개발자는 고객이 겪는 문제상황을 '소프트웨어'로 해결해 주는 사람이다. (김우근, 자바/스프링 개발자를 위한 실용주의 프로그래밍)

즉 우리는 고객이 가지고 있는 문제 상황을 분석하고 이를 해결할 수 있는 소프트웨어적인 해결책을 제시하여야 한다. 소프트웨어 개발의 시작은 도메인을 분석하는 것으로부터 시작된다.

unispace 프로젝트에서의 문제점

사실 도메인에 대한 지식을 알음알음 알고 있었고 unispace 프로젝트를 진행할 때에도 나 나름대로 도메인을 분석하고 설계한다고 생각했다.

https://velog.io/@devtab/unispace-예약-시간-관련-요구사항-구현

위 게시글은 그때의 흔적이다. 당시 내가 가졌던 '도메인 설계' 에 대한 생각을 볼 수 있다.

게시글을 보면 요구사항을 분석하고 곧바로 요구사항을 구현하기 위한 JPA 엔티티를 설계한다.

JPA 엔티티는 필연적으로 관계형 데이터베이스를 고려할 수 밖에 없다. 도메인 객체가 가져야 하는 역할을 생각할 겨를도 없이 관계형 데이터베이스에 종속되는 프로그램을 개발한 것이다.

도메인 객체와 영속성 객체는 구분해야 할까?

도메인이 가지고 있는 특성과 요구사항을 제대로 이해하고 이를 기반으로 소프트웨어를 설계해야 한다는 도메인 주도 설계는 쉽게 이해하고 적용할 수 있는 개념은 아닌 것 같다.

도메인 주도 설계의 개념을 이해하고 도메인 객체를 중심으로 소프트웨어를 설계하려고 하다 보니, 도메인 모델과 영속성 객체를 구분해야 하는지에 대한 고민이 생겼다.

unispace 프로젝트에서 내가 작성했던 코드에서는 도메인 모델과 영속성 객체를 통합하여 하나의 클래스로 구현했다. 이 과정에서 JPA와 데이터베이스의 관계에 집중하다 보니 데이터 중심의 사고방식에 빠졌다.

이를 개선하기 위해서 자연스럽게 도메인 모델과 영속성 객체를 분리해야 한다는 생각이 들었는데 (김우근, 자바/스프링 개발자를 위한 실용주의 프로그래밍) 책에 두가지 방식에 대한 설명이 소개되어 이를 정리해보려고 한다.

통합하기 전략

unispace 프로젝트에서 사용했던 방식이다. 클레스 하나에 도메인 모델과 영속성 객체의 역할을 모두 몰아넣겠다는 의미다.

JPA의 역할이 ORM이라는 점을 상기했을 때 통합하기 전략을 사용하는 것은 매우 자연스러운 선택이다. ORM은 객체-관계 매핑의 줄임말 이므로 그 목적을 고려했을 때 도메인 모델과 영속성 객체를 분리하려는 시도는 잘못된 것일 수 있다. (김우근, 자바/스프링 개발자를 위한 실용주의 프로그래밍)

작은 규모의 프로젝트에서는 도메인 객체와 영속성 객체를 통합하는 것이 유리할 수 있다. 통합된 클래스는 간단한 구조를 유지하며 빠른 개발과 테스트가 가능하다. 또한 JPA에서 제공하는 여러가지 기능(변경 감지(dirty checking) 등)을 편리하게 사용할 수 있다.

@Entity
@Getter
@NoArgsConstructor
public class Room extends BaseEntity {
    @Id
    @GeneratedValue
    private Long id;

    private String name;
    
    public void changeName(String name) {
        this.name = name;
    }
}
    @Transactional
    public void updateRoomName(Long roomId, String newName) {
        // 1. 영속성 컨텍스트에서 Room 객체를 조회
        Room room = roomRepository.findById(roomId)
                .orElseThrow();

        // 2. Room 객체의 이름 변경 메서드 호출
        room.changeName(newName);

        // 더티 체킹으로 인해 변경된 내용은 자동으로 DB에 반영됨
    }

다만 이 전략을 사용할 경우 클래스의 책임이 제대로 눈에 들어오지 않는다는 단점이 있다. 또한 도메인 모델에 영속성과 관련된 코드가 같이 들어 있으므로 내가 그랬던 것 처럼 데이터베이스 위주의 사고를 하기 쉽다.

구분하기 전략

도메인 모델을 위한 클래스와 영속성 객체를 위한 클래스를 분리하는 전략이다.

@Builder
public class Room {

    private Long id;
    private String name;

    public void changeName(String name) {
        this.name = name;
    }
}

도메인 객체 Room에는 도메인에 요구되는 요구사항인 changeName메서드가 위치한다.

@Data
@Entity(name = "room")
public class RoomJpaEntity {

    @Id
    @GeneratedValue
    private Long id;
    
	@Column
    private String name;

    // 도메인 객체로 변환하는 메서드
    public Room toModel() {
        return Room.builder()
            .id(this.id)
            .name(this.name)
            .build();
    }

    // 도메인 객체를 영속성 객체로 변환하는 정적 메서드
    public static RoomJpaEntity from(Room room) {
        RoomJpaEntity entity = new RoomJpaEntity();
        entity.name = room.getName();
        return entity;
    }
}

영속성 객체 RoomJpaEntity에는 영속성 객체를 도메인 모델로 변환하는 메서드와 반대로 도메인 모델을 영속성 객체로 변환하는 메서드가 포함된다.

이 방법을 사용하면 JPA에서 제공하는 기능 사용이 제한된다.

public void updateName(Long roomId, String newName) {
        // 기존 영속성 객체 가져오기
        RoomJpaEntity existingRoomJpaEntity = 
        entityManager.find(RoomJpaEntity.class, roomId);
        
        // 기존 영속성 객체를 도메인 모델로 변환
        Room room = existingRoomJpaEntity.toModel();
        
        // 도메인 모델에서 닉네임 변경
        room.changeName(newName);

        // 새로운 영속성 객체 생성
        RoomJpaEntity newRoomJpaEntity = RoomJpaEntity.from(room);
        
        // 기존 객체를 준영속 상태로 변경
        entityManager.detach(existingRoomJpaEntity);
        
        // 새로운 객체를 영속성 컨텍스트에 등록
        entityManager.merge(newRoomJpaEntity);
    }

더티 체킹 기능을 사용할 수 없기 때문에 위와 같은 방식으로 영속성 객체의 상태를 업데이트해야 한다.

언듯 불편해 보이지만 사실 이것이 헥사고날 아키텍처와 의존성 역전 원칙이 추구하는 방향이라고 한다. 두 이론 모두 시스템이 특정 라이브러리에 종속되지 않으려면 어떻게 해야 하는지를 이야기하고 있다.

JPA의 더티 체킹은 간편하게 영속성 상태를 관리해 주지만 반대로 이야기하면 애플리케이션이 JPA라는 라이브러리에 강결합 되어 있다는 것을 의미한다.

구분하기 전략은 애플리케이션이 특정 라이브러리와 데이터베이스에 의존하지 않도록 해준다.

또한 통합 전략에서의 Room 객체는 도메인 모델과 영속성 객체의 역할이 통합된 객체이기 때문에 단일 책임 원칙에도 위배된다.

결론

두 전략 중 '객체지향'에 가까운 전략은 구분하기 전략임이 확실해 보인다. 그러나 설계에서의 선택은 항상 트레이드오프가 존재한다.

구분하기 전략은 객체지향 원칙에 더 부합하며 장기적으로 시스템의 유지보수성과 확장성을 높이는 데 기여할 수 있다. 반면 통합하기 전략은 초기 개발의 편리함과 속도를 제공한다.

전략장점단점
통합하기 전략- 편리한 사용: JPA의 더티 체킹과 변경 감지 기능을 그대로 활용하여 코드의 양이 줄어듦.- 단일 책임 원칙 위배: 도메인 모델과 영속성 객체의 역할이 혼합되어 각 클래스의 책임이 불명확해짐.
- 신속한 개발과 테스트: 단일 클래스를 사용하여 초기 개발 속도가 빨라짐.- 데이터 중심 사고: 데이터베이스에 종속적인 사고 방식으로 이어질 수 있음.
- 직관적인 데이터 처리: 비즈니스 로직과 데이터 저장 로직이 통합되어 데이터 상태 변경이 용이함.- 유지 보수성 저하: 코드가 복잡해질수록 유지 보수가 어려워질 수 있음.
구분하기 전략- 단일 책임 원칙 준수: 도메인 객체와 영속성 객체가 분리되어 각 클래스의 책임이 명확해짐.- 기능 사용 제한: JPA의 더티 체킹과 같은 기능을 사용할 수 없어 추가적인 코드가 필요함.
- 명확한 비즈니스 로직: 도메인 객체에 비즈니스 로직이 집중되어 이해하기 쉬움.- 초기 개발 지연: 초기 개발에서 더 많은 코드와 시간이 소요될 수 있음.
- 의존성 역전 원칙 준수: 애플리케이션이 특정 라이브러리에 강하게 결합되지 않도록 함.- 변환 로직 추가 필요: 도메인 객체와 영속성 객체 간의 변환 로직을 작성해야 함.

(표 작성 : GPT-4o-mini)

출처 & 참고자료

김우근, 『자바/스프링 개발자를 위한 실용주의 프로그래밍』, 위키북스(2024),
조영호, 『오브젝트』, 위키북스(2019),
OpenAI. (2024). ChatGPT-4o-mini (24.07.18 version)

profile
안녕하세요!

0개의 댓글