Service를 작성하던 중 단일 책임의 원칙에 위반하는 코드를 작성했음을 알게 되었다. 따라서 어떻게 책임분리를 하는 것이 적절할것인가에 대한 고민을 하게 되었다.
@Service
@RequiredArgsConstructor
public class UserService {
private final UserRepository userRepository;
@Transactional
public void createUser(final UserCreateRequestDto userCreateRequestDto){
Users user = new Users(
userCreateRequestDto.userName(),
userCreateRequestDto.email(),
userCreateRequestDto.password()
);
userRepository.save(user);
}
@Transactional
public UserDetailResponseDto getUser(final Long userId){
return userRepository.findById(userId)
.map(UserDetailResponseDto::from)
.orElseThrow(() -> new NotFoundException(ErrorCode.USER_NOT_FOUND));
}
@Transactional
public UserUpdateResponseDto updateUser(final Long userId, final UserUpdateRequestDto userUpdateRequestDto){
Users user = userRepository.findById(userId)
.orElseThrow(() -> new NotFoundException(ErrorCode.USER_NOT_FOUND));
user.update(userUpdateRequestDto.userName(), userUpdateRequestDto.email(), userUpdateRequestDto.password());
return UserUpdateResponseDto.from(user);
}
@Transactional
public void deleteUser(final Long userId, final UserDeleteRequestDto userDeleteRequestDto){
Users user = userRepository.findById(userId)
.orElseThrow(() -> new NotFoundException(ErrorCode.USER_NOT_FOUND));
// 비밀 번호 일치 여부 확인
if (!userDeleteRequestDto.password().equals(user.getPassword())) {
throw new CustomException(ErrorCode.INVALID_PASSWORD);
}
userRepository.deleteById(userId);
}
}
다음과 같이 Service 코드를 구현했는데, user를 userId를 기준으로 찾고, id가 없는 경우 예외를 반환하는 로직이 여기저기서 겹친다는 느낌을 받았다. 따라서 어떻게 하면 중복 코드를 없앨 수 있는지에 대해 고민에 빠졌다. 게다가 update, delete 메서드의 경우 한 메서드에서 여러 가지의 책임을 동시에 처리하고 있음을 볼 수 있다.
예시로 update 메서드를 들어서 설명하겠다.
1. 유저 조회 책임
Users user = userRepository.findById(userId)
.orElseThrow(() -> new NotFoundException(ErrorCode.USER_NOT_FOUND));
2. 유저 업데이트 책임
user.update(userUpdateRequestDto.userName(), userUpdateRequestDto.email(), userUpdateRequestDto.password());
하나의 메서드가 2가지의 책임을 지고 있다.
🔥🔥🔥 이는 SRP(단일 책임 원칙)를 위반하고 있다. 🔥🔥🔥
따라서 문제가 있음을 확인했고, 이런 저런 해결 방안을 생각하게 되었다.
1️⃣ Repository에서 예외를 던질까 ?
public interface UserRepository extends JpaRepository<Users, Long> {
Users findByIdOrThrow(Long userId) {
return findById(userId)
.orElseThrow(() -> new NotFoundException(ErrorCode.USER_NOT_FOUND));
}
}
다음과 같이 Repository에 코드를 옮긴 후 사용하게 된다면 아마 중복은 피할 수 있을 것이다. 하지만 "예외를 던지는 로직이 Repository의 책임인가?" 에 대해 생각하면 “아니다” !!!
Repository는 데이터 접근의 책임만 가져야하고, 비즈니스 로직(예외처리) 의 경우 Service 계층의 책임이기 때문이다.
구분 | 역할 / 책임 |
---|---|
Repository | DB 접근 및 단순 조회/저장 기능 (e.g., findById , save , existsByEmail ) |
Service | 비즈니스 규칙 적용, 예외 처리, 트랜잭션 처리 등 |
다음 표를 참고하면 더 이해가 쉬울 것이다. 따라서 Repository에 중복 코드를 위임하면, 관심사 분리를 위반하게 되고 Repository가 비즈니스 정책을 알게 되므로 바람직하지 않다.
2️⃣ 그렇다면 무조건 Service에서 처리를 해야하는건데 ,,,,, !!!!
뭔가 더 좋은 뾰족한 방법이 없나 찾던 중, Helper Class의 존재에 대해 알게 되었다. Helper 클래스는 객체지향 프로그래밍에서 어떤 기능을 도와주는 클레스인데, 메인 기능은 아니고 어떠한 기능을 담당하는 클래스다.
여기서 바로 지금 유저를 찾고 예외를 반환해주는 비즈니스 로직을 헬퍼 클래스에 적용하면 딱이라는 생각이 들었다.
Helper Class
Helper Class란 ?객체 지향 프로그래밍 에서 헬퍼 클래스는 해당 애플리케이션이나 해당 클래스의 주요 목적이 아닌 일부 기능을 제공하는 데 사용한다. 위키피디아의 Helper Class 를 보면 다음과 같
yeunever.tistory.com
헬퍼 클래스에 대한 글은 다음을 참고하면 된다.
▶️ UserServiceHelper
@Component
@RequiredArgsConstructor
public class UserServiceHelper {
private final UserRepository userRepository;
public Users findUserOrThrow(Long userId){
return userRepository.findById(userId)
.orElseThrow(() -> new NotFoundException(ErrorCode.USER_NOT_FOUND));
}
}
▶️ UserService
@Service
@RequiredArgsConstructor
public class UserService {
private final UserRepository userRepository;
private final UserServiceHelper userServiceHelper;
@Transactional
public void createUser(final UserCreateRequestDto userCreateRequestDto){
Users user = new Users(
userCreateRequestDto.userName(),
userCreateRequestDto.email(),
userCreateRequestDto.password()
);
userRepository.save(user);
}
@Transactional
public UserDetailResponseDto getUser(final Long userId){
Users user = userServiceHelper.findUserOrThrow(userId);
return UserDetailResponseDto.from(user);
}
@Transactional
public UserUpdateResponseDto updateUser(final Long userId, final UserUpdateRequestDto userUpdateRequestDto){
Users user = userServiceHelper.findUserOrThrow(userId);
user.update(userUpdateRequestDto.userName(), userUpdateRequestDto.email(), userUpdateRequestDto.password());
return UserUpdateResponseDto.from(user);
}
@Transactional
public void deleteUser(final Long userId, final UserDeleteRequestDto userDeleteRequestDto){
Users user = userServiceHelper.findUserOrThrow(userId);
// 비밀 번호 일치 여부 확인
if (!userDeleteRequestDto.password().equals(user.getPassword())) {
throw new CustomException(ErrorCode.INVALID_PASSWORD);
}
userRepository.deleteById(userId);
}
}
그 결과 UserService의 코드가 간결해졌다 !
'Backend > Spring' 카테고리의 다른 글
Helper Class (0) | 2025.04.02 |
---|---|
BaseTimeEntity와 JPA Auditing: 왜 사용할까? (0) | 2025.04.01 |
JPA와 영속성 컨텍스트 (0) | 2025.03.31 |
[Spring 기초] 일정 관리 앱 만들기 (0) | 2025.03.26 |
JWT 토큰 인증 이란? (0) | 2025.03.25 |