자바에서 인터페이스를 왜 사용하는가?

김지인·2023년 2월 26일
0

들어가기 앞서서..

프로젝트를 진행하면서 자바의 인터페이스를 사용하면서 정확한 장점을 모르고 사용했던 경험이 있다. 이러한 경우, 해당 기능을 사용하는 이유를 모른 채 사용하면 오히려 코드의 복잡성을 증가시키거나 유지보수성을 떨어뜨릴 수 있다. 따라서, 자바의 인터페이스를 사용하는 이유를 자세히 알아보고자 한다.


인터페이스란 ?

인터페이스는 클래스와 마찬가지로 자바에서 정의할 수 있는 하나의 타입이다. 하지만 클래스와는 달리 구현 코드를 포함하고 있지 않다. 대신 인터페이스는 메소드(메소드 이름, 매개변수, 반환값)를 정의하고, 해당 메소드를 구현할 클래스에서 반드시 구현하도록 강제한다. 이러한 인터페이스의 특징 때문에 다음과 같은 장점이 있다.


1. 다형성

인터페이스를 사용하면 다형성을 구현할 수 있다. 다형성이란 같은 코드에서 여러 가지 타입을 다룰 수 있는 능력을 의미한다. 인터페이스는 여러 클래스에서 구현될 수 있으므로, 하나의 인터페이스를 사용하여 여러 클래스의 객체를 다룰 수 있다. 이를 통해 코드의 일관성을 유지하고, 코드의 재사용성과 유지보수성을 높일 수 있다.

다형성 구현 예시


public interface Animal {
    public void makeSound();
}

public class Dog implements Animal {
    @Override
    public void makeSound() {
        System.out.println("멍멍");
    }
}

public class Cat implements Animal {
    @Override
    public void makeSound() {
        System.out.println("야옹");
    }
}

public class Main {
    public static void main(String[] args) {
        Animal dog = new Dog();
        Animal cat = new Cat();

        dog.makeSound(); // 멍멍
        cat.makeSound(); // 야옹
    }
}

위의 예시에서 Animal 인터페이스를 구현하는 Dog, Cat 클래스를 생성하고, Main 클래스에서 각각 인터페이스 타입으로 참조하여 다형성을 구현한다. 이를 통해, 같은 인터페이스를 구현하는 다른 클래스들을 일관성 있게 다룰 수 있다.


2. 유연한 설계

인터페이스는 클래스의 외부에서 클래스와의 상호작용을 정의한다. 인터페이스를 사용하면 클래스와의 결합도를 낮추고, 유연하고 확장성 있는 설계를 할 수 있다. 인터페이스는 다른 클래스와의 상호작용을 추상화하여, 클래스 간 결합도를 낮추기 때문이다.

유연한 설계 예시


public interface DataAccessObject {
    public void connect();
    public void select();
    public void disconnect();
}

public class OracleDAO implements DataAccessObject {
    @Override
    public void connect() {
        // 오라클 DB에 연결하는 코드
    }

    @Override
    public void select() {
        // 오라클 DB에서 데이터를 조회하는 코드
    }

    @Override
    public void disconnect() {
        // 오라클 DB와 연결을 끊는 코드
    }
}

public class MySqlDAO implements DataAccessObject {
    @Override
    public void connect() {
        // MySql DB에 연결하는 코드
    }

    @Override
    public void select() {
        // MySql DB에서 데이터를 조회하는 코드
    }

    @Override
    public void disconnect() {
        // MySql DB와 연결을 끊는 코드
    }
}

public class Main {
    public static void main(String[] args) {
        DataAccessObject dao = new OracleDAO(); // 또는 MySqlDAO()
        dao.connect();
        dao.select();
        dao.disconnect();
    }
}

위의 예시에서, DataAccessObject 인터페이스를 구현하는 OracleDAO, MySqlDAO 클래스를 생성하고, Main 클래스에서는 DataAccessObject 인터페이스 타입으로 참조하여 다형성을 구현한다. 이를 통해, 데이터베이스 종류에 상관없이, DataAccessObject 인터페이스를 구현하는 클래스를 사용하여 일관성 있는 DB 연결 및 작업을 수행할 수 있다.


3. 코드 재사용

인터페이스를 사용하면 인터페이스를 구현하는 다양한 클래스에서 공통적으로 사용되는 메소드를 정의할 수 있다. 이를 통해 코드의 재사용성을 높일 수 있다.

코드 재사용 예시

public interface Shape {
    public double calculateArea();
}

public class Rectangle implements Shape {
    private double width;
    private double height;

    public Rectangle(double width, double height) {
        this.width = width;
        this.height = height;
    }

    @Override
    public double calculateArea() {
        return width * height;
    }
}

public class Circle implements Shape {
    private double radius;

    public Circle(double radius) {
        this.radius = radius;
    }

    @Override
    public double calculateArea() {
        return Math.PI * radius * radius;
    }
}

