Java Service와 Repository 역할 차이: 초보자를 위한 완벽 가이드

Java Service와 Repository, 왜 구분해야 할까요?

Java로 개발을 하다 보면 ‘Service’와 ‘Repository’라는 용어를 자주 접하게 됩니다. 처음에는 둘 다 데이터베이스와 관련 있거나 비즈니스 로직을 처리하는 것처럼 느껴져 혼동하기 쉽습니다. 하지만 이 둘은 애플리케이션의 구조와 유지보수성을 결정하는 매우 중요한 역할을 담당합니다. 명확한 역할 분담은 코드의 재사용성을 높이고, 테스트를 용이하게 하며, 개발팀 간의 협업을 원활하게 만듭니다.

간단히 말해, Service 계층은 비즈니스 로직을 담당하고, Repository 계층은 데이터 영속성(Persistence)을 담당합니다.

이 글에서는 Java에서 Service와 Repository의 역할 차이를 명확히 이해하고, 각 계층이 왜 필요한지, 어떻게 구현해야 하는지 상세하게 알아보겠습니다.

Service 계층: 핵심 비즈니스 로직

Service 계층은 애플리케이션의 핵심 비즈니스 로직을 담고 있는 곳입니다. 사용자의 요청을 받아 여러 단계의 처리를 거쳐 최종 결과를 반환하는 역할을 합니다. 이 과정에서 Service는 여러 Repository를 사용하거나, 다른 Service를 호출하여 복잡한 비즈니스 규칙을 수행할 수 있습니다.

Service의 주요 역할

  1. 비즈니스 로직 구현: 사용자의 요구사항을 충족시키기 위한 핵심 로직을 구현합니다. 예를 들어, ‘상품 주문’이라는 기능이 있다면, 주문 정보를 검증하고, 재고를 확인하며, 결제를 처리하고, 주문 내역을 저장하는 모든 과정이 Service 계층에서 이루어집니다.
  2. 트랜잭션 관리: 데이터의 일관성을 유지하기 위해 여러 데이터베이스 작업을 하나의 단위로 묶어 처리하는 트랜잭션을 관리합니다. 예를 들어, 주문 시 상품 재고를 줄이고 주문 정보를 저장하는 두 가지 작업이 있는데, 둘 중 하나라도 실패하면 이전 작업도 모두 취소하여 데이터 불일치를 막아야 합니다. 이러한 트랜잭션 관리는 주로 Service 계층에서 @Transactional과 같은 어노테이션을 통해 이루어집니다. => 트랜잭션 관리도 굉장히 중요한 부분입니다. 추후 더 자세히 다루어보겠습니다.
  3. 데이터 접근 추상화: Service 계층은 직접적으로 데이터베이스와 상호작용하지 않습니다. 대신 Repository 계층에 데이터 조회를 요청하고, 받은 데이터를 가공하여 최종 결과를 만듭니다. 이를 통해 Service 계층은 데이터베이스 종류나 접근 방식에 상관없이 비즈니스 로직에만 집중할 수 있습니다.
  4. 다른 Service와의 연동: 하나의 복잡한 비즈니스 로직을 구현하기 위해 다른 Service의 기능을 호출할 수도 있습니다. 예를 들어, ‘회원 가입’ Service는 ‘이메일 발송’ Service를 호출하여 환영 이메일을 보낼 수 있습니다.

Service 계층의 특징

  • 비즈니스 규칙: Service는 특정 비즈니스 규칙이나 정책을 적용하는 곳입니다.
  • 복잡성: 여러 단계를 거치거나 여러 데이터를 조합해야 하는 복잡한 로직이 포함될 수 있습니다.
  • 책임: 특정 기능(예: 주문 처리, 회원 관리)에 대한 책임을 가집니다.
  • 테스트 용이성: 외부 의존성(데이터베이스 등)을 최소화하고 순수 Java 로직으로 구성하면 단위 테스트가 쉽습니다.

Service 구현 예시 (Spring Framework 기반)

// UserService.java

@Service // Spring에게 이 클래스가 Service임을 알립니다.

public class UserService {

private final UserRepository userRepository; // Repository 주입

private final EmailService emailService; // 다른 Service 주입

// 생성자 주입 (Dependency Injection)

public UserService(UserRepository userRepository, EmailService emailService) {

this.userRepository = userRepository;

this.emailService = emailService;

}

@Transactional // 트랜잭션 관리

public User createUser(User user) {

// 1. 비즈니스 로직: 이메일 중복 확인 (Repository 사용)

if (userRepository.existsByEmail(user.getEmail())) {

throw new IllegalArgumentException("이미 사용 중인 이메일입니다.");

}

// 2. 데이터 저장 (Repository 사용)

User savedUser = userRepository.save(user);

// 3. 다른 Service 호출: 환영 이메일 발송

emailService.sendWelcomeEmail(savedUser.getEmail());

return savedUser;

}

public User findUserById(Long id) {

// Repository를 통해 사용자 조회

return userRepository.findById(id)

.orElseThrow(() -> new IllegalArgumentException("사용자를 찾을 수 없습니다."));

}

}

위 예시에서 UserService는 사용자 생성이라는 비즈니스 로직을 담당합니다. UserRepository를 사용하여 이메일 중복을 확인하고 사용자를 저장하며, EmailService를 호출하여 환영 이메일을 보냅니다. @Transactional 어노테이션은 이 메서드 내의 모든 데이터베이스 작업이 하나의 트랜잭션으로 처리되도록 보장합니다.

Repository 계층: 데이터베이스와 상호작용

Repository 계층은 데이터베이스와의 모든 상호작용을 담당합니다. 데이터를 저장(Create), 조회(Read), 수정(Update), 삭제(Delete)하는 CRUD 작업을 수행하며, Service 계층에는 데이터 접근에 필요한 인터페이스만을 제공합니다. 마치 도서관의 사서처럼, 사용자가 원하는 책(데이터)을 찾아주고, 책을 정리하고, 새로운 책을 보관하는 역할을 합니다.

Repository의 주요 역할

  1. 데이터 접근: 데이터베이스, 파일 시스템, 외부 API 등 다양한 저장소에서 데이터를 읽어오거나 저장하는 역할을 합니다.
  2. CRUD 작업 수행: 기본적인 데이터 조작(Create, Read, Update, Delete) 메서드를 제공합니다.
  3. 데이터 매핑: 애플리케이션 객체와 데이터베이스 테이블 간의 변환(매핑)을 담당합니다. (ORM 프레임워크 사용 시)
  4. 데이터 접근 로직 캡슐화: Service 계층은 Repository의 구체적인 데이터 접근 방식(SQL 쿼리, JPA 엔티티 등)을 알 필요 없이, 제공되는 메서드만 호출하면 됩니다.

Repository 계층의 특징

  • 데이터 중심: 데이터베이스 스키마와 직접적으로 연관됩니다.
  • 단순성: 주로 데이터 조회, 저장, 수정, 삭제와 같은 기본적인 작업만 수행합니다.
  • 책임: 특정 엔티티(예: User, Product)에 대한 데이터 접근 책임을 가집니다.
  • 기술 의존성: JPA, MyBatis, JDBC 등 데이터 접근 기술에 대한 의존성을 가집니다.

Repository 구현 예시 (Spring Data JPA 기반)

// UserRepository.java

@Repository // Spring에게 이 클래스가 Repository임을 알립니다.

public interface UserRepository extends JpaRepository<User, Long> { // JpaRepository 상속

// JpaRepository가 제공하는 기본 CRUD 메서드 외에

// 사용자 정의 쿼리 메서드를 추가할 수 있습니다.

// 이메일로 사용자 존재 여부 확인

boolean existsByEmail(String email);

// 이메일로 사용자 조회

Optional<User> findByEmail(String email);

// 이름으로 사용자 목록 조회 (Spring Data JPA가 자동으로 쿼리 생성)

List<User> findByNameContaining(String name);

}

위 예시에서 UserRepositoryJpaRepository 인터페이스를 상속받아 기본적인 CRUD 기능을 자동으로 얻습니다. existsByEmail이나 findByEmail과 같은 메서드는 Spring Data JPA가 메서드 이름을 분석하여 자동으로 SQL 쿼리를 생성해줍니다. User 엔티티와 데이터베이스 테이블 간의 매핑은 @Entity 어노테이션 등으로 이미 정의되어 있다고 가정합니다.

Service와 Repository, 어떻게 협력할까요?

Service와 Repository는 계층형 아키텍처에서 서로 다른 역할을 수행하지만, 긴밀하게 협력하여 전체 애플리케이션을 구성합니다.

  1. Service가 Repository를 호출합니다.
  2. Service는 비즈니스 로직을 수행하기 위해 데이터가 필요할 때 Repository에게 데이터를 요청합니다.
  3. 예: userService.createUser(user) 메서드는 userRepository.save(user)를 호출하여 사용자 데이터를 저장합니다.
  4. Repository는 데이터를 Service에게 반환합니다.
  5. Repository는 데이터베이스에서 데이터를 조회하거나 조작한 결과를 Service에게 전달합니다.
  6. 예: userRepository.findById(id)Optional<User> 객체를 반환하고, Service는 이 객체를 받아 사용자 정보가 있는지 확인합니다.
  7. Service는 Repository에서 받은 데이터를 가공하여 최종 결과를 만듭니다.
  8. Service는 Repository로부터 받은 원시 데이터를 비즈니스 로직에 맞게 가공하거나, 여러 데이터를 조합하여 최종 응답을 준비합니다.

협력 흐름 예시: 사용자 정보 조회

  1. Controller (또는 다른 Service): 특정 사용자의 정보를 조회해야 한다고 UserService에게 요청합니다. (예: userService.getUserProfile(userId))
  2. UserService:
  3. @Transactional 어노테이션으로 트랜잭션을 시작합니다.
  4. userRepository.findById(userId)를 호출하여 데이터베이스에서 사용자 정보를 조회합니다.
  5. UserRepository는 데이터베이스에서 User 엔티티를 찾아 Optional<User> 형태로 UserService에게 반환합니다.
  6. UserService는 반환된 User 객체에서 필요한 정보(이름, 이메일 등)를 추출합니다.
  7. 필요하다면, orderRepository.findRecentOrdersByUser(userId) 등을 호출하여 주문 정보도 함께 조회합니다.
  8. 조회된 사용자 정보와 주문 정보를 조합하여 ‘사용자 프로필’ 객체를 만듭니다.
  9. 트랜잭션을 커밋하거나 롤백하고, 최종적으로 ‘사용자 프로필’ 객체를 반환합니다.
  10. Controller: UserService로부터 받은 ‘사용자 프로필’ 객체를 클라이언트에게 응답으로 보냅니다.

이처럼 Service는 비즈니스 로직의 ‘의사 결정자’ 역할을 하고, Repository는 ‘데이터 실행자’ 역할을 하며 서로 간의 책임을 명확히 분담합니다.

흔한 실수와 주의사항

Service와 Repository의 역할을 명확히 구분하지 않으면 다음과 같은 문제들이 발생할 수 있습니다.

  1. Service 안에 데이터베이스 쿼리 작성: Service 계층에 직접 SQL 쿼리나 JPA 쿼리를 작성하면, 비즈니스 로직과 데이터 접근 로직이 뒤섞여 코드가 복잡해지고 유지보수가 어려워집니다. 또한, 데이터 접근 방식이 변경될 때 Service 코드 전체를 수정해야 할 수 있습니다.
  2. Repository 안에 비즈니스 로직 포함: Repository는 순수하게 데이터 접근만을 담당해야 합니다. 만약 Repository에 복잡한 비즈니스 규칙을 넣으면, 해당 로직이 다른 곳에서 재사용되기 어렵고 테스트하기 힘들어집니다.
  3. 트랜잭션 관리의 모호성: 트랜잭션 관리는 비즈니스 로직의 일관성을 보장하는 중요한 부분입니다. Service 계층에서 @Transactional을 사용하여 트랜잭션을 관리하는 것이 일반적입니다. Repository에 트랜잭션 관련 코드를 넣으면 혼란을 야기할 수 있습니다.
  4. 과도한 Repository 호출: Service 메서드 하나에서 너무 많은 Repository 메서드를 호출하면, 해당 Service 메서드의 실행 시간이 길어지고 성능 문제가 발생할 수 있습니다. 또한, 트랜잭션 관리도 복잡해집니다. 필요한 데이터만 효율적으로 가져오도록 Repository 메서드를 설계해야 합니다.
  5. DTO (Data Transfer Object) 활용 부족: Service와 Repository 간, 또는 계층 간 데이터 전달 시 DTO를 사용하면 데이터의 형태를 명확히 하고 불필요한 데이터 노출을 막을 수 있습니다. 예를 들어, Repository는 엔티티 객체를 반환하지만, Service는 이를 가공하여 클라이언트에게 필요한 정보만 담은 DTO를 만들어 반환할 수 있습니다.

Service vs Repository: 핵심 차이점 요약

| 구분 | Service | Repository |

| :————- | :———————————— | :——————————————- |

| 주요 역할 | 비즈니스 로직 구현, 트랜잭션 관리 | 데이터 영속성(CRUD) 관리, 데이터 접근 |

| 주요 관심사| “무엇을” 해야 하는가 (업무 규칙) | “어떻게” 데이터를 가져오고 저장할 것인가 |

| 의존성 | Repository, 다른 Service, 외부 API | 데이터베이스 드라이버, ORM 프레임워크, SQL |

| 책임 | 특정 기능(기능 단위) | 특정 엔티티(데이터 단위) |

| 테스트 | 비즈니스 로직 단위 테스트 (Mocking) | 데이터 접근 로직 테스트 (In-memory DB, 실제 DB) |

| 예시 | 주문 처리, 회원 가입, 상품 검색 | 사용자 정보 저장/조회, 상품 목록 조회 |

| 주요 키워드| @Service, @Transactional | @Repository, JpaRepository, DAO |

결론: 역할 분담으로 더 나은 Java 애플리케이션 만들기

Java 애플리케이션 개발에서 Service와 Repository의 역할을 명확히 구분하는 것은 매우 중요합니다.

  • Service 계층은 애플리케이션의 핵심 비즈니스 로직을 담당하며, 트랜잭션 관리 및 다른 Service와의 연동을 통해 복잡한 업무 흐름을 처리합니다.
  • Repository 계층은 데이터베이스와의 모든 상호작용을 담당하며, 데이터의 영속성을 보장하고 Service 계층에는 데이터 접근에 필요한 인터페이스만을 제공합니다.

이 두 계층의 명확한 역할 분담은 다음과 같은 이점을 가져다줍니다.

  • 코드의 가독성 및 유지보수성 향상: 각 계층의 책임이 명확해져 코드를 이해하고 수정하기 쉬워집니다.
  • 테스트 용이성 증대: Service 계층은 Repository를 Mocking하여 비즈니스 로직만 테스트할 수 있고, Repository는 데이터 접근 로직만 집중적으로 테스트할 수 있습니다.
  • 재사용성 증가: 독립적인 Service나 Repository는 다른 모듈이나 애플리케이션에서 재사용될 가능성이 높아집니다.
  • 확장성 및 유연성 확보: 데이터베이스 기술이 변경되더라도 Repository 계층만 수정하면 되므로, 전체 애플리케이션에 미치는 영향을 최소화할 수 있습니다.

처음에는 혼란스러울 수 있지만, Service와 Repository의 역할을 정확히 이해하고 적용하는 것은 견고하고 확장 가능한 Java 애플리케이션을 구축하기 위한 필수적인 과정입니다.

만약 이해가 잘 되지 않는 다면 이것만은 꼭 기억해보세요

service: 핵심 비즈니스로직 담당

repository: 데이터베이스와 상호작용 담당

저는 처음 이 구조에 대해 배웠을 때 개념이 이해가 되지 않아 이렇게 두개만 생각하며 코드를 구현했습니다.

어려우시더라도 각 계층의 역할을 명확히 구분하여 코드를 작성하고 리팩토링하는 습관을 들이시길 바랍니다.

해당 개념들은 추후 실무에서 매우 많은 도움이 될 것이라 확신합니다.

적용 방법 제안

  1. 기존 코드 검토: 현재 작성 중인 Java 프로젝트에서 Service와 Repository의 역할 분담이 잘 이루어지고 있는지 검토해보세요. Service에 데이터 접근 로직이 섞여 있거나, Repository에 비즈니스 로직이 포함된 부분은 없는지 확인합니다.
  2. 리팩토링 계획 수립: 역할 분담이 명확하지 않은 부분은 리팩토링 계획을 세워 점진적으로 수정해 나갑니다. 작은 단위부터 시작하여 점차 전체 구조를 개선하는 것이 좋습니다.
  3. 팀원들과의 공유: Service와 Repository의 역할에 대한 팀원들의 이해도를 높이기 위해 이 글이나 관련 자료를 공유하고 논의하는 시간을 가지세요.

댓글 달기

이메일 주소는 공개되지 않습니다. 필수 필드는 *로 표시됩니다

위로 스크롤