User 엔티티
@Entity
@NoArgsConstructor(access = AccessLevel.PROTECTED)
public class User {
@Id
@GeneratedValue
private Long id;
private String name;
public void changeName(String name) {
this.name = name;
}
}
UserRepository
public interface UserRepository extends JpaRepository<User, Long> {
}
UserService
@Service
@RequiredArgsConstructor
public class UserService {
private final UserRepository userRepository;
@Transactional
public void updateUserName(Long userId, String name) {
User user = userRepository.findById(userId)
.orElseThrow(() -> new BusinessException(ErrorCode.NOT_FOUND));
user.changeName(name);
}
}
NOT_FOUND에서 NOT_FOUND_USER로 변경하기로 했다.변경 전
User user = userRepository.findById(userId)
.orElseThrow(() -> new BusinessException(ErrorCode.NOT_FOUND));
변경 후
User user = userRepository.findById(userId)
.orElseThrow(() -> new BusinessException(ErrorCode.NOT_FOUND_USER));
문제점
처음에 생각한 해결 방안은 프록시를 사용하는 것이었습니다. 프록시는 JpaRepository 인스턴스를 감싸고, 사용자는 JpaRepository 인스턴스를 직접 사용하는 것이 아니라 프록시를 통해 간접적으로 JpaRepository 인스턴스를 사용하도록 합니다. 그런 다음 프록시는 사용자를 대신하여 Optional에 대한 처리를 수행한 후 엔티티를 반환하거나 예외를 던지도록 합니다.
하지만 이 방식은 적용할 수 없었는데 그 이유는 프록시는 감싸는 인스턴스와 동일한 인터페이스를 가져와야 하기 때문입니다. JpaRepository를 사용하는 클라이언트의 관점에서는 객체가 프록시인지 실제 JpaRepository 인스턴스인지 알 수 없도록 동작해야 합니다. 따라서 프록시는 실제 객체와 동일한 인터페이스를 가져야 합니다.
JpaRepository의 findById 메서드는 Optional<T>를 반환하므로 프록시가 해당 메서드를 감싸 예외 처리를 한다고 해도 결과적으로 반환값은 여전히 Optional<T>로 유지됩니다. 즉, 클라이언트는 여전히 Optional을 처리해야 합니다.
이러한 이유로 인해 프록시를 사용하여 Optional을 처리하는 방식은 제한적이며, 클라이언트에서 여전히 Optional을 다루어야 한다는 점을 고려해야 합니다.
public interface UserRepository extends JpaRepository<User, Long> {
User findByIdOrElseThrow(Long id);
}
@Service
@RequiredArgsConstructor
public class UserService {
private final UserRepository userRepository;
@Transactional
public void updateUserName(Long userId, String name) {
User user = userRepository.findByOrElseThrow();
user.changeName(name);
}
}
서비스 코드에서 예외를 직접 처리하는 코드가 사라져서 한 단계 더 나아졌다.
하지만 여러 리포지토리(User, Shop 등) 인터페이스에 findByIdOrElseThrow를 정의하고 구현작업을 해야 한다.
ID는 모든 엔티티가 반드시 가져야하기 때문에 findById에 대해서는 공통화 작업이 가능하다.
이런 중복되는 작업을 JpaRepository에 공통 메서드를 추가해서 해결할 수 있을까?
ExtendedRepository 인터페이스
@NoRepositoryBean
public interface ExtendedRepository<T, ID extends Serializable> extends JpaRepository<T, ID> {
T findByIdOrElseThrow(ID id);
}
ExtendedRepositoryImpl 클래스
public class ExtendedRepositoryImpl<T, ID extends Serializable> extends SimpleJpaRepository<T, ID> implements ExtendedRepository<T, ID> {
private final EntityManager entityManager;
private static final String ID_MUST_NOT_BE_NULL = "The given id must not be null!";
public ExtendedRepositoryImpl(JpaEntityInformation<T, ?> entityInformation, EntityManager entityManager) {
super(entityInformation, entityManager);
this.entityManager = entityManager;
}
@Override
public T findByIdOrElseThrow(ID id) {
Assert.notNull(id, ID_MUST_NOT_BE_NULL);
Class<T> domainType = getDomainClass();
T result = entityManager.find(domainType, id);
if (result == null) {
throw new BusinessException(ErrorCode.valueOf("NOT_FOUND_" + getClassName()), List.of(id));
}
return result;
}
private String getClassName() {
Class<T> domainType = getDomainClass();
return StringUtils.capitalize(domainType.getSimpleName())
.replaceAll("(.)(\\p{javaUpperCase})", "$1_$2")
.toUpperCase();
}
}
UserRepository 인터페이스
public interface UserRepository extends ExtendedRepository<User, UUID> {
}
상속을 사용하는 것이 안전한가?
@Service
@RequiredArgsConstructor
public class UserService {
private final UserRepository userRepository;
@Transactional
public void updateUserName(Long userId, String name) {
User user = userRepository.findByOrElseThrow();
user.changeName(name);
}
}
참고