객체 지향 프로그래밍
- 객체 지향 프로그래밍은 개발과 유지보수를 쉽게 만들기 위해 생겨난 개념이다.
- 다형성, DIP, OCP등을 통해 장점을 알아보자.
Reference Code
public class OrderServiceImpl {
// Bad
private final OrderRepository orderRepository = new MemoryOrderRepository();
}
public interface OrderRepository {
void save(Order order);
}
public class MemoryOrderRepository implements OrderRepository {}
public class JpaOrderRepository implements OrderRepository {}
다형성
개념
- 하나의 타입(인터페이스, 클래스, 메소드)이 다른 역할을 하는 것
인터페이스, 클래스
- 구현 또는 상속 필요
OrderRepository
는 하나의 인터페이스지만, 자신을 구현하는 MemoryOrderRepository
를 대입할 수도 있고 JpaOrderRepository
를 대입할 수도 있다.
메소드
Overriding
- 구현 또는 상속 필요
save(Order order)
는 하나의 메소드지만 MemoryOrderRepository
와 JpaOrderRepository
에서 다르게 구현할 수 있다.
Overloading
println(String x)
, println(int x)
는 같은 이름의 메소드지만 다른 타입의 파라미터를 대입할 수 있다.
장점
- 역할(Interface)과 구현(Class)을 분리할 수 있다.
- 클라이언트를 변경하지 않고 서버의 구현 기능을 유연하게 변경할 수 있다.
- 인터페이스를 구현한 객체 인스턴스를 실행 시점에 유연하게 변경할 수 있다.
DIP(Dependency Inverse Principle)
설명
- 상위 모듈(
OrderServiceImpl
)은 하위 모듈(MemoryOrderRepository
, JpaOrderRepository
)을 import해선 안된다.
- 상위 모듈과 하위 모듈은 같은 추상화(
OrderRepository
)를 의존해야 한다. (추상화: Interface 또는 abstract class)
- 상위 모듈에선 import
- 하위 모듈에선 implements 또는 extends
- 추상화(
OrderRepository
)는 세부 사항(MemoryOrderRepository
, JpaOrderRepository
)에 의존하지 않고 세부 사항이 추상화에 의존한다.
- 추상화는 세부 사항을 import하지 않기 때문에 의존 관계를 알 수 없다.
- 세부 사항은 추상화를 implements 또는 extends 하기 때문에 의존 관계를 알 수 있다.
OCP(Open-Close Principle)
설명
DIP, OCP 위배
Reference Code의 문제점
OrderServiceImpl
내부에서는 OrderRepository
를 의존하고 있는 것처럼 보이지만, 동시에 MemoryOrderRepository
도 의존하고 있다.
- 상위 모듈(OrderServiceImpl)에서 하위 모듈(MemoryOrderRepository)을 import하고 있으므로 DIP에 위배된다.
- 만약 요구사항이 바뀌어서
MemoryOrderRepository
를 JpaOrderRepository
로 바꾸어야 한다면 OrderServiceImpl
을 다음과 같이 수정해야 한다.
private final OrderRepository = new MemoryOrderRepository();
-> private final OrderRepository = new JpaOrderRepository();
- 수정이 일어났으므로 OCP에 위배된다.
- 다형성을 사용했지만 DIP와 OCP가 전부 위배되었다.
- 다형성 만으로는 DIP, OCP를 준수할 수 없다.
해결방법
- 상위 모듈에서 모든 하위 모듈과의 의존을 끊고, 인터페이스만 선언하며, 상위 모듈의 생성자에서 하위 모듈을 주입 받는다.
public class OrderServiceImpl {
// private final OrderRepository orderRepository = new MemoryOrderRepository();
private final OrderRepository orderRepository;
public OrderServiceImpl(OrderRepository orderRepository) {
this.orderRepository = orderRepository;
}
}
- 객체를 생성하고 연관관계를 맺어주는 역할을 할 설정 클래스를 생성한다.
public class AppConfig {
public OrderService orderService() {
return new OrderServiceImpl(orderRepository());
}
public OrderRepository orderRepository() {
return new MemoryOrderRepository();
}
}
- 객체가 필요한 곳에서는 이제
new OrderServiceImpl()
이 아닌 설정 클래스(AppConfig
)를 통해 가져온다.
public class OrderServiceTest {
public static void main(String[] args) {
AppConfig appConfig = new AppConfig();
OrderService orderService = appConfig.orderService();
}
}
DIP, OCP 준수
- 이제부턴
MemoryOrderRepository
를 JpaOrderRepository
로 바꾸어야 한다면, 논리 코드를 변경할 필요 없이 설정 코드만 변경하면 된다.
- 다형성과 설정 클래스를 통해서 DIP, OCP를 준수할 수 있게 되었다.
IoC, DI
IoC(Inversion of Control, 제어의 역전)
- 기존에는 상위 모듈(
OrderServiceImpl
)이 스스로 필요한 하위 모듈 객체를 생성하고, 연결하고, 실행했다. 즉, 상위 모듈이 프로그램의 제어 흐름까지 담당했다.
- 이제는 설정 클래스(
AppConfig
)가 제어 흐름(OrderServiceImpl
-> MemoryOrderRepository
)을 담당한다. 즉, 설정 클래스가 하위 모듈 객체를 생성하고, 상위 모듈 연결까지 담당하게 되었다.
- 상위 모듈(
OrderServiceImpl
)은 추상화(OrderRepository
)에 어떤 객체가 연결되는지와 관계없이 자신의 로직 실행만 담당하면 된다.
- 이렇듯 프로그램의 제어 흐름을 작성한 코드 외부에서 담당하는 것을 제어의 역전(IoC)이라고 한다.
프레임워크, 라이브러리
- 프레임워크는 내가 작성한 코드를 대신 제어하고, 대신 실행한다.(Spring, Spring Security, JUnit, JPA)
- 라이브러리는 내가 작성한 코드가 직접 제어 흐름을 담당한다.(Gson, SnakeYAML, Math, String)
DI(Dependency Injection, 의존성 주입)
- 객체(
OrderServiceImpl
)의 외부(AppConfig
)에서 의존 객체(MemoryOrderRepository
)를 주입한다.
- 클래스(
OrderServiceImpl
)에 인터페이스만 선언되어 있을 때, 자신은 어떤 객체가 주입될 지 알 수 없다.
스프링 컨테이너(Spring Container)
- 기존에는 개발자가 직접 Java 코드로
AppConfig
를 만들어서 직접 객체를 생성하고 DI를 했지만, 이제는 스프링 컨테이너를 통해서 객체를 생성하고 등록하고 찾아서 DI를 한다.
ApplicationContext
를 스프링 컨테이너라고 한다.
- 스프링 컨테이너에 등록된 객체를 스프링 빈이라고 한다.
@Component
, @Controller
, @Service
, @Repository
, @Configuration
, @Bean
- 스프링 컨테이너는
@Configuration
이 붙은 클래스(AppConfig
)를 설정 정보로 사용한다.
@Configuration
클래스 안에 있는 @Bean
이 붙은 메소드를 모두 호출하고 반환된 객체를 스프링 컨테이너에 등록한다.
@Bean
이 붙은 메소드명을 그대로 스프링 빈의 이름(key)으로 사용한다.
applicationContext.getBean(key)
메소드를 사용해서 스프링 빈을 찾을 수 있다.