개발을 공부하기 전, 흥미가 있을때 엄청 헷갈리던 개념이었습니다. 강사님은 제어의 역전 유/무로 구분한다고 표현하셨는데요.
뭘 제어하지? 뭐랑 제어 역전이 된다는걸까?
-> 개발하면서 '컨테이너를 생성하고 메소드를 호출하는 것'을 개발자가 소스코드에 표현 한다면 제어의 역전이 없다고 보고, 이를 시스템이 한다면 제어의 역전이 있다고 합니다.
제어 = '컨테이너를 생성하고 메소드를 호출하는 것'을 개발자가 소스코드에 표현하는 것
Library는 제어의 역전이 없고 ,
FrameWork는 제어의 역전이 있다.
docker와 VM ware의 차이점(OSI 7Layer의 2단계=docker, 7단계=VM ware)을 공부하면서 container라고 개념을 처음 접하였습니다. 그 때에는 '애플리케이션을 추상화할 수 있는 논리 패키징 메커니즘' 이라고 익혔습니다.
그러나 Spring의 container는 'App운용에 필요한 bean(재사용 가능한 구성 요소, 객체)들의 생명(생성, 호출, 삭제)과 의존 관계를 관리하는 것'입니다.
BeanFactory는 Bean 객체를 생성하고 의존관계를 설정해주는 가장 최상위 interface입니다. getBean() 메서드를 통해 Bean을 가져 올 수 있으며 반환되는 객체의 자료형은 Object입니다.
ApplicationContext : BeanFactory를 확장한 interface이기에 다국어 처리, 이벤트 처리, AOP 등 다양한 기능을 제공하며 모든 Spring 애플리케이션에서 사용 가능한 기본적인 Spring Container입니다. XML, Java Config, Annotation 등 다양한 방식으로 Bean을 등록하고 관리할 수 있습니다.
WebApplicationContext : ApplicationContext를 확장하여, Servlet, Filter, Listener 등을 등록할 수 있고 DispatcherServlet과 연동하여 요청 처리와 응답 처리를 담당하는 Controller, View Resolver 등을 등록할 수 있는 웹 애플리케이션에서 필요한 기능을 추가로 제공합니다.
Spring Container에는 BeanFactory(bean LifeCycle관리), ApplicationContext(확장, 기본 컨테이너), WebApplicationContext(확장, WebApp필요 기능 추가제공) 세 종류가 있습니다.
일반적인 FrameWork는 특정 기술 분야에 집중이 되지만, SpringFrameWork는 국한되지 않고 Application의 전영역을 포괄하는 범용적인 FrameWork입니다.
엔터프라이즈 서비스 기능을 POJO(Plain Old Java Object)에 제공하는 것
이게 무슨말인가를 알려면 javaEE(Enterprise Edition), 결합도와 응집도, 객체지향적 설계라는 단어들을 알아야 합니다.
javaEE는 javaSE스펙을 기반으로 하며 다수 벤더와 선두업체사이의 협업을 나타내고 애플리케이션을 위한 인프라스트럭처 지원을 제공합니다.
IBM에서 javaEE를 저렇게 정의하고 있습니다. 좀 어렵게 느껴질 수 있지만, 사업체 간에 문제가 없을 정도 규모의 시스템, 보안이나 DB의 트랜잭션같은 레벨의 무언가인가봐요. 그런데 이런걸 구현하려면 상속도 좀 많이 받고 API등을 여기저기서 끌어와야 할 거 같은데 말이죠. 그럼 뭔가 수정사항 생기면 골치아플거 같은데요...!
결합도는 낮출수록, 응집도는 높일수록 좋다.
위와같은 상황을 예방하기 위해서는 결합도는 낮고, 응집도는 높은 독립적인 모듈로 만들어야 합니다. 정보처리기사를 공부하셨다면 결합도와 응집도라는 단어들이 낯설지 않으실건데요. 자격증 공부적인 설명을 하기 보다는, 이 포스트에서 언급 된 이유는 이들이 모듈의 독립성을 판단하는 지표들이기 때문이죠. 결합도가 높으면 코드를 수정할 때 연쇄적으로 수정할 것이 많아지기에 낮은게 좋고, 응집도가 높으면 필요한 내용들끼리 뭉쳐있을 것이기에 높을 수록 좋습니다.
이렇게 하기 위한 대 원칙이 객체지향적 설계이며, 이를 위한 5대원칙이 SOLID입니다.
Single Responsibility Principle(단일 책임 원칙)
"어떤 클래스를 변경해야 하는 이유는 오직 하나 뿐이어야 한다." -로버트 C. 마틴
단일 책임 원칙은 클래스나 모듈 등의 소프트웨어 엔티티는 하나의 책임, 기능, 역할만 가져야 한다는 원칙입니다. 클래스에 너무 많은 기능이 포함되면 코드가 복잡해지고 유지보수가 어려워질 수 있습니다. 이를 방지하기 위해 각각의 클래스나 모듈은 단 하나의 책임만을 가져야 하며, 다른 책임에 대해서는 다른 클래스나 모듈에 위임해야 합니다. 이렇게 하면 코드가 더욱 명확해지고 확장성이 높아집니다. 예를 들어, 한 사람에게 기획, 개발, 매장 관리와 같은 다양한 메인 직무를 모두 담당시키면 업무능률이 나오지 않듯이 메인 직무 별 적합한 사원을 채용하듯이, 각 기능별로 클래스를 분리해서 구현하는 것이 좋습니다.
Open Closed Principle(개방 폐쇄 원칙)
"소프트웨어 엔티티는 확장에 대해서는 열려 있어야 하지만 변경에 대해서는 닫혀 있어야 한다." - 로버트 C. 마틴
위 문장에서 소프트웨어 엔티티는 클래스, 모듈, 메서드 등을 의미합니다. 확장이란 새로운 기능 추가를 의미하며 이에 대해 열림, 즉 지향 해야 하나 기존 코드의 수정인 변경에 대해서는 닫힘, 즉 지양함을 의미합니다. 개방 폐쇄 원칙이 잘 적용되면, 기존 코드를 수정하지 않고 새로운 기능을 추가할 수 있습니다.
Liskov Substitution Principle(리스코프 치환 원칙)
“프로그램의 객체는 프로그램의 정확성을 깨뜨리지 않으면서 하위 타입의 인스턴스로 바꿀 수 있어야 한다.” - 로버트 C. 마틴
코드의 안정성과 확장성을 보장하기 위한 원칙으로서 프로그램의 서브 타입(subtype)은 항상 그것의 기반 타입(base type)으로 교체될 수 있어야 한다는 원칙입니다. 즉, 기반 클래스에서 파생된 모든 서브 클래스는 기반 클래스에서 정의된 메서드와 속성을 사용하여 문제없이 대체 가능해야 합니다
List<String> list = new ArrayList<>();
ArrayList 클래스는 상위인 List 인터페이스를 구현하므로, List 인터페이스로 선언된 변수에 ArrayList 클래스의 객체를 할당하는 것이 가능합니다. 이를 통해, List 인터페이스를 사용하는 코드에서는 ArrayList 클래스의 구현 세부사항을 신경쓰지 않고 일관된 방식으로 객체를 사용할 수 있습니다.
Interface Segregation Principle(인터페이스 분리 원칙)
“특정 클라이언트를 위한 인터페이스 여러 개가 범용 인터페이스 하나보다 낫다.” - 로버트 C. 마틴
위의 문장을 저의 페르소나(외부에 드러내는 나의 모습, 외적 인격, 행동)에 비유해보자면, 교육생일때의 저는 "학습한다"가 주요 행위겠지요. 퇴근 후 식사시간의 저는 "요리한다", 새벽시간대의 저는 "잔다." 이렇듯 다양한 메서드(행동)을 할 수 있기에, 이 모든걸 뭉뚱그려서 "집중한다."라고 하면 어색하지요.
이렇듯 인터페이스 분리 원칙은 인터페이스를 클라이언트에 특화된 작은 단위로 분리하는 것이 범용 인터페이스 하나보다 낫다는 것을 강조합니다. 따라서 인터페이스 분리 원칙을 적용하면 각각의 클라이언트는 필요한 메서드만 구현하고 사용할 수 있으며, 불필요한 메서드를 갖지 않아서 의존성이 줄어들고 유지보수성이 높아집니다.
Dependency Inversion Principle (의존 역전 원칙)
"추상화에 의존해야지, 구체화에 의존하면 안된다" - 로버트 C. 마틴
즉, 가능하다면 class를 상속받기 보단 공통된 것들을 추상화한 interface를 implement하여 구현하는 것을 권장한다는 원칙입니다. class는 단일 상속만이 가능하고, interface는 다중상속이 가능하기 때문입니다. 이를 통해 코드의 유연성과 재사용성을 높일 수 있으며, 코드의 의존성을 느슨하게 유지함으로써 유지보수성이 높아지고 변경에 대한 비용이 줄어듭니다.
그렇기에 의존 역전 원칙이란, 고수준 모듈은 저수준 모듈에 의존하면 안 되며 이를 위해 추상화된 인터페이스에 의존해야 한다는 원칙입니다. 객체 간의 의존성이 추상화된 인터페이스에 의존하도록 구성함으로써 모듈 간의 결합도를 낮추고, 유연하고 확장 가능한 시스템을 설계할 수 있습니다.
프로그래밍의 모든 원칙들의 대 원칙은 "결집도는 낮게, 응집도는 크게(높게)"을 위해 존재합니다.
객체 지향 프로그래밍에서 한 객체가 다른 객체를 참조하고 사용하는 것입니다.
Bean 간의 결합도를 낮추고, 유연하고 확장 가능한 애플리케이션을 구성하기 위해 Bean과 같은 객체 간 의존관계를 Spring 컨테이너가 자동으로 처리하는 방식입니다.
a. Constructor Injection(생성자 주입)
가장 단순한 DI로, 생성자를 통해 객체 생성 시에 필요한 의존 객체를 주입하는 방식입니다. 이 방식을 사용하면 안정적인 객체 생성과 의존성 해결을 보장할 수 있습니다. 이 방식은 클래스의 의존성이 필수적일 경우 사용합니다. 아래는 예시 코드입니다.
public class FooService {
private BarService barService; //의존객체
//기본생성자는 생략되어 있음
//생성자 파라미터로 BarService 객체를 받아와서
public FooService(BarService barService) {
//this.barService 필드에 주입(injection)
this.barService = barService;
}
}
b. Setter Injection(수정자 주입)
생성자를 통해 필요한 의존성 객체를 주입하는 Constructor Injection과는 달리, Setter 메서드를 통해 필요한 의존 객체를 주입하는 방식입니다. 이 방식은 객체 생성 이후에도 필요한 의존성을 주입할 수 있으며, 생성자가 필요 없어 구현이 간단합니다. 하지만, Setter 메서드를 통해 객체 상태를 변경해야 하기 때문에 객체의 불변성이 보장되지 않을 수 있습니다. 이 방식은 의존성이 선택적일 경우에 사용됩니다.
아래는 예시코드 입니다.
public class FooService {
// BarService 타입의 멤버 변수 선언
private BarService barService;
// BarService 객체를 주입하는 setter 메서드 정의
public void setBarService(BarService barService) {
// 주입된 BarService 객체를 멤버 변수에 할당
this.barService = barService;
}
}
c. NameSpace(C, P)
NameSpace는 이름과 속성을 grouping하여 요소간의 충돌을 방지하는 것입니다.
그러므로 Constructor Injection(생성자 주입)과 Setter Injection(수정자 주입)을 한번에 사용 가능하고, XML 파일에 DI 설정 정보를 작성함으로써 코드와 DI 설정을 분리할 수 있는 DI방식입니다. 이를 통해 DI 설정 정보를 유연하게 변경할 수 있고, 관리하기 용이합니다.
NameSpace의 'C' 는 Constructor Injection을, P는 properties의 약자로 Setter Injection을 대신하여 XML 파일에서 프로퍼티 값을 설정할 수 있도록 지원합니다.
아래는 예시코드입니다.
<beans xmlns="http://www.springframework.org/schema/beans"
xmlns:c="http://www.springframework.org/schema/c"
xmlns:p="http://www.springframework.org/schema/p"
default-autowire="byName">
<bean id="fooService" class="com.example.FooService">
<constructor-arg ref="barService" />
</bean>
<bean id="barService" class="com.example.BarService">
<property name="barName" value="exampleBar" />
</bean>
</beans>