▶ 하루
=> 공부 관련
1) 스프링 개구리책 CH6
2) MVC 객체지향 리팩토링
3) 금일 배운 내용 복습
=> 파이널 프로젝트 관련
1) DB 재확인
▶ 개인 공부
5장 객체 지향 설계 5원칙
객체 지향 설계 5원칙
- 객체 지향의 개념과 4대 특성
- 좋은 도구을 알게됨.
- 좋은 도구가 있어도 올바르게 사용하는 방법을 알아야 좋은 요리가 나옴.
- 객체 지향 설계 (Object Oriented Design) 5원칙
- SOLID (객체 지향을 올바르게 사용하는 방법)
- SRP (Single Responsibility Principle) : 단일 책임 원칙
- OCP (Open Closed Princinple) : 개방 폐쇄 원칙
- LSP (Liskov Substitution Principle) : 리스코프 치환 원칙
- ISP (Interface Segregation Principle) : 인터페이스 분리 원칙
- DIP (Dependency Inversion Principle) : 의존 역전 원칙
- SOLID (객체 지향을 올바르게 사용하는 방법)
응집도는 높이고 결합도는 낮추는 객체 지향의 원칙을 지킬 수 있는 방법들.
- 결합도가 낮으면 모듈 간의 상호 의존성이 줄어들어 객체의 재사용이나 수정, 유지보수가 용이함.
- 응집도가 높은 모듈은 하나의 책임에 집중하고 독립성이 높아져 재사용이나 기능의 수정, 유지보수가 용이함.
SOLID를 녹여낸 소프트웨어
SOLID는 소프트웨어에 녹여내야하는 개념.
- SOLID를 잘 녹여낸 소프트웨어는 이해하기 쉽고 리팩터링과 유지보수가 쉽다.
- SOLID는 으로 삼고 이며 이다.디자인 패턴의 뼈대
- 스프링 프레임워크의 근간
- 객체 지향 4대 특성을 발판
SRP - 단일 책임 원칙
"어떤 클래스를 변경해야 하는 이유는 오직 하나뿐이여야 한다" - 로버트 C. 마틴
남자는 너무 많은 역할과 책임을 가진다
객체 지향 세계에서는 이런 경우 "나쁜 냄새"가 난다고 한다.
남자는 너무 많은 역할(책임)을 가진다
남자의 책임을 분리해 여러 개의 클래스를 만들자.
남자에게 단일 책임 원칙 적용
남자라는 클래스가 역할과 책임에 따라 네 개의 클래스로 쪼개졌다. 클래스명 또한 역할과 일치하기 때문에 이해하기 쉽다.
- 남자 친구는 여자 친구와 이별하더라도 다른 곳에는 영향을 주지 않는다.
- 단일 책임 원칙은 "속성, 메서드, 패키지, 모듈, 컴포넌트, 프레임워크" 등에 적용할 수 있다.
속성의 SRP 위배
- 문제 코드
public class 사람 {
String 군번;
public static void main(String[] args) {
사람 로미오 = new 사람();
사람 줄리엣 = new 사람();
줄리엣.군번 = "11-730411994";
}
}
줄리엣은 여자이므로 '군번'을 가질 수 없다. 하지만 줄리엣은 군번 속성에 값을 할당하거나 읽어올 수 있는 코드이다.
따라서 이 코드는 현재 냄새가 나고 있다...
- 가질 수 없는 속성 개선*
이를 개선하기 위해선 남자 클래스만 군번을 갖게 하면된다. 여자와 남자의 공통적만 모아 사람 클래스를 상위 클래스로 만들 수도 있고 둘의 공통점이 없다면 남자 클래스와 여자 클래스로 나눠서 구현할 수 있다.
- 하나의 속성이 여러 속성을 가지는 경우*
데이터베이스 테이블에 존재하는 하나의 필드가 여러 속성을 가르키게 되면 정규화를 통해 단일 책임 원칙을 유지하도록 해야한다.
메서드가 SRP 위배
- 문제 코드
public class 강아지 {
final static Boolean 숫컷 = true;
final static Boolean 암컷 = false;
Boolean 성별;
void 소변보다() {
if (this.성별 == 숫컷) {
// 한쪽 다리를 들고 소변을 본다.
} else {
// 뒤다리 두 개로 앉은 자세로 소변을 본다.
}
}
}
강아지가 "수컷"인지 "암컷"인지에 따라 메서드에서 분기 처리가 진행되고 있다.
이는 강아지 클래스에서 "소변보다"의 **암컷과 수컷 강아지의 행위를 모두 구현하려고 했기 때문에 "단일 책임 원칙"**을 위배하게 되었다.
- 개선 코드
package srp.good;
public abstract class 강아지 {
abstract void 소변보다();
}
public class 숫컷강아지 extends 강아지 {
void 소변보다() {
// 한쪽 다리를 들고 소변을 본다.
}
}
public class 암컷강아지 extends 강아지 {
void 소변보다() {
// 뒤다리 두 개로 앉은 자세로 소변을 본다.
}
}
추상 메서드를 통해 행위가 다른 메서드의 구현을 하위 클래스에게 위임한다.
- 암컷과 수컷 강아지의 서로 다른 행위의 구현을 하위 클래스에 맡겼기 때문에 "단일 책임 원칙"을 지킬 수 있었다.
"상속"은 "단일 책임 원칙"을 지키키 위한 도구로 사용한다.
모델링을 담당하는 "추상화"는 "단일 책임 원칙"과 가장 관계가 깊다.
- > "애플리케이션 경계(context)"를 정하고 추상화를 통해 클래스의 속성과 메서드를 설계할 때 반드시 단일 책임 원칙을 고려하자!!
OCP - 개방 폐쇄 원칙
소프트웨어 엔티티(클래스, 모듈, 함수 등)는 확장에 대해서는 열려 있고 변경에 대해서는 닫혀 있어야 한다." - 로버트 C. 마틴
- -> 위 인용을 의역해보면 라는 말이라고 한다...
- "자신의 확장에는 열려 있고, 주변의 변화에 대해서는 닫혀 있어야 한다"
<문제 코드>
운전자(자신)가 "소나타"를 운전하든 "마티즈"(주변)를 운전하든 행동에 변화가 오면 안된다.
- 주변의 변화에 자신이 변화해야 하는 경우 (문제)
현실 세계 였다면 자신이 변화하면 되는 문제지만 객체지향 세계는 다른 해결책을 사용한다.
- 개선 방법
개방 폐쇄 원칙 적용
상위 클래스 or 인터페이스를 중간에 둠으로써 다양한 자동차가 생긴다고 해도 객체지향세계의 "운전자"는 영향을 받지 않게 된다.
- 운전자의 입장에선 주변의 변화에 영향을 받지 않게 된다. (마티즈 - 소나타 든 신경안씀)
- 자동차의 입장에선 자신의 확장에 개방되 있는 것이다. (마티즈 - 소나타로 확장)
OCP의 예
JDBC
OCP예 : JDBC
- 자바 애플리케이션은 JDBC 인터페이스를 완충 장치 삼아 변화에 영향을 받지 않게 된다. (변화에 닫혀있음)
- 데이터베이스를 다른 데이터베이스로 교체하는 것은 자신의 확장에 열려있는 것이다. (확장에는 열려있음)
다른 예로는-> 개발자가 JVM이라는 완충 장치가 있기에 개발자가 작성한 소스코드는 운영체제에 닫혀있고 각 운영체제별 JVM은 확장에 열려있는 구조이다.
개방 폐쇄 원칙을 무시하고 프로그램을 작성하면 객체 지향의 장점인 유연성, 재사용성, 유지보수성 등의 이점을 얻을 수 없다.
==> 그 역할에 주어진 책임을 수행할 수 있다면 누구나 그 역할이 될 수 있다.
==> 스프링은 개방 폐쇄 원칙의 김연아라고 할 정도로 원칙을 잘 활용하고 있다.
⇒ 확장은 못하고 변화에 영향을 받지 않는다..??? (DIP!!)
LSP - 리스코프 치환 원칙
"서브 타입은 언제나 자신의 기반 타입(base type)으로 교체할 수 있어야 한다" - 로버트 C. 마틴
상속에서 기억해야 할 것 -> 객체 지향의 상속은 계층도가 아닌 분류도가 되어야 한다. (슈퍼클래스 서브클래스 구조)
- 리스코프 치환 원칙을 잘 지키고 있는 클래스
- 하위 클래스 is a kind of 상위 클래스 - 하위 분류는 상위 분류의 한 종류이다.
- 구현 클래스 is able to 인터페이스 - 구현 분류는 인터페이스할 수 있어야 한다.
잘못된 상속 예시 (계층도)
아버지 - 딸 (기반 타입 - 서브 타입)은 계층도 형태의 잘못된 상속이라고 볼 수 있다.
그렇다면 무엇이 문제일까? 한번 리스코프 치환 법칙을 적용해보자.
- 아버지 춘향이 = new 딸() -> 불가능!!!!
딸이 태어나 아버지의 역할을 하는 것은 말이 안된다.
춘향이는 아버지형 객체 참조 변수를 가지므로 아버지 객체가 가진 행위를 할 수 있어야 한다...(할 수 없음)
올바른 예시 (분류도)
- 동물 뽀로로 = new 펭귄()
펭귄 한 마리가 새롭게 태어나 동물의 행위를 한다. -> 이상한 점 없음
리스코프 치환 원칙을 만족한다.
- 결론*
- > "하위 클래스 인스턴스는 상위형 객체 참조 변수에 대입해 상위 클래스의 인스턴스 역할을 하는 데 문제가 없어야 한다"
리스코프 치환 원칙 적용 사례
리스코프 치환 원칙은 상속이라는 특성을 올바르게만 사용하면 자연스럽게 얻을 수 있다.
그렇다면 리스코프 치환 원칙이 위배되었을 때 어떤 문제가 발생하는가?
ISP - 인터페이스 분리 원칙
클라이언트는 자신이 사용하지 않는 메서드에 의존 관계를 맺으면 안 된다." - 로버트 C. 마틴
단일책임 원칙을 적용하기 전 남자 클래스를 살펴보자.
너무 많은 책임과 역할을 가진 남자..
서로 간의 영향을 줄이기 위해 **"단일 책임 원칙"**을 적용시킬 수 있었다.
이때 클래스를 토막내는 것 말고는 다른 방법은 없을까???
새로운 방법 : 인터페이스 분할 원칙
인터페이스 분할 원칙이 적용된 남자 클래스
이 방법은 남자 클래스를 책임과 역할에 따라 클래스를 쪼개는 것이 아니다.
대신 남자를 다중 인격화(?)(제한)시켜직장상사와 있을 땐 "출근하기(), 아부하기()" 행위만 하도록 제한하고
어머니와 있을 땐 "효도하기(), 안마하기()" 행위만 하도록 제한시키는 방법이다.
결론적으로 "단일 책임 원칙"과 "인터페이스 분할 원칙"은 같은 문제(다중 책임)에 대한 두 가지 다른 해결책이다.
- 인터페이스 분할 원칙은 인터페이스 최소주의 원칙을 가진다.
- 최소주의란 인터페이스를 통해 외부에 메서드 제공시 최소한의 메서드만 제공해야한다.
- 예를 들어, 사원 인터페이스에 키스하기() 메서드를 제공하면 안되는 것과 같은 이유이다. (사원의 책임에 부합하지 않는다.)
상위 클래스는 풍성할수록 좋고 "인터페이스"는 작을수록 좋다.
빈약한 상위 클래스 vs 풍성한 상위 클래스
빈약한 상위클래스를 사용하면 속성과 메서드가 하위클래스에서 중복되는 경우가 발생한다.
하지만 풍성한 상위 클래스에서는 하위 클래스에서 공통으로 가질 속성과 메서드를 상속한다.
DIP - 의존 역전 원칙
"고차원 모듈은 저차원 모듈에 의존하면 안된다. 이 둘은 모두 다른 추상화된 것에 의존해야한다."
의존 역전 원칙 적용 전(구체 클래스에 의존)
위의 예시처럼 자동차와 스노우타이어 사이에는 의존 관계가 존재한다.
자동차가 스노우타이어에 의존하는 관계이다.
자동차는 10년이상도 탈 수 있지만 스노우타이어는 계절이 바뀌면 일반 타이어로 바꿔줘야 한다.
- 즉, 스노우타이어가 자동차보다 자주 변경된다.
그렇기 때문에 자동차는 자신보다 자주 변경되는 클래스에 의존해 부서지기 쉽고 냄새를 풍기는 클래스이다. 이를 개선해보자!
개선 방법
의존 역전 원칙 적용 후
위 그림처럼 자동차가 "스노우타이어"가 아닌 추상화된 타이어 인터페이스에 의존하게 해보자.
인터페이스의 구현체가 변경(스노우 -> 일반)되도 자동차는 그 영향을 받지 않는다.
이 해결책은 "개방 폐쇄 원칙"도 녹아들어 있는 것을 볼 수 있다. 하나의 해결책에 여러 설계를 찾을 수 있다.
*자, 그러면 왜 **"의존 역전 원칙"*이라고 부를까??
이제 개선된 방법의 스노우타이어와 기존 방법의 스노우타이어를 비교해보자.
기존 스노우타이어는 어떤 곳에도 의존하지 않는 클래스였지만 개선 방법에선 추상적인 타이어 인터페이스에 의존하게 되었다.
즉, 자동차는 자신이 의존하던 타이어 대신 더 추상화된 타이어 인터페이스에 의존하고 스노우 타이어 또한 추상적인 타이어 인터페이스에 의존하도록 하는 것이다. (의존 방향이 바뀐 것을 볼수 있다)
⇒ 확장은 못하고 변화에 영향을 받지 않는다..??? (DIP!!)
- ***결론 -> "자신보다 변하기 쉬운 것에 의존하지 마라." ****
6장 스프링이 사랑한 디자인 패턴
'객체지향의 4대 특성'은 객체지향을 잘 사용하기위한 '도구'이다.
'객체지향의 5대 원칙'은 이러한 도구를 올바르게 사용하는 원칙으로 볼 수 있다.
그렇다면 디자인패턴은 무엇에 비유할 수 있을까?
'디자인 패턴'은 레시피에 비유할 수 있다.
실제 개발 현장에서 비즈니스 요구 사항을 처리하면서 만들어진 다양한 해결책 중 많은 사람들이 인정한 '베스트 프렉티스'를 정리한 것이다. (디자인 패턴은 당연히 객체 지향 특성과 설계 원칙을 기반으로 구성)
- > 스프링 역시 다양한 디자인 패턴을 활용하고 있다!
- 스프링의 공식적 정의 : "자바 엔터프라이즈 개발을 편하게 해주는 오픈소스 경량급 애플리케이션 프레임워크"
디자인 패턴은 객체 지향의 특성 중 '상속', '인터페이스', '합성'을 이용한다. (합성은 객체를 속성으로 사용하는 것)
어댑터 패턴 (Adapter Pattern)
한 클래스의 인터페이스를 클라이언트에서 사용하고자하는 다른 인터페이스로 변환한다.
ODBC/JDBC가 어댑터 패턴을 이용해 다양한 데이터베이스 시스템을 단일한 인터페이스로 조작할 수 있게 해준다.
'어플리케이션' - '어댑터' - '실제 구현체'
어댑터 패턴 사용
public class AdapterServicA {
ServiceA sa1 = new ServiceA();
void runService() {
sa1.runServiceA();
}
}
public class AdapterServicB {
ServiceB sb1 = new ServiceB();
void runService() {
sb1.runServiceB();
}
}
메인 메서드
public class ClientWithAdapter {
public static void main(String[] args) {
AdapterServicA asa1 = new AdapterServicA();
AdapterServicB asb1 = new AdapterServicB();
asa1.runService();
asb1.runService();
}
}
어탭터에서 동일한 이름의 runService()를 사용해서 각각의 runServiceX를 호출하고 있다.
좀 더 개선해보자면 어탭터가 특정 인터페이스를 구현하게 해서 하나의 인터페이스에서 의존관계 주입을 통해 똑같은 메서드로 각각 다른
구현체의 메서드를 호출할 수 있다.
"호출당하는 쪽의 메서드를 호출하는 쪽의 코드에 대응하도록 중간에 변환기를 통해 호출하는 패턴"
프록시 패턴(Proxy Pattern)
프록시는 대리자라는 의미이다. 대리자라고 하면 다른 누군가를 대신해 그 역할을 수행하는 존재를 말한다.
프록시 패턴 사용
- 클래스
public interface IService {
String runSomething();
}
public class Proxy implements IService {
IService service1;
public String runSomething() {
System.out.println("호출에 대한 흐름 제어가 주목적, 반환 결과를 그대로 전달");
service1 = new Service();
return service1.runSomething();
}
}
public class Service implements IService {
public String runSomething() {
return "서비스 짱!!!";
}
}
- 메인 메서드
public class ClientWithProxy {
public static void main(String[] args) {
// 프록시를 이용한 호출
IService proxy = new Proxy();
System.out.println(proxy.runSomething());
}
}
프록시 패턴을 적용한 후 시퀀스 다이어그램
서비스 객체가 들어갈 자리에 대리자 객체를 대신 투입한다.
프록시 패턴의 중요 포인트
- 대리자(프록시)는 실제 서비스와 같은 이름의 메서드를 구현
- 대리자는 실제 서비스에 대한 참조 변수를 갖는다. (합성)
- 대리자는 실제 서비스의
- 같은 이름을 가진 메서드를 호출하고 그 값을 클라이언트에게 반환한다.
- 대리자는 실제 서비스의 메서드 호출 전후에 별도의 로직을 수행할 수도 있다.
프록시 패턴은
실제 서비스의 반환값은 변경하지
데코레이터 패턴
원본에 장식을 더하는 패턴!
프록시 패턴과 구현 방법이 같으나 최종적으로 반환하는 반환값에 장식을 덧입힌다.
public class Decoreator implements IService {
IService service;
public String runSomething() {
System.out.println("호출에 대한 장식 주목적, 클라이언트에게 반환 결과에 장식을 더하여 전달");
service = new Service();
return "정말" + service.runSomething();
}
}
반환 값에 다른 값이 추가된 것을 확인할 수 있다.
데코레이터 패턴의 중요포인트는 프록시 패턴의 중요 포인트에 **'반환값에 변화를 줄 수 있다'**는 점이다.
싱글턴 패턴 (Singleton Pattern)
싱글턴 패턴이란 인스턴스를 하나만 만들어 사용하기 위한 패턴
'커넥션 풀', '스레드 풀', '디바이스 설정 객체' 등과 같은 경우 인스턴스를 여러 개 만들게 되면
불필요한 자원을 사용하게 되고, 프로그램이 예상치 못한 결과를 만들 수 있다.
싱글턴 패턴을 적용하면 두 개의 객체가 존재할 수 없으므로
- 객체 생성을 위한 new에 제약을 걸어야 하고
- 만들어진 단일 객체를 반환할 수 있는 메서드가 필요하다.
- new를 실행할 수 없도록 생성자에 private 접근 제어자 설정
- 유일한 단일 객체를 반환할 수 있는 필요
- "정적 메서드"
- 유일한 단일 객체를 참조할 필요
- "정적 참조 변수"
싱클턴 패턴 적용을 위한 클래스 구성
public class Singleton {
static Singleton singletonObject;// 정적 참조 변수private Singleton() {
};// private 생성자// 객체 반환 정적 메서드public static Singleton getInstance() {
if (singletonObject == null) {
singletonObject = new Singleton();
}
return singletonObject;
}
}
생성자를 사용하지 못하도록 설정했고 **"정적 메서드"**를 실행해 정적 참조 변수에 객체가 할당되어 있지않으면 할당해서 반환하고 할당되어 있으면 참조 변수를 반환하도록 만들었다.
테스트 코드
public class Client {
public static void main(String[] args) {
// private 생성자임으로 new 할 수 없다.// Singleton s = new Singleton();
Singleton s1 = Singleton.getInstance();
Singleton s2 = Singleton.getInstance();
Singleton s3 = Singleton.getInstance();
System.out.println(s1);
System.out.println(s2);
System.out.println(s3);
s1 = null;
s2 = null;
s3 = null;
}
}
위 테스트 코드는 s1, s2, s3 모두 같은 인스턴스를 참조하게 된다.
T메모리 구성
4개의 참조 변수가 모두 하나의 단일 객체를 참조하는 것을 확인할 수 있다.'
싱글턴 패턴의 중요 포인트
- private 생성자를 갖는다.
- 단일 객체 참조 변수를 "정적 속성"으로 갖는다.
- 단일 객체 참조 변수가 참조하는 "단일 객체"를 반환하는 getInstance() 정적 메서드를 갖는다.
- 단일 객체는 쓰기 가능한 속성을 갖지 않는 것이 정석이다. (읽기 전용은 가능)
템플릿 메서드 패턴
상위 클래스의 견본 메서드에서 하위 클래스가 오버라이딩한 메서드를 호출하는 패턴
공통적으로 사용하는 메서드는 상위클래스에서 구현.
하위클래스마다 달라지는 것은 추상클래스로 구현 강제화.
하위클래스마다 달라질 수도 있는 것은 오버라이드 가능한 훅 메서드로 만듬.
개와 고양이를 놀아주는 playWithOwner라는 메서드가 있을 때 하나의 상위 클래스로 실제 인스턴스에 맞는 메서드로 실행하고 싶을 때 사용한다.
Animal 추상 클래스
public abstract class Animal {
// 템플릿 메서드public void playWithOwner() {
System.out.println("귀염둥이 이리온...");
play();
runSomething();
System.out.println("잘했어");
}
// 추상 메서드abstract void play();
// Hook(갈고리) 메서드void runSomething() {
System.out.println("꼬리 살랑 살랑~");
}
}
상위클래스에서 템플릿을 제공하는 playWithOwner() 템플릿 메서드를 제공한다.
그리고 템플릿 메서드 안에 있는 play() 추상 메서드와 runSomething() 메서드가 있다.
- > 추상 메서드 하위 클래스에서 구현 강제함
- > Hook 메서드 오버라이드 자유
이 때 템플릿 메서드의 구성요소를 알아 보자.
- 템플릿 메서드 : 공통 로직 수행, 로직 수행 중 추상메서드/훅 메서드 호출
- 템플릿 메서드에서 호출하는 추상메서드 ->
- 반드시 하위클래스가 오버라이딩 해야한다.
- 템플릿 메서드에서 호출하는 훅 메서드 -> 하위 클래스가
- 선택적으로 오버라이딩 한다.
템플릿 메서드 패턴의 클래스 다이어그램
팩터리 메서드 패턴 (Factory Method Pattern)
오버라이드 된 메서드가 객체를 반환하는 패턴
팩터리 메서드는 객체를 생성 반환하는 메서드를 말한다.
팩터리 메서드 패턴은 하위 클래스에서 팩터리 메서드를 오버라이딩 해서 객체를 반환하는 것을 의미한다.
추상 팩토리 메서드
public abstract class Animal {
// 추상 팩터리 메서드abstract AnimalToy getToy();
}
추상 팩토리 메서드 오버라이딩
public class Dog extends Animal {
// 추상 팩터리 메서드 오버라이딩@Override
AnimalToy getToy() {
return new DogToy();
}
}
메인 메서드
public class Driver {
public static void main(String[] args) {
// 팩터리 메서드를 보유한 객체들 생성
Animal bolt = new Dog();
Animal kitty = new Cat();
// 팩터리 메서드가 반환하는 객체들
AnimalToy boltBall = bolt.getToy();
AnimalToy kittyTower = kitty.getToy();
// 팩터리 메서드가 반환한 객체들을 사용
boltBall.identify();
kittyTower.identify();
}
}
팩터리 메서드 패턴은..
"오버라이드된 메서드가 객체를 반환하는 패턴"
전략 패턴 (Strategy Pattern)
전략 패턴 구성요소 3가지
- 전략 메서드를 가진 전략 객체
- 전략 객체를 사용하는 컨텍스트 (전략 객체의 사용자/소비자)
- 전략 객체를 생성해 컨테스트에 주입하는 클라이언트 (전략 객체의 공급자)
전략 객체의 개념도
전략 인터페이스
public interface Strategy {
public abstract void runStrategy();
}
전략 인터페이스 구현
public class StrategyGun implements Strategy {
@Override
public void runStrategy() {
System.out.println("탕, 타당, 타다당");
}
}
public class StrategySword implements Strategy {
@Override
public void runStrategy() {
System.out.println("챙.. 채쟁챙 챙챙");
}
}
전략을 사용하는 컨텍스트
public class Soldier {
void runContext(Strategy strategy) {
System.out.println("전투 시작");
strategy.runStrategy();
System.out.println("전투 종료");
}
}
전략 패턴의 클라이언트
public class Client {
public static void main(String[] args) {
Strategy strategy = null;
Soldier rambo = new Soldier();
// 총을 람보에게 전달해서 전투를 수행하게 한다.
strategy = new StrategyGun();
rambo.runContext(strategy);
System.out.println();
// 검을 람보에게 전달해서 전투를 수행하게 한다.
strategy = new StrategySword();
rambo.runContext(strategy);
System.out.println();
// 활을 람보에게 전달해서 전투를 수행하게 한다.
strategy = new StrategyBow();
rambo.runContext(strategy);
}
}
클라이언트는 전략을 다양하게 변경하면서 컨텍스트를 실행할 수 있다.
전략 패턴을 한 문장으로 요약하면..
클라이언트가 전략을 생성해 전략을 실행할 컨텍스트에 주입하는 패턴
전략 패턴에는 OCP, DIP가 적용된다.
템플릿 콜백 패턴 (Template Callback Pattern)
템플릿 콜백 패턴은 전략 패턴의 변형으로, 스프링 3대 프로그래밍 모델 중 하나인 DI에서 사용하는 특별한 형태의 전략 패턴이다.
전략패턴과 모든 것이 동일한데 전략을 익명 내부 클래스로 정의해서 사용한다.
따라서, 전략패턴에서 사용했던 StrategyGun, StrategySword는 필요가 없다.
public class Client {
public static void main(String[] args) {
Soldier rambo = new Soldier();
rambo.runContext(new Strategy() {
@Override
public void runStrategy() {
System.out.println("총! 총초종총 총! 총!");
}
});
System.out.println();
rambo.runContext(new Strategy() {
@Override
public void runStrategy() {
System.out.println("칼! 카가갈 칼! 칼!");
}
});
System.out.println();
rambo.runContext(new Strategy() {
@Override
public void runStrategy() {
System.out.println("도끼! 독독..도도독 독끼!");
}
});
}
}
익명 내부 클래스를 사용해서 오버라이드해서 사용하는 것을 볼 수 있다. (따로 클래스를 구현하지 않고 사용함 [콜백])
위 코드는 중복 코드가 발생한다...즉, 리팩터링이 가능하다.
리팩터링 해보기
public class Soldier {
void runContext(String weaponSound) {
System.out.println("전투 시작");
executeWeapon(weaponSound).runStrategy();
System.out.println("전투 종료");
}
private Strategy executeWeapon(final String weaponSound) {
return new Strategy() {
@Override
public void runStrategy() {
System.out.println(weaponSound);
}
};
}
}
클라이언트
public class Client {
public static void main(String[] args) {
Soldier rambo = new Soldier();
rambo.runContext("총! 총초종총 총! 총!");
System.out.println();
rambo.runContext("칼! 카가갈 칼! 칼!");
System.out.println();
rambo.runContext("도끼! 독독..도도독 독끼!");
}
}
중복되는 전략을 생성하는 코드가 컨텍스트 내부로 들어왔다. (중복되는 부분을 컨텍스트로 이관)
스프링은 이런 형식으로 리팩터링된 템플릿 콜백 패턴을 DI에 적극 활용하고 있다.
-
- 요약 **
"전략을 익명 내부 클래스로 구현한 전략 패턴"
OCP, DIP 적용된 설계 패턴
▶ 수업 내용
Spring Framework
Annotation
@Configuration
- Spring Container가 관리하기 위한 클래스를 Spring Bean으로 등록하기 기능을 제공하는 환경설정 클래스를 구현하기 위한 어노테이션
- ⇒ Bean Configuration File의 beans 엘리먼트와 유사한 기능을 제공하는 클래스
@Bean
- Spring Container가 관리하기 위한 클래스를 Spring Bean으로 등록하는 어노테이션
- ⇒ Bean Configuration File의 Bean 엘리먼트와 유사한 기능을 제공하는 어노테이션
- ⇒ Spring Bean으로 등록하기 위한 클래스를 객체로 생성하여 반환하는 메서드를 어노테이션으로 설정
- ⇒ 기본적으로 메서드명을 beanName으로 사용
- ⇒ @Bean 어노테이션의 name 속성을 사용하여 BeanName을 변경 가능
@Component =⇒ 아주 중요
- 클래스를 Spring Bean으로 등록하는 어노테이션
- ⇒ 기본적으로 클래스명을 beanName으로 설정 - 첫문자는 소문자로 변환
- 어노테이션의 value 속성을 사용하여 beanName 변경 가능
- ⇒ 다른 속성이 없는 경우 속성값만 설정
XML
⇒ Context Namespace를 추가해서 엘리먼트를 추가로 이용 가능함
component-scan
- Spring Annotation을 Spring Container가 처리할 수 있도록 설정하는 엘리먼트
- ⇒ context 네임스페이스의 spring-context.xsd 파일에서 제공하는 엘리먼트
- base-package 속성 : Spring Container가 Spring Annotation을 사용한 클래스를 검색하기 위한 패키지를 속성값으로 설정
constructor-arg
- 생성자의 매개변수에 값(객체)을 전달하기 위한 엘리먼트
- ⇒ 엘리먼트의 갯수만큼 매개변수가 선언된 생성자를 반드시 작성
- value 속성 : 매개변수에 전달할 값을 속성값으로 설정 - Value Injection
- ⇒ 전달값은 기본적으로 문자열(String)으로 전달 - 매개변수의 자료형에 의해 자동 형변환
- ⇒ 전달값이 자동으로 형변환 될 경우 NumberFormatException 발생 가능
- 엘리먼트 작성 순서에 따라 매개변수에 값(객체)가 차례대로 전달되어 저장
- index 속성 : 전달값이 저장될 생성자의 매개변수의 순서를 속성값으로 설정
- ⇒ 속성값은 0부터 1씩 증가되는 정수값 사용
- ref 속성 : 매개변수에 전달될 Spring Bean 객체의 beanName을 속성값으로 설정 - dependency Injection (값이 아닌 의존 관계 주입) ⇒ 포함관계를 구현하기 위해!
⇒ 객체 생성 후 주입된 것 위 클래스 객체를 아래 클래스의 필드에 주입함으로써 의존관계가 되었다.
property
- 필드의 Setter 메서드를 호출하여 필드값을 변경하는 엘리먼트
- name 속성 : 값을 변경할 필드명을 속성값으로 설정 (엄밀히 따지면 메서드명임) - 자동 완성 기능
- value 속성 : Setter 메서드의 매개변수에 전달될 변경값을 속성값으로 설정 - Value Injection
- ref 속성 : 매개변수에 전달될 Spring Bean 객체의 beanName을 속성값으로 설정 - dependency Injection (값이 아닌 의존 관계 주입) ⇒ 포함관계를 구현하기 위해!
- ⇒ ref 속성값을 변경하면 의존관계 변경
- ⇒ 프로그램이 아닌 Bean Configuration File을 이용하여 의존관계 보다 쉽게 설정하고 변경 가능
- Set 객체를 생성하기 위한 엘리먼트 [Set 인터페이스]
- 요소값을 추가하는 엘리먼트
- Spring Bean 객체를 요소로 추가하는 엘리먼트
- bean 속성 : Spring Bean의 beanName를 속성값으로 설정
- value
- List 객체를 생성하기 위한 엘리먼트 [List 인터페이스]
- Map 객체를 생성하기 위한 엘리먼트
- Map 객체에 엔트리(Entry)를 추가하는 엘리먼트
- 맵키를 설정하기 위한 엘리먼트
- 맵키로 사용될 이름을 설정하기 위한 엘리먼트
- value
- 맵키를 설정하기 위한 엘리먼트
- key
- Map 객체에 엔트리(Entry)를 추가하는 엘리먼트
- entry
- Properties 객체를 생성하기 위한 엘리먼트
- ⇒ 필드의 자료형이 Map<String, String>인 경우 props 엘리먼트로 초기화 가능
- Properties 객체에 엔트리(Entry)를 추가하는 엘리먼트
- ⇒ 엔트리의 Key & Value는 문자열로만 구성 가능
- ⇒ 엘리먼트 내용의 엔트리의 값으로 설정
- key 속성 : 맵키를 속성값으로 설정
- prop
- Set 객체를 생성하기 위한 엘리먼트 [Set 인터페이스]
- set
⇒ Constructor Injection & Setter Injection을 같이 사용하여 필드 초기화 설정 가능
PropertyPlaceholderConfigurer
- Properties 파일의 값을 Bean Configuration File에서 사용할 수 있는 기능을 제공하는 클래스
- ⇒ locations 필드에는 Properties 파일의 경로를 전달하여 저장
- ⇒ Bean Configuration File 에서는 ${Key} 형식으로 표현하여 값을 제공받아 사용
⇒ Spring 5.2 이상에서는 PropertySourcePlaceholderConfigurer 클래스 사용 권장
Dependency 관리
- Spring에서 Framework가 관리하는 Bean을 다른 Bean에서 사용할 수 있도록 설정해주는 역할까지 대행하는 것
- 두 Bean 간의 결합도가 최소화되어 유지보수에 큰 도움
DI (Dependency Injection) 의존 주입
Constructor Injection
- 생성자를 이용하여 필드 초기화 처리
- 생성자의 매개변수가 값을 전달받아 필드에 저장 - 필드 초기화
Setter Injection
- Setter 메서드를 호출하여 필드값 변경 처리
DI의 핵심은 인터페이스다!
- 필드의 자료형은 무조건 인터페이스 타입
⇒ 인터페이스를 못 만들었을 시 이런 기능이 있다~
Class
Service 클래스
- 프로그램 실행에 필요한 기능을 제공하기 위한 클래스 - 컴퍼넌트(모듈)
- ⇒ Service 클래스의 메서드는 DAO 클래스의 메서드를 호출하여 프로그램 실행에 필요한 기능 제공
- ⇒ Service 클래스가 변경돼도 프로그램 작성에 영향을 최소화하기 위해 인터페이스를 상속받아 작성
인터페이스를 상속받은 자식클래스의 객체를 필드에 저장해야 포함관계 성립
- ⇒ 부모 인터페이스를 참조변수(필드)로 선언하면 모든 자식 클래스의 객체 저장 가능
- => 부모 인터페이스의 참조변수(필드)로 메소드를 호출하면 참조변수(필드)에 저장된 자식클래스 객체의 메소드 호출 - 오버라이드에 의한 다형성
- 객체간의 결합도를 낮추어 유지보수의 효율성 증가
- => DAO 클래스가 클래스가 변경돼도 Service 클래스 클래스에 미치는 영향 최소화
의존 관계 자동 설정
bean 엘리먼트
autowire 속성
- no(기본), byName, byType, constructor 중 하나를 속성값으로 설정
- ⇒ 의존관계를 Spring Container가 자동으로 구현하기 위한 기능을 제공하는 속성
- no 속성 값 : 자동으로 의존관계를 설정하는 기능을 미제공
- byName 속성 값 : 필드와 같은 beanName의 Spring Bean 객체를 이용하여 Spring Container가 자동으로 의존관계를 설정 - Setter Injection
- ⇒ 필드명과 같은 이름의 beanName의 Spring Bean 객체가 없는 경우 자동으로 의존관계 미설정 - NullPointerException 발생
- byType 속성 값 : 필드의 자료형과 같은 자료형의 Spring Bean 객체를 이용하여 Spring Container가 자동으로 의존 관계 설정 - Setter Injection
- ⇒ 필드의 자료형이 인터페이스인 경우 자식 클래스의 Spring Bean 객체로 의존 관계가 자동 설정됨
'레거시' 카테고리의 다른 글
2022.03.11~03.13의 기록 (2) | 2022.03.12 |
---|---|
2022.03.08~03.10 의 기록 (0) | 2022.03.09 |
2022.03.05~03.06 의 기록 (0) | 2022.03.05 |
2022.03.03 의 기록 (0) | 2022.03.03 |
2022.02.28의 기록 (0) | 2022.02.28 |