3 분 소요

extends vs implements

두 개가 비슷한 건데 이번에 JPA를 통해 객체지향적으로 만들면서 배운 내용을 정리

@Repository  
public interface JpaTodoRepository extends JpaRepository<Todo, Long> {  
    List<Todo> findByCompleted(boolean completed);  
}
public interface TodoRepository {  
  
  Todo save(Todo todo);  
  
  Optional<Todo> findById(Long id);  
  
  List<Todo> findAll();  
  
  void deleteById(Long id);  
  
  List<Todo> findByCompleted(boolean completed);  
}
@Repository  
public class TodoRepositoryImpl implements TodoRepository {  
      
    private final JpaTodoRepository jpaTodoRepository;  
      
    public TodoRepositoryImpl(JpaTodoRepository jpaTodoRepository) {  
        this.jpaTodoRepository = jpaTodoRepository;  
    }  
      
    @Override  
    public Todo save(Todo todo) {  
        return jpaTodoRepository.save(todo);  
    }  
      
    @Override  
    public Optional<Todo> findById(Long id) {  
        return jpaTodoRepository.findById(id);  
    }  
      
    @Override  
    public List<Todo> findAll() {  
        return jpaTodoRepository.findAll();  
    }  
      
    @Override  
    public void deleteById(Long id) {  
        jpaTodoRepository.deleteById(id);  
    }  
      
    @Override  
    public List<Todo> findByCompleted(boolean completed) {  
        return jpaTodoRepository.findByCompleted(completed);  
    }  
}

extends vs implements 차이

1. extends

  • 클래스끼리 혹은 인터페이스끼리 상속할 때 사용해.

  • 즉, 클래스가 다른 클래스를 상속하거나, 인터페이스가 다른 인터페이스를 확장할 때 쓰여.

  • 예시:

public interface JpaTodoRepository extends JpaRepository<Todo, Long>

👉 JpaTodoRepositoryJpaRepository 인터페이스를 확장(extends) 해서, 그 안의 메서드들을 상속받아 사용할 수 있어.

2. implements

  • 클래스가 인터페이스를 구현할 때 사용해.

  • 예시:

public class TodoRepositoryImpl implements TodoRepository

👉 TodoRepositoryImplTodoRepository 인터페이스에 선언된 메서드들을 직접 구현해야 해.

✅ 의존성 주입 방향 (DI 흐름)

스프링이 의존성 주입을 해줄 때는 일반적으로 인터페이스에 구현체를 자동으로 연결해줘.

  1. @Repository 어노테이션이 붙은 클래스들은 빈(bean) 으로 등록돼.

  2. TodoRepositoryImplTodoRepository 인터페이스를 implements 하니까, 스프링은 이 구현체를 TodoRepository 타입으로 인식해.

  3. 누군가가 TodoRepository 타입을 의존성 주입으로 원하면, 스프링은 TodoRepositoryImpl을 주입해줘.

그리고 TodoRepositoryImpl 생성자 안을 보면:

public TodoRepositoryImpl(JpaTodoRepository jpaTodoRepository) {
    this.jpaTodoRepository = jpaTodoRepository;
}
  • 여기서 JpaTodoRepositoryJpaRepository<Todo, Long>extendsSpring Data JPA의 구현체야.

  • 스프링은 JpaTodoRepository도 자동으로 구현체를 만들어서 주입해줘.

🔁 간단 흐름도

[Spring Container]
       
JpaTodoRepository (extends JpaRepository)
        주입
TodoRepositoryImpl (implements TodoRepository)
        주입
TodoService (uses TodoRepository)

🔍 정리하자면

구분 설명 예시
extends 클래스나 인터페이스를 상속 interface A extends B
implements 클래스가 인터페이스를 구현 class A implements B
주입 방향 Spring이 인터페이스를 보고 구현체를 주입 TodoRepository → TodoRepositoryImpl

✅ 한눈에 보는 구조 비교

항목 변경 전 변경 후
구조 JPA 인터페이스가 도메인 인터페이스 직접 구현 도메인 인터페이스, JPA 인터페이스, 구현 클래스로 분리
의존성 도메인 ↔ JPA 직접 연결 도메인 ↔ 구현체 ↔ JPA (간접 연결)
유연성 낮음 높음
테스트 어려움 (JPA mock 필요) 쉬움 (Impl만 Mock 처리 가능)
책임 분리 애매 명확

User 레포지토리 분리

변경 전

user/
├── domain/
│   └── repository/
│       └── UserRepository.java (인터페이스만 존재)
└── infrastructure/
    └── persistence/
        └── JpaUserRepository.java (JPA 인터페이스)

변경 후

user/
├── domain/
│   ├── model/
│   │   └── User.java
│   └── repository/
│       └── UserRepository.java (인터페이스)
└── infrastructure/
    └── persistence/
        ├── JpaUserRepository.java (JPA 인터페이스)
        └── UserRepositoryImpl.java (구현체)

요약

[ 인터페이스 ] UserRepository     ←   도메인에 가까운 추상화 계층
                           ↑
[ 구현 클래스 ] UserRepositoryImpl   ←   JPA 기술과의 연결, 구현체
                           ↑
[ Spring Data JPA ] JpaUserRepository ←   진짜 DB 동작 담당


✅ 이렇게 변경하면 얻는 이점

  1. JPA 기술과 도메인 레이어를 분리

    JPA가 아닌 다른 저장소(MongoDB, Redis 등)로 바꾸더라도 UserRepository는 그대로 유지 가능.

    테스트에서 Mock을 만들기도 쉬움 (e.g. InMemoryUserRepository).

  2. 도메인 주도 설계(Domain-Driven Design) 철학에 맞는 구조

    인프라와 도메인을 느슨하게 결합해서 유지보수성이 높아짐.

  3. 유연한 커스터마이징

    UserRepositoryImpl에서 일부만 캐싱하거나, 로직을 추가할 수도 있음.

@Override
public Optional<User> findByEmail(String email) {
    log.info("이메일로 사용자 조회 요청: {}", email);
    return jpaUserRepository.findByEmail(email);
}

✅ 구조 비교

항목 변경 전 변경 후
구조 JPA 인터페이스가 도메인 인터페이스 직접 구현 도메인 인터페이스, JPA 인터페이스, 구현 클래스로 분리
의존성 도메인 ↔ JPA 직접 연결 도메인 ↔ 구현체 ↔ JPA (간접 연결)
유연성 낮음 높음
테스트 어려움 (JPA mock 필요) 쉬움 (Impl만 Mock 처리 가능)
책임 분리 애매 명확
        [Application Layer]
               ↓
         UserService (도메인 서비스)

        [Domain Layer]
               ↓
     UserRepository (Port/도메인 추상화)

        [Infrastructure Layer]
               ↓
UserRepositoryImpl (Adapter)
               ↓
JpaUserRepository (JPA 어댑터)

✅ 헥사고날 아키텍처 vs DDD 아키텍처

항목 헥사고날 아키텍처 (Hexagonal / Ports & Adapters) 도메인 주도 설계 (DDD 아키텍처)
핵심 개념 도메인 로직(핵심)과 외부 세계(입출력)를 포트와 어댑터로 분리 도메인을 중심으로 소프트웨어를 모델링하고 계층화
관심사 아키텍처적인 구조 분리 (입출력과 도메인 분리) 도메인 모델링과 책임 분리
대표 구조 - Domain (Core)
- Port (Interface)
- Adapter (Impl)
- Entity, Value Object, Aggregate
- Repository, Service, Factory 등
목적 외부 의존성과 느슨한 연결
유지보수성과 테스트성 향상
복잡한 비즈니스 로직을 명확하게 구조화
예시 - UserRepository = Port
- UserRepositoryImpl = Adapter
- JpaUserRepository = Infra Adapter
- User = Aggregate Root
- UserRepository = Domain Layer
- UserService = Application Service
  • DDD 아키텍처:

    • UserRepository는 도메인 중심 인터페이스로 도메인과 인프라를 분리

    • User는 도메인 모델 (엔티티 or 애그리거트)

  • 헥사고날 아키텍처:

    • UserRepository = Port (외부에서 들어오는 인터페이스)

    • UserRepositoryImpl, JpaUserRepository = Adapter (실제 구현체)

댓글남기기