public class Main {
	public static void main(String[] args) {
	
    	Rectangle rectangle = new Rectangle(5, 10);
		Circle circle = new Circle(7);
    
    	System.out.println("Rectangle Area: " + rectangle.calculateArea()); 
        // Rectangle Area: 50.0
    	System.out.println("Circle Area: " + circle.calculateArea()); 
        // Circle Area: 153.93804002589985
}

    

위의 예시에서, Shape 인터페이스를 구현하는 Rectangle, Circle 클래스를 생성하고, Main 클래스에서는 각 클래스 타입으로 참조하여 calculateArea() 메소드를 호출하여 도형의 넓이를 구한다. 이를 통해, 넓이를 구하는 기능이 Shape 인터페이스에 의해 표준화되어, 동일한 기능을 수행하는 다른 클래스에서 코드를 재사용할 수 있다.


4. 코드의 일관성

인터페이스를 사용하면 클래스의 메소드들이 동일한 이름을 가지고 동일한 매개변수를 받도록 정의할 수 있다. 이를 통해 코드의 일관성을 유지할 수 있다.

코드의 일관성 예시

public interface Car {
    public void start();
    public void accelerate();
    public void brake();
}

public class Sedan implements Car {
    @Override
    public void start() {
        System.out.println("Sedan start");
    }

    @Override
    public void accelerate() {
        System.out.println("Sedan accelerate");
    }

    @Override
    public void brake() {
        System.out.println("Sedan brake");
    }
}

public class SportsCar implements Car {
    @Override
    public void start() {
        System.out.println("SportsCar start");
    }

    @Override
    public void accelerate() {
        System.out.println("SportsCar accelerate");
    }

    @Override
    public void brake() {
        System.out.println("SportsCar brake");
    }
}

public class Main {
    public static void main(String[] args) {
        Car sedan = new Sedan();
        Car sportsCar = new SportsCar();

        sedan.start(); // Sedan start
        sedan.accelerate(); // Sedan accelerate
        sedan.brake(); // Sedan brake

        sportsCar.start(); // SportsCar start
        sportsCar.accelerate(); // SportsCar accelerate
        sportsCar.brake(); // SportsCar brake
    }
}

위의 예시에서, Car 인터페이스를 구현하는 Sedan, SportsCar 클래스를 생성하고, Main 클래스에서는 각 클래스 타입으로 참조하여 Car 인터페이스에 정의된 start(), accelerate(), brake() 메소드를 호출하여 자동차의 동작을 구현한다. 이를 통해, Car 인터페이스를 구현하는 클래스에서는 일관된 메소드를 구현해야 하므로, 코드의 일관성을 유지할 수 있다.


5. 유지보수성

인터페이스를 사용하면 인터페이스를 구현하는 클래스의 구현 세부사항과는 독립적으로 인터페이스를 수정할 수 있다. 이를 통해 인터페이스에 대한 수정 작업을 수행함으로써, 전체 애플리케이션의 수정 작업을 최소화하고 유지보수성을 높일 수 있다.

유지보수성 예시

public interface PaymentProcessor {
    public void processPayment(double amount);
}

public class CreditCardProcessor implements PaymentProcessor {
    @Override
    public void processPayment(double amount) {
        System.out.println("Processing credit card payment of $" + amount);
    }
}

public class PayPalProcessor implements PaymentProcessor {
    @Override
    public void processPayment(double amount) {
        System.out.println("Processing PayPal payment of $" + amount);
    }
}

public class PaymentService {
    private PaymentProcessor paymentProcessor;

    public PaymentService(PaymentProcessor paymentProcessor) {
        this.paymentProcessor = paymentProcessor;
    }

    public void processPayment(double amount) {
        paymentProcessor.processPayment(amount);
    }
}

public class Main {
    public static void main(String[] args) {
        PaymentProcessor creditCardProcessor = new CreditCardProcessor();
        PaymentProcessor payPalProcessor = new PayPalProcessor();

        PaymentService paymentService = new PaymentService(creditCardProcessor);
        paymentService.processPayment(100.0); // Processing credit card payment of $100.0

        paymentService = new PaymentService(payPalProcessor);
        paymentService.processPayment(50.0); // Processing PayPal payment of $50.0
    }
}

위의 예시에서, PaymentProcessor 인터페이스를 구현하는 CreditCardProcessor, PayPalProcessor 클래스를 생성하고, PaymentService 클래스에서는 PaymentProcessor 타입으로 주입받아 processPayment() 메소드를 호출하여 결제를 처리한다. 이를 통해, 결제 처리 기능이 PaymentProcessor 인터페이스에 의해 표준화되어, 새로운 결제 수단이 추가될 경우 PaymentProcessor를 구현하는 새로운 클래스만 작성하면 되므로 유지보수성을 높일 수 있다.

profile
에러가 세상에서 제일 좋아

0개의 댓글