Spring Data JPA를 사용한다면 항상 다음과 같은 로직을 작성할 것이다.
Entity findByXX(Long id, ...){
return EntityRepository.findByXX(id, ...)
.orElseThrow(() -> new BusinessException());
}
JPA에서 단건 조회시 반환 타입은 Optional로 감싸져 반환된다. 많은 사람들이 Optional을 서비스 계층에서 있으면 반환, 없으면 예외를 던지는 방식으로 많이 사용한다.
하지만 생각해보면 조회 메소드는 정말 많아 진다. 많아진 조회 메소드만큼 Optional 처리 로직이 반복적으로(orElseThrow 지옥) 발생한다. 이러면 예외가 변경되면 EntityRepository를 의존하는 서비스들이 많다면 변경의 여파가 많은 클래스로 전파될 것이다. 이를 어떻게 해결할까?
Entity findByXX1(Long id, ...){
return EntityRepository.findByXX1(id, ...)
.orElseThrow(() -> new BusinessException());
}
Entity findByXX2(Long id, ...){
return EntityRepository.findByXX2(id, ...)
.orElseThrow(() -> new BusinessException());
}
Entity findByXX3(Long id, ...){
return EntityRepository.findByXX3(id, ...)
.orElseThrow(() -> new BusinessException());
}
Entity findByXX4(Long id, ...){
return EntityRepository.findByXX4(id, ...)
.orElseThrow(() -> new BusinessException());
}
우선 EntityRepository와 서비스 계층 사이에 구현 계층을 추가하여 조회 로직을 하나의 클래스로 관리하는 방법이 있을 것이다.
(Controller/ service / implementation / repository)
@Component
@RequiredArgsConstructor
public class EntityReader{
private final EntityRepository entityRepository;
public Entity findByXX1(Long id, ...){
return EntityRepository.findByXX1(id, ...)
.orElseThrow(() -> new BusinessException());
}
public Entity findByXX2(Long id, ...){
return EntityRepository.findByXX2(id, ...)
.orElseThrow(() -> new BusinessException());
}
public Entity findByXX3(Long id, ...){
return EntityRepository.findByXX3(id, ...)
.orElseThrow(() -> new BusinessException());
}
public Entity findByXX4(Long id, ...){
return EntityRepository.findByXX4(id, ...)
.orElseThrow(() -> new BusinessException());
}
}
이렇게 구현 계층을 추가한다면 예외가 변경되더라도 변경의 여파는 EntityReader 클래스 내부로 축소된다. 하지만 orElseThrow의 반복은 여전하다. 이를 어떻게 효율적으로 처리할 수 있을까?
우선 Optional을 파라미터로 받는 메소드를 선언하여 사용하는 방법을 생각할 수 있다. 하지만 파라미터로 Optional을 사용하는 것은 다들 권장하지 않는 방법이다.그러면 어떻게 해결할 수 있을까?
이를 Java의 함수형 인터페이스를 이용해서 해결했다. 이중 Supplier를 이용했다.
Supplier는 간단하게 입력 없이 반환값만 있음을 나타내는 함수형 인터페이스이다.
Supplier을 사용하여 로직을 리팩터링하면 다음과 같아진다.
@Component
@RequiredArgsConstructor
public class EntityReader{
private final EntityRepository entityRepository;
public Entity findByXX1(Long id, ...){
return handleOptionalEntity(() -> EntityRepository.findByXX1(id, ...))
}
public Entity findByXX2(Long id, ...){
return handleOptionalEntity(() -> EntityRepository.findByXX2(id, ...))
}
public Entity findByXX3(Long id, ...){
return handleOptionalEntity(() -> EntityRepository.findByXX3(id, ...))
}
public Entity findByXX4(Long id, ...){
return handleOptionalEntity(() -> EntityRepository.findByXX4(id, ...))
}
private Entity handleOptionalEntity(Supplier<Optional<Entity>> findMethod){
return findMethod.get()
.orElseThrow(() -> new BusinessException());
}
}
이렇게 리팩터링하니 어떤가? 예외를 변경하더라도 한줄이면 끝난다. 변경의 여파가 하나의 메소드로 좁혀지는 것이다. 이를 응용한다면 여러 조회 메소드들을 공통적으로 처리할 수 있을 것이다.
유용한 글 감사합니다.