Notice
Recent Posts
Recent Comments
Link
«   2025/05   »
1 2 3
4 5 6 7 8 9 10
11 12 13 14 15 16 17
18 19 20 21 22 23 24
25 26 27 28 29 30 31
Tags
more
Archives
Today
Total
관리 메뉴

이지은님의 블로그

250115 - Java 상속과 추상화, 다형성: 인터페이스와 추상클래스의 개념이해와 차이점(마커 인터페이스) 본문

TIL

250115 - Java 상속과 추상화, 다형성: 인터페이스와 추상클래스의 개념이해와 차이점(마커 인터페이스)

queenriwon3 2025. 1. 15. 22:01

▷ 오늘 배운 것

어제 배운 결합도 특강에서 나온 인터페이스에 대해 이해 과정을 정리하고 더 자세한 인터페이스와 추상클래스에 대해서 조사 후 정리해보도록 하겠다.

 

 

<< 차례 >>

1. 상속을 사용하는 방법

2. 인터페이스

3. 인터페이스와 추상클래스의 차이점

    1) 인터페이스

    2) 추상클래스

4. 마커 인터페이스

 

 

 


 

Java는 객체지향 프로그램이다. 객체 지향 프로그램에는 4대 특성이 있는데,

  1. 캡슐화 (Encapsulation)
    • 외부에서 접근을 제어하는 것
  2. 상속 (Inheritance)
    • 부모 클래스의 속성과 기능을 자식 클래스가 물려받는 것
  3. 추상화 (Abstraction)
    • 중요한 정보만을 표현하고 불필요한 사항은 숨기는 것
  4. 다형성 (Polymorphism)
    • 같은 타입의 참조변수가 여러가지 형태를 가질 수 있는 것

이 중 상속을 배우면서 추상화와 다형성으로 확장 시켜보도록 하겠다.

 

 

 

1. 상속을 사용하는 방법

public abstract class ParentHouse {

    public void openDoor() {
        System.out.println("부모님 집 문을 열었습니다.");
    }
}

public class ChildHouse extends ParentHouse {
}

public class Main {
    public static void main(String[] args) {
        ChildHouse childHouse = new ChildHouse();
        childHouse.openDoor();
    }
}

// 실행 결과: 부모님 집 문을 열었습니다.

 

추상클래스를 상속받으면 추상클래스에서 정의한 메서드(추상메서드가 아니라도)를 상속받은 자식클래스가 사용할 수 있다.

 

추상클래스(abstract) 정의된 ParentHouse 클래스를 ChildHouse 상속받는다. 이렇게 경우 ChildHouse 부모클래스의 메서드를 사용할 있다.

 

 

2. 인터페이스

다음과 같은 환율에 대한 예제코드가 있다.

@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;
    }
}

 

@JsonIgnoreProperties(ignoreUnknown = true)
현재 코드에서 API를 통해 return 받은 데이터의 필드가 class와 일치하지 않으면 에러가 나는데, 이를 ignore하는 어노테이션.

 

어노테이션에 대한 내용도 학습을 해야할 것 같다.

 

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 class PaymentService {

    @SneakyThrows
    public Payment prepare(Long orderId, String currency, BigDecimal foreignCurrencyAmount) {
        // url을 통해 환율 가져오기
        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);
    }
}


public class ObjectApplication {
    public static void main(String[] args) {

        // PaymentService 클래스 사용
        PaymentService paymentService = new PaymentService();
        Payment payment = paymentService.prepare(100L, "USD", BigDecimal.valueOf(55.5));
        System.out.println(payment);
    }
}

 

1) 이 복잡한 코드를 재사용성을 높이기 위해 클래스로 분리 할 것이다.

 

우선 환율을 가져오는 부분을 ExRateProvider로 분리해주어 getExRate 메서드에 환율을 가져오는 역할을 하도록 구현한다.

그래서 return 값을 PaymentService에서 처리할 있도록 한다.

 

기능에 따라 클래스 분리
// ExRateProvider.java
public class ExRateProvider {
    @SneakyThrows
    public BigDecimal getExRate(String currency) {
        // url을 통해 환율 가져오기
        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");
    }
}

// PaymentService.java
public class PaymentService {

    // ExRateProvider 클래스를 필드로 선언 + 생성자에서 초기화 
    // --> 이럴경우 main()에서 인스턴스생성 수정필요!!
    private final ExRateProvider exRateProvider;

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

    public Payment prepare(Long orderId, String currency, BigDecimal foreignCurrencyAmount) {
        
        // getExRate에서 받아온 값
        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);
    }
}

 

, 이렇게 정의할 경우 PaymentService 클래스의 인스턴스를 생성할 매개변수에 값을 작성해주어야 한다.

PaymentService paymentService = new PaymentService(new ExRateProvider());

 

 

 

2) 다른 환율값을 사용하고 싶을 때는?

 

다른 환율 값을 사용하고 싶을 SimpleExRateProvider 클래스 사용하고자 한다. 그러면 다음과 같이 단순히 매개 변수 값을 수정하면 될까?

PaymentService paymentService = new PaymentService(new SimpleExRateProvider());

>> 정답은 No.

 

 

PaymentService 안에는 ExRateProvider 클래스를 필드로 선언하고 있다. 그러니 SimpleExRateProvider 클래스를 사용하고 싶을 때는 PaymentService안에서 필드로 선언한 클래스를 바꾸어야한다.

public class PaymentService {

    // SimpleExRateProvider 클래스를 필드로 선언 + 생성자에서 초기화 
    // private final ExRateProvider exRateProvider; 에서
    private final SimpleExRateProvider exRateProvider;

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

 

이럴 경우 수정에 용이하지 않다. PaymentService 클래스 입장에서는 ExRateProvider SimpleExRateProvider 유연하게 받아들이는 일만 하게 두어야한다. 

 

 

 

3) 인터페이스 사용

이를 해결하기 위해 다형성을 이용할 것인데, IExRateProvider라는 인터페이스를 생성해주도록 하자. 

public interface IExRateProvider {
    BigDecimal getExRate(String currency);
}

 

👉 Interface의 메서드에는 왜 접근제어자가 없나요?
>> public이 생략됨
👉 추상클래스(abstract class)가 아닌 인터페이스(interface)를 더 사용하는 이유

1. 추상클래스는 구현부가 있어 인터페이스 보다는 유연함이 부족하다.
2. 스프링에는 interface에 대한 언급이 훨씬 많다.

 

 

이제 만든 인터페이스를 ExRateProvider SimpleExRateProvider 상속받도록 한다.

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;
    }
}

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);
    }
}

 

이렇게 하나의 인터페이스로 implements 하게되면 각 클래스를 인터페이스의 역할 대로 사용할 수가 있게 된다.

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);
    }
}

 

PaymentService 필드도 IExRateProvider 받도록 수정을 하게되면 main()에서 IExRateProvider 인터페이스의 상속(implements)을 받은 클래스라면 생성자의 매개변수가 있다.

// ExRateProvider() 사용
PaymentService paymentService = new PaymentService(new ExRateProvider());

// SimpleExRateProvider() 사용
PaymentService paymentService = new PaymentService(new SimpleExRateProvider());

 

 

 

4) main()에서 역할 분리하기(결합도를 낮춰보자)

main()외에 다른 곳에서 ExRateProvider() 사용할지, SimpleExRateProvider() 사용할지 컨트롤 있게 하면서 결합도를 낮춰보자.(다운탑 방식으로 작성하였음)

public class ObjectFactory {
    public PaymentService paymentService() {
        return new PaymentService(exRateProvider());
    }

    public IExRateProvider exRateProvider() {
        return new SimpleExRateProvider();
    }
}
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);
    }
}

다음과 같이 objectFactory 객체에서 paymentService 공급하게끔 작성한다.

 

다음과 같이 main은 단순히 인스턴스를 생성하고 기능을 실행하도록 구현을 하고 다른 부가적인 기능은 다른 클래스가 담당하도록 할 수 있다. 이와 같이 각 클래스가 가진 기능을 분리함으로서 결합도를 낮출 수 있다.

 

 

 

3. 추상클래스와 인터페이스의 차이

늘 java를 공부할 때마다, 추상클래스와 인터페이스는 사용 하는 방법이 비슷하다고 느꼈다. 아무리 인터페이스를 사용하는 경우가 많다고는 하나 차이점을 구분하는 것도 좋다고 생각했다.

 

구분 추상클래스 인터페이스
사용 키워드 Abstract  Interface
사용가능 변수 제한 없음 static final (상수)
(주로 public static final 상수)
사용가능 접근제어자 제한 없음 Public
사용가능 메서드 제한 없음 abstract method, default method, static method, private method
(주로 public abstract 정의)
상속 키워드 extends implements
다중상속 불가능 가능
공통점 1. 추상 메소드 가지고 있어야 한다.
2. 
인스턴스화 없다 (new 생성자 사용 X)
3.
인터페이스 혹은 추상 클래스를 상속받아 구현한 구현체의 인스턴스를 사용해야 한다.
4. 인터페이스와 추상클래스를 구현, 상속한 클래스는 추상 메소드를 반드시 구현하여야 한다.
사용 용도 자신의 기능들을 하위 클래스로 확장할 때 인터페이스에 정의된 메서드를 클래스의 목적에 맞게 기능을 구현
타입 묶음 클래스끼리 논리적인 타입묶음 클래스끼리 상속에 얽매이지 않고 자유로운 타입 묶음

 

 

1) 인터페이스 (interface)

  • 내부의 모든 메서드는 public abstract 로 정의 (default 메소드 제외)
  • 내부의 모든 필드는 public static final 상수
  • 클래스에 다중 구현 지원. 인터페이스 끼리는 다중 상속 지원.
  • 인터페이스는 클래스와 별도로 구현 객체가 같은 동작을 한다는 것을 보장하기 위해 사용하는 것에 초점
    --> 인터페이스에 정의된 메서드를 각 클래스의 목적에 맞게 기능을 구현

  • 어플리케이션의 기능을 정의해야 하지만 그 구현 방식이나 대상에 대해 추상화 할때
  • 서로 관련성이 없는 클래스들을 묶어 주고 싶을때 (형제 관계)
  • 다중 상속(구현)을 통한 추상화 설계를 해야할때
  • 특정 데이터 타입의 행동을 명시하고 싶은데, 어디서 그 행동이 구현되는지는 신경쓰지 않는 경우
  • 구체적인 클래스 타입으로 통신하는 것이 아닌 인터페이스 라는 중개 타입을 이용하여 통신하는 경우
    (서로 전혀 연관 관계가 없는 클래스들을 A클래스에 전달해서 데이터를 파일로 저장하기 위해선, 인터페이스로 타입 통합하여 형제 관계를 구성하여 A클래스의 인터페이스 객체 필드로 넘기는 식으로, 상속에 얽매히지 않은 자유로운 인터페이스의 다형성 이용)

 

2) 추상클래스 (abstract class)

: 추상클래스는 하위 클래스들의 공통점들을 모아 추상화하여 만든 클래스

  • 추상클래스는 다중 상속이 불가능하여 단일 상속만 허용.
  • 추상클래스는 추상 메소드 외에 일반클래스와 같이 일반적인 필드, 메서드, 생성자를 가질 수 있음.
  • 추상화(추상 메서드)를 하면서 중복되는 클래스 멤버들을 통합 및 확장을 할 수 있다.
  • 추상 메서드 외 일반 메서드는 구현부 작성이 필요하다.

  • 상속 받을 클래스들이 공통으로 가지는 메소드와 필드가 많아 중복 멤버 통합을 할때(인터페이스는 상수외 선언 불가)
  • 멤버에 public 이외의 접근자(protected, private) 선언이 필요한 경우
  • non-static, non-final 필드 선언이 필요한 경우 (각 인스턴스에서 상태 변경을 위한 메소드가 필요한 경우)
  • 요구사항과 함께 구현 세부 정보의 일부 기능만 지정했을 때
  • 하위 클래스가 오버라이드하여 재정의하는 기능들을 공유하기 위한 상속 개념을 사용할 때
  • 객체들의 공통점을 찾아 추상화시켜 놓은 것
    --> 상속 관계를 타고 올라갔을 때 같은 부모 클래스를 상속하며 부모 클래스가 가진 기능들을 구현해야할 경우 사용
  • 추상클래스는 클라이언트(ExamConsole)에서 자료형을 사용하기 전에 미리 논리적인 클래스 상속 구조를 만들어 놓고 사용이 결정되는 느낌
  • 클래스 끼리 명확한 계층 구조가 필요할때, 클래스와 의미있는 연관 관계를 구축할때 사용

 

 

4. 마커 인터페이스

일반적인 인터페이스와 동일하지만 사실상 아무 메소드도 선언하지 않은 빈 껍데기 인터페이스

객체의 타입과 관련된 정보만을 제공(타입 체크용)

 

 

 

 

 

▷ 참고 블로그

https://inpa.tistory.com/entry/JAVA-%E2%98%95-%EC%9D%B8%ED%84%B0%ED%8E%98%EC%9D%B4%EC%8A%A4-vs-%EC%B6%94%EC%83%81%ED%81%B4%EB%9E%98%EC%8A%A4-%EC%B0%A8%EC%9D%B4%EC%A0%90-%EC%99%84%EB%B2%BD-%EC%9D%B4%ED%95%B4%ED%95%98%EA%B8%B0

 

☕ 인터페이스 vs 추상클래스 용도 차이점 - 완벽 이해

인터페이스 vs 추상클래스 비교 이 글을 찾아보는 독자분들은 아마도 이미 인터페이스와 추상클래스 개념을 학습한 뒤에 이 둘에 대하여 차이의 모호함 때문에 방문 했겠지만, 그래도 다시한번

inpa.tistory.com