1. 의존성 관리 부재로 인한 문제점
1-1. 문제 상황
OOP는 오브젝트와 데이터에 초점을 두고 있는 패러다임이다. 따라서 OOP 기반으로 설계된 프로그램은 객체와 객체 간 관계를 통해서 프로그램을 정의할 수 있다.
하지만, 객체 간 관계인 의존관계(dependency)에 있어서 한 객체가 변경되었을 때 다른 연관 객체들에서 문제가 발생하는 의존 관계 문제가 발생할 수 있다. 이러한 의존 관계 문제는 복잡한 객체 간 의존 관계를 갖는 프로그램에서 그 문제가 극대화될 수 있다. 이는 클래스의 재사용성이 낮아지고, 좋지 않은 프로그래밍 패턴으로 이어질 수 있기에 의존 관계 관리를 통해서 적절하게 해결해야 한다.
2. 클래스 간 상속의 단점
2-1. 클래스 간 상속의 문제점
상속은 이전에 코드의 재사용성을 높이는 강력한 기술로 널리 사용되어왔다.
하지만, 클래스 간 상속은 아래와 같은 문제들이 있다.
- 상속의 관계는 비대칭적이다.
- 자식 클래스는 부모 클래스에 접근 가능하지만, 부모 클래스는 자식 클래스에 접근할 수 없다.
- 상속은 부모 클래스와 자식 클래스 간 매우 강한 결합을 생성한다.
- 부모 클래스의 구현이 변경되면, 이를 사용하는 자식 클래스에서 문제가 발생할 수 있다.
- 자식 클래스가 부모 클래스의 필드를 수정한다면, 부모 클래스에서 문제가 문제가 발생할 수 있고 캡슐화가 깨진다.
- 런타임 환경에서 클래스 간 관계가 고정된다.
- 결과적으로 동적인 교체가 불가능하므로 런타임에 다른 객체를 주입하거나 변경하는 것이 어렵다.
2-2. 개선 방향
이를 개선하는 방법은 접근 제어자를 통해서 부모 클래스에서 필수적인 인터페이스만을 자식 클래스에 공개하는 것이다.
다만, 일반적으로 상속은 매우 복잡하기에 인터페이스 등을 활용한 다형성을 사용할 목적이 아니라면 아래 컴포지션을 사용하는 것이 좋다.
3. 컴포지션 ( composition )
3-1. 컴포지션 기본 개념
컴포지션은 어떤 객체의 기능을 재사용하고 싶으면, 그 기능을 가진 객체를 다른 객체에 포함( has - a )시키는 것이다.
즉, 필요한 기능을 갖는 객체의 소유를 새로운 기능을 갖는 객체에 위임하는 것을 말한다.
아래 예시는 컴포지션을 적용한 적절한 예시이다.
interface Engine{
public void start();
public void stop();
}
class CarEngine impelemnts Engine{
public void start(){
// todo
}
public void stop(){
// todo
}
}
class Car{
private final Engine engine;
private boolean isStarted = false;
public Car(){
this.engine = new CarEngine();
}
public void run(){
engine.start();
isStarted = true;
}
public void stop(){
engine.stop();
isStarted = false;
}
}
위 코드에서는 관계의 주인인 Car 객체가 CarEngine 객체의 생명주기를 관리하고, 기능을 재사용한다.
이처럼 컴포지션은 객체 간 계층 구조를 형성하닌 것이 아닌 필요한 객체를 새로운 객체 내부에 생성하는 구조이다.
컴포지션의 경우, 객체 간 관계의 주인이 명확하고 객체들의 생명주기는 관계의 주인을 따른다.
3-2. 컴포지션 장단점
장점
- 소유자 객체에 의해 소유된 객체는 소유자 객체가 제공하는 인터페이스를 통해서만 접근 가능 ( 캡슐화 극대화 )
- 내부 구현을 몰라도 외부에 제공되는 인터페이스를 통해 기능을 재사용 가능 ( black box reuse )
- 컴포지션의 확장인 애그리게이션을 통해 런타임에 동적으로 의존 관계 설정이 가능함 ( DI )
단점
- 동적 의존 관계 설정을 위해서 더 많은 객체를 생성해야함
- 상속은 자식 클래스만 생성하면 되지만, 컴포지션은 연관 클래스의 객체를 모두 생성해야함
- 인터페이스는 많은 객체에서 재사용하기 쉽도록 신중하게 정의되어야함
3-3. Association, Aggregation, Composition
Association
Association은 두 개 이상의 객체가 서로 알고 있는 상태로, 객체들이 서로 독립적인 생명 주기를 갖는 관계를 말한다.
객체 간 소유권이 없기에 한 객체가 삭제되어도 다른 객체는 여전히 존재할 수 있다.
class School {}
class Student {
private School school;
}
이처럼 Association은 아래와 같이 정리할 수 있다.
- 관계 : has - a
- 관계의 주인 : 없음
- 생명주기 : 독립
- 목적 : 단순 참조
Aggregation
Association은 Association을 확장한 개념으로, 마찬가지로 객체 간 독립적인 생명 주기를 갖는 관계이다.
하지만, Aggregation은 객체 간 소유권이 존재한다는 특징이 있다 ( has-a + whole-part )
class Player {}
class Team {
private List<Player> players;
}
즉 Aggregation은 아래와 같이 정리 가능하다.
- 관계 : has - a + whole-part ( 부분-전체 관계 )
- 관계의 주인 : 있음
- 생명주기 : 독립
- 목적 : 전체-부분 의미 강조
Composition
Composition은 Aggregation의 확장 개념으로, 객체들의 생명 주기가 관계의 주인을 따른다.
class Car {
private final Engine engine = new Engine();
}
class Engine {}
따라서 Composition은 아래와 같이 나타낼 수 있다.
- 관계 : has - a + whole-part ( 부분-전체 관계 ) + 생명주기 의존
- 관계의 주인 : 있음
- 생명주기 : 의존
4. 의존성 관리 방법
4-1. 인터페이스 기반의 서비스 정의
의존성 관리를 위한 가장 대표적인 방법은 인터페이스를 기반으로 서비스를 추상화하는 것이다.
public interface PaymentMethod{
void pay(int cost);
}
public class KakaoPaymentMethod implements PaymentMethod {
// todo
}
public class CreditCardPaymentMethod implements PaymentMethod {
// todo
}
public class ProductService {
public void buy(Product product, PaymentMethod method){
method.pay(product.cost);
}
}
위 방식은 인터페이스 기반 추상화와 다형성을 통해서 의존 관계를 런타임 시점에 동적으로 주입받을 수 있다는 장점이 있다.
또한, 인터페이스는 고정된 계약이므로 외부에 노출된 메서드 등의 시그니처가 변경되지 않기에 내부를 몰라도 재사용이 용이하다.
4-2. 상속을 인터페이스 기반으로 변경
다음으로 상속을 인터페이스 기반으로 변경하는 방법이다.
이는 클래스 간 상속 방식을 클래스와 인터페이스 간 관계로 바꾸어 클래스 간 높은 결합을 완화시키는 역할을 한다.
public interface PaymentMethod{
void pay(int cost);
}
public class KakaoPaymentMethod implements PaymentMethod {
// todo
}
public class PaymentService implements PaymentMethod {
private final PaymentMethod paymentMethod;
public PaymentService(PaymentMethod paymentMethod){
this.paymentMethod = paymentMethod;
}
public void buy(int price){
paymentMethod.pay(price);
System.out.println("buy product");
}
}
이처럼 인터페이스 기반으로 PaymentService와 KakaoPaymentMethod 클래스 간 강한 결합을 낮췄다.
이를 통해서 기존 상속의 문제를 어느정도 보완하고 다형성의 장점을 극대화할 수 있다.
4-3. 서비스를 일련의 기준을 통해 정의
기본적으로 인터페이스는 클라이언트와 서비스 제공자 간 일련의 계약이다.
해당 계약은 Precondition과 Postcondition으로 나눌 수 있다.
- Precondition : 매개변수 등의 서비스 호출 시의 규약
- Postcondition : 반환 값과 그 의미 등의 서비스 호출 후의 규약
이러한 일련의 계약을 통해 서비스 사용 시 내부 구조를 모르더라도 해당 기능을 사용 가능하다.
뿐만 아니라 필요한 기능만을 명시할 수 있기에 시스템의 복잡도도 낮출 수 있다.
5. 효과적인 인터페이스 사용 전략
5-1. 인터페이스 기반 설계의 문제
지금까지는 인터페이스와 의존성 관리를 통해 복잡한 객체 연관관계를 완화시키는 방법에 대해서 알아보았다.
확실히 인터페이스를 사용했을 때 객체 지향 프로그래밍의 장점을 극대화할 수 있는 것을 확인할 수 있었다.
하지만 인터페이스에는 장점만 있는 것이 아니다.
- 단일 구현체만 존재하는 경우, 인터페이스를 사용하는 것은 관리해야하는 객체의 수를 늘리므로 관리 난이도가 올라간다.
- 인터페이스는 객체의 역할을 고정하는 역할을 하기에, 추후 요구사항이 변경된다면 인터페이스 수정 및 확장에서 어려움이 있을 수 있다
따라서 적절한 사용 전략을 수립하여 인터페이스를 사용하는 것이 좋다.
5-2. 인터페이스 사용 전략
인터페이스를 어떻게 사용할지는 객체 지향 프로그래밍에서 뜨거운 논쟁거리이다.
이 부분은 크게 정답이 없다고 판단되어 아래와 같이 정의하여 사용해볼 예정이다.
사용하면 좋은 상황
- 여러개의 구현체가 존재하거나 존재할 가능성이 있는 경우
- 구현체가 한 개 이상일 때, 테스트 및 병렬 개발이 필요한 경우
- 구현체의 교체 가능성이 있는 경우
사용하면 안좋은 경우
- 단일 구현체만 존재하고, 해당 상황이 지속될 가능성이 높은 경우
- 구현체의 변경 및 교체 가능성이 거의 없는 경우
물론 이 것이 정답은 아니다.
현재 개발하고 있는 서비스의 상황에 맞게 적절하게 사용하는 것이 중요할 것이다.
6. 마무리
객체 지향 프로그래밍에서 어떻게 의존 관계를 관리하면서 재사용성과 확장성 등을 개선하는 것은 소프트웨어 품질 향상에서 중요하다.
따라서 이번에는 추상화와 다형성 등을 활용하여 의존 관계를 관리하는 기법에 대해서 알아보았다.
하지만, 모든 것에는 trade-off가 있듯이, 이를 활용함으로써 발생하는 득과 실을 명확히 하고 전략을 세우는 것이 매우 중요할 것이다.
'CS 및 기본 개념' 카테고리의 다른 글
| [ SQL ] 인덱스와 동작 원리 (0) | 2025.11.05 |
|---|---|
| [ cs ] Object Oriented Method (1) | 2025.09.10 |
| [ cs ] 객체 지향 프로그래밍( OOP ) 4가지 특징 (1) | 2025.09.04 |
| [ Java 공식문서 ] Lock과 Executor (1) | 2025.08.20 |
| [Java 공식문서] 불변 객체 (4) | 2025.08.15 |