Service란 무엇인가?
Service는 Spring Boot 어플리케이션에서 비즈니스 로직을 담당하는 레이어이다.
Controller와 Repository를 연결해주는 중간 다리 역할이라고 이해하면 된다.
주로 어플리케이션을 구성하는 비즈니스 로직을 바로 이 Service 레이어에서 구현하게 된다.
비즈니스 로직이 Controller에 포함되지 않도록 분리하여 코드의 가독성과 유지보수성을 높인다.
또한 데이터를 처리하는 과정과 Client 요청 처리 과정을 분리하여 역할을 명확히 나누어야 한다.
Service 코드 작성법
@Service
@Transactional
@RequiredArgsConstructor
public class UserServiceImpl implements UserService {
private final UserRepository userRepository;
@Override
public Long registerUser(UserDto userDto) {
User user = userDto.toEntity();
User savedUser = userRepository.save(user);
return savedUser.getId();
}
@Override
public Long deleteUser(Long userId) {
User user = userRepository.findById(userId).orElseThrow(() -> new EntityNotFoundException("User not found with id: " + userId));
userRepository.delete(user);
return user.getId();
}
}
서비스는 동일한 Entity에 대한 비즈니스 로직을 모아둔 클래스라고 생각하면 된다.
따라서 대게 주요 Entity에 대응되는 Service 클래스를 생성하여 구현한다. 본인은 interface를 따로 두고 Impl class를 생성하여 오버라이드 메소드로 구현한다.
Entity나 Repository와 마찬가지로 @Service 어노테이션을 Impl 클래스에 붙여주면 된다.
또한 서비스 로직 내에서 접근할 Entity에 대한 Repository를 private final로 명시해주고 생성자에서 할당해주면 된다.
@RequiredArgsConstructor 어노테이션을 사용하면 자동으로 Repository가 할당되어 편리하다.
서비스의 역할은 크게 2개로 나눌 수 있다.
1. 데이터 처리와 검증
public User createUser(UserDto userDto) {
// 데이터 검증
if (userRepository.existsByEmail(userDto.getEmail())) {
throw new IllegalArgumentException("Email already exists");
}
// Entity 변환 및 저장
User user = new User(userDto.getName(), userDto.getEmail());
return userRepository.save(user);
}
2. 복잡한 비즈니스 로직 구현
public Order createOrder(Long userId, OrderDto orderDto) {
User user = userRepository.findById(userId)
.orElseThrow(() -> new EntityNotFoundException("User not found"));
Order order = new Order(user, orderDto.getItems(), orderDto.getTotalPrice());
return orderRepository.save(order);
}
@Transactional
@Transactional은 데이터베이스 트랜잭션을 관리하기 위해 사용하는 Spring의 어노테이션이다. ACID 원칙을 준수하는 작업을 보장한다.
- Atomicity (원자성): 작업이 모두 성공하거나 모두 실패하도록 보장한다.
- Consistency (일관성): 트랜잭션 전후에 데이터베이스가 항상 일관된 상태를 유지한다.
- Isolation (격리성): 여러 트랜잭션이 동시에 실행될 때 서로 간섭하지 않도록 보장한다.
- Durability (지속성): 트랜잭션이 커밋되면 변경 사항이 영구적으로 저장된다.
주로 클래스 level에 @Transactional을 적용하면 내부 구현 메소드에도 모두 적용된다. 또한 읽기 전용 Transactional을 따로 선언할 수도 있다.
@Transactional 사용시 주의해야할 점은 다음과 같다.
- Lazy Loading: 트랜잭션 내에서 Lazy 로딩된 데이터에 접근해야 한다. 트랜잭션이 종료되면 영속성 컨텍스트가 사라지기 때문이다.
- Checked Exception: Spring은 기본적으로 Unchecked Exception에서만 롤백한다. 따라서 rollbackFor를 사용하여 롤백대상이 되는 Exception을 명시해주어야 한다.
예시는 다음과 같다.
@Transactional(rollbackFor = Exception.class)
public void transferFunds(Long fromUserId, Long toUserId, BigDecimal amount) {
User fromUser = userRepository.findById(fromUserId)
.orElseThrow(() -> new EntityNotFoundException("Sender not found"));
User toUser = userRepository.findById(toUserId)
.orElseThrow(() -> new EntityNotFoundException("Receiver not found"));
fromUser.decreaseBalance(amount);
toUser.increaseBalance(amount);
userRepository.save(fromUser);
userRepository.save(toUser);
}
DTO class 사용 이유
서비스 로직 내에서는 Entitc 오브젝트를 그대로 사용하는 것이 아니라 DTO(Data Transfer Object)로 변환하여 사용한다.
그 이유는 Entity를 직접 외부로 노출하면 데이터베이스 구조나 비즈니스 로직이 노출될 수 있고, 불필요하거나 민감한 데이터까지 함께 클라이언트 단에 전달될 수 있기 때문이다. 따라서 Entity는 데이터베이스 매핑, DTO는 데이터 전송을 담당하여 역할이 분리된다고 생각하면 된다.
DTO 정의는 편하게 class 정의라고 생각하면 된다.
@Getter
@Setter
public class UserDto {
private String name;
private String email;
}
Entity와 DTO간 변환 방법은 DTO 내부에 변환 메소드를 정의하거나 ModelMapper를 사용하면 된다.
본인은 주로 변환 메소드를 정의하여 필요한 정보만 가져오는 방식으로 정의한다.
---수동 변환 방식
public UserDto toDto(User user) {
UserDto dto = new UserDto();
dto.setName(user.getName());
dto.setEmail(user.getEmail());
return dto;
}
public User toEntity(UserDto dto) {
return new User(dto.getName(), dto.getEmail());
}
---Model Mapper
ModelMapper modelMapper = new ModelMapper();
UserDto dto = modelMapper.map(user, UserDto.class);