Adding-A-Method-In-All-Repositories
1 모든 Repository에 메서드 추가하기
- Spring Data JPA를 사용하면 Repository 인터페이스만 정의하면 구현체가 자동으로 만들어지는 기능을 상당히 잘 사용하고 잇다.
- 그 중에서도 JpaRepository가 제공하는 findById 메서드를 많이 사용하는데 반환 값이 Optional이기 때문에 반복되는 Optional 처리가 상당히 귀찮아지기 시작했다.
- JpaRepository에 Optional을 직접 처리해야되는 findById 메서드 대신 Optional을 처리해주고 엔티티를 바로 반환해주는 메서드가 있으면 좋겠다는 생각을 했다.
- 모든 엔티티는 Id를 가지고 있기 때문에 Id로 엔티티를 조회하는 기능은 모든 리포지토리에서 공통적으로 사용할 수 있다고 생각했다.
2 문제점
- 간단한 예제 프로그램으로 당시 프로젝트를 진행하면서 겪었던 문제점을 개선해보자.
2.1 상황
User 엔티티
- 간단한 사용자 엔티티로 id와 name을 가지고 있다.
@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
- Spring Data JPA 사용하면 JpaRepository를 상속한 인터페이스만 정의하면 UserRepository의 구현체를 만들어 준다.
- JpaRepository가 상속한 CrudRepository 인터페이스에 findById 메서드가 정의되어 있다.
- 해당 메서드는 Optional을 반환하므로 UserRepository의 클라이언트는 필히 Optional을 처리해야 한다.
public interface UserRepository extends JpaRepository<User, Long> {
}
UserService
- UserRepository를 사용하는 서비스 컴포넌트다.
- 엔티티를 아이디로 조회하는 일은 여러 서비스 코드에서 많이 사용되고 있는 메서드인데 그런 모든 곳에서 아래와 같이 예외 처리 코드가 중복적으로 들어가고 있다.
@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);
}
}
2.2 문제점
- UserRepository가 많은 서비스 컴포넌트에서 사용 중인데 예외 코드를 변경해달라는 요구 사항이 들어왔다.
- 더 자세한 에러 코드를 내려주기 위해
NOT_FOUND
에서NOT_FOUND_USER
로 변경하기로 했다.
변경 전
User user = userRepository.findById(userId)
.orElseThrow(() -> new BusinessException(ErrorCode.NOT_FOUND));
변경 후
- UserRepository를 사용하는 모든 서비스의 코드에서 아래와 같이 에러코드를 수정해야 한다.
- 중복 코드가 가지는 가장 큰 문제는 코드를 수정하는 데 필요한 노력을 몇 배로 증가시킨다.
User user = userRepository.findById(userId)
.orElseThrow(() -> new BusinessException(ErrorCode.NOT_FOUND_USER));
문제점
- 변경 사항이 발생했을 때 하나의 클래스만 수정한다면 응집성이 높은 것이고 그렇지 않다면 응집성이 낮다는 것을 의미한다.
- 현 상태에서 응집성을 높이려면 에러 처리를 클라이언트가 처리하는 것이 아닌 UserRepository가 예외를 처리하도록 하는 것이 좋다고 생각했다.
- UserRepository가 직접 처리해야 하는 부분을 클라이언트에 노출하므로써 결합도가 올라갔기 때문에 예외 코드를 변경할 때 연관된 UserService도 수정이 필요해 졌다.
3 해결 방안
3.1 프록시
처음에 생각한 해결 방안은 프록시를 사용하는 것이었습니다. 프록시는 JpaRepository 인스턴스를 감싸고, 사용자는 JpaRepository 인스턴스를 직접 사용하는 것이 아니라 프록시를 통해 간접적으로 JpaRepository 인스턴스를 사용하도록 합니다. 그런 다음 프록시는 사용자를 대신하여 Optional에 대한 처리를 수행한 후 엔티티를 반환하거나 예외를 던지도록 합니다.
하지만 이 방식은 적용할 수 없었는데 그 이유는 프록시는 감싸는 인스턴스와 동일한 인터페이스를 가져와야 하기 때문입니다. JpaRepository를 사용하는 클라이언트의 관점에서는 객체가 프록시인지 실제 JpaRepository 인스턴스인지 알 수 없도록 동작해야 합니다. 따라서 프록시는 실제 객체와 동일한 인터페이스를 가져야 합니다.
JpaRepository의 findById 메서드는 Optional<T>
를 반환하므로 프록시가 해당 메서드를 감싸 예외 처리를 한다고 해도 결과적으로 반환값은 여전히 Optional<T>
로 유지됩니다. 즉, 클라이언트는 여전히 Optional을 처리해야 합니다.
이러한 이유로 인해 프록시를 사용하여 Optional을 처리하는 방식은 제한적이며, 클라이언트에서 여전히 Optional을 다루어야 한다는 점을 고려해야 합니다.