중복 코드를 줄이기 위해 Template Method 패턴을 적용하여 리팩토링해보도록 하겠습니다.
현재 서비스에는 문제 풀이(Record)를 임시 저장하는 기능과 완전히 등록하는 기능이 있습니다.
결국 Record를 저장하는 건 똑같기 때문에 아래와 같이 공통 로직이 존재하는 상태였습니다.
//등록 기능
@Transactional
public RecordCreateResponse saveRecord(Long userId, RecordSaveRequest request) throws IOException {
//공통: 사용자 조회
User user = userService.getUserById(userId);
//공통: Record 객체 생성
Record record = createRecord(request.toRecord(user), request.getTempRecordId());
codeblockService.createCodeblocks(request.getCodeblocks(), record);
recordCategoryMappingService.createRecordCategoryMapping(request.getTags(), record);
//등록 기능: 후처리
int successRate = getSuccessRate(user);
user.saveRecord(record, successRate);
record.changeRecordNum(user.getSolvedCount());
githubCommitService.commitRecordToGithub(record);
return RecordCreateResponse.of(record);
}
//임시 저장 기능
@Transactional
public RecordCreateResponse createTempRecord(Long userId, RecordTempRequest request) {
//공통: 사용자 조회
User user = userService.getUserById(userId);
//임시 기능: 전처리
if (request.getTempRecordId() == null) {
checkTempRecordLimit(user);
}
//공통: Record 객체 생성
Record record = createRecord(request.toRecord(user), request.getTempRecordId());
codeblockService.createCodeblocks(request.getCodeblocks(), record);
recordCategoryMappingService.createRecordCategoryMapping(request.getTags(), record);
return RecordCreateResponse.of(record);
}
전체 로직의 흐름은 고정되어 있기 때문에 추가적인 단계만 확장하여 코드의 중복을 줄일 수 있을 것 같아 Template Method 패턴으로 리팩토링하려고 합니다.
사실 이전에 리팩토링을 시도하다 오히려 코드가 복잡해져서 다시 갈아엎었지만...
이번에는 좀 더 간결한 코드를 유지해보려고 합니다.
전체적인 구조는 아래와 같습니다.
상위에 추상 클래스인 AbstractRecordCreateService를 두고, 그 안에는 createRecord라는 템플릿 메서드 역할을 하는 메서드를 둘 것입니다.
그리고 RecordSaveService와 RecordTempService라는 하위 클래스에서 필요한 메서드를 세부적으로 구현할 것입니다.

먼저 공통 로직을 관리할 수 있는 상위 클래스를 만들었습니다.
//템플릿 메서드
@Transactional
public RecordCreateResponse createRecord(Long userId, AbstractRecordCreateRequest request) throws IOException {
User user = userService.getUserById(userId);
preprocess(user, request);
Record record = createRecordAndMappings(user, request);
postprocess(user, record);
return RecordCreateResponse.of(record);
}
//하위 구현 메서드
protected abstract void preprocess(User user, AbstractRecordCreateRequest request);
//세부 구현 메서드
protected abstract void postprocess(User user, Record record) throws IOException;
//공통 로직
protected Record createRecordAndMappings(User user, AbstractRecordCreateRequest request) {
.
.
.
.
}
템플릿 메서드에서 전처리와 후처리를 담당할 preprocess, postprocess 메서드를 로직에 넣어 놓고, 하위 클래스에서 각 메서드의 세부 구현을 담당할 것입니다. 메서드명이 모호한 점이 마음에 안 들지만 편의성을 위해 우선 이렇게 해 두었습니다 😥
그리고 등록 기능과 임시 저장 기능의 요청 DTO도 AbstractRecordCreateRequest로 상위에서 추상화해줬습니다.
하위 클래스 중 하나인 문제 풀이를 등록하는 서비스입니다.
@Service
public class RecordSaveService extends AbstractRecordCreateService {
//필드 및 생성자 생략
.
.
@Override
protected void preprocess(User user, AbstractRecordCreateRequest request) {}
@Override
protected void postprocess(User user, Record record) throws IOException {
int successRate = recordService.getSuccessRate(user);
user.saveRecord(record, successRate);
record.changeRecordNum(user.getSolvedCount());
githubCommitService.commitRecordToGithub(record);
}
}
preprocess는 빈 메서드로 두고, postprocess에서 필요한 처리를 해줬습니다.
다른 하위 클래스인 문제 풀이를 임시저장하는 서비스입니다.
@Service
public class RecordTempService extends AbstractRecordCreateService {
//필드 및 생성자 생략
.
.
@Override
protected void preprocess(User user, AbstractRecordCreateRequest request) {
if (request.getTempRecordId() == null) {
checkTempRecordLimit(user);
}
}
private void checkTempRecordLimit(User user) {
Long tempRecordsCount = recordService.getRecordsCountByUserAndRecordStatus(user, RecordStatus.TEMP);
if (tempRecordsCount >= TEMP_RECORD_LIMIT) {
throw new IllegalArgumentApplicationException("임시 저장 최대 개수를 초과했습니다.");
}
}
@Override
protected void postprocess(User user, Record record) {}
}
반대로 postprocess는 빈 메서드로 두고, preprocess에서 필요한 처리를 해줬습니다.
클라이언트인 RecordController에서는 아래와 같이 주입 받아 각 서비스의 메서드를 사용해줬습니다.
public class RecordController {
private final AbstractRecordCreateService recordSaveService;
private final AbstractRecordCreateService recordTempService;
.
.
.
이렇게 구현하니 전보다 클래스의 수가 조금 늘어나긴 했지만
공통 로직을 따로 관리하여 중복 코드를 줄일 수 있었고,
변경 사항이 생겼을 때도 필요한 부분만 추가하면 되기 때문에 확장성을 좀 더 높일 수 있었습니다.