Skip to main content

SOLID 리팩토링 (feat. Processor)

Intro

개요

개발을 진행하면서 내가 가장 큰 문제라고 느꼈던 부분이 바로 Service 계층의 비대화였다. 처음에는 깔끔했던 Service 코드가 특정 비즈니스 로직, 외부 API 연동, 복잡한 데이터 검증 같은 것들이 덕지덕지 붙으면서 점점 무거워지는 현상을 겪었다.

이게 단순히 코드가 길어지는 문제를 넘어, SOLID 원칙을 정면으로 위반하고 있다는 생각이 들었다. 특히 단일 책임 원칙(SRP) 이 심각하게 깨지고 있었다.


뭐가 문제였나

UserService 하나가 패스워드 암호화, 이메일 중복 검사, 포인트 계산, 외부 PG사 연동까지 다 하고 있었다. @Transactional 메소드 안에 비즈니스 로직이 직접 박혀 있으니 흐름이 뭔지 로직이 뭔지도 섞여 보였다.

결제 방식이 바뀌면 UserService를 고쳐야 했다. 인증 로직이 바뀌어도 UserService였다. 뭔가 바꿀 때마다 이 클래스를 열어야 하는데, 건드리기가 무서워졌다. 한 군데 고치면 어디가 같이 터질지 몰라서.


해결책: Processor 패턴 도입으로 책임 분리

그래서 나는 복잡한 비즈니스 로직을 Service에서 분리해 Processor라는 독립적인 컴포넌트로 만들기로 했다.

이 구조를 통해 Service는 '흐름 관리' 라는 단일 책임을 갖게 되었고, Processor는 '로직 처리' 라는 단일 책임을 갖게 되었다.

컴포넌트새로운 책임역할 정의
Service흐름 관리트랜잭션을 설정하고, Processor들을 호출하여 전체 시나리오를 관리한다.
Processor로직 처리단 하나의 비즈니스 로직 (예: 비밀번호 인코딩, 외부 API 호출) 처리에만 집중한다.

리팩토링 과정: 인터페이스 및 구현체 재조립

1. 인터페이스 분리 (ISP 적용)

가장 먼저 UserService 인터페이스부터 쪼개서 ISP(인터페이스 분리 원칙) 를 적용했다. 기존 인터페이스는 너무 많은 책임을 가지고 있었다.

구분기존 UserService 인터페이스리팩토링 후 인터페이스
목적모든 사용자 관련 기능 포함역할별로 기능 분리
변경 전createUser(), login(), updateUserInfo(), getUserInfo(), adminDeleteUser()모두 포함-
변경 후 (분리)UserCommandService: 상태 변경 (쓰기) 기능만 담당 (createUser, updateUserInfo, deleteUser)UserQueryService: 데이터 조회 (읽기) 기능만 담당 (getUserInfo, getAllUsers, isEmailExists)

2. 구현체 내부: 로직을 해체하고 Processor로 재조립

제 프로젝트의 UserServiceImpl을 예시로, 가장 복잡했던 회원 생성 로직을 걷어내고 Processor를 주입받아 호출하는 방식으로 변경했다.

[기존 문제 코드 스타일 (예시: 회원 생성)]

// UserServiceImpl.java (리팩토링 전)
@Transactional
@Override
public ResCreateUserDto createUser(ReqCreateUserDto dto) {
validateDuplicate(dto); // 로직 1: 유효성 검사 (재사용성 낮았음)
String encodedPw = passwordEncoder.encode(dto.getPassword()); // 로직 2: 인코딩
// 로직 3: 엔티티 생성 및 DB 저장
User saved = userRepository.save(user);
return ResCreateUserDto.fromEntity(saved);
}

[리팩토링 후 코드 스타일]

  1. Processor 생성: UserValidationProcessor, UserCreationProcessor 등 로직 전문가를 생성했다.
  2. Service 단순화: Service는 Processor를 호출하기만 하면 됨.
// UserCommandServiceImpl.java (리팩토링 후)
@Service
@RequiredArgsConstructor
public class UserCommandServiceImpl implements UserCommandService {
private final UserCreationProcessor userCreationProcessor; // Processor만 의존

@Transactional // Service는 트랜잭션과 흐름 관리
@Override
public ResCreateUserDto createUser(ReqCreateUserDto dto) {
// 여기엔 딱 한 줄! 모든 복잡한 로직은 Processor에 위임했다.
User savedUser = userCreationProcessor.process(dto);

return ResCreateUserDto.fromEntity(savedUser);
}
}

리팩토링 후 얻은 결정적인 이점

Processor 패턴을 적용하고 나서 코드가 확실히 달라졌다.

  • SRP 회복 및 OCP 준수:
  • Service는 이제 트랜잭션과 흐름만 책임지기 때문에, 오직 '흐름'이 바뀔 때만 수정하면 되었다. 로직이 바뀔 때는 해당 Processor만 수정하고 Service는 그대로 둬서 OCP를 지킬 수 있었다.
  • 재사용성 극대화:
  • UserValidationProcessor처럼 공통 로직을 담은 Processor는 다른 AdminServiceBoardService 등에서도 주입받아 바로 사용할 수 있게 되어 코드 중복이 사라졌다.
  • 테스트 용이성:
  • 복잡한 로직이 Processor 단위로 분리되어, 인코딩 로직만 테스트하고 싶을 때 UserCreationProcessor만 독립적으로 테스트하면 되었다. Service 테스트 시 복잡한 의존성들을 Mocking할 필요가 없어 테스트 코드가 압도적으로 단순해졌다.

마무리

솔직히 개발 초반에는 비즈니스 로직들을 다 때려박고 인터페이스와 구현체로만 분리하면 될 줄 알았다. 전혀 아니었다. Service가 커질수록 건드리기 무서워지고, 뭔가 바꾸려면 어디가 영향을 받는지도 파악이 안 됐다.

Processor로 쪼개고 나서는 달랐다. 어디를 고쳐야 할지가 명확해졌고, 테스트도 쉬워졌다. 구조가 먼저 잡혀 있어야 코드가 쌓여도 버틸 수 있다는 걸 직접 체감했다.