1. 추상 클래스보다 인터페이스를 주로 사용하는 이유
자바에서 인터페이스는 추상 클래스와 비슷한 역할을 하지만, 특정 상황에서는 더 유연하고 효과적인 설계 방식을 제공합니다. 특히, 다중 구현과 같은 특성 때문에 추상 클래스보다 인터페이스를 더 자주 사용하는 경향이 있습니다. 그 이유는 다음과 같습니다.
1.1 다중 구현이 가능하다
자바는 클래스 간의 다중 상속을 허용하지 않습니다. 즉, 한 클래스는 오직 하나의 부모 클래스만 상속받을 수 있습니다. 하지만 인터페이스는 여러 개를 동시에 구현할 수 있습니다. 이를 통해 클래스가 여러 역할을 동시에 가질 수 있게 되어 설계의 유연성이 크게 증가합니다.
// 인터페이스 1
interface Drivable {
void drive();
}
// 인터페이스 2
interface Flyable {
void fly();
}
// 하나의 클래스가 여러 인터페이스를 동시에 구현 가능
class FlyingCar implements Drivable, Flyable {
@Override
public void drive() {
System.out.println("The car is driving.");
}
@Override
public void fly() {
System.out.println("The car is flying.");
}
}
public class Main {
public static void main(String[] args) {
FlyingCar car = new FlyingCar();
car.drive(); // Drivable의 메서드 사용
car.fly(); // Flyable의 메서드 사용
}
}
위 코드에서는 FlyingCar
가 Drivable
과 Flyable
을 동시에 구현하고 있습니다. 자바는 다중 상속을 지원하지 않지만, 인터페이스를 통해 다중 구현이 가능해 다양한 기능을 클래스에 쉽게 추가할 수 있습니다.
1.2 역할과 행동 정의
인터페이스는 특정 기능을 정의하기 위한 계약을 제공합니다. 이를 통해 서로 다른 구현체들이 동일한 계약(인터페이스)을 따르도록 강제할 수 있습니다. 이로써 각기 다른 구현 클래스들이 일관된 방식으로 동작하도록 보장할 수 있습니다.
interface Payment {
void processPayment();
}
// 신용카드 결제 방식
class CreditCardPayment implements Payment {
@Override
public void processPayment() {
System.out.println("Processing credit card payment...");
}
}
// 페이팔 결제 방식
class PayPalPayment implements Payment {
@Override
public void processPayment() {
System.out.println("Processing PayPal payment...");
}
}
public class PaymentService {
public static void main(String[] args) {
Payment payment1 = new CreditCardPayment();
payment1.processPayment(); // 신용카드 결제 처리
Payment payment2 = new PayPalPayment();
payment2.processPayment(); // 페이팔 결제 처리
}
}
이 예제에서 Payment
인터페이스는 결제 처리를 강제하는 계약 역할을 합니다. 이를 구현하는 CreditCardPayment
와 PayPalPayment
는 각각의 결제 방식을 정의하며, 일관된 방식으로 처리됩니다. 추상 클래스 대신 인터페이스를 사용하면 여러 결제 방식이 유연하게 확장될 수 있습니다.
1.3 구현과 무관한 설계
인터페이스는 필드를 가질 수 없고, 기본적으로 메서드의 시그니처만 정의합니다. 이는 인터페이스가 구현과는 무관한 설계를 제공하기 때문에, 구현체에 대한 의존성을 낮출 수 있습니다.
interface Storage {
void save(String data);
}
class DatabaseStorage implements Storage {
@Override
public void save(String data) {
System.out.println("Saving data to database: " + data);
}
}
class FileStorage implements Storage {
@Override
public void save(String data) {
System.out.println("Saving data to file: " + data);
}
}
public class StorageService {
public static void main(String[] args) {
Storage storage = new DatabaseStorage(); // 데이터베이스에 저장
storage.save("User data");
storage = new FileStorage(); // 파일에 저장
storage.save("User data");
}
}
Storage
인터페이스는 저장이라는 기능을 정의하고, DatabaseStorage
와 FileStorage
는 각기 다른 저장 방식으로 구현됩니다. 이렇게 구현과 무관한 인터페이스를 통해, 의존성을 줄이고 유지보수를 쉽게 할 수 있습니다.
2. 다중 상속 VS 다중 구현
2.1 다중 상속 (Multiple Inheritance)
자바에서 인터페이스 간의 다중 상속은 가능하지만, 클래스 간의 다중 상속은 허용되지 않습니다. 인터페이스 간의 다중 상속을 사용하면 하나의 인터페이스가 여러 개의 다른 인터페이스로부터 상속받아, 상위 인터페이스들의 메서드를 모두 통합하여 제공할 수 있습니다.
// 첫 번째 인터페이스: 날 수 있는 행동 정의
interface Flyable {
void fly();
}
// 두 번째 인터페이스: 수영할 수 있는 행동 정의
interface Swimmable {
void swim();
}
// 세 번째 인터페이스: 두 가지 행동을 모두 상속받아 통합하는 인터페이스
interface AquaticBird extends Flyable, Swimmable {
// 이 인터페이스는 Flyable과 Swimmable을 다중 상속 받음
// 별도의 메서드 추가 없이 두 인터페이스의 기능을 통합
}
// AquaticBird 인터페이스를 구현하는 클래스
class Duck implements AquaticBird {
@Override
public void fly() {
System.out.println("The duck is flying.");
}
@Override
public void swim() {
System.out.println("The duck is swimming.");
}
}
public class Main {
public static void main(String[] args) {
Duck duck = new Duck();
// Duck 클래스는 AquaticBird 인터페이스를 구현했기 때문에 fly와 swim 메서드를 모두 사용할 수 있음
duck.fly(); // 출력: The duck is flying.
duck.swim(); // 출력: The duck is swimming.
}
}
Flyable
인터페이스는fly()
메서드를 정의합니다.Swimmable
인터페이스는swim()
메서드를 정의합니다.AquaticBird
인터페이스는 다중 상속을 통해Flyable
과Swimmable
을 상속받아 두 가지 행동을 모두 제공할 수 있는 인터페이스가 됩니다.Duck
클래스는AquaticBird
를 구현함으로써fly()
와swim()
메서드를 모두 구현해야 합니다.
이처럼 인터페이스 간의 다중 상속을 통해, 여러 행동을 상속받고 이를 하나의 인터페이스로 통합할 수 있습니다. 이를 구현하는 클래스는 이 통합된 인터페이스의 모든 메서드를 구현해야 합니다.
2.2 다중 구현 (Multiple Implementation)
다중 구현은 한 클래스가 여러 인터페이스를 동시에 구현하는 것을 의미합니다. 자바에서 매우 흔히 사용되는 패턴이며, 클래스가 여러 가지 역할을 동시에 수행할 수 있도록 해줍니다.
// Flyable 인터페이스 정의: 날 수 있는 행동 정의
interface Flyable {
void fly();
}
// Swimmable 인터페이스 정의: 수영할 수 있는 행동 정의
interface Swimmable {
void swim();
}
// 클래스가 두 개의 인터페이스를 동시에 구현
class SuperBird implements Flyable, Swimmable {
@Override
public void fly() {
System.out.println("The bird is flying.");
}
@Override
public void swim() {
System.out.println("The bird is swimming.");
}
}
public class Main {
public static void main(String[] args) {
SuperBird bird = new SuperBird();
// SuperBird는 Flyable과 Swimmable 인터페이스를 동시에 구현했으므로 두 가지 기능 모두 가능
bird.fly(); // 출력: The bird is flying.
bird.swim(); // 출력: The bird is swimming.
}
}
Flyable
과Swimmable
두 인터페이스는 각각의 행동을 정의하고 있습니다.SuperBird
클래스는 이 두 인터페이스를 다중 구현하고 있으며, 두 가지 행동을 모두 제공합니다.- 메인 함수에서는
SuperBird
객체가 두 가지 역할(날기, 수영하기)을 수행할 수 있습니다.
3. JPA에서 다중 상속 및 다중 구현을 사용한 예시
3.1 JPA 레포지토리 기본 구조와 다중 상속
JPA에서 기본적으로 사용하는 레포지토리는 JpaRepository
라는 인터페이스를 상속받아 사용합니다. JpaRepository
는 Spring Data JPA에서 제공하는 인터페이스로, 엔티티에 대해 CRUD(Create, Read, Update, Delete) 기능을 자동으로 제공합니다. 이 기본적인 리포지토리 외에, 커스텀 기능을 추가하기 위해 또 다른 인터페이스를 상속받을 수 있습니다. 이때, 다중 상속 개념이 사용됩니다.
// JpaRepository를 상속받아 기본적인 CRUD 기능 제공
// 추가로 커스텀 인터페이스(SalesCommercialCustom)도 상속하여 확장된 기능 제공
public interface SalesCommercialRepository extends JpaRepository<SalesCommercial, Long>, SalesCommercialCustom {
// 기본적인 CRUD 메서드 외에 커스텀 메서드도 함께 사용할 수 있음
}
JpaRepository<SalesCommercial, Long>
:SalesCommercial
엔티티에 대한 기본적인 CRUD 메서드(예:findById
,save
,delete
등)를 제공SalesCommercialCustom
: 커스텀 쿼리나 복잡한 비즈니스 로직을 처리할 수 있는 메서드를 추가하기 위한 사용자 정의 인터페이스
3.2 다중 상속의 의미
- 다중 상속이란, 여기서
SalesCommercialRepository
가 두 개의 인터페이스(JpaRepository
와SalesCommercialCustom
)를 상속받아, 두 인터페이스의 기능을 모두 하나로 통합하는 것을 의미합니다. 이를 통해 기본적인 CRUD 기능과 커스텀 기능을 모두 하나의 레포지토리에서 사용할 수 있습니다. - 이 구조 덕분에, 서비스 레이어에서는 레포지토리 객체를 사용하여 기본 기능과 확장된 기능을 동시에 사용할 수 있습니다. 즉, 레포지토리 하나로 두 가지 역할을 동시에 수행할 수 있게 되는 것이죠.
3.3 커스텀 인터페이스와 구현체에서 다중 구현
JpaRepository
만으로는 복잡한 쿼리나 커스텀 로직을 처리하는 데 한계가 있습니다. 이를 해결하기 위해 커스텀 인터페이스를 정의하고, 이를 구현하는 구현 클래스를 만들어 복잡한 비즈니스 로직이나 커스텀 쿼리를 처리할 수 있습니다.
// 커스텀 인터페이스 정의: 복잡한 비즈니스 로직 또는 커스텀 쿼리 작성
public interface SalesCommercialCustom {
// 커스텀 메서드 정의: 특정 기간과 상업 코드를 기준으로 다른 매출을 가져오는 메서드
Long getOtherSalesByPeriodCodeAndCommercialCode(String periodCode);
}
SalesCommercialCustom
인터페이스는 JPA의 기본 CRUD 메서드 외에, 커스텀 메서드를 정의하기 위한 인터페이스입니다. 이 인터페이스를 구현하는 클래스에서 직접 쿼리를 작성하거나, 비즈니스 로직을 처리할 수 있습니다.
다음으로, 이 커스텀 인터페이스를 구현하는 클래스가 필요합니다. 이때 다중 구현 개념을 적용합니다.
// 커스텀 리포지토리를 구현하는 클래스
@Repository
@RequiredArgsConstructor
public class SalesCommercialCustomImpl implements SalesCommercialCustom {
// JPAQueryFactory를 통해 커스텀 쿼리 작성 (QueryDSL 사용)
private final JPAQueryFactory queryFactory;
@Override
public Long getOtherSalesByPeriodCodeAndCommercialCode(String periodCode) {
// Q 클래스는 QueryDSL에서 생성된 엔티티 클래스
QSalesCommercial salesCommercial = QSalesCommercial.salesCommercial;
// QueryDSL을 사용하여 커스텀 쿼리 작성
return queryFactory
.select(salesCommercial.monthSales.sum())
.from(salesCommercial)
.where(salesCommercial.periodCode.eq(periodCode))
.fetchOne(); // 쿼리 실행 후 결과 반환
}
}
SalesCommercialCustomImpl
클래스는SalesCommercialCustom
인터페이스를 구현하고 있으며, 이 구현체에서 복잡한 비즈니스 로직 또는 커스텀 쿼리를 처리합니다.- 이 예시에서는 QueryDSL을 사용해 커스텀 쿼리를 작성하고 있습니다. QueryDSL을 사용하면 타입 안전한 쿼리를 작성할 수 있으며, 복잡한 SQL 쿼리 대신 자바 코드로 데이터베이스 질의를 수행할 수 있습니다.
3.4 서비스 레이어에서의 다중 상속 및 다중 구현 사용
JPA에서 리포지토리 인터페이스는 다중 상속을 통해 기본 CRUD 기능과 커스텀 기능을 모두 제공하게 됩니다. 이를 서비스 레이어에서 빈 주입을 통해 사용하게 됩니다. 레포지토리를 빈으로 주입하는 방식은 @Autowired
나 @RequiredArgsConstructor
를 사용하여 생성자 주입을 통해 처리합니다.
@Service
@RequiredArgsConstructor
public class SalesCommercialService {
// 리포지토리를 생성자 주입 방식으로 주입받음
private final SalesCommercialRepository salesCommercialRepository;
public void processSalesData(String commercialCode, String serviceCode) {
// 커스텀 리포지토리 메서드 사용하여 데이터 조회
List<SalesCommercial> sales = salesCommercialRepository.findByCommercialCodeAndServiceCodeAndPeriodCodeIn(
commercialCode, serviceCode, List.of("2021", "2022")
);
// 조회된 데이터 처리
System.out.println("Sales data processed: " + sales.size());
}
public Long getCustomSalesData(String periodCode) {
// 커스텀 메서드를 호출하여 커스텀 쿼리 실행
return salesCommercialRepository.getOtherSalesByPeriodCodeAndCommercialCode(periodCode);
}
}
- 리포지토리 빈 주입:
SalesCommercialRepository
는 다중 상속을 통해JpaRepository
와SalesCommercialCustom
의 기능을 모두 상속받습니다. 이를 서비스 클래스에 생성자 주입으로 주입받습니다. - 기본 기능 사용:
findByCommercialCodeAndServiceCodeAndPeriodCodeIn
메서드는 JpaRepository에서 제공하는 기본적인 CRUD 메서드 중 하나입니다. - 커스텀 기능 사용:
getOtherSalesByPeriodCodeAndCommercialCode
메서드는 커스텀 리포지토리 구현에서 정의된 메서드로, 서비스 레이어에서 호출할 수 있습니다.
3.5 JPA에서 다중 상속과 다중 구현의 장점
1) 코드 재사용성 증가
기본 CRUD 기능과 커스텀 로직을 하나의 리포지토리에서 통합하여 사용할 수 있기 때문에, 각 기능을 별도로 구현할 필요가 없어 코드의 재사용성이 높아집니다.
2) 유연한 설계
다중 상속을 통해 필요한 인터페이스를 통합하고, 다중 구현을 통해 복잡한 비즈니스 로직이나 커스텀 쿼리를 유연하게 구현할 수 있습니다.
3) Spring Data JPA의 자동 처리
JpaRepository
인터페이스는 Spring Data JPA에 의해 자동으로 구현됩니다. 별도의 Impl
클래스를 작성하지 않고도 기본적인 CRUD 기능을 사용할 수 있으며, 커스텀 기능이 필요할 경우에만 CustomImpl
클래스를 추가로 작성하면 됩니다.
4) 비즈니스 로직과 데이터 접근의 분리
커스텀 리포지토리를 사용하면 데이터베이스에 직접 접근하는 로직과 비즈니스 로직을 분리할 수 있어, 서비스 레이어의 코드가 보다 깔끔해지고 유지보수성이 향상됩니다.
4. 정리
- 인터페이스의 다중 상속은 여러 인터페이스의 메서드를 통합하여 하나의 인터페이스로 결합할 수 있으며, 자바는 이를 통해 코드의 재사용성을 극대화할 수 있습니다.
- 다중 구현은 한 클래스가 여러 인터페이스를 동시에 구현하여 다양한 역할을 수행할 수 있게 해줍니다. 이는 특히 JPA에서 커스텀 레포지토리나 다양한 서비스 계층에서 활용됩니다.
- JPA에서 다중 상속과 다중 구현을 통해 기본적인 CRUD 기능과 커스텀 기능을 동시에 사용할 수 있으며, 서비스 레이어에서는 빈 주입을 통해 이를 쉽게 사용할 수 있습니다.
'BackEnd > JAVA' 카테고리의 다른 글
[JAVA] 제네릭 (Generic) 정리 (1) | 2025.01.26 |
---|---|
[JAVA] 스레드 (Thread) 정리 (2) | 2025.01.25 |
[JAVA] try-with-resources 정리 (0) | 2025.01.19 |
[JAVA] 자바 부모클래스 및 인터페이스 심화: 오버라이딩과 메서드 동작 (0) | 2025.01.17 |
[JAVA] 인터페이스 정리 (0) | 2025.01.12 |