Template Method 패턴으로 중복 코드 줄이기

양말고양이·2025년 1월 20일

프로젝트 이슈 🐟

목록 보기
6/7

중복 코드를 줄이기 위해 Template Method 패턴을 적용하여 리팩토링해보도록 하겠습니다.


1. 기존 코드

현재 서비스에는 문제 풀이(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 패턴으로 리팩토링하려고 합니다.

사실 이전에 리팩토링을 시도하다 오히려 코드가 복잡해져서 다시 갈아엎었지만...

이번에는 좀 더 간결한 코드를 유지해보려고 합니다.




2. 적용해보기

전체적인 구조는 아래와 같습니다.

상위에 추상 클래스인 AbstractRecordCreateService를 두고, 그 안에는 createRecord라는 템플릿 메서드 역할을 하는 메서드를 둘 것입니다.

그리고 RecordSaveServiceRecordTempService라는 하위 클래스에서 필요한 메서드를 세부적으로 구현할 것입니다.


2.1 AbstractRecordCreateService

먼저 공통 로직을 관리할 수 있는 상위 클래스를 만들었습니다.

	//템플릿 메서드
	@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로 상위에서 추상화해줬습니다.


2.2 RecordSaveService

하위 클래스 중 하나인 문제 풀이를 등록하는 서비스입니다.


@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에서 필요한 처리를 해줬습니다.


2.3 RecordTempService

다른 하위 클래스인 문제 풀이를 임시저장하는 서비스입니다.


@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에서 필요한 처리를 해줬습니다.




3. 결과

클라이언트인 RecordController에서는 아래와 같이 주입 받아 각 서비스의 메서드를 사용해줬습니다.


public class RecordController {
	private final AbstractRecordCreateService recordSaveService;
	private final AbstractRecordCreateService recordTempService;
    .
    .
    .

이렇게 구현하니 전보다 클래스의 수가 조금 늘어나긴 했지만
공통 로직을 따로 관리하여 중복 코드를 줄일 수 있었고,
변경 사항이 생겼을 때도 필요한 부분만 추가하면 되기 때문에 확장성을 좀 더 높일 수 있었습니다.




참고

https://inpa.tistory.com/entry/GOF-%F0%9F%92%A0-%ED%85%9C%ED%94%8C%EB%A6%BF-%EB%A9%94%EC%86%8C%EB%93%9CTemplate-Method-%ED%8C%A8%ED%84%B4-%EC%A0%9C%EB%8C%80%EB%A1%9C-%EB%B0%B0%EC%9B%8C%EB%B3%B4%EC%9E%90#%ED%81%B4%EB%9E%98%EC%8A%A4_%EA%B5%AC%EC%84%B1

profile
백엔드 개발자입니다

0개의 댓글