Files
spring-boot-examples/docs/06_서비스 계층과 비즈니스 로직.md
2025-04-08 19:56:24 +09:00

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)); // 비즈니스 로직이 컨트롤러에 있음
    }
}

🚨 문제점:

  1. 컨트롤러가 너무 많은 책임을 가짐 → 가독성과 유지보수성이 떨어짐.
  2. 비즈니스 로직이 중복될 가능성 증가 → 여러 컨트롤러에서 같은 로직을 작성할 가능성이 큼.
  3. 테스트가 어려움 → 서비스 계층이 없으면 컨트롤러 단위 테스트가 복잡해짐.

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)을 서비스 계층에서 수행하여 데이터 무결성을 보장.
테스트가 용이하도록 서비스 계층을 단독으로 분리.

서비스 계층을 잘 활용하면, 더 유지보수하기 좋은 코드를 만들 수 있습니다.