[Todo] 트러블 슈팅 & TIL (1)
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>
👉 JpaTodoRepository
는 JpaRepository
인터페이스를 확장(extends) 해서, 그 안의 메서드들을 상속받아 사용할 수 있어.
2. implements
-
클래스가 인터페이스를 구현할 때 사용해.
-
예시:
public class TodoRepositoryImpl implements TodoRepository
👉 TodoRepositoryImpl
은 TodoRepository
인터페이스에 선언된 메서드들을 직접 구현해야 해.
✅ 의존성 주입 방향 (DI 흐름)
스프링이 의존성 주입을 해줄 때는 일반적으로 인터페이스에 구현체를 자동으로 연결해줘.
-
@Repository
어노테이션이 붙은 클래스들은 빈(bean) 으로 등록돼. -
TodoRepositoryImpl
은TodoRepository
인터페이스를implements
하니까, 스프링은 이 구현체를TodoRepository
타입으로 인식해. -
누군가가
TodoRepository
타입을 의존성 주입으로 원하면, 스프링은TodoRepositoryImpl
을 주입해줘.
그리고 TodoRepositoryImpl
생성자 안을 보면:
public TodoRepositoryImpl(JpaTodoRepository jpaTodoRepository) {
this.jpaTodoRepository = jpaTodoRepository;
}
-
여기서
JpaTodoRepository
는JpaRepository<Todo, Long>
를extends
한 Spring 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 동작 담당
✅ 이렇게 변경하면 얻는 이점
-
JPA 기술과 도메인 레이어를 분리
JPA가 아닌 다른 저장소(MongoDB, Redis 등)로 바꾸더라도 UserRepository는 그대로 유지 가능.
테스트에서 Mock을 만들기도 쉬움 (e.g. InMemoryUserRepository).
-
도메인 주도 설계(Domain-Driven Design) 철학에 맞는 구조
인프라와 도메인을 느슨하게 결합해서 유지보수성이 높아짐.
-
유연한 커스터마이징
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 (실제 구현체)
-
댓글남기기