스프링이 가장 관심을 많이 두는 대상은 '오브젝트'다. 스프링은 오브젝트를 어떻게 효과적으로 설계하고, 구현하고, 사용하고, 개선해 나갈 것인가에 대한 기준을 마련해준다
→ 스프링이 관심을 갖는 대상인 오브젝트의 설계와 구현, 동작원리에 집중해보자!
1.1 초난감 DAO
: 사용자를 생성(add), 조회(get)하는 메소드를 각각 다음과 같은 순서로 작성했다
1. DB connection 획득
2. SQL 담은 Statement 생성, 실행
3. 결과를 받아 오브젝트에 옮기기
4. 작업을 위해 생성된 리소스(Connection Statement, ResultSet) 닫기
5. main()에 테스트코드 작성
→ 다음과 같이 작성한 코드도 의도된 동작을 충실히 수행하며 & 테스트도 가능하다
→ 하지만 이 코드를 개선해나가며
1.2 관심사의 분리
- 사용자 비즈니스, 요구사항은 바뀐다 → 오브젝트에 대한 설계, 코드가 변한다
→ 개발자에게 객체를 설계할 때 가장 중요한 사항 : 미래의 변화에 어떻게 대비해야 하는가?
→ 변화의 폭을 최소한으로 줄여야 한다
> 관심사의 분리 : 한 가지 관심이 한 군데에 집중되게 하는 것
: 관심이 같은 것끼리 모으고 + 관심이 다른 것끼리는 영향을 주지 않도록 분리한다
= 변경이 일어날 때 필요한 작업 최소화 + 변경이 다른 곳에 문제를 일으키지 않게 함
<초난감 DAO의 관심사 분리>
1. 중복메소드 추출
: DB 연결을 위한 커넥션 / Statement 생성, 실행 / 리소스 반환
: 해당 관심사들이 현재 메소드들에 중복&흩어져 있음
→ 중복코드의 메소드를 추출해내자 : " 변경이 일어날 때 필요한 작업 최소화 " 성공
→ getConnection()이라는 메소드 추출
리팩토링 : 외부의 동작방식에는 변화가 없이, 내부 구조를 변경해서 재구성 하는 작업
2. 상속을 통한 확장 : 클래스 계층구조를 통한 분리
: 중복메소드(관심사)를 추상메소드로 표현 → 상속을 통해 서브클래스로 분리 & 원하는 대로 추상메소드 구현
: 변경에 용이하며 + 확장도 가능하다
: 추상메소드 getConnection() → UserDAO를 상속받은 NUserDAO, DUserDAO에서 각각 getConnection()을 구현
- 상속을 통해 기능을 확장시키는 디자인패턴
템플릿메소드패턴
: 슈퍼클래스에서 기본적인 로직 흐름 구성 → 서브클래스에서 필요에 맞게 구현하여 사용
- 슈퍼클래스 : 기본 알고리즘 담음 + 추상메소드 or 오버라이딩 가능한 메소드 정의
- 서브클래스 : 추상메소드 구현 or 오버라이드하여 기능을 확장함
팩토리메소드패턴
: 슈퍼클래스에서 서브클래스를 호출해 필요한 타입의 오브젝트를 리턴받아 사용한다
(인터페이스 타입으로 받음 + 서브클래스에서 오브젝트 생성하는 방법 재정의)
→ 오브젝트 생성방법, 클래스를 서브클래스에서 결정할 수 있게 하며, 기존 코드에서 독립시킴
2-1. 상속을 통한 확장의 한계
: 상속관계는 서로 다른 관심사에 대해 "긴밀한 결합"을 허용한다
3. 클래스의 분리
: DB 커넥션 관련 내용을 완전히 독립적인 클래스로 분리하기
public class UserDAO {
private SimpleConnectionMaker simpleConnectionMaker;
public UserDAO() {
simpleConnectionMaker = new SimpleConnectionMaker();
}
public void add(User user) {
Connection c = simpleConnectionMaker.makeNewconnection();
//c를 이용해 로직수행...
}
}
- 문제점 : 분리는 했지만, 자유로운 확장이 불가능하다
- 원인 : UserDAO → SimpleConnectionMaker에 종속적이기 때문
- 클래스의 이름, 메소드의 이름, ... 을 알고 있어야 한다 = 구체적인 방법에 종속되어 버린다
- 기본생성자 부분 : simpleConnectionMake = new SimpleConnectionMaker
3-1. 인터페이스의 도입
- 추상화 : 공통적인 성격을 뽑아내 이를 따로 분리하는 작업 : 추상화에 가장 유용한 도구가 인터페이스의 활용!
- UserDAO → ConnectionMaker라는 인터페이스에만 종속적
→ 실제적인 구현방식에는 영향x / 제공하는 기능만 이용하면 됨
- 문제점 : 여전히 구체적인 클래스의 이름이 나옴 : 의존성이 발생함
- 원인 : 구현클래스를 결정하는 코드가 남아있다
public class UserDAO {
private ConnectionMaker connectionMaker;
public UserDAO() {
// 구체적인 클래스의 이름 등장
connectionMaker = new DConnectionMaker;
}
}
3-2. 관계설정 책임의 분리
- 원인 : 구현클래스를 결정하는 코드가 남아있다 → 구현클래스를 결정해주는 책임을 가진 클래스를 분리해보자
- 클라이언트 오브젝트 : 다른 오브젝트의 기능을 사용하는 오브젝트
- 서비스 오브젝트 : 다른 오브젝트에 기능을 제공하는 오브젝트
- UserDAO를 사용하는, 클라이언트 오브젝트에게 관계결정의 책임을 부여하자
※ 오브젝트 사이의 관계설정?
: 런타임 시에 한쪽이 다른 오브젝트의 레퍼런스를 갖고 있는 방식
: 클래스 간의 관계x(코드에 직접 드러남) / 오브젝트(인스턴스) 사이의 동적인 관계
: 코드에는 보이지 않는, 런타임 시점에 오브젝트가 만들어진후 관계가 생성된다
- "사용" : 오브젝트의 레퍼런스를 변수에 넣어 사용한다
만들어진 오브젝트(인스턴스)를 받는 방법
1. 직접 생성자 호출해 생성
2. 외부에서 만든걸 가져오기 : 메소드 파라미터 or 생성자 파라미터
< 클라이언트의 역할>
1. 구현클래스 선택 : 실제 구현클래스 결정, 오브젝트 생성
ConnectionMaker connectionMaker = new DConnectionMaker();
2. 의존관계 설정(생성자를 통해 오브젝트 제공)
: 반드시 전달받을 오브젝트의 인터페이스 타입이어야 함
: 구현클래스에 관계없이 오브젝트를 제공받기 위함
UserDAO dao = new UserDAO(connectionMaker);
//UserDAO의 기본생성자 : 오브젝트를 인터페이스 타입으로 받음
public UserDAO(ConnectionMaker connectionMaker) {
this.connectionMaker = connectionMaker;
}
▶ 결론 : 인터페이스 도입 + 클라이언트(관계설정책임)클래스 도입 : UserDAO의 코드 변경 없이 다른 관심사의 기능을 확장하여 사용할 수 있게 됐다
[ 원칙과 패턴 ]
< OCP > : 개방폐쇄원칙
- 클래스는 확장에는 열려있고 / 변경에는 닫혀있어야 한다
- 기존 클래스에 영향없이 기능을 확장할 수 있고 + 다른 코드의 변화에 영향을 받지 않고 핵심기능을 유지할 수 있다
< 높은응집도 + 낮은결합도 >
- 높은 응집도 = 하나의 모듈이 하나의 책임에만 집중되어 있다 → 수정할 부분이 명확하며, 변화 시에 다른 클래스의 기능에 영향을 주지 않는다
- 낮은 결합도 = 다른 오브젝트에 변화를 요구하는 정도가 낮다 → 변경이 전파되지 않는 상태
UserDAO : 인터페이스를 통한 느슨한 연결 (낮은결합도) + 관심사 별 분리 (높은 응집도)
< 전략패턴 >
- 기능맥락에서 변경이 필요한 알고리즘을 인터페이스를 통해 외부로 분리 → 이를 구현한 구체적 클래스를 필요에 따라 변경해서 사용하는 디자인 패턴
클라이언트의 필요성 (UserDAOTest)
1. 컨텍스트가 사용할 전략을 선택하고 (DConnectionMaker)
2. 컨텍스트의 생성자 등을 통해 제공하기
1.4.1 팩토리
팩토리 : 객체의 생성방법 결정 + 만들어진 오브젝트를 제공(생성)하는 역할의 오브젝트
- 설계도 역할 : 오브젝트를 구성하고 관계를 정의하는 책임을 맡는다
-실직적 데이터 로직 담당/ 컴포넌트의 구조를 정의한 설계도 담당
//UserDAO에서 생성 책임을 맡은 팩토리 클래스 생성해보기
public class DAOFactory {
public UserDAO userDAO() {
//오브젝트 제공
return new UserDAO(connectioMaker);
}
public MessageDAO messageDAO() {
return new MessageDAO(connectionMaker);
}
//중복되는 오브젝트 생성코드는 분리하자
//오브젝트 생성방법 결정
public ConnectionMaker connectionMaker() {
return new DConnectionMaker();
}
}
//위 팩토리 클래스를 사용하는 코드
public class UserDAOTest {
public static void main(String[] args) throws ClassNotFoundException, SQLException {
//의존관계가 이미 주입된 UserDAO를 제공받는다
UserDAO userDAO = new DAOFactory().userDAO();
//...
}
}
1.4.3 제어의역전
- 오브젝트가 자신이 사용할 오브젝트를 직접 선택, 생성하지 않는다.
- 위임받은 제어권한을 가진 특별한 오브젝트가 다른 오브젝트를 결정하고 만든다.
- 제어의역전 개념의 활용
- 템플릿메소드 패턴 : 제어권을 상위 템플릿 메소드에 넘기고 필요시에 호출되어 사용된다
- 프레임워크 : 애플리케이션 코드가 프레임워크에 의해 사용된다. 프레임워크가 흐름을 주도하는 중에 애플리케이션 코드를 사용하는 방식이다.
1.5 스프링의 IoC
스프링 IoC의 용어정리
- 빈 Bean : 스프링이 IoC 방식으로 관리하는 오브젝트, 스프링이 직접 생성과 제어를 담당하는 오브젝트
제어의 역전이 적용된 오브젝트
- 빈 팩토리 Bean Factory : IoC를 담당하는 핵심 컨테이너 : 빈 등록, 생성, 조회, 반환, 관리 기능
- Application Contenxt : BeanFactory를 상속하는, 빈팩토리에 스프링이 제공하는 부가 서비스를 더한, 애플리케이션 지원 기능을 모두 포함하는 개념
- 설정정보 Configuration Metadata : 빈팩토리가 IoC를 적용하기 위해 사용하는 메타정보
1.5.2 애플리케이션 컨텍스트의 동작방식
- 애플리케이션 컨텍스트vs 오브젝트팩토리
:직접 오브젝트 생성, 관계맺기 x/ 별도의 설정정보를 통해 생성정보, 연관관계 정보를 얻는다
1. 클라이언트가 구체적인 팩토리 클래스를 알 필요가 없다
: 원하는 오브젝트를 생성하기 위한 팩토리 크래스를 직접 생성할 필요가 없다. 설정정보로 등록되어 있다면 애플리케이션 컨텍스트를 사용해 반환받을 수 있다
2. 생성, 관계설정 뿐만 아닌 오브젝트 생성방식, 시점, 전략을 바꾸는 종합적인 IoC 서비스를 제공받을 수 있다.
3. 다양한 방법으로 빈을 검색할 수 있다
: 빈 이름, 타입, 애노테이션 설정 ...
- 동작방식
1. 설정정보 @Configuration에 등록된 @Bean이 붙은 메소드의 이름을 가져와 빈 목록을 만든다
2. 클라이언트가 호출시, 빈 목록에서 요청한 이름 찾아서
3. 빈을 생성하는 메소드를 호출해 오브젝트 생성, 클라이언트에 반환
1.6 싱글톤 레지스트리
- 동일성 : 완전히 같은 오브젝트이다. 한 개의 오브젝트를 가리키는 레퍼런스이다
: 스프링은 빈을 여러 번 요청해도 동일성까지 보장한다.
- 동등성 : 동일한 값을 갖는다
: 오브젝트팩토리는 매 번 새로운 오브젝트를 생성하기 때문에 동등성만 보장한다
1.6.1 싱글톤 레지스트리 - 애플리케이션 컨텍스트
서버애플리케이션과 싱글톤
: 많은 요청이 들어오는 대규모 엔터프라이즈 서버환경은 높은 성능이 요구된다.
다양한 기능을 담당하는 오브젝트들이 참여하는 계층형 구조, 복잡한 비즈니스 로직을 갖기도 한다.
따라서 매번 새로운 오브젝트를 생성하는 대신, 서블릿 클래스 당 하나의 오브젝트만 만들고, 여러 스레드에서 하나의 오브젝트를 공유해 동시에 사용하는 서비스 오브젝트, 서비스 싱글톤의 사용이 권장된다.
// java에서의 싱글톤 구현 방식
public class UserDAO {
//생성된 싱글톤 오브젝트를 저장하는 스태틱 필드
private static UserDAO INSTANCE;
//외부에서 생성하지 못하도록 private 생성자 사용
private UserDAO(ConnectionMaker connectionMaker) {
this.connectionMaker = connectionMaker;
}
//스태틱 팩토리 메소드
public static synchronized UserDAO getInstance() {
//최초 호출시점에 한 번만 오브젝트가 생성된다
if(INSTANCE==null) INSTANCE = new UserDAO(???);
//이미 만들어진 오브젝트를 반환한다
return INSTANCE;
}
}
< 싱글톤 방식의 한계 >
1. private 생성자 → 상속 불가능 : 객체지향적인 설계의 장점을 이용할 수 없다.
2. 테스트 하기 어렵다 : 생성자를 통한 동적인 주입이 어렵기 때문에 테스트용 오브젝트로 대체하기 힘들다
3. 서버환경에서는 완전한 싱글톤을 보장하지 못한다
4. 전역상태로 사용될 가능성이 있다 : static 메소드로 싱글톤에 쉽게 접근할 수 있어 전역상태로 사용되기 쉽다.
< 싱글톤 레지스트리 >
: 기존의 싱글톤 패턴의 단점을 없앤, 스프링이 직접 싱글톤 형태의 오브젝트를 만들고 관리하는 기능 제공
: public 생성자를 가질 수 있다 → 객체지향적 설계방식, 디자인 패턴의 적용에 아무런 제약이 없다
= 스프링은 IoC 컨테이너 면서 + 싱글톤 레지스트리 이다
1.6.2 싱글톤과 오브젝트 상태
싱글톤 오브젝트 주의점
: 상태정보를 갖지 않는 stateless 한 방식이어야 한다
- 매 번 값이 바뀔 수 있는 정보는 인스턴스변수로 저장하면 안 된다 : 여러 사용자의 동시접속에 의해 값이 덮어쓰여질 위험
- 변경되는 값은 로컬변수로 사용하거나, 파라미터로 주고받으며 생성해야 한다
- 인스턴스 변수에는 읽기전용의 정보만 저장하자 : 다른 싱글톤 빈 저장 용도, ...
1.7 의존관계 주입(DI)
※ 의존관계 ※
- 의존대상의 변화에 영향을 받는다
- A가 B에 의존한다 = B의 변화에 A가 영향을 받는다 (ex. 사용관계)
1. 클래스를 통해 드러나는 의존관계 : 클래스 모델, 코드에서 드러나는 의존관계
2. 런타임 시점의 의존관계 : 런타임 시에 오브젝트 사이에 만들어지는 의존관계
- 구체적 클래스가 아닌, 인터페이스에만 의존하며 느슨한 연결관계를 설정해준 경우
- 클래스모델, 코드에는 의존관계가 드러나지 않음
- 의존오브젝트 : 런타임 시에 의존관계를 맺는 대상, 실제 사용대상 오브젝트
- 클라이언트 : 의존오브젝트를 사용할 주체
- 제 3의 존재(컨테이너, 팩토리)가 런타임의 의존관계를 결정해준다
- 사용할 오브젝트에 대한 레퍼런스를 외부에서 제공해줌으로써 생성된다.
Dependency Injection 의존관계 주입
: 오브젝트의 레퍼런스를 외부로부터 제공받아서 오브젝트와의 동적인 의존관계가 만들어지는 것.
1.7.3 의존관계 검색 vs 주입
< 의존관계주입 >
1. 생성자 파라미터를 통해 오브젝트의 레퍼런스 전달받기 : 런타임 의존관계를 갖는 오브젝트
2. 인스턴스 변수에 저장
public Class UserDAO {
// 2. 인스턴스 변수에 저장
private ConnectionMaker connectionMaker;
//1. 생성자 파라미터를 통해 오브젝트의 레퍼런스 전달받음
public UserDAO(ConnectionMaker connectionMaker) {
this.connectionMaker = connectionMaker;
}
}
< 의존관계 검색 >
의존 오브젝트를 능동적으로 찾는다
= 오브젝트 결정, 생성은 컨테이너가 수행 + 가져올때 스스로 컨테이너에게 요청한다
public UserDAO() {
AnnotationConfigAppliationContext context = new
AnnotationConfigAppliationContext(DAOFactory.class);
// getBeanㅁ소드로 컨테이너에 빈을 요청
this.connectionMaker = context.getBean("connectionMaker", ConnectionMaker.class);
}
- 주입 vs 검색 차이점
: 의존관계를 주입받으려면 오브젝트 자기 자신도 빈 오브젝트여야 한다.
: 주입받는 UserDAO에 대한 생성, 초기화 권한을 컨테이너가 갖고 있어야 하기 때문
- 결론 : 의존관계 주입을 사용하자
- 이유 : 서비스 오브젝트(UserDAO) 안에 팩토리 클래스(DAOFactory)가 나타나고, 스프링 API를 활용하는게 드러나는 것은 바람직하지 못하다
1.7.4 의존관계 주입의 응용
1. 기능구현의 교환
: @Configuration이 붙은 설정정보의 변경만으로 사용할 구현클래스를 변경할 수 있다
→ 개발/운영환경의 변경, 부서별 환경의 변경, 테스트시의 변경, ... 다양한 환경에 대해 기능구현을 쉽게 변경할 수 있으며DAO의 코드를 수정하지 않으므로 오류도 발생하지 않음을 보장할 수 있다.
@Bean
public ConnectionMaker connectionMaker() {
//여기만 바꾸면 됨!!
return new LocalDBConnectionMaker();
}
2. 부가기능 추가
: DAO가 의존하는 인터페이스를 구현하고 있다면 어떤 것이든 DI가 가능하다
= 부가기능을 추가한 인터페이스를 중간에 추가하여 새로운 런타임 의존관계를 만들 수 있다
1.7.5 메소드를 이용한 의존관계 주입
1. 수정자 메소드를 이용한 주입
: set으로 시작하며 + 한 개의 파라미터만 갖는, 내부의 애트리뷰트 값 변경 목적의 수정자 메소드를 통해 값 주인이 가능하다. 파라미터로 전달된 값을 내부의 인스턴스 변수에 저장한다
생성자 메소드를 사용한 주입에 비해 주입시점이 달라진다.
팩토리 같은 자바 코드 대신, XML을 사용해 설정하는 경우에 자바빈 규약을 따른 수정자 메소드가 편리하다.
2. 일반메소드를 이용한 주입
: 여러 개의 파라미터를 가질 수 있어 여러 개의 초기화 메소드를 만들 수 있다
3. 생성자메소드를 이용한 주입
1.8 XML을 이용한 설정
XML을 이용해 의존관계 설정정보를 만들 수 있다.
단순한 텍스트 파일로 다루기 용이함 / 별도의 컴파일, 빌드 작업 필요없음 / 빠른 변경사항 반영 가능
- 애노테이션 - 대응되는 XML
: XML로도 DI 정보의 세 가지 요소인 빈 설정파이, 이름, 클래스를 파악할 수 있다
- 빈 설정파일 : @Configuration → <beans>
- 빈 : @Bean → <bean>
- 빈 이름 @Bean method name → <bean>의 id애트리뷰트
- 빈의 클래스 return → class 애트리뷰트 (패키지 경로까지 전부 포함)
- 의존오브젝트와의 관계 : <property>
- <property name="'> : name 애트리뷰트 : 프로퍼티의 이름
- <property ref =""> : ref 애트리뷰트 : 주입해줄 오브젝트의 빈 이름 지정
<beans>
<bean id="connectionMaker" class="spring.dao.DConnectionMaker" />
<bean id="userDAO" class="spring.dao.UserDAO">
<property name="connectionMaker" ref="connectionMaker" />
</bean>
@Bean
public ConnectionMaker connectionMaker() {
return new DConnectionMaker();
}
//@Bean = <bean>
//conntionMaker() = <bean id="connectionMaer" ...
// return new DConnectionMaker = <bean class="spring.dao.DConnectionMaker" ...
//...
userDAO.setConnectionMaker(connectionMaker());
// setConnectionMaker = property.name
// connectionMaker = property.ref
- XML을 이용해 Application Context 설정하기
- xml 설정파일 이름 : applicationContext.xml로 설정
- GeneralApplicationContext()로 생성
- 생성자에는 설정파일의 클래스패스 넣기
ApplicationContext context = new GeneralApplicationContext("applicationContext.xml");
- ClassPathApplicationContext :XML을 클래스패스에서 가져올 때의 편의기능 추가 : 상대경로
- DataSource
: DB 커넥션을 가져오는 용도의 인터페이스가 이미 존재한다
//UserDAO
public class UserDAO {
private DataSource dataSource;
//레퍼런스 전달받는 생성자
public void setDataSource(DataSource dataSource) {
this.dataSource = dataSource
}
public void add(User user) throws SQLException {
Connection c = dataSource.getConnection();
//...
}
//dataSource 빈 정의 메소드
public DataSource dataSource() {
SimpleDriverDataSource dataSource = new SimpleDriverDataSource();
//DB 연결정보 : 수정자메소드로 주이반다
dataSource.setDriverClass(com.mysql.Driver.class);
dataSource.setUsername("spring");
//...
return DataSource;
}
//userDao 빈 정의 메소드
public UserDAO userDAO() {
UserDAO userDAO = new USerDAO();
userDAO.setDataSource(dataSource());
return userDAO;
}
<bean id="dataSource"
class="org.springframework.jdbc.dataSource.SimpleDriverDataSource" />
<property name="driverClass" value="com.mysql.jdbc.Driver" />
<property name="username" value="spring" />
</bean>
<bean id="userDAO"
class="spring.DAO.UserDAO">
<property name="dataSource" ref="dataSource />
</bean>
property 태그의 value 애트리뷰트 - 프로퍼티의 값 주입
: 다른 빈 오브젝트뿐만 아니라 단순 정보도 오브젝트를 초기화하는 과정에서 수정자 메소드에 넣을 수 있다
: 스프링은 수정자메소드(name 참고)의 파라미터 타입을 참고해 value의 스트링을 적절한 자바타입으로 자동 변환해준다 = 자동변환
- 헷갈리는 키워드
- 리플렉션
-추상클래스 vs 인터페이스
- 싱글톤 스코프 아닌 것
'Group Study (2024-2025) > Spring 심화' 카테고리의 다른 글
[ Spring 심화 ] 6주차 - AOP(1) (0) | 2024.12.01 |
---|---|
[ Spring 심화 ] 5주차 - 서비스 추상화 (2) | 2024.11.06 |
[ Spring 심화 ] 4주차 - 예외처리 (1) | 2024.10.29 |
[ Spring 심화 ] 3주차 - 템플릿 (0) | 2024.10.29 |
[ Spring 심화 ] 2주차 - 테스트 (6) | 2024.10.10 |