Java에서 오브젝트란 클래스의 인스턴스, 또는 배열이다. 이는 기술적으로는 완벽한 대답
PaymentService.prepare
코드 예제를 살펴보자
Payment
객체를 리턴public class PaymentService {
public Payment prepare(Long orderId, String currency, BigDecimal foreignCurrencyAmount) throws IOException {
// 환율 가져오기
URL url = new URL("https://open.ir-api.com/v6/latest/" + currency);
HttpURLConnection connection = (HttpURLConnection) url.openConnection();
BufferedReader br = new BufferedReader(new InputStreamReader(connection.getInputStream()));
String response = br.lines().collect(Collectors.joining());
br.close();
ObjectMapper mapper = new ObjectMapper();
ExRateData data = mapper.readValue(response, ExRateData.class);
BigDecimal exRate = data.rates().get("KRW");
BigDecimal convertedAmount = foreignCurrencyAmount.multiply(exrate);
LocalDateTime validUntil = LocalDateTime.now().plusMinutes(30);
return new Payment(orderId, currency, foreignCurrencyAmount, exRate, convertedAmount, validUntil);
}
}
Payment
를 준비하는 로직Payment
준비 로직은 서비스의 로직이 변경되면 변경될 것이다.public class PaymentService {
public Payment prepare(Long orderId, String currency, BigDecimal foreignCurrencyAmount) throws IOException {
BigDecimal exRate = getExRate(currency);
BigDecimal convertedAmount = foreignCurrencyAmount.multiply(exrate);
LocalDateTime validUntil = LocalDateTime.now().plusMinutes(30);
return new Payment(orderId, currency, foreignCurrencyAmount, exRate, convertedAmount, validUntil);
}
private BigDecimal getExRate(String currency) throws IOException {
// ...
return exRate;
}
}
PaymentService
는 여전히 관심사가 분리되어 있지 않다.
ExRate
생성 책임을 서브 클래스에게 위임하는 방식abstract public class PaymentService {
public Payment prepare(Long orderId, String currency, BigDecimal foreignCurrencyAmount) throws IOException {
BigDecimal exRate = getExRate(currency);
BigDecimal convertedAmount = foreignCurrencyAmount.multiply(exrate);
LocalDateTime validUntil = LocalDateTime.now().plusMinutes(30);
return new Payment(orderId, currency, foreignCurrencyAmount, exRate, convertedAmount, validUntil);
}
abstract BigDecimal getExRate(String currency) throws IOException {
}
PaymentService
를 구현하는 구현체를 작성해보자
WebApiExRatePaymentService
public class WebApiExRatePaymentService extends PaymentService {
@Override
BigDecimal getExRate(String currency) throws IOException {
// ...
return exRate;
}
}
PaymentService
부모 클래스는 변하지 않게 확장할 수 있다.public class PaymentService {
private final WebApiExRateProvider exRateProvider;
public PaymentService() {
this.exRateProvider = new WebApiExRateProvider();
}
public Payment prepare(Long orderId, String currency, BigDecimal foreignCurrencyAmount) throws IOException {
BigDecimal exRate = exRateProvider.getExRate(currency);
BigDecimal convertedAmount = foreignCurrencyAmount.multiply(exrate);
LocalDateTime validUntil = LocalDateTime.now().plusMinutes(30);
return new Payment(orderId, currency, foreignCurrencyAmount, exRate, convertedAmount, validUntil);
}
}
public class WebApiExRateProvider {
BigDecimal getExRate(String currency) throws IOException {
// ...
return exRate;
}
public class PaymentService {
private final SimpleApiExRateProvider exRateProvider; // 클래스 변경
public PaymentService() {
this.exRateProvider = new SimpleApiExRateProvider(); // 변경!
}
public Payment prepare(Long orderId, String currency, BigDecimal foreignCurrencyAmount) throws IOException {
BigDecimal exRate = exRateProvider.findExRate(currency); // 메서드 이름도 다를 수도!
// ...
}
}
public interface ExRateProvider {
BigDecimal getExRate(String currency) throws IOException
}
public class PaymentService {
private final ExRateProvider exRateProvider;
public PaymentService() {
this.exRateProvider = new SimpleApiExRateProvider();
}
public Payment prepare(Long orderId, String currency, BigDecimal foreignCurrencyAmount) throws IOException {
BigDecimal exRate = exRateProvider.getExRate(currency);
// ...
}
}
PaymentService
가 ExRateProvider
인터페이스를 의존한다.
ExRateProvider
인터페이스가 변하지 않는 한 PaymentService
는 변하지 않는다.PaymentService
의 생성자 내부에는 구현 클래스를 의존하고 있기 때문에 유연성이 떨어진다.PaymentService
가 ExRateProvider
를 의존하고 있는 코드 자체는 클래스 레벨의 의존이다.PaymentService
가 인터페이스로 getExRate
메서드를 호출해도 실제 호출되는 구현 클래스는 WebApiExRateProvider
라면 이는 런타임 의존관계다.PaymentService
는 ExRateProvider
의 구현 클래스와 런타임 의존 뿐만 아니라 클래스 레벨의 의존 또한 가지고 있는 것public class PaymentService {
private final ExRateProvider exRateProvider;
public PaymentService(ExRateProvider exRateProvider) {
this.exRateProvider = exRateProvider;
}
public Payment prepare(Long orderId, String currency, BigDecimal foreignCurrencyAmount) throws IOException {
BigDecimal exRate = exRateProvider.getExRate(currency);
// ...
}
}
PaymentService
를 조립하는 관계 설정 책임을 지니는 Client
쪽에서 구현 클래스를 넣어준다.public class Client {
public static void main(String[] args) throws IOException {
PaymentService paymentService = new PaymentService(new WebApiExRateProvider());
Payment payment = paymentService.prepare(100L, "USD", BigDecimal.valueOf(50.7));
System.out.println(payment);
}
}
Client
코드도 지금 2가지 관심사를 가진다.
PaymentService
와 ExRateProvider
오브젝트 사이의 관계 설정 책임ObjectFactory
라는 다른 클래스로 분리해보자.
public class ObjectFactory {
public PaymentService paymentService() {
return new PaymentService(exRateProvider());
}
public ExRateProvider exRateProvider() {
return new WebApiExRateProvider();
}
}
public class Client {
public static void main(String[] args) throws IOException {
ObjectFactory objectFactory = new ObjectFactory();
PaymentService paymentService = objectFactory.paymentService();
Payment payment = paymentService.prepare(100L, "USD", BigDecimal.valueOf(50.7));
System.out.println(payment);
}
}
PaymentService
는 확장이 일어날 때 자신은 변경되지 않는 구조가 되었다.public class PaymentService {
private final ExRateProvider exRateProvider;
public PaymentService(ExRateProvider exRateProvider) {
this.exRateProvider = exRateProvider;
}
// ...
}
Payment
생성)ExRateProvider
)Collections.sort()
도 정렬에 사용할 전략 오브젝트를 전달 받아 사용한다.
Comparator
인터페이스를 구현한 원소를 받아 전략대로 정렬PaymentService
에서 제어의 역전을 살펴보자
PaymentService
→ WebApiExRateProvider
로 의존 방향성이 존재했다.
Client
내부에서 ObjectFactory
로 관계 설정 책임을 넘겨주자 의존 방향이 반대가 되었다.ObjectFactory
가 BeanFactory
가 된다.
BeanFactory
를 제공해 주고 관계 설정의 책임을 지게 된다.PaymentService
와 WebApiExRateProvider
가 바로 빈이다.ObjectFactory
에 해당 정보가 모두 있었다.public class ObjectFactory {
public PaymentService paymentService() { // 빈
return new PaymentService(exRateProvider()); // 관계 설정
}
public ExRateProvider exRateProvider() { // 빈
return new WebApiExRateProvider();
}
}
BeanFactory
가 이미 존재한다.import org.springframework.beans.factory.BeanFactory;
public class Client {
public static void main(String[] args) throws IOException {
BeanFactory beanFactory = new BeanFactory();
PaymentService paymentService = beanFactory.getBean(PaymentService.class);
Payment payment = paymentService.prepare(100L, "USD", BigDecimal.valueOf(50.7));
System.out.println(payment);
}
}
ObjectFactory
에 구성 정보가 있으니 넘겨줄 수 있다.@Configuration
public class ObjectFactory {
@Bean
public PaymentService paymentService() {
return new PaymentService(exRateProvider());
}
@Bean
public ExRateProvider exRateProvider() {
return new WebApiExRateProvider();
}
}
public class Client {
public static void main(String[] args) throws IOException {
BeanFactory beanFactory = new AnnotationConfigApplicationContext(ObjectFactory.class);
PaymentService paymentService = beanFactory.getBean(PaymentService.class);
Payment payment = paymentService.prepare(100L, "USD", BigDecimal.valueOf(50.7));
System.out.println(payment);
}
}
bean
)이라고 불리는 애플리케이션을 구성하는 오브젝트를 관리하는 기능을 담당한다.@Configuration
과 @Bean
애노테이션으로도 구성 정보와 의존관계를 정의할 수 있지만 다른 방법도 존재한다.
@Component
스캔@Component
애노테이션이 붙은 클래스를 모두 찾아보는 빈 스캐닝 방식이다.@Configuration
/@Bean
두 가지 방색을 혼합해서 사용한다.PayementService
등이 POJO였다.private
생성자를 가지기에 상속할 수 없다.@Configuration
내부 @Bean
으로 관리되는 오브젝트들은 코드 상으로 new
를 여러번 호출하는 것처럼 보여도 스프링이 싱글톤을 보장한다.
@Bean
이 붙은 메서드마다 이미 스프링 빈이 존재하면 존재하는 빈을 반환하고, 스프링 빈이 없으면 생성해서 스프링 빈으로 등록하고 반환하는 코드가 동적으로 만들어진다WebApiExRateProvider
에 캐시 기능을 추가하려면 코드를 수정할 수밖에 없을까?
데코레이터 패턴 - 오브젝트에 부가적인 기능/책임을 동적으로 부여하는 패턴
CachedExRateProvider
를 구현해보자
public class CachedExRateProvider implements ExRateProvider {
private final ExRateProvider target;
private BigDecimal cachedExRate;
public CacheExRateProvider(ExRateProvider target) {
this.target = target;
}
@Override
public BigDecimal getExRate(String currency) throws IOException {
if (cachedExRate == null) {
cachedExRate = this.target.getExRate(currency);
}
return cachedExRate;
}
}
ExRateProvider
인터페이스를 의존하는 PaymentService
는 그 구현체가 캐시를 쓰는 구현체인지, 아닌 구현체인지 신경 쓸 필요가 전혀 없다.@Configuration
public class ObjectFactory {
@Bean
public PaymentService paymentService() {
return new PaymentService(cachedExRateProvider());
}
@Bean
public ExRateProvider cachedExRateProvider() {
return new CachedExRateProvider(exRateProvider());
}
@Bean
public ExRateProvider exRateProvider() {
return new WebApiExRateProvider();
}
}
PaymentService
등 비즈니스 정책이 존재하는 곳PaymentService
가 WebApiExRateProvider
를 그대로 썼다면 코드 레벨 의존성도 Policy → Mechanism이다.PaymentService
가 추상화인 ExRateProvider
인터페이스를 의존ExRateProvider
인터페이스를 Policy Layer에 배치
WebApiExRateProvider
는 그대로 Mechanism Layer에 배치