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를 구현하는 구현체를 작성해보자
WebApiExRatePaymentServicepublic 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에 배치