Repository Default Method 가이드 - 반복 코드 제거의 핵심

geoson·2025년 6월 5일

Spring & 백엔드

목록 보기
10/18

Repository Default Method란?

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);
    }
}

문제점:

  • 같은 패턴의 코드가 반복됨
  • 예외 메시지가 일관되지 않을 수 있음
  • 코드 중복으로 인한 유지보수의 어려움

해결 방법: Repository Default Method

✅ Repository에 Default Method 추가

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 코드

@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);
    }
}

Before vs After 비교

❌ Before: 반복되는 코드

@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);
    }
}

✅ After: 깔끔한 코드

@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);
    }
}

주요 패턴 3가지

1. 조회 + 예외 처리 패턴 🔍

default EntityType findByIdOrThrow(Long id) {
    return findById(id)
        .orElseThrow(() -> new EntityNotFoundException("엔티티를 찾을 수 없습니다. ID: " + id));
}

2. 중복 검증 패턴 ✅

default void validateFieldNotExists(String field) {
    if (existsByField(field)) {
        throw new DuplicateException("이미 존재합니다: " + field);
    }
}

3. 권한 검증 패턴 🔐

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);
}

실무 활용 예시

일반적인 User Repository

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에서 활용

@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);
    }
}

언제 사용할까?

✅ 적합한 경우

  • 반복되는 조회 + 예외 처리 패턴
  • 간단한 유효성 검증
  • 권한 검증이 포함된 조회
  • 중복 체크 로직

❌ 부적합한 경우

  • 복잡한 비즈니스 로직 (Service 계층에서 처리)
  • 트랜잭션이 필요한 복잡한 작업
  • 여러 Repository를 조합하는 로직
  • 외부 API 호출이나 알림 발송 등

결론

Repository Default Method는 데이터 접근 계층의 반복 코드를 효과적으로 제거할 수 있는 강력한 도구입니다.

🎯 핵심 이점

  • 코드 중복 제거 - 반복되는 findById().orElseThrow() 패턴 제거
  • 일관된 예외 처리 - 동일한 예외 타입과 메시지 사용
  • 가독성 향상 - Service 계층 코드가 깔끔해짐
  • 유지보수성 개선 - 예외 메시지 변경 시 Repository에서만 수정

💡 핵심 원칙

단순한 데이터 접근 로직에만 사용하고, 복잡한 비즈니스 로직은 Service 계층에서 처리하세요!

실무에서는 이 3가지 패턴만 잘 활용해도 대부분의 반복 코드 문제를 해결할 수 있습니다:
1. findByIdOrThrow() - 조회 + 예외 처리
2. validateXxxNotExists() - 중복 검증
3. findByXxxAndYyyOrThrow() - 권한 검증

0개의 댓글