
지난 포스트에서 컨트롤러가 요청을 받아 서비스를 호출해 비즈니스 로직을 처리한다와 같이 설명했습니다.
물론 컨트롤러 내에 비즈니스 로직을 넣을 수도 있지만 이 경우 컨트롤러가 복잡해지고, 책임이 늘어나면서 MVC 패턴의 의미가 희석되고 유지보수가 어려워집니다.
즉, 서비스는 실제로 요청받은 일을 수행해서 결과를 반환하는 중요한 부분이 됩니다. 주로 사용자의 요청에 따라 수행하는 일은 DB에 접근해서 CRUD를 수행하고 그 결과를 반환해 다시 컨트롤러로 전달하게 됩니다.
클래스 레벨에 @Service 어노테이션을 붙임으로써 스프링에게 이 클래스가 서비스 클래스(비즈니스 로직)임을 알리고 스프링 컨테이너에 등록하게 됩니다.
다음과 같은 간단한 서비스(회원 정보: 닉네임 수정) 클래스를 선언했습니다.
@Service
public class MyService {
//리포지토리 의존성 주입
private final UserRepository userRepository;
@Autowired //생성자 주입
public MyService(UserRepository userRepository) {
this.userRepository = userRepository;
}
@Transactional //트랜잭션 관리
public void updateUsername(UpdateUsernameDto updateUsernameDto) {
User user = userRepository.findById(updateUsernameDto.getUserId());
/*
* 비즈니스 로직 코드
* ex) 변경 닉네임 중복 체크 등
*/
user.setUsername(updateUsernameDto.getNewUsername());
userRepository.save(user);
}
}
의존 관계 주입(DI)은 이 포스트를 참조해주세요. 여기서는 설명을 생략합니다.
Repository를 통해 DB의 데이터를 요청하고 그를 요청에 맞게 처리합니다.
이렇게 서비스에 비즈니스 로직을 분리하게 되면 재사용도 편리합니다.
컨트롤러에 해당 로직을 작성했다고 가정해봅니다.
유저 마이 페이지에서도 닉네임 변경을 시도하고, 관리자 페이지에서도 관리자 페이지의 닉네임을 변경한다고 하면 각 컨트롤러에 동일한 로직을 두 번 작성해야겠죠? 이렇게 서비스로 분리하면 각 컨트롤러에서 위 서비스의 메소드만 호출하면 되므로 재사용이 매우 편리해집니다.
서비스를 분리함에 있어 또 다른 중요 작업은 트랜잭션 관리입니다.
다음과 같은 알고리즘으로 실행되는 비즈니스 로직이 있다라고 가정해봅니다.
1. 계좌에서 100원 인출됨
2. 계좌 잔고 업데이트
만약 2번과 3번 동작 사이에 어떠한 오류가 발생한다면 3번이 실행되지 않을 것이고, 그렇게 된다면 돈은 빠져나갔는데 잔고는 그대로인 정말 위험한 상황이 발생합니다.
이러한 경우 아예 1번 작업까지 취소해야 안정적인 시스템을 유지할 수 있게 됩니다.
이러한 DBMS에서의 작업 분리, 장애 시 복구 단위를 트랜잭션이라고 합니다.
MySQL 기준이지만 트랜잭션의 정의는 이 포스트를 참조해주세요.
비즈니스 로직에서도 당연히 이러한 작업 단위를 지정해야하며, 주로 메소드 레벨에 붙여서 메소드 호출 후 작업 도중 에러가 발생하면 해당 작업을 전부 롤백하고 호출 이전 시점으로 데이터를 되돌립니다.
//서비스 클래스 내부라고 가정
@Transactional //트랜잭션 선언
public void withdrawCash(Long cash) {
//1.계좌에서 100원 인출
//2.계좌 잔고 업데이트
}
@Transactional 어노테이션을 메소드 레벨에 붙이면 해당 메소드가 하나의 트랜잭션으로 관리됩니다. 해당 메소드 호출 이후 어디서든 오류가 발생하면 현재 메소드의 작업을 취소하고 데이터를 호출 시점 이전으로 되돌립니다. (Rollback)
이를 통해 트랜잭션을 관리하고 데이터의 무결성을 보장하게 됩니다.
아까 서비스 예제 코드에서 리포지토리를 의존성 주입했었습니다.
@Service
public class MyService {
//리포지토리 의존성 주입
private final UserRepository userRepository;
@Autowired //생성자 주입
public MyService(UserRepository userRepository) {
this.userRepository = userRepository;
}
@Transactional
public void updateUsername(UpdateUsernameDto updateUsernameDto) {
User user = userRepository.findById(updateUsernameDto.getUserId());
(...)
userRepository.save(user);
}
}
코드를 보시면 서비스는 리포지토리에게 특정 데이터(레코드)를 찾게 요청(findByXxxxx())하거나 저장(갱신/save())할 때 호출하는 것을 보실 수 있습니다.
즉, 서비스는 리포지토리에게 DB 데이터에 대한 접근을 요청하며, 리포지토리는 DB에 직접 접근해 요청에 따라 CRUD를 직접적으로 수행하는 부분입니다.
이로인해 서비스 클래스에서는 SQL을 직접 명시(SELECT 구문)하지 않아도 리포지토리에게 "찾아줘!" 요청(findByXxxxx)만 하면 결과를 받아볼 수 있게 됩니다.
리포지토리가 DB 접근에 대한 일을 대신 해주기 때문에 서비스에서는 DBMS가 변경되어도 코드를 따로 변경하지 않을 수 있게 됩니다.
즉, 요청을 받고(controller) - 요청을 처리하고(Service) - 처리 시 DB에 접근 후 Service에게 반환(Repository)라는 세 단계의 분리 구조가 완성이 됩니다.
스프링에서는 Spring Data JPA라는 아주 강력한 프레임워크를 통해 정말 편리하게 리포지토리를 다루고 DB에 접근할 수 있게 됩니다.
Spring Data JPA에 대한 설명은 이 포스트를 참조해주세요.