Spring - 다형성이란

지니·2023년 8월 20일
0

spring

목록 보기
10/13

스프링 프레임워크에서 중시하는 다형성에 대해 알아보자

1. 다형성이란?

다형성

  • 다형성은 객체지향 프로그래밍의 핵심 원칙 중 하나로, 하나의 인터페이스, 메서드 또는 클래스가 다양한 방법으로 동작하는 것을 의미한다.
    동일한 인터페이스를 통해 서로 다른 작동 방식의 객체를 사용함으로써, 코드의 유연성과 재사용성이 높아진다.

다음은 코드를 통한 예시다.

  • 아래의 코드에서 Animal 인터페이스는 sound()라는 메서드를 정의하고 있다. 이는 모든 동물이 소리를 내는 것을 추상화한 것이다.

  • Dog와 Cat 클래스는 이 Animal 인터페이스를 구현(implements)하고, 각자의 방식으로 sound 메서드를 오버라이드(@Override)한다.
    이렇게 하면, 같은 Animal 인터페이스를 가지고(구현하고) 있지만 Dog와 Cat은 각자 다른 소리를 낼 수 있다.

public interface Animal {
    void sound();
}

public class Dog implements Animal {
    @Override
    public void sound() {
        System.out.println("Woof!");
    }
}

public class Cat implements Animal {
    @Override
    public void sound() {
        System.out.println("Meow!");
    }
}

위의 인터페이스 사용예시

  • myDog와 myCat은 모두 Animal 타입의 참조 변수이지만, 각각 Dog와 Cat의 인스턴스를 참조하고 있다. 이 때 sound 메서드를 호출하면, 참조하고 있는 실제 객체의 sound 메서드가 호출된다.

  • 이렇게 인터페이스를 통해 서로 다른 동작을 하는 객체를 동일하게 다룰 수 있게 되는 것이 다형성이다. 이로 인해 코드는 보다 유연해지고 확장성을 가지게 된다.

public class Main {
    public static void main(String[] args) {
        Animal myDog = new Dog();
        Animal myCat = new Cat();
        myDog.sound();  // Prints "Woof!"
        myCat.sound();  // Prints "Meow!"
    }
}

2. 개방-폐쇄 원칙 (OCP)란

개방-폐쇄 원칙 OCP

  • 개방-폐쇄 원칙(OCP)은 "소프트웨어 개체(클래스, 모듈, 함수 등등)는 확장에 대해서는 열려 있어야 하고, 수정에 대해서는 닫혀 있어야 한다"는 소프트웨어 디자인 원칙이다.

  • 이는 기존 코드의 변경 없이 새로운 기능을 추가하거나 기존 기능을 변경할 수 있도록 해준다.

코드 예시

  • 상품을 다양한 기준으로 정렬하는 상황으로 가정한다. Product 클래스와 정렬을 수행하는 ProductSorter 클래스가 있다.
public class Product {
    private String name;
    private double price;
    // constructors, getters and setters...
}

public class ProductSorter {
    public List<Product> sortByPrice(List<Product> products) {
        // sort products by price and return
    }
}
  • 위의 예시코드에서 상품을 이름으로 정렬하고 싶다면 ProductSortersortByName() 메서드를 추가할 수 있다.
    하지만 이렇게 되면 ProductSorter는 계속해서 변경되어야 한다.

  • 이런 방식은 OCP를 위반하게 되는것이다. OCP를 적용하기 위해, 우선 정렬 방식을 인터페이스로 추상화시키자

// 인터페이스를 만들어서 추상화시켰다.
public interface ProductSortStrategy {
    List<Product> sort(List<Product> products);
}

public class PriceSortStrategy implements ProductSortStrategy {
    @Override
    public List<Product> sort(List<Product> products) {
        // sort by price and return
    }
}

public class NameSortStrategy implements ProductSortStrategy {
    @Override
    public List<Product> sort(List<Product> products) {
        // sort by name and return
    }
}
  • 그리고 이 인터페이스를 ProductSorter 클래스에서 사용하도록 변경해 준다. 기존에는 strategy라는 ProductSortStrategy 인터페이스를
    주입받지 않았었다.
public class ProductSorter {
    private ProductSortStrategy strategy;

    public ProductSorter(ProductSortStrategy strategy) {
        this.strategy = strategy;
    }

    public List<Product> sort(List<Product> products) {
        return strategy.sort(products);
    }
}
  • 이렇게 코드를 작성하면, ProductSorter는 정렬 방식에 따라 변경될 필요가 없게 된다. 새로운 정렬 방식이 필요하다면 새로운 ProductSortStrategy 구현체를 만들기만 하면 된다.

  • 이것이 바로 OCP를 잘 따르는 코드인것이다.


3. 인터페이스를 사용하는 것의 장점

1. 테스트 용이성

  • ProductService라는 서비스 클래스가 있고, 이 클래스는 ProductDao라는 DAO 인터페이스에 의존한다고 가정하자.

  • 이 경우, 단위 테스트를 수행할 때 ProductDao의 실제 구현체를 사용하는 대신에, 모의 객체(Mock Object)를 만들어 사용할 수 있다. 이렇게 하면 데이터베이스 연결과 같은 외부 종속성 없이 ProductService의 비즈니스 로직만 테스트를 할수가 있다.

@Test
public void testGetProductDetails() {
    ProductDao mockProductDao = mock(ProductDao.class);
    ProductService productService = new ProductService(mockProductDao);

    Product product = new Product(1L, "Test product");
    when(mockProductDao.findById(1L)).thenReturn(product);

    Product result = productService.getProductDetails(1L);

    assertEquals(product, result);
}

2. 유연성

  • ProductDao 인터페이스의 두 가지 구현체, 예를 들어 ProductDaoJdbcImpl (JDBC를 사용하는 구현체)와 ProductDaoJpaImpl (JPA를 사용하는 구현체)가 있다고 가정해 보자.

  • 이러한 경우, 데이터베이스 액세스 기술을 변경하려면 애플리케이션 코드를 변경할 필요 없이 구현체만 교체하면 된다.

3. 다형성

  • 같은 Shape 인터페이스를 구현하는 Circle 클래스와 Rectangle 클래스가 있다고 가정하자. 이 인터페이스에는 draw()라는 메서드가 있다. 이 경우, draw() 메서드는 CircleRectangle에서 서로 다른 방식으로 구현될 수 있다.

  • 실행 시점에서 어떤 객체를 사용하냐에 따라 draw() 메서드의 동작이 달라진다.

public interface Shape {
    void draw();
}

public class Circle implements Shape {
    @Override
    public void draw() {
        System.out.println("Drawing a circle");
    }
}

public class Rectangle implements Shape {
    @Override
    public void draw() {
        System.out.println("Drawing a rectangle");
    }
}

public class ShapeDrawer {
    public void drawShape(Shape shape) {
        shape.draw();
    }
}
  • 사용하기
// In a client code
ShapeDrawer drawer = new ShapeDrawer();

Shape shape = new Circle(); // or new Rectangle();

drawer.drawShape(shape);
  • 위의 코드에서 ShapeDrawer 클래스는 Shape 인터페이스의 어떤 구현체든 사용할 수 있다. 이것이 바로 다형성의 힘이다. 실행 시점에서 Shape의 구현체를 바꾸면, ShapeDrawerdrawShape() 메서드가 그에 따라 다르게 동작한다.
  • 따라서, 인터페이스는 다형성을 지원하고, 코드의 유연성과 테스트 용이성을 높이며, 객체 간의 느슨한 결합(loose coupling)을 허용한다.

  • 이러한 요소들은 전체적으로 소프트웨어의 품질을 높이고 유지보수를 용이하게 한다.

  • 스프링 프레임워크에서는 이러한 인터페이스의 장점을 최대한 활용하여 효과적인 소프트웨어 설계와 구현을 지원한다.


4. 스프링에서 Interface를 활용하여 코드를 구현하는 방법

스프링 프레임워크에서는 인터페이스를 적극적으로 활용하여 이 원칙을 구현한다.

  • 예를 들어, DAO(Data Access Object)@Service와 같은 스프링 빈들은 특정 인터페이스를 구현한다.

  • 이렇게 하면 인터페이스를 통해 다른 컴포넌트와 상호작용하며, 실제 구현체의 세부 사항에 대해 알 필요 없이 메서드를 호출할 수 있다.

  • 이는 코드의 결합도(coupling)를 낮추고, 변경에 대한 영향을 최소화하여 유지보수성을 높인다.

인터페이스 활용예시1

  • 데이터 액세스 레이어(repository)에서 DAO를 구현할 때는 인터페이스를 사용하는 것이 일반적이다. 먼저 ProductDao 인터페이스를 만들고, 이를 구현하는 ProductDaoImpl 클래스를 생성할 수 있다.
public interface ProductDao {
    List<Product> findAll();
    Product findById(Long id);
    // Other CRUD methods...
}

@Repository
public class ProductDaoImpl implements ProductDao {

    @Autowired
    private JdbcTemplate jdbcTemplate;

    @Override
    public List<Product> findAll() {
        // Implementation of findAll...
    }

    @Override
    public Product findById(Long id) {
        // Implementation of findById...
    }
    // Other CRUD methods...
}
  • 이제 ProductService에서 ProductDao 인터페이스를 통해 데이터에 접근할 수 있다.
@Service
public class ProductService {

    private final ProductDao productDao;

    @Autowired
    public ProductService(ProductDao productDao) {
        this.productDao = productDao;
    }

    public List<Product> getAllProducts() {
        return productDao.findAll();
    }

    public Product getProductById(Long id) {
        return productDao.findById(id);
    }
    // Other methods...
}
  • 이 코드에서 ProductServiceProductDao 인터페이스에 의존하고 있으며, 구체적인 구현체인 ProductDaoImpl에 대해서는 알 필요가 없다.

  • 이렇게 인터페이스를 통해 DAO의 구현을 추상화하면, 데이터 액세스 기술이 변경되더라도 ProductService는 변경될 필요가 없다.

인터페이스 활용예시2

  • 만약 JdbcTemplate 대신 JPA를 사용하려면 ProductDao를 구현하는 새로운 클래스만 만들면 된다. 각기 다른 기술 스택으로 동일한 ProductDao 인터페이스를 구현하는 여러 구현체를 만들 수 있다.

JdbcTemplate를 사용하는 ProductDaoImpl과 JPA를 사용하는 ProductDaoJPAImpl를 생성할 수 있다.

@Repository
public class ProductDaoJPAImpl implements ProductDao {

    @PersistenceContext
    private EntityManager entityManager;

    @Override
    public List<Product> findAll() {
        // JPA Implementation of findAll...
    }

    @Override
    public Product findById(Long id) {
        // JPA Implementation of findById...
    }
    // Other CRUD methods...
}
  • 스프링 설정에서 어떤 구현체를 빈으로 등록할지 결정하면 된다. 예를 들어, JPA 구현체를 사용하려면 ProductDaoJPAImpl를 빈으로 등록하면 된다.

  • 이 방식은 코드의 유연성을 높이고, 기술 스택의 변경에 따른 영향을 최소화한다. 이는 개방-폐쇄 원칙(Open/Closed Principle)에 따라 기존 코드의 수정 없이 새로운 기능을 추가하거나 기존 기능을 변경할 수 있도록 해주기 때문이다.

profile
탐구하는 Backend 개발자

0개의 댓글