Java 8의 인터페이스 디폴트 메서드를 활용하여 Spring Data JPA Repository에서 반복되는 코드를 해결할 수 있는 강력한 기능입니다.
@Service
public class ScheduleService {
@Autowired
private ScheduleRepository scheduleRepository;
public ScheduleResponseDto getSchedule(Long id) {
// 매번 반복되는 코드
Schedule schedule = scheduleRepository.findById(id)
.orElseThrow(() -> new EntityNotFoundException("일정을 찾을 수 없습니다. " + id));
return ScheduleResponseDto.from(schedule);
}
public ScheduleResponseDto updateSchedule(Long id, ScheduleRequestDto request) {
// 또 다시 반복되는 코드
Schedule schedule = scheduleRepository.findById(id)
.orElseThrow(() -> new EntityNotFoundException("일정을 찾을 수 없습니다. " + id));
schedule.update(request.getTitle(), request.getContent());
return ScheduleResponseDto.from(schedule);
}
public void deleteSchedule(Long id) {
// 계속 반복되는 코드
Schedule schedule = scheduleRepository.findById(id)
.orElseThrow(() -> new EntityNotFoundException("일정을 찾을 수 없습니다. " + id));
scheduleRepository.delete(schedule);
}
}
문제점:
public interface ScheduleRepository extends JpaRepository<Schedule, Long> {
// 핵심 패턴 1: 조회 + 예외 처리
default Schedule findByIdOrThrow(Long id) {
return findById(id)
.orElseThrow(() -> new EntityNotFoundException("일정을 찾을 수 없습니다. ID: " + id));
}
// 핵심 패턴 2: 중복 검증
default void validateTitleNotExists(String title, Long userId) {
if (existsByTitleAndUserId(title, userId)) {
throw new DuplicateException("이미 동일한 제목의 일정이 존재합니다: " + title);
}
}
// 핵심 패턴 3: 권한 검증이 포함된 조회
default Schedule findByIdAndUserIdOrThrow(Long id, Long userId) {
return findByIdAndUserId(id, userId)
.orElseThrow(() -> new AccessDeniedException("해당 일정에 접근할 권한이 없습니다."));
}
// Spring Data JPA 쿼리 메서드들
Optional<Schedule> findByIdAndUserId(Long id, Long userId);
boolean existsByTitleAndUserId(String title, Long userId);
}
@Service
public class ScheduleService {
@Autowired
private ScheduleRepository scheduleRepository;
public ScheduleResponseDto getSchedule(Long id) {
// 한 줄로 깔끔하게!
Schedule schedule = scheduleRepository.findByIdOrThrow(id);
return ScheduleResponseDto.from(schedule);
}
public ScheduleResponseDto createSchedule(Long userId, ScheduleRequestDto request) {
// 중복 검증도 간단하게
scheduleRepository.validateTitleNotExists(request.getTitle(), userId);
Schedule schedule = Schedule.createSchedule(request.getTitle(), request.getContent(), userId);
Schedule savedSchedule = scheduleRepository.save(schedule);
return ScheduleResponseDto.from(savedSchedule);
}
public ScheduleResponseDto updateSchedule(Long id, Long userId, ScheduleRequestDto request) {
// 권한 검증까지 한 번에
Schedule schedule = scheduleRepository.findByIdAndUserIdOrThrow(id, userId);
schedule.update(request.getTitle(), request.getContent());
return ScheduleResponseDto.from(scheduleRepository.save(schedule));
}
public void deleteSchedule(Long id, Long userId) {
Schedule schedule = scheduleRepository.findByIdAndUserIdOrThrow(id, userId);
scheduleRepository.delete(schedule);
}
}
@Service
public class UserService {
public void updateUser(Long id, UserUpdateRequest request) {
// 매번 반복되는 조회 + 예외 처리
User user = userRepository.findById(id)
.orElseThrow(() -> new EntityNotFoundException("사용자를 찾을 수 없습니다."));
// 매번 반복되는 중복 체크
if (userRepository.existsByEmail(request.getEmail())) {
throw new DuplicateException("이미 존재하는 이메일입니다.");
}
user.updateProfile(request.getName(), request.getEmail());
}
public void deleteUser(Long id) {
// 또 반복되는 조회 + 예외 처리
User user = userRepository.findById(id)
.orElseThrow(() -> new EntityNotFoundException("사용자를 찾을 수 없습니다."));
userRepository.delete(user);
}
}
@Service
public class UserService {
public void updateUser(Long id, UserUpdateRequest request) {
User user = userRepository.findByIdOrThrow(id);
userRepository.validateEmailNotExists(request.getEmail());
user.updateProfile(request.getName(), request.getEmail());
}
public void deleteUser(Long id) {
User user = userRepository.findByIdOrThrow(id);
userRepository.delete(user);
}
}
default EntityType findByIdOrThrow(Long id) {
return findById(id)
.orElseThrow(() -> new EntityNotFoundException("엔티티를 찾을 수 없습니다. ID: " + id));
}
default void validateFieldNotExists(String field) {
if (existsByField(field)) {
throw new DuplicateException("이미 존재합니다: " + field);
}
}
default EntityType findByIdAndUserIdOrThrow(Long id, Long userId) {
return findByIdAndUserId(id, userId)
.orElseThrow(() -> new AccessDeniedException("접근 권한이 없습니다."));
}
// 1. 일관된 예외 타입 사용
default User findByIdOrThrow(Long id) {
return findById(id)
.orElseThrow(() -> new EntityNotFoundException("사용자를 찾을 수 없습니다. ID: " + id));
}
// 2. 명확한 메서드 이름
findByIdOrThrow() // 조회 + 예외 처리
validateEmailNotExists() // 중복 검증
findByIdAndUserIdOrThrow() // 권한 검증
// 3. 단순한 로직만 포함
default void validateEmailNotExists(String email) {
if (existsByEmail(email)) {
throw new DuplicateException("이미 존재하는 이메일입니다: " + email);
}
}
// 1. 혼재된 예외 타입 (일관성 없음)
default User findByIdOrThrow(Long id) {
return findById(id)
.orElseThrow(() -> new IllegalArgumentException("...")); // ❌ 다른 예외 타입
}
// 2. 모호한 메서드 이름
getEntity() // ❌ 무엇을 하는지 모름
checkData() // ❌ 어떤 검증인지 모름
findSome() // ❌ 무엇을 찾는지 모름
// 3. 복잡한 비즈니스 로직 포함 (Service에서 처리해야 함)
default Schedule processScheduleUpdate(Long id, ScheduleRequest request) {
Schedule schedule = findByIdOrThrow(id);
schedule.update(request);
// 알림 발송, 로그 기록, 외부 API 호출 등... ❌
return save(schedule);
}
public interface UserRepository extends JpaRepository<User, Long> {
// 기본 조회 + 예외 처리
default User findByIdOrThrow(Long id) {
return findById(id)
.orElseThrow(() -> new EntityNotFoundException("사용자를 찾을 수 없습니다. ID: " + id));
}
// 이메일로 조회 + 예외 처리
default User findByEmailOrThrow(String email) {
return findByEmail(email)
.orElseThrow(() -> new EntityNotFoundException("해당 이메일의 사용자를 찾을 수 없습니다: " + email));
}
// 이메일 중복 검증
default void validateEmailNotExists(String email) {
if (existsByEmail(email)) {
throw new DuplicateException("이미 존재하는 이메일입니다: " + email);
}
}
// 활성 사용자만 조회
default User findActiveUserByIdOrThrow(Long id) {
return findByIdAndStatus(id, UserStatus.ACTIVE)
.orElseThrow(() -> new EntityNotFoundException("활성 상태의 사용자를 찾을 수 없습니다. ID: " + id));
}
// Spring Data JPA 메서드들
Optional<User> findByEmail(String email);
boolean existsByEmail(String email);
Optional<User> findByIdAndStatus(Long id, UserStatus status);
}
@Service
@RequiredArgsConstructor
public class UserService {
private final UserRepository userRepository;
public UserResponseDto createUser(UserCreateRequest request) {
// 이메일 중복 검증 - 간단하게!
userRepository.validateEmailNotExists(request.getEmail());
User user = User.createUser(request.getName(), request.getEmail());
User savedUser = userRepository.save(user);
return UserResponseDto.from(savedUser);
}
public UserResponseDto getUser(Long id) {
// 조회 + 예외 처리 - 한 줄로!
User user = userRepository.findByIdOrThrow(id);
return UserResponseDto.from(user);
}
public UserResponseDto updateUser(Long id, UserUpdateRequest request) {
User user = userRepository.findByIdOrThrow(id);
user.updateProfile(request.getName(), request.getEmail());
return UserResponseDto.from(userRepository.save(user));
}
public void deleteUser(Long id) {
User user = userRepository.findActiveUserByIdOrThrow(id);
user.deactivate(); // 실제 삭제가 아닌 비활성화
userRepository.save(user);
}
}
Repository Default Method는 데이터 접근 계층의 반복 코드를 효과적으로 제거할 수 있는 강력한 도구입니다.
단순한 데이터 접근 로직에만 사용하고, 복잡한 비즈니스 로직은 Service 계층에서 처리하세요!
실무에서는 이 3가지 패턴만 잘 활용해도 대부분의 반복 코드 문제를 해결할 수 있습니다:
1. findByIdOrThrow() - 조회 + 예외 처리
2. validateXxxNotExists() - 중복 검증
3. findByXxxAndYyyOrThrow() - 권한 검증