1. 상황
로아투두 프로젝트를 진행하면서 Member Entity를 조회하는 여러 메소드들이 있었는데,
그 중 회원 한명을 조회하는 Get 메소드는 입력받은 파라미터만 다르고 거의 동일한 join 구문과 fetch 로직이 반복되고 있었습니다.
이러한 상황에서 새로운 join이 필요하거나 파라미터가 추가될 때마다 모든 관련 메소드를 수정해야 하는 번거로움이 있었죠.
[username으로 조회하는 메소드와 id로 조회하는 메소드]
@Override
public Optional<Member> get(String username) {
return Optional.ofNullable(
factory.select(member)
.from(member)
.leftJoin(member.characters, character).fetchJoin()
.leftJoin(character.dayTodo.chaos, dayContent).fetchJoin()
.leftJoin(character.dayTodo.guardian, dayContent).fetchJoin()
.where(eqUsername(username))
.fetchOne()
);
}
@Override
public Optional<Member> get(Long id) {
return Optional.ofNullable(
factory.select(member)
.from(member)
.leftJoin(member.characters, character).fetchJoin()
.leftJoin(character.dayTodo.chaos, dayContent).fetchJoin()
.leftJoin(character.dayTodo.guardian, dayContent).fetchJoin()
.where(member.id.eq(id))
.fetchOne()
);
}
2. 리팩토링 방안
이러한 중복 코드를 제거하기 위해 Function 인터페이스를 활용한 리팩토링을 진행했습니다.
Java의 Function 인터페이스는 하나의 입력을 받아 다른 타입의 출력을 생성하는 함수형 인터페이스인데,
이를 활용하여 where 조건만 다르고 나머지는 동일한 조회 로직을 하나로 통합할 수 있었습니다.
리팩토링 코드
@Override
public Member get(String username) {
return findMemberBy(member -> member.username.eq(username));
}
@Override
public Member get(Long id) {
return findMemberBy(member -> member.id.eq(id));
}
private Member findMemberBy(Function<QMember, BooleanExpression> whereCondition) {
return Optional.ofNullable(
factory.select(member)
.from(member)
.leftJoin(member.characters, character).fetchJoin()
.leftJoin(character.dayTodo.chaos, dayContent).fetchJoin()
.leftJoin(character.dayTodo.guardian, dayContent).fetchJoin()
.where(whereCondition.apply(member))
.fetchOne()
).orElseThrow(() -> new EntityNotFoundException(MEMER_NOT_FOUND));
}
개선 내용
- 코드 중복 제거: 공통된 조회 로직을 findMemberBy 메소드로 추출하여 코드 중복을 제거.
- 유지보수성 향상: 새로운 join이 필요하거나 조회 로직이 변경되어야 할 때 findMemberBy 메소드만 수정하면 됨.
- 확장성 개선: 새로운 조건으로 Member를 조회해야 할 때 findMemberBy 메소드를 재사용.
- 가독성 향상: 각 메소드가 어떤 조건으로 조회하는지가 더 명확해졌음.
3. 이론 한스푼
Function 함수형 인터페이스는 자바의 java.util.function 패키지 안에 정의되어 있으며,
하나의 입력을 받아서 어떤 결과를 반환하는 연산을 수행한다.
이 인터페이스는 입력을 받아서 그 입력을 다른 형태로 변환하거나, 계산을 수행한 결과를 반환할 때 사용된다.
/**
* Represents a function that accepts one argument and produces a result.
*
* <p>This is a <a href="package-summary.html">functional interface</a>
* whose functional method is {@link #apply(Object)}.
*
* @param <T> the type of the input to the function
* @param <R> the type of the result of the function
*
* @since 1.8
*/
@FunctionalInterface
public interface Function<T, R> {
/**
* Applies this function to the given argument.
*
* @param t the function argument
* @return the function result
*/
R apply(T t);
...
}
R apply(T t): 주어진 객체 t에 대한 연산을 수행하고, 결과를 반환한다. 여기서 R은 반환 타입, T는 입력 타입을 의미합니다.
위의 상황에서는
반환 타입(R)이 BooleanExpression 이며, 입력 타입(T)는 QMember 입니다.