반응형
스프링에서 중요한 개념중 하나인 다형성을 알아보자
1. 다형성이란 무엇인가: 객체지향 프로그래밍의 핵심 원리
객체지향 프로그래밍에서 다형성은 매우 중요한 개념이다. 이 개념은 단순하게 말해, 하나의 인터페이스나 클래스가 다양한 형태로 동작할 수 있다는 것을 의미한다. 다형성은 코드의 유연성과 재사용성을 높여주는 중요한 원칙이다.
1-1. 다형성의 실제 예시
- 예를 들어, 'Animal'이라는 인터페이스가 있고 이 인터페이스에는 'sound'라는 메서드가 정의되어 있다고 생각해보자. 이 'sound' 메서드는 모든 동물이 내는 소리를 추상화한 것이다.
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!");
}
}
- 여기서, 'Dog'와 'Cat' 클래스는 'Animal' 인터페이스를 구현한다. 두 클래스 모두 'sound' 메서드를 자신의 방식대로 구현한다. 이렇게 되면, 같은 'Animal' 인터페이스를 사용하지만 'Dog'와 'Cat'은 각각 다른 소리를 낼 수 있다.
public class Main {
public static void main(String[] args) {
Animal myDog = new Dog();
Animal myCat = new Cat();
myDog.sound(); // 출력: "Woof!"
myCat.sound(); // 출력: "Meow!"
}
}
- 이 코드에서 'myDog'와 'myCat'은 모두 'Animal' 타입의 참조 변수이지만, 각각 'Dog'와 'Cat'의 인스턴스를 참조하고 있다. 'sound' 메서드를 호출하면 참조하고 있는 실제 객체의 'sound' 메서드가 호출된다. 이처럼 인터페이스를 통해 서로 다른 동작을 하는 객체를 동일한 방식으로 다룰 수 있게 되며, 이는 코드의 유연성과 확장성을 향상시킨다.
2. 개방-폐쇄 원칙(OCP) 이해하기: 소프트웨어의 확장성과 유연성
개방-폐쇄 원칙(OCP)은 소프트웨어 설계의 핵심 원칙 중 하나다. 이 원칙은 "소프트웨어 컴포넌트(클래스, 모듈, 함수 등)는 확장에는 열려 있어야 하고, 수정에는 닫혀 있어야 한다"고 말한다. 이 원칙의 핵심은 기존 코드를 변경하지 않으면서도 새로운 기능을 추가하거나 기존 기능을 변경할 수 있도록 하는 데 있다.
2-1. OCP의 적용 예시: 상품 정렬
- 상품을 다양한 기준으로 정렬하는 상황을 생각해보자. 우선 Product 클래스와 이를 정렬하는 ProductSorter 클래스가 있다. 상품을 이름으로 정렬하고 싶다면 ProductSorter에 sortByName 메서드를 추가할 수 있다. 하지만 이 방식은 ProductSorter가 계속 변경되어야 한다는 문제가 있다. 이는 OCP를 위반하는 것이다.
public class Product {
private String name;
private double price;
// 생성자, 게터 및 세터...
}
public class ProductSorter {
public List<Product> sortByPrice(List<Product> products) {
// 가격으로 상품 정렬 후 반환
}
}
- OCP를 적용하기 위해서는 먼저 정렬 방식을 인터페이스로 추상화해보자.
public interface ProductSortStrategy {
List<Product> sort(List<Product> products);
}
public class PriceSortStrategy implements ProductSortStrategy {
@Override
public List<Product> sort(List<Product> products) {
// 가격으로 정렬 후 반환
}
}
public class NameSortStrategy implements ProductSortStrategy {
@Override
public List<Product> sort(List<Product> products) {
// 이름으로 정렬 후 반환
}
}
- 그리고 ProductSorter 클래스에서 이 인터페이스를 사용하도록 변경한다.
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. 인터페이스 사용의 장점: 테스트 용이성, 유연성, 다형성
인터페이스 사용은 소프트웨어 개발에서 매우 중요한 역할을 한다. 인터페이스를 통해 코드의 유연성, 테스트 용이성, 그리고 다형성을 크게 향상시킬 수 있다.
3-1. 테스트 용이성: 모의 객체를 통한 단위 테스트
- 예를 들어, ProductService라는 서비스 클래스가 ProductDao 인터페이스에 의존한다고 가정해보자. 이 경우, 단위 테스트를 수행할 때 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);
}
3-2. 유연성: 구현체의 교체 용이성
- 다음으로, ProductDao 인터페이스가 있다고 생각해보자. 이 인터페이스에는 ProductDaoJdbcImpl (JDBC를 사용하는 구현체)와 ProductDaoJpaImpl (JPA를 사용하는 구현체)과 같은 여러 구현체가 있을 수 있다. 이 경우, 데이터베이스 액세스 기술을 변경하고 싶을 때, 애플리케이션 코드를 수정할 필요 없이 구현체만 교체하면 된다.
3-3. 다형성: 같은 인터페이스, 다른 동작
- Shape 인터페이스를 구현하는 Circle과 Rectangle 클래스를 예로 들어보자. 이 인터페이스에는 draw()라는 메서드가 정의되어 있으며, Circle과 Rectangle에서는 이 메서드가 서로 다르게 구현된다. 실행 시점에 어떤 객체를 사용하느냐에 따라 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();
}
}
// 클라이언트 코드
ShapeDrawer drawer = new ShapeDrawer();
Shape shape = new Circle(); // 또는 new Rectangle();
drawer.drawShape(shape);
- ShapeDrawer 클래스는 Shape 인터페이스의 어떤 구현체도 사용할 수 있다. 이것이 바로 다형성의 힘이다. 실행 시점에 Shape의 구현체를 바꾸면 ShapeDrawer의 drawShape 메서드가 그에 따라 다르게 동작한다.
따라서, 인터페이스는 다형성을 지원하고, 코드의 유연성과 테스트 용이성을 높이며, 객체 간의 느슨한 결합(loose coupling)을 허용한다. 이러한 요소들은 전체적으로 소프트웨어의 품질을 높이고 유지보수를 용이하게 한다. 스프링 프레임워크에서는 이러한 인터페이스의 장점을 최대한 활용하여 효과적인 소프트웨어 설계와 구현을 지원한다.
4. 스프링에서 인터페이스 활용하기: 유연하고 확장 가능한 코드 작성법
스프링 프레임워크는 인터페이스의 활용을 통해 코드의 유연성과 확장성을 극대화한다. 인터페이스를 활용하는 방식은 스프링에서 매우 중요한 개념이며, 다양한 컴포넌트 간의 느슨한 결합(loose coupling)과 유지보수성 향상을 가능하게 한다.
4-1. 인터페이스를 통한 데이터 액세스 레이어 구현
- 예를 들어, 데이터 액세스 레이어(repository)에서 DAO(Data Access Object)를 구현하는 경우, 인터페이스의 사용은 표준이다. 먼저, ProductDao 인터페이스를 만들고, 이 인터페이스를 구현하는 ProductDaoImpl 클래스를 생성한다.
public interface ProductDao {
List<Product> findAll();
Product findById(Long id);
// 기타 CRUD 메서드들...
}
@Repository
public class ProductDaoImpl implements ProductDao {
@Autowired
private JdbcTemplate jdbcTemplate;
@Override
public List<Product> findAll() {
// findAll 구현...
}
@Override
public Product findById(Long id) {
// findById 구현...
}
// 기타 CRUD 메서드들...
}
4-2. 인터페이스를 활용한 서비스 계층
- 이제 ProductService는 ProductDao 인터페이스를 통해 데이터에 접근할 수 있다. 이렇게 인터페이스를 통해 구체적인 구현을 추상화하면, 데이터 액세스 기술이 변경되어도 ProductService 코드는 변경할 필요가 없다.
@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);
}
// 기타 메서드들...
}
4-3. 기술 스택 변경의 용이성
- 만약 데이터 액세스 기술을 JdbcTemplate에서 JPA로 변경하고 싶다면, 새로운 ProductDaoJPAImpl 클래스를 만들어 ProductDao 인터페이스를 구현하면 된다. 스프링 설정에서 이 새로운 구현체를 빈으로 등록하기만 하면, 기존 ProductService 코드를 변경하지 않고도 데이터 액세스 기술을 변경할 수 있다.
@Repository
public class ProductDaoJPAImpl implements ProductDao {
@PersistenceContext
private EntityManager entityManager;
@Override
public List<Product> findAll() {
// JPA로 findAll 구현...
}
@Override
public Product findById(Long id) {
// JPA로 findById 구현...
}
// 기타 CRUD 메서드들...
}
4-4. 결론
이러한 방식은 코드의 유연성을 높이고 기술 스택의 변경에 따른 영향을 최소화한다. 개방-폐쇄 원칙(OCP)에 따라, 기존 코드의 수정 없이 새로운 기능을 추가하거나 기존 기능을 변경할 수 있는 환경을 조성한다. 이는 소프트웨어 개발에서의 지속 가능한 성장과 변화에 적응하는 데 중요한 역할을 한다.
스프링에서 인스턴스를 생성하는 2가지 방법이 궁금하다면?
반응형
'Spring 기초 > Spring 기초 지식' 카테고리의 다른 글
Spring Boot 심화: 커스텀 어노테이션 만들기 (2편) (0) | 2023.08.08 |
---|---|
Spring Boot 기초: 어노테이션 활용하기 (1편) (0) | 2023.08.08 |
@ControllerAdvice, @RestControllerAdvice - 중앙집중 예외처리 (0) | 2023.08.07 |
스프링의 핵심: Singleton 패턴 알아보기 (0) | 2023.08.07 |
스프링 프레임워크의 핵심: 제어의 역전(IoC) (0) | 2023.08.07 |