2025-04-08T19:56:24
This commit is contained in:
213
docs/06_서비스 계층과 비즈니스 로직.md
Normal file
213
docs/06_서비스 계층과 비즈니스 로직.md
Normal file
@@ -0,0 +1,213 @@
|
||||
# **서비스 계층과 비즈니스 로직**
|
||||
|
||||
Spring Boot 애플리케이션을 설계할 때, 코드의 **재사용성, 유지보수성, 테스트 용이성**을 높이려면 **비즈니스 로직을 서비스 계층(Service Layer)에 분리**하는 것이 중요합니다.
|
||||
이 글에서는 **서비스 계층의 역할, 설계 방식, 그리고 예제 코드**를 통해 비즈니스 로직을 어떻게 다루어야 하는지 설명합니다.
|
||||
|
||||
---
|
||||
|
||||
## **1. 서비스 계층이란?**
|
||||
Spring Boot 애플리케이션은 일반적으로 **MVC (Model-View-Controller) 구조**를 따릅니다.
|
||||
이때 **서비스 계층은 컨트롤러와 데이터 계층(Repository) 사이에서 비즈니스 로직을 처리하는 역할**을 합니다.
|
||||
|
||||
### **📌 계층별 역할**
|
||||
| 계층 | 역할 |
|
||||
|------|------|
|
||||
| **Controller (컨트롤러 계층)** | 사용자 요청을 받아 서비스 계층에 전달 |
|
||||
| **Service (서비스 계층)** | 비즈니스 로직을 처리하고 트랜잭션을 관리 |
|
||||
| **Repository (데이터 계층)** | 데이터베이스와 직접적인 통신을 담당 |
|
||||
|
||||
---
|
||||
|
||||
## **2. 서비스 계층의 필요성**
|
||||
|
||||
### **📌 컨트롤러에서 직접 로직을 처리하는 문제점**
|
||||
```java
|
||||
@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)); // 비즈니스 로직이 컨트롤러에 있음
|
||||
}
|
||||
}
|
||||
```
|
||||
**🚨 문제점:**
|
||||
1. **컨트롤러가 너무 많은 책임을 가짐** → 가독성과 유지보수성이 떨어짐.
|
||||
2. **비즈니스 로직이 중복될 가능성 증가** → 여러 컨트롤러에서 같은 로직을 작성할 가능성이 큼.
|
||||
3. **테스트가 어려움** → 서비스 계층이 없으면 컨트롤러 단위 테스트가 복잡해짐.
|
||||
|
||||
---
|
||||
|
||||
## **3. 서비스 계층을 활용한 개선된 설계**
|
||||
|
||||
### **📌 1) Entity (데이터 모델)**
|
||||
```java
|
||||
@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 (데이터 계층)**
|
||||
```java
|
||||
@Repository
|
||||
public interface UserRepository extends JpaRepository<User, Long> {
|
||||
}
|
||||
```
|
||||
**설명:**
|
||||
- `JpaRepository<User, Long>` → `User` 엔티티를 다루는 JPA 리포지토리.
|
||||
- `findById(id)`, `save(entity)`, `deleteById(id)` 등 기본적인 DB 연산 제공.
|
||||
|
||||
---
|
||||
|
||||
### **📌 3) Service (비즈니스 로직 계층)**
|
||||
```java
|
||||
@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 (컨트롤러 계층)**
|
||||
```java
|
||||
@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`을 붙이면, **예외가 발생할 경우 자동으로 롤백**됨.
|
||||
|
||||
### **📌 예제: 트랜잭션을 적용한 서비스**
|
||||
```java
|
||||
@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을 활용한 서비스 계층 테스트**
|
||||
```java
|
||||
@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`)을 서비스 계층에서 수행**하여 데이터 무결성을 보장.
|
||||
✔ **테스트가 용이하도록 서비스 계층을 단독으로 분리**.
|
||||
|
||||
서비스 계층을 잘 활용하면, **더 유지보수하기 좋은 코드**를 만들 수 있습니다.
|
||||
Reference in New Issue
Block a user