[Java] 객체 간의 결합도와 다형성

석연걸·2025년 1월 14일

스파르타 코딩클럽

목록 보기
7/17

시작하기 전에 동등성과 동일성에 대해 먼저 이야기해보자.

동등성(Equals) : 내용이 같음
동일성(Identity) : 메모리 주소가 같음

  • 내 나름대로의 해석 : 두 택배 박스(객체)가 있음. 택배기사(컴퓨터)가 배달을 할 때, 안의 내용물이 같아도(동등성), 주소가 다르기 때문
    에 서로 다른 집으로 박스가 가는 것. 즉 두 박스는 다른 객체이자 메모리 주소값이 다름


◆ 개요
오늘 강의에서 배운 것은 Spring에 들어갈 때 반드시 필요하다는 것에 대해 이야기를 들었다.

  • 추상화 : 필요한 정보만을 보여주고, 불필요한 정보를 숨기는 것

  • 인터페이스 : 무언가를 만들 때 맞춰야되는 규격 (즉 클래스를 설계하는 큰 설계도임)

Spring에서는 추상 클래스보다 인터페이스를 위주로 사용한다고 이야기를 들었으며, 추상 클래스를 공부하되, 딥하게 공부 안하는 것을 추천해주셨다.



예제 코드를 통해 추상화와 다형성, 맛보기

  • 환율 클래스
// 현재 코드에서 API를 통해 return 받은 데이터의 필드가 class와 일치하지 않으면 에러가 나는데, 이를 ignore하는 어노테이션
@JsonIgnoreProperties(ignoreUnknown = true)
public class ExRate {
    private String result;
    private Map<String, BigDecimal> rates;

    public String getResult() {
        return result;
    }

    public Map<String, BigDecimal> getRates() {
        return rates;
    }
}
  • 고객정보 클래스
public class Payment {
    private final Long orderId;
    private final String currency;
    private final BigDecimal foreignCurrencyAmount;
    private final BigDecimal exRate;
    private final BigDecimal convertedAmount;
    private final 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;
    }

    public Long getOrderId() { return orderId; }

    public String getCurrency() { return currency; }

    public BigDecimal getForeignCurrencyAmount() { return foreignCurrencyAmount; }

    public BigDecimal getExRate() { return exRate; }

    public BigDecimal getConvertedAmount() { return convertedAmount; }

    public LocalDateTime getValidUntil() { return validUntil; }
    
    @Override
    public String toString() {
        return "Payment{" +
                "orderId=" + orderId +
                ", currency='" + currency + '\'' +
                ", foreignCurrencyAmount=" + foreignCurrencyAmount +
                ", exRate=" + exRate +
                ", convertedAmount=" + convertedAmount +
                ", validUntil=" + validUntil +
                '}';
    }
}
  • 메인 클래스
public class ObjectApplication {

    public static void main(String[] args) {
        PaymentService paymentService = new PaymentService();
        Payment payment = paymentService.prepare(100L, "USD", BigDecimal.valueOf(55.5));
        System.out.println(payment);
    }
}
  • 고객정보의 서비스 클래스
public class PaymentService {

    @SneakyThrows
    public Payment prepare(Long orderId, String currency, BigDecimal foreignCurrencyAmount) {
        // 환율 가져오기
        URL url = new URL("https://open.er-api.com/v6/latest/" + currency);
        HttpURLConnection connection = (HttpsURLConnection) url.openConnection();
        BufferedReader bufferedReader = new BufferedReader(new InputStreamReader(connection.getInputStream()));
        String response = bufferedReader.lines().collect(Collectors.joining());
        bufferedReader.close();

        ObjectMapper objectMapper = new ObjectMapper();
        ExRate exrate = objectMapper.readValue(response, ExRate.class);

        BigDecimal exRate = exrate.getRates().get("KRW");

        // 금액 계산
        BigDecimal convertedAmount = foreignCurrencyAmount.multiply(exRate);

        // 유효 시간 계산
        LocalDateTime validUntil = LocalDateTime.now().plusMinutes(30);

        return new Payment(orderId, currency, foreignCurrencyAmount, exRate, convertedAmount, validUntil);
    }
}

우선 처음으로 하고싶은 것은 위의 PaymentService 클래스가 너무 길어 복잡하기 때문에 메서드를 분리해 리팩토링을 하고싶음. (코드의 재사용성 증가, 결합도 감소)


환율 가져오기 부분을 ExRateProvider 클래스를 생성해 옮겨 리팩토링을 하고, PaymentService에 만든 클래스를 주입해 동일한 로직을 실행시키도록 함.

public class ExRateProvider {
    @SneakyThrows
    public BigDecimal getExRate(String currency) {
        URL url = new URL("https://open.er-api.com/v6/latest/" + currency);
        HttpURLConnection connection = (HttpsURLConnection) url.openConnection();
        BufferedReader bufferedReader = new BufferedReader(new InputStreamReader(connection.getInputStream()));
        String response = bufferedReader.lines().collect(Collectors.joining());
        bufferedReader.close();

        ObjectMapper objectMapper = new ObjectMapper();
        ExRate exRate = objectMapper.readValue(response, ExRate.class);
        return exRate.getRates().get("KRW");
    }
}
public class PaymentService {

    private final ExRateProvider exRateProvider;

    public PaymentService(ExRateProvider exRateProvider) {
        this.exRateProvider = exRateProvider;
    }

    public Payment prepare(Long orderId, String currency, BigDecimal foreignCurrencyAmount) {
        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 ObjectApplication {

    public static void main(String[] args) {
        PaymentService paymentService = new PaymentService(new ExRateProvider());
        Payment payment = paymentService.prepare(100L, "USD", BigDecimal.valueOf(55.5));
        System.out.println(payment);
    }
}

main 메서드에서 PaymentService의 생성자 부분을 매개변수 조건대로 맞춰줌


새로운 요구사항 : 만약 currency가 "USD"라면 무조건 1,000을 리턴해주는 SimpleExRateProvider 클래스 개발

public class SimpleExRateProvider {
    public BigDecimal getExRate(String currency) {
        if (currency.equals("USD")) {
            return new BigDecimal("1000");
        }

        throw new IllegalArgumentException("Unsupported currency: " + currency);
    }
}

그러나 이렇게 작성하면 PaymentService 클래스에서 ExRateProvider가 필요할 때는

private final ExRateProvider exRateProvider;
    public PaymentService(ExRateProvider exRateProvider) {
        this.exRateProvider = exRateProvider;
    }

반대로 SimpleExRateProvider가 필요할 때는

private final SimpleExRateProvider exRateProvider;
    public PaymentService(SimpleExRateProvider exRateProvider) {
        this.exRateProvider = exRateProvider;
    }

계속 이렇게 수정해야하는 불편함이 있다. 이를 해결하기위해 getExRate() 구현부를 가진 인터페이스를 생성해 SimpleExRateProvider 클래스와 ExRateProvider 클래스에 implements를 한다.
(해당 인터페이스를 implements 하고 있으면 알아서 사용할 수 있도록)


인터페이스 생성 (인터페이스 내에는 public abstract가 자동으로 생략됨)

public interface IExRateProvider {
	// 둘이 똑같음
	// public abstract BigDecimal getExRate(String currency);
    
       BigDecimal getExRate(String currency);
}

ExRateProvider 클래스에 생성한 인터페이스 implements

public class ExRateProvider implements IExRateProvider {
    @SneakyThrows
    @Override
    public BigDecimal getExRate(String currency) {
        URL url = new URL("https://open.er-api.com/v6/latest/" + currency);
        HttpURLConnection connection = (HttpsURLConnection) url.openConnection();
        BufferedReader bufferedReader = new BufferedReader(new InputStreamReader(connection.getInputStream()));
        String response = bufferedReader.lines().collect(Collectors.joining());
        bufferedReader.close();

        ObjectMapper objectMapper = new ObjectMapper();
        ExRate exrate = objectMapper.readValue(response, ExRate.class);
        BigDecimal exRate = exrate.getRates().get("KRW");
        return exRate;
    }
}

SimpleExRateProvider 클래스에도 똑같이 해줌

public class SimpleExRateProvider implements IExRateProvider {
    @Override
    public BigDecimal getExRate(String currency) {
        if (currency.equals("USD")) {
            return new BigDecimal("1000");
        }

        throw new IllegalArgumentException("Unsupported currency: " + currency);
    }
}

마지막으로 PaymentService에서 필드와 생성자 매개변수를 인터페이스로 변경

public class PaymentService {
    private final IExRateProvider exRateProvider;

    public PaymentService(IExRateProvider exRateProvider) {
        this.exRateProvider = exRateProvider;
    }

    public Payment prepare(Long orderId, String currency, BigDecimal foreignCurrencyAmount) {
        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);
    }
}

메인 메서드

// ExRateProvider() 사용
public class ObjectApplication {

    public static void main(String[] args) {
        PaymentService paymentService = new PaymentService(new ExRateProvider());	// 매개변수 부분에 SimpleExRateProvider 둘 중 하나 선택
        Payment payment = paymentService.prepare(100L, "USD", BigDecimal.valueOf(55.5));
        System.out.println(payment);
    }
}

마지막으로 우리는 메인 메서드에서는 출력만 하고싶은데, 자꾸 이 메인 메서드에서 ExRateProvider와 SimpleExRateProvider 중 어떤 것을 사용할지 결정하는게 불편함.


메인 메서드에서 뭘 사용할지 결정하기 싫기 때문에 외부에서 ObjectFactory라는 클래스를 만들어 이를 컨트롤 시킬 것 이다.

먼저 main() 에 미리 ObjectFactory 관련 코드를 작성(탑다운 방식) 후, objectFactory 객체에서 paymentService를 공급시킴.

public class ObjectApplication {

    public static void main(String[] args) {
        ObjectFactory objectFactory = new ObjectFactory();
        PaymentService paymentService = objectFactory.paymentService();
        Payment payment = paymentService.prepare(100L, "USD", BigDecimal.valueOf(55.5));
        System.out.println(payment);
    }
}
public class ObjectFactory {
    public PaymentService paymentService() {
        return new PaymentService(exRateProvider());
    }

    public IExRateProvider exRateProvider() {
        return new SimpleExRateProvider();
    }
}

이상 추상화와 다형성을 이용한 예제코드 끝.

0개의 댓글