프로그래밍 언어/java

[Java] 도메인 계층 vs 서비스 계층

하루이2222 2024. 8. 24. 20:58

도메인 계층 vs. 서비스 계층: 역할과 책임 명확히 구분하기

객체 지향 설계를 하거나 클린 아키텍처를 적용할 때, 많은 개발자가 "이 비즈니스 로직은 어디에 두어야 할까?"라는 고민에 빠지곤 합니다. 특히 **도메인 계층(Domain Layer)**과 **서비스 계층(Service Layer)**의 역할을 명확히 구분하는 것은 견고하고 유지보수하기 좋은 애플리케이션을 만드는 핵심입니다.

오늘은 이 두 계층의 역할과 책임을 실제 코드 예시를 통해 명확하게 알아보겠습니다.

1. 핵심 비즈니스의 심장, 도메인 계층 (Domain Layer)

도메인 계층은 애플리케이션의 가장 핵심적인 부분입니다. 비즈니스의 규칙과 상태, 그리고 그에 따른 로직이 모두 이곳에 정의됩니다.

  • 역할: 애플리케이션의 핵심 비즈니스 규칙과 로직을 캡슐화합니다.
  • 관심사: 데이터와 그 데이터를 조작하는 비즈니스 로직을 하나로 묶은 도메인 객체를 관리합니다. 외부의 다른 계층에 의존하지 않는 순수한 비즈니스 그 자체를 표현합니다.

예시 코드: User 도메인 객체

사용자(User)가 자신의 비밀번호를 변경하는 상황을 생각해 봅시다. 비밀번호 변경의 조건(기존 비밀번호가 일치해야 함)은 User 객체 스스로가 책임져야 할 고유한 비즈니스 규칙입니다.

public class User {
    private String username;
    private String password;

    public User(String username, String password) {
        this.username = username;
        this.password = password;
    }

    /**
     * 비밀번호 변경이라는 핵심 비즈니스 로직을 수행합니다.
     * 이 로직은 User 객체의 상태를 직접 변경합니다.
     */
    public void changePassword(String oldPassword, String newPassword) {
        if (!this.password.equals(oldPassword)) {
            throw new IllegalArgumentException("기존 비밀번호가 일치하지 않습니다.");
        }
        if (newPassword == null || newPassword.isEmpty()) {
            throw new IllegalArgumentException("새로운 비밀번호는 비어 있을 수 없습니다.");
        }
        this.password = newPassword;
    }

    // Getter 및 기타 메소드
}

여기서 changePassword 메소드는 비밀번호 변경이라는 핵심 비즈니스 규칙을 온전히 책임집니다. 이 로직은 User 도메인 객체의 일부이며, 다른 어떤 계층도 이 규칙에 직접 관여해서는 안 됩니다.


2. 비즈니스 흐름을 지휘하는 오케스트라, 서비스 계층 (Service Layer)

서비스 계층은 도메인 계층에 정의된 핵심 로직들을 엮어 실제 사용 사례(Use Case)나 비즈니스 프로세스를 완성시키는 역할을 합니다.

  • 역할: **비즈니스 로직의 실행을 조율(Orchestrate)**하고, 여러 도메인 객체나 외부 서비스 간의 상호작용을 관리합니다.
  • 관심사: 트랜잭션 관리, 데이터 영속성 처리, 외부 API 호출 등 **애플리케이션의 특정 흐름(workflow)**을 담당합니다.

예시 코드: UserService

UserService는 사용자의 비밀번호 변경 요청을 처리하는 전체 흐름을 담당합니다.

public class UserService {
    private final UserRepository userRepository;

    public UserService(UserRepository userRepository) {
        this.userRepository = userRepository;
    }

    /**
     * '사용자 비밀번호 변경'이라는 사용 사례(Use Case)를 처리합니다.
     * 여러 컴포넌트(도메인, 리포지토리)의 흐름을 조율합니다.
     */
    public void changeUserPassword(String username, String oldPassword, String newPassword) {
        // 1. 사용자 조회 (데이터베이스 상호작용)
        User user = userRepository.findByUsername(username);
        if (user == null) {
            throw new IllegalArgumentException("사용자를 찾을 수 없습니다.");
        }

        // 2. 핵심 로직 위임 (도메인 객체의 비즈니스 로직 호출)
        user.changePassword(oldPassword, newPassword);

        // 3. 변경된 상태 저장 (데이터베이스 상호작용)
        userRepository.save(user);

        // 4. (필요 시) 변경 완료 이메일 발송 등 추가 작업
        // emailService.sendPasswordChangeNotification(user.getEmail());
    }
}

UserService의 역할을 단계별로 살펴보면 그 책임이 명확해집니다.

  1. 사용자 조회: 리포지토리를 통해 User 도메인 객체를 데이터베이스에서 가져옵니다.
  2. 핵심 로직 위임: User 객체의 changePassword 메소드를 호출하여 실제 비밀번호 변경 로직은 도메인 객체에게 맡깁니다. 서비스 계층은 비밀번호가 맞는지 직접 검사하지 않습니다.
  3. 데이터 저장: 로직이 성공적으로 수행되면, 변경된 User 객체를 다시 리포지토리를 통해 데이터베이스에 저장합니다.

한눈에 보는 요약

구분 도메인 계층 (Domain Layer) 서비스 계층 (Service Layer)
핵심 역할 **무엇(What)**을 할 것인가? (비즈니스 규칙 정의) 어떻게(How) 할 것인가? (프로세스 실행 및 조율)
관심사 도메인 객체의 상태와 핵심 로직, 비즈니스 규칙 애플리케이션의 흐름(workflow), 트랜잭션, 외부 연동
코드 예시 user.changePassword(...) userService.changeUserPassword(...)
주요 질문 "이 객체의 고유한 책임은 무엇인가?" "이 비즈니스 프로세스를 완료하려면 무엇을 해야 하는가?"

마치며

도메인 계층에 순수한 비즈니스 규칙을 모아두고, 서비스 계층에서 이를 조립하여 응용 프로그램을 만들면 각자의 책임이 명확해집니다. 이렇게 역할을 분리하면 코드가 훨씬 더 깔끔해지고, 테스트하기 쉬우며, 비즈니스 로직의 변경이 다른 부분에 미치는 영향을 최소화할 수 있습니다.