6.9 KiB
서비스 계층과 비즈니스 로직
Spring Boot 애플리케이션을 설계할 때, 코드의 재사용성, 유지보수성, 테스트 용이성을 높이려면 비즈니스 로직을 서비스 계층(Service Layer)에 분리하는 것이 중요합니다.
이 글에서는 서비스 계층의 역할, 설계 방식, 그리고 예제 코드를 통해 비즈니스 로직을 어떻게 다루어야 하는지 설명합니다.
1. 서비스 계층이란?
Spring Boot 애플리케이션은 일반적으로 MVC (Model-View-Controller) 구조를 따릅니다.
이때 서비스 계층은 컨트롤러와 데이터 계층(Repository) 사이에서 비즈니스 로직을 처리하는 역할을 합니다.
📌 계층별 역할
| 계층 | 역할 |
|---|---|
| Controller (컨트롤러 계층) | 사용자 요청을 받아 서비스 계층에 전달 |
| Service (서비스 계층) | 비즈니스 로직을 처리하고 트랜잭션을 관리 |
| Repository (데이터 계층) | 데이터베이스와 직접적인 통신을 담당 |
2. 서비스 계층의 필요성
📌 컨트롤러에서 직접 로직을 처리하는 문제점
@RestController
@RequestMapping("/users")
public class UserController {
@Autowired
private UserRepository userRepository; // 데이터 계층 직접 접근
@PostMapping
public ResponseEntity<User> createUser(@RequestBody User user) {
if (user.getName() == null || user.getAge() < 0) {
throw new IllegalArgumentException("Invalid user data");
}
return ResponseEntity.ok(userRepository.save(user)); // 비즈니스 로직이 컨트롤러에 있음
}
}
🚨 문제점:
- 컨트롤러가 너무 많은 책임을 가짐 → 가독성과 유지보수성이 떨어짐.
- 비즈니스 로직이 중복될 가능성 증가 → 여러 컨트롤러에서 같은 로직을 작성할 가능성이 큼.
- 테스트가 어려움 → 서비스 계층이 없으면 컨트롤러 단위 테스트가 복잡해짐.
3. 서비스 계층을 활용한 개선된 설계
📌 1) Entity (데이터 모델)
@Entity
public class User {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
private String name;
private int age;
// 기본 생성자 및 getter, setter
public User() {}
public User(String name, int age) {
this.name = name;
this.age = age;
}
public String getName() { return name; }
public int getAge() { return age; }
}
📌 2) Repository (데이터 계층)
@Repository
public interface UserRepository extends JpaRepository<User, Long> {
}
설명:
JpaRepository<User, Long>→User엔티티를 다루는 JPA 리포지토리.findById(id),save(entity),deleteById(id)등 기본적인 DB 연산 제공.
📌 3) Service (비즈니스 로직 계층)
@Service
public class UserService {
@Autowired
private UserRepository userRepository;
@Transactional
public User createUser(User user) {
if (user.getName() == null || user.getAge() < 0) {
throw new IllegalArgumentException("Invalid user data");
}
return userRepository.save(user);
}
public User getUserById(Long id) {
return userRepository.findById(id)
.orElseThrow(() -> new RuntimeException("User not found"));
}
}
🚀 개선된 점:
✔ 컨트롤러에서 비즈니스 로직이 제거됨 → 역할이 명확해짐.
✔ 비즈니스 로직이 서비스 계층에 집중됨 → 재사용성 증가.
✔ 트랜잭션 관리 가능 → @Transactional을 활용하여 데이터 일관성 유지.
📌 4) Controller (컨트롤러 계층)
@RestController
@RequestMapping("/users")
public class UserController {
@Autowired
private UserService userService;
@PostMapping
public ResponseEntity<User> createUser(@RequestBody User user) {
return ResponseEntity.ok(userService.createUser(user));
}
@GetMapping("/{id}")
public ResponseEntity<User> getUser(@PathVariable Long id) {
return ResponseEntity.ok(userService.getUserById(id));
}
}
🚀 컨트롤러가 깔끔해짐!
✔ 비즈니스 로직을 서비스 계층에 위임.
✔ 컨트롤러는 단순히 요청을 받고 응답을 반환하는 역할만 수행.
4. 서비스 계층에서 트랜잭션 관리
📌 트랜잭션 (@Transactional)이란?
- 여러 개의 DB 연산을 하나의 단위로 묶어 **"모두 성공하거나, 하나라도 실패하면 롤백"**하도록 보장.
@Transactional을 붙이면, 예외가 발생할 경우 자동으로 롤백됨.
📌 예제: 트랜잭션을 적용한 서비스
@Service
public class UserService {
@Autowired
private UserRepository userRepository;
@Transactional
public void registerUsers(List<User> users) {
for (User user : users) {
if (user.getAge() < 0) {
throw new RuntimeException("Invalid age");
}
userRepository.save(user);
}
}
}
🚀 특징:
- 만약
user.getAge() < 0인 사용자가 있다면, 이전까지 저장된 사용자도 롤백됨. - 데이터 무결성을 보장할 수 있음.
5. 서비스 계층을 활용한 테스트
📌 JUnit을 활용한 서비스 계층 테스트
@SpringBootTest
@RunWith(SpringRunner.class)
public class UserServiceTest {
@Autowired
private UserService userService;
@Test
public void testCreateUser() {
User user = new User("Alice", 25);
User savedUser = userService.createUser(user);
assertNotNull(savedUser);
assertEquals("Alice", savedUser.getName());
}
}
🚀 서비스 계층이 분리되어 있어, 테스트가 쉬워짐!
✔ UserService만 단독으로 테스트 가능.
✔ UserRepository가 직접 드러나지 않으므로, Mocking(가짜 데이터)도 쉽게 적용 가능.
6. 정리
| 계층 | 설명 |
|---|---|
| Controller | 요청을 받고 응답을 반환 (비즈니스 로직 X) |
| Service | 비즈니스 로직 처리 및 트랜잭션 관리 |
| Repository | 데이터베이스와 직접 통신 |
✅ 좋은 서비스 계층 설계 원칙
✔ 컨트롤러는 서비스에 로직을 위임하고, 직접 처리하지 않음.
✔ 비즈니스 로직은 서비스 계층에서 일괄적으로 관리하여 재사용성을 높임.
✔ 트랜잭션 관리 (@Transactional)을 서비스 계층에서 수행하여 데이터 무결성을 보장.
✔ 테스트가 용이하도록 서비스 계층을 단독으로 분리.
서비스 계층을 잘 활용하면, 더 유지보수하기 좋은 코드를 만들 수 있습니다.