3. 오브젝트와 의존관계1
1. 의존관계
의존관계는 반드시 두 개 이상의 대상이 존재하고, 하나의 대상이 다른 대상을 사용, 호출, 생성, 인스턴스화, 전송 등을 할 때 의존관계가 있다고 이야기한다.
쉽게 생각해서 하나의 객체에 다른 객체를 사용하기 위해 내부적으로 변수를 정의하고 있다면 의존관계가 있다고 이야기한다.
클래스 사이에 의존관계가 있을 때 의존 대상이 변경되면 이를 사용하는 클래스의 코드도 영향을 받는다.
클래스 레벨의 의존관계는 코드를 보면 알 수 있다.
오브젝트 간 의존관계는 런타임에 이루어지기 때문에, 코드를 보아도 알 수 없는 경우가 있다.
오브젝트 간 의존관계는 인터페이스를 통해서 코드에 보여지기 때문에, 명확하게 알 수 없는 경우가 있다.
예시 코드
아래 코드는 의존관계가 매우 강하게 연결되어 있다.
PaymentService.prepare()메서드에서 환율정보를 가져오고, 원화로 계산하는 모든 기능을 담당하고 있다.
package spring.hellospring;
import java.math.BigDecimal;
import java.time.LocalDateTime;
public class Payment {
private Long orderId;
private String currency;
private BigDecimal foreignCurrencyAmount;
private BigDecimal exRate;
private BigDecimal convertedAmount;
private LocalDateTime validUntil;
public Payment(Long orderId, String currency, BigDecimal foreignCurrencyAmount, BigDecimal exRate, BigDecimal convertedAmount, LocalDateTime validUntil) {
this.orderId = orderId;
this.currency = currency;
this.foreignCurrencyAmount = foreignCurrencyAmount;
this.exRate = exRate;
this.convertedAmount = convertedAmount;
this.validUntil = validUntil;
}
// getter, setter, toString
}2. 관심사의 분리 (Separation of concerns)
관심사는 동일한 이유로 변경되는 코드의 집합으로 현재
PaymentService로직에는 2개의 관심사가 있다.관심사1 : 환율정보 가져오기
관심사2 : 가져온 환율을 기준으로
Payment오브젝트 리턴
한 메서드 안에 여러개의 관심사가 있다면 분리한다.
리팩터링리팩터링은 기존의 코드를 외부의 동작방식에는 변화 없이 내부 구조를 변경해서 재구성하는 작업 또는 기술을 말한다.
리팩토링을 하면 코드 내부의 설계가 개선되어 코드를 이해하기가 더 편해지고, 변화에 효율적으로 대응할 수 있다. 결국, 생산성은 올라가고, 코드의 품질은 높아지며, 유지보수하기 용이해지고, 견고하면서도 유연한 제품을 개발할 수 있다.
리팩토링이 필요한 코드의 특징을 나쁜 냄새라고 부르기도 한다. 대표적으로, 중복된 코드는 매우 흔하게 발견되는 나쁜 냄새다.
메서드 추출(extract method) 리팩터링
PaymentService.prepare()매서드 내 2가지 책임관심사1 : 환율정보 가져오기
관심사2 : 가져온 환율을 기준으로
Payment오브젝트 리턴
메서드 추출을 통해서 한 메서드 내에 가지고 있는 관심을 분리해주었다.
3. 상속을 통한 확장
하지만 조금 더 큰 관점에서 바라보자면, 여러 관심사를 하나의 클래스에서 처리하는 것은 유지보수 측면에서 바람직하지 못하다.
때문에, 상속을 통한 확장을 통해서 환율 정보를 가져오는 메서드를 다른 클래스로 분리해보자.
아 그리고,
PaymentSerivce를 실행하는 관심사도 다른 클래스(Client)로 분리해보자.
이후부터는 상속을 통한 클래스 구현으로 환율을 가져오는 정책을 변경할 수 있다.
고정된 환율을 가져오는 SimpleExRatePaymentService 를 만들어보자.
SimpleExRatePaymentService 를 만들어보자.문제 없이 사용되는 것을 볼 수 있다.
문제점
상속은 관심사가 다른 코드를 분리해내고, 서로 독립적으로 변경 또는 확장할 수 있도록 만드는 간단하면서도 효과적인 방법이다.
하지만, 상속은 매우 큰 단점이 있다.
자바는 다중상속을 허용하지 않는다. 따라서 또 다른 관심사를 분리할 경우, 기능을 확장하기 매우 어렵다!
예를 들어, VIP 에게 추가 환율을 적용해주는 정책이 추가되면 해당 기능을 구현하는 메서드를 추가해야 한다.
해당 정책을 사용하지 않는 서브 클래스 또한, 해당 메서드를 구현해야 한다.
또, 상속은 상하위 클래스의 관계가 매우 밀접하다. 상위 클래스의 변경에 따라 하위 클래스를 모두 변경하는 상황이 발생할 수 있다.
유연성이 높은 설계는 결합도를 낮추는 것이 중요하다.
디자인 패턴에 일부 적용된 특별한 목적을 위해서 사용하는 경우가 아니라면, 상속을 통한 확장은 관심사 분리에 사용하기에는 분명한 단점이 있다. (상속의 한계)
4. 클래스 분리
단순히 상속을 사용하는 것은 부모, 자식 클래스 간 강한 결합이 발생한다는 점이 큰 단점이다.
관심사에 따라서 클래스를 분리해서 각각 독립적으로 사용할 수도 있다.
예를 들어, 환율정보를 가져오는 관심사를 클래스 분리를 통해 각각 독립적으로 사용할 수 있다.
여기서 한발자국 더 나아가보자, PaymentService 입장에서 환율을 가져오는 로직은 필수적이다 때문에, 해당 정책을 관리하는 인스턴스 변수를 만들고 생성자를 통해서 초기화해주자.
PaymentService 입장에서 환율을 가져오는 로직은 필수적이다 때문에, 해당 정책을 관리하는 인스턴스 변수를 만들고 생성자를 통해서 초기화해주자.문제점
결과적으로 클래스 레벨에 사용 의존관계가 만들어지기 때문에, 강한 코드 수준의 결합이 생긴다. 실제로 사용할 클래스가 변경되면 이를 이용하는 쪽에서도 변경이 일어나게 된다. 상속을 통한, 확장 처럼 유연한 확장도 불가능해진다..
상속한 것이 아니기 때문에 사용하는 클래스의 메서드 이름과 구조도 제각각일 수 있다. 그래서 클래스가 변경되면 많은 코드가 따라서 변경되어야 한다.
클래스가 다르다는 것을 제외하면 관심사의 분리가 잘 된 방법은 아니다..
5. 인터페이스 도입
현재
PaymentService입장에서 환율을 가져오는 방법이 변경되면 아예 사용하는 코드가 달라질 수 있다.목적은 같지만, 동작의 일관성이 없을 수 있다..
환율을 가져오는 클래스는 인터페이스를 상속해
PaymentService입장에서는 동일한 인터페이스의 메서드를 사용하도록 한다. 때문에PaymentService가 의존하는 클래스가 변경되더라도 사용하는 메서드 이름의 변경이 일어나지 않는다.
문제점
하지만, 클래스 인스턴스는 만드는 생성자를 호출하는 코드에는 클래스 이름이 등장하기 때문에, 사용하는 환율 정보를 가져오는 클래스가 변경되면
PaymentService의 코드도 일부분이지만 따라서 변경되어야 한다.PaymentService에서는 환율을 가져오는 구현체와 강력한 결합도를 가진다.
여전히 상속을 통한 확장만큼의 유연성도 가지지 못한다.
6. 관계설정 책임의 분리
인터페이스를 사용했을 때 단점
앞에서
ExRateProvider인터페이스를 사용하도록 작성했지만, 구현 클래스에 대한 정보를 가지고 있다면,PaymentService여전히ExRateProvider를 구현한 특정 클래스에 의존하는 코드가 된다.자신이(
PaymentService) 어떤 클래스의 오브젝트를 사용할지를 결정한다면 관계설정 책임을 직접 가지고 있는 것이다.이 관계설정 책임을 자신을 호출하는 앞의 오브젝트에게(
Client) 넘길 수 있다. 이렇게 되면 코드 레벨의 의존관계에서 자유로워진다. 이후에는 오직 인터페이스에만 의존하는 코드가 되기 때문에, 구현 클래스의 오브젝트를 사용하게 되더라도PaymentService의 코드가 변경되지 않는다.관계설정의 책임을 가진 앞의 클래스(
Client) 는 생성자를 통해서 어떤 클래스의 오브젝트를 사용할지 결정한 것을 전달해주면 된다.


7. 오브젝트 팩토리
현재
Client는 클라이언트로서의 책임과PaymentService의 관계를 설정해주는 2가지의 책임을 가지고 있다.관계설정의 책임은 다른 클래스로 넘겨주도록 하자.
결합도가 낮은 설계는 하나의 클래스에 하나의 책임만 있어야 한다는 것이다.
ObjectFactory클래스를 만들어 관계설정의 책임을 부여하자.

8. 원칙과 패턴
원칙은 객체지향의 원칙, 패턴은 디자인 패턴을 의미한다.
개방-폐쇄 원칙(Open-Closed Principle)
클래스나 모듈은 확장에 열려 있어야 하고, 변경에는 닫혀 있어야 한다.
클래스가 기능을 확장할 때 클래스의 코드는 변경되지 않아야 한다.
아래 그림을 볼 때
ExRateProvider의 기능이 변경(확장) 되더라도,PaymentService의 코드는 변경되지 않는다.아래 그림에서는
ExRateProvider의 구현체를Client에서 넣어준다.

높은 응집도와 낮은 결합도(High Coherence and low coupling)
응집도가 높다는 것은 하나의 모듈이 하나의 책임 또는 관심사에 집중되어 있다는 뜻. 때문에, 변화가 일어날 때 해당 모듈에서 변하는 부분이 크다.
응집도가 높은 코드는 코드의 변경이 일어날 시, 다른 코드에 영향도가 적다.
유지보수 비용이 적다!
책임과 관심사가 다른 모듈과는 낮은 결합도, 즉 느슨하게 연결된 형태를 유지하는 것이 좋다.
전략 패턴(Strategy Pattern)
자신의 기능 맥락(Context) 에서, 필요에 따라서 변경이 필요한 알고리즘을 인터페이스를 통해 통째로 외부로 분리시키고, 이를 구현한 구체적인 알고리즘 클래스를 필요에 따라서 바꿔 사용할 수 있게 하는 디자인 패턴
우리 코드에서는
PaymentService라는 맥락 안에서ExRateProvider에 해당 하는 전략을Client가 수정하여 사용할 수 있다.다른 예로, 아래처럼
Collections.sort()를 사용할 때 다양한 전략들을 넣어주어 사용할 수 있다.전략은 아마 함수형 인터페이스이지 않을까?
제어의 역전(Inversion of Control)
제어권 이전을 통한 제어관계 역전 (프레임워크 기본동작 원리)
아래 그림과 같이 우리는 코드를 발전시켜 나가면서
PaymentService에 있던 객체 생성의 책임을Client로 이전했다.


Last updated