1. 어댑터 패턴(Adapter Pattern)
어댑터(변환기)를 사용하여 어떤 클래스의 인터페이스를 클라이언트가 사용하기 위한 인터페이스로 변환
클래스간 인터페이스 호환 문제를 간단하게 해결 가능
사용할 클래스의 인터페이스가 변경되어도 클라이언트는 기존의 인터페이스를 그대로 사용 가능
객체 어댑터(Object Adapter)
// 클라이언트 인터페이스 interface ClientInterface { int versionNumber(); String informationString(); } // 클라이언트 인터페이스로 조작하려는 인터페이스 interface TargetInterface { String introduce(); int version(); } // TargetInterface를 구현하는 클래스들 class TargetInterfaceImpl1 implements TargetInterface { public String introduce() { return "this is TargetInterfaceImpl1"; } public int version() { return 1; } } class TargetInterfaceImpl2 implements TargetInterface { public String introduce() { return "this is TargetInterfaceImpl2"; } public int version() { return 2; } } // TargetInterface의 구현체들을 ClientInterface로 사용 가능하도록 // 변환해주는 어댑터 클래스 class AdapterClass implements ClientInterface { private final TargetInterface targetInterface; public AdapterClass(TargetInterface targetInterface) { this.targetInterface = targetInterface; } public int versionNumber() { return this.targetInterface.version(); } public String informationString() { return this.targetInterface.introduce(); } } public class Main { public static void main(String[] args) { ClientInterface clientInterface; // TargetInterfaceImpl1을 clientInterface로 조작 // result: // 1 // this is TargetInterfaceImpl1 clientInterface= new AdapterClass(new TargetInterfaceImpl1()); System.out.println(clientInterface.versionNumber()); System.out.println(clientInterface.informationString()); // TargetInterfaceImpl2를 clientInterface로 조작 // result: // 2 // this is TargetInterfaceImpl2 clientInterface= new AdapterClass(new TargetInterfaceImpl2()); System.out.println(clientInterface.versionNumber()); System.out.println(clientInterface.informationString()); } }어댑터 클래스가 클라이언트 인터페이스를 상속한 뒤 구현부에서 타겟 인터페이스의 구현체를 사용
클래스 어댑터(Class Adapter)
어댑터 클래스가 클라이언트 인터페이스와 타겟 인터페이스 모두를 구현하도록 하는 방식
Java의 경우 다중상속이 허용되지 않기 때문에 구현 불가능
Spring에서는 Spring Integration이 대표적인 예시이다. Spring Integration은 어댑터 패턴을 사용하여 Spring 프레임워크 내부와 외부 시스템의 통합을 지원한다.
2. 프록시 패턴(Proxy Pattern)
특정 작업을 Proxy, 즉 대리자가 대신 수행해주는 패턴
Proxy는 실제로 호출할 서비스와 동일한 인터페이스를 구현
Proxy의 특정 메소드를 호출할 경우 실제 서비스의 해당 메소드를 호출한 결과를 반환
실제 서비스의 메소드 호출 전후로 별도의 로직을 수행하는 경우도 있음
예시
// 학생의 정보를 저장하고 id로 조회할 수 있는 서비스 interface SchoolService { String findById(Long id); String save(String studentInfo); } // SchoolService를 구현한 클래스 class SchoolServiceImpl implements SchoolService { public String findById(Long id) { return "student[id] Dto"; } public String save(String studentInfo) { return "inserted student Dto"; } } // 학생용 Proxy class StudentProxy implements SchoolService { SchoolService schoolService; public StudentProxy(SchoolService schoolService) { this.schoolService = schoolService; } public String findById(Long id) { return this.schoolService.findById(id); } // 학생이기 때문에 학생정보 삽입은 허가되지 않음 public String save(String studentInfo) { return "Permission Denied"; } } // 강사용 Proxy class TeacherProxy implements SchoolService { SchoolService schoolService; public TeacherProxy(SchoolService schoolService) { this.schoolService = schoolService; } public String findById(Long id) { return this.schoolService.findById(id); } // 강사는 학생정보 삽입이 가능 public String save(String studentInfo) { return this.schoolService.save(studentInfo); } } public class Main { public static void main(String[] args) { SchoolService service; // 학생 프록시 // result: // student[id] Dto // Permission Denied service = new StudentProxy(new SchoolServiceImpl()); System.out.println(service.findById(3L)); System.out.println(service.save("info")); // 강사 프록시 // result: // student[id] Dto // inserted student Dto service = new TeacherProxy(new SchoolServiceImpl()); System.out.println(service.findById(3L)); System.out.println(service.save("info")); } }실제 서비스인 SchoolServiceImpl에 직접 접근하지 않고 StudentProxy와 TeacherProxy를 통해 접근
TeacherProxy를 통해서만 학생 데이터 추가 작업을 수행할 수 있도록 설정
Proxy의 유형
보호 프록시(Protection Proxy)
실제 객체에 대한 접근을 권한에 따라 제한하기 위해 사용되는 프록시
실제 객체의 public, protected 등의 메소드도 프록시를 거치는 것으로 숨길 수 있음
위의 예시가 이에 해당
원격 프록시(Remote Proxy)
원격 객체를 대신하여 원격 객체가 로컬에 존재하는 것 처럼 서비스하기 위한 프록시
가상 프록시(Virtual Proxy)
아직 생성되지 않은 객체가 존재하는 것 처럼 보여주기 위한 프록시
실제 객체의 생성을 필요한 순간까지 지연
리소스의 요구량이 많은 작업을 필요한 순간까지 미루고 리소스가 적게드는 작업만 프록시가 처리하여 보여주도록 작업을 분산할 수 있음
이 외에도 다양한 유형의 프록시가 존재
단점
실제 객체에 접근시 프록시 생성의 오버헤드가 존재
로직이 복잡해지고 가독성이 떨어질 수 있음
프록시 클래스 내에 중복이 발생
Spring에서는 Java에서 지원되는 Dynamic Proxy를 사용하여 동적으로 프록시를 생성
Spring에서의 Proxy 패턴의 대표적인 예시는 어노테이션을 통한 AOP(Aspect Oriented Programming)
- ex) @Transactional 을 사용하면 메소드 호출 전후로 적절한 처리를 하여 메소드를 트랜잭션화
3. 데코레이터 패턴(Decorator Pattern)
기본적인 구조는 프록시 패턴과 동일
프록시는 서비스 호출 전후로 별도의 처리를 하더라도 서비스 호출 결과 자체는 변화없이 그대로 반환
그에 반해 데코레이터는 서비스 호출의 결과 자체에 변화를 가한 뒤 반환
예시
import java.util.List; import java.time.LocalDateTime; // 통계분석 결과를 반환하는 result 함수가 정의된 인터페이스 interface StatisticsService { String result(List<Integer> seq); } // StatisticsService를 구현한 실제 서비스 class StatisticsServiceImpl implements StatisticsService { public String result(List<Integer> seq) { int sum_val = 0; int cnt_val = seq.size(); for(int num: seq) { sum_val += num; cnt_val += 1; } return String.format("==== Result ====\ncount: %d\ntotal: %d\naverage: %d", cnt_val, sum_val, sum_val / cnt_val); } } // StatisticsService를 구현한 데코레이터 // 실제 서비스의 결과에 현재 Datetime을 덧붙여 반환 class TimeStampDecorator implements StatisticsService { StatisticsService statisticsService; public TimeStampDecorator(StatisticsService statisticsService) { this.statisticsService = statisticsService; } public String result(List<Integer> seq) { String result = this.statisticsService.result(seq); result += String.format("\n\n==== Datetime ====\n%s", LocalDateTime.now()); return result; } } public class Main { public static void main(String[] args) { StatisticsServiceImpl statisticsService = new StatisticsServiceImpl(); TimeStampDecorator timeStampDecorator = new TimeStampDecorator(statisticsService); List<Integer> seq = List.of(4, 5, 8, 6, 2, 5, 9); // 데코레이터를 통해 실제 서비스를 호출 // result: // ==== Result ==== // count: 14 // total: 39 // average: 2 // // ==== Datetime ==== // 2022-04-24T16:06:34.714910300 System.out.println(timeStampDecorator.result(seq)); } }프록시와 완벽하게 동일한 구조를 가짐
프록시와는 달리 결과값에 변화를 주어 반환
실제 서비스 호출 결과에 장식(Decoration)을 붙이기 위해 사용 가능
프록시 패턴과 거의 동일한 구조이기에 장/단점 또한 공유
4. 싱글톤 패턴(Singleton Pattern)
특정 객체의 인스턴스를 단 하나만 만들어 사용하는 디자인 패턴
싱글톤 패턴에 따라 설계된 객체는 처음에 하나의 인스턴스만을 생성하여 이를 재사용
만들어진 인스턴스는 static 영역에서 관리되기 때문에 클래스간에 공유 가능
처음에 만든 인스턴스를 재사용하기 때문에 메모리/시간 면에서 효율적
의미상 여러개의 인스턴스가 필요하지 않은 객체들이 하나의 인스턴스만을 가짐을 보장 가능
예시
class SharingCounter { static SharingCounter sharingCounter; private int count; // 생성자를 private로 선언하여 외부에서 new를 사용할 수 없도록 함 private SharingCounter() { this.count = 0; } // 외부에서 인스턴스를 얻기위해 호출해야할 메소드 public static SharingCounter getInstance() { if (sharingCounter == null) { sharingCounter = new SharingCounter(); } return sharingCounter; } // count를 1 증가시키는 메소드 public void increase() { this.count += 1; } // count값을 반환하는 메소드 public int getCount() { return this.count; } } public class Main { public static void main(String[] args) { // SharingCounter 인스턴스를 획득한 뒤 count값을 받아와 출력 // result: // 0 SharingCounter cnt = SharingCounter.getInstance(); System.out.println(cnt.getCount()); // count값 1 증가 cnt.increase(); // SharingCounter 인스턴스를 다시 획득한 뒤 count값을 받아와 출력 // 인스턴스를 한번만 생성하고 재사용하기 때문에 이전에 증가시켜둔 // count 값이 유지되고있는 것을 확인할 수 있음 // result: // 1 cnt = SharingCounter.getInstance(); System.out.println(cnt.getCount()); } }인스턴스가 처음에 단 한번만 생성되고 그 후 getInstance 를 아무리 호출해도 같은 인스턴스를 반환
Singleton 객체의 데이터는 결국 공유 가능한 데이터이기 때문에 동시성 문제를 고려한 설계가 필요
Singleton 객체의 책임이 너무 커져 이를 공유하는 객체들이 늘어나면 객체간 결합도가 높아져 객체지향의 원칙에 위배하게됨에 주의
동시성 문제나 결합도 문제를 생각하여 쓰기 가능한 속성(writable attribute)을 가지지 않도록 설계하는게 일반적
Spring에서는 Bean으로 등록된 객체들을 기본적으로 싱글톤으로 관리하는 방식으로 싱글톤 패턴을 적용
쓰기 가능한 속성을 가지는 객체를 Singleton Bean으로 등록하는 것은 지양해야함
별도로 @Scope 어노테이션을 사용하여 Prototype Bean으로 등록할 경우 해당 Bean은 싱글톤이 아니게되어 주입받을 때 마다 새로 인스턴스를 생성
단독으로 적용할 경우 객체지향의 원칙에 위배될 수 있는 위험이 많은 패턴이므로 적용시 주의 필요
5. 템플릿 메소드 패턴(Template Method Pattern)
변하지 않는 기능(공통 부분)은 상위 클래스에서, 자주 변경되고 확장될 기능은 하위 클래스에서 구현하도록 하는 디자인 패턴
예시
// 상위 클래스 Person abstract class Person { private String name; private int age; public Person(String name, int age) { this.name = name; this.age = age; } // 공통부분은 상위 클래스에서 정의(My name is ~) public void introduce() { System.out.printf("My name is %s, and I'm %d years old\n", name, age); giveExtraInfo(); } // 변경/확장이 일어날 부분은 하위클래스에게 구현하도록 함 abstract void giveExtraInfo(); } class Student extends Person { public Student(String name, int age) { super(name, age); } // 학생은 자신을 학생으로 소개 void giveExtraInfo() { System.out.println("I am Student"); } } class Teacher extends Person { public Teacher(String name, int age) { super(name, age); } // 강사는 자신을 강사로 소개 void giveExtraInfo() { System.out.println("I am Teacher"); } } public class Main { public static void main(String[] args) { Person student = new Student("David", 21); Person teacher = new Teacher("Sam", 35); // 학생과 강사의 자기소개 // result: // My name is David, and I'm 21 years old // I am Student // My name is Sam, and I'm 35 years old // I am Teacher student.introduce(); teacher.introduce(); } }추상클래스인 Person을 상속한 학생과 강사가 공통부분을 제외한 부분만을 오버라이딩
코드의 중복을 줄이고 하위 클래스는 하위 클래스의 로직에만 집중 가능
구현해야할 abstract method가 너무 많을 경우 사용하기가 복잡해짐
Spring에서는 대표적으로 Dispatcher Servlet이 이 패턴을 사용하여 구현되어있음
- FrameworkServlet에서 공통로직 처리 후 추상메소드 doService 메소드를 호출
- DispatcherServlet는 FrameworkService를 상속하여 doService를 구현
6. 팩토리 메소드 패턴(Factory Method Pattern)
객체를 생성하는 로직의 구현을 하위 클래스에게 위임하는 방식
예시
// Item Factory abstract class ItemFactory { abstract Item getItem(); } // Item abstract class Item { abstract String info(); } // ItemFactory 와 Item 을 상속한 클래스를 두 개씩 생성 class WeaponFactory extends ItemFactory { public Item getItem() { return new Weapon(); } } class Weapon extends Item { public String info() { return "This is Weapon"; } } class ArmorFactory extends ItemFactory { public Item getItem() { return new Armor(); } } class Armor extends Item { public String info() { return "This is Armor"; } } public class Main { public static void main(String[] args) { // 무기 factory 와 방어구 factory 생성 ItemFactory weaponFactory = new WeaponFactory(); ItemFactory armorFactory = new ArmorFactory(); // 각 factory 로부터 아이템 획득 Item fromWeaponFactory = weaponFactory.getItem(); Item fromArmorFactory = armorFactory.getItem(); // result: // This is Weapon // This is Armor System.out.println(fromWeaponFactory.info()); System.out.println(fromArmorFactory.info()); } }WeaponFactory와 ArmorFactory의 getItem 메소드를 통해 객체를 획득
서브클래스에 변경사항이 생기더라도 다른곳에는 수정사항이 발생하지 않음(결합도 감소)
확장에는 열려있고 수정에는 닫혀있음(Open-Closed Principle)
Spring에서 팩토리 패턴이 사용된 대표적 예시로 BeanFactory 가 있음
- 일반적으로 직접 호출할 일은 없지만 BeanFactory 의 getBean 메소드를 통해 Bean 객체 생성 가능
- Bean으로 등록된 객체들중 요청받은 Bean을 생성하여 반환
- 이 때, Singleton 으로 설계된 Bean의 경우 처음 생성한 객체를 다시 반환
7. 전략 패턴(Strategy Pattern)
로직에서 공통되는 부분만 구현해두고 변하는 부분은 외부로부터 주입받아 사용하는 디자인 패턴
변하는 부분만을 별도로 구현한다는 점에서 템플릿 메소드 패턴과 유사
예시
import java.util.List; // 전략 메소드를 가진 전략 인터페이스 interface SelectStrategy { int select(List<Integer> candidate); } // 전략 메소드를 구현한 객체들 // 가장 큰 숫자를 고르는 전략 객체 class MaxValueStrategy implements SelectStrategy { public int select(List<Integer> candidate) { int result = Integer.MIN_VALUE; for(int item: candidate) { if (item > result) { result = item; } } return result; } } // 절댓값이 가장 작은 숫자를 고르는 전략 객체 class MinAbsStrategy implements SelectStrategy { public int select(List<Integer> candidate) { int result = Integer.MAX_VALUE; for(int item: candidate) { if (Math.abs(item) < Math.abs(result)) { result = item; } } return result; } } // 전략 객체를 사용할 Context class Selector { public void selectNumber(List<Integer> numbers, SelectStrategy strategy) { System.out.println("숫자를 하나 뽑겠습니다."); System.out.printf("뽑힌 숫자는 %d 입니다.", strategy.select(numbers)); System.out.println("감사합니다."); } } public class Main { public static void main(String[] args) { // Selector 를 생성 Selector s = new Selector(); // 숫자 후보군 리스트를 생성 List<Integer> numbers = List.of(-20, -7, 1, 15, 67); // 절댓값이 가장 작은 수를 선택하는 전략을 주입하여 selectNumber 실행 s.selectNumber(numbers, new MinAbsStrategy()); // 가장 큰 수를 선택하는 전략을 주입하여 selectNumber 실행 s.selectNumber(numbers, new MaxValueStrategy()); } }- 공통 부분은 전략 객체를 사용할 컨텍스트 객체인 Selector에서 구현
- 로직이 변하는 부분은 SelectStrategy를 구현한 전략 객체를 주입받아 작업 수행
- 주입할 전략을 변경하는 것 만으로 메소드의 로직을 동적으로 변경/확장할 수 있음
8. Spring의 전략 패턴
Spring 에서는 의존성 주입(DI)과 팩토리 패턴을 사용하여 전략 패턴을 적용
예시
import java.util.HashMap; import java.util.List; import java.util.Map; import java.util.Set; // 전략 메소드를 가진 전략 인터페이스 interface SelectStrategy { int select(List<Integer> candidate); SelectStrategyType getSelectStrategyType(); } // 전략을 구분하기 위한 key 값을 정의한 Enum 클래스 enum SelectStrategyType { MIN_ABS, MAX_VALUE } // 전략 메소드를 구현한 객체들 // 가장 큰 숫자를 고르는 전략 객체 // @Component class MaxValueStrategy implements SelectStrategy { public int select(List<Integer> candidate) { int result = Integer.MIN_VALUE; for(int item: candidate) { if (item > result) { result = item; } } return result; } public SelectStrategyType getSelectStrategyType() { return SelectStrategyType.MAX_VALUE; } } // 절댓값이 가장 작은 숫자를 고르는 전략 객체 // @Component class MinAbsStrategy implements SelectStrategy { public int select(List<Integer> candidate) { int result = Integer.MAX_VALUE; for(int item: candidate) { if (Math.abs(item) < Math.abs(result)) { result = item; } } return result; } public SelectStrategyType getSelectStrategyType() { return SelectStrategyType.MIN_ABS; } } // 전략 객체를 생성해줄 Factory 클래스 // @Component class SelectStrategyFactory { private final Map<SelectStrategyType, SelectStrategy> selectStrategySet; // @Autowired public SelectStrategyFactory(Set<SelectStrategy> selectStrategies) { this.selectStrategySet = new HashMap<>(); for(SelectStrategy strategy: selectStrategies) { this.selectStrategySet.put(strategy.getSelectStrategyType(), strategy); } } public SelectStrategy getSelectStrategy(SelectStrategyType selectStrategyType) { return selectStrategySet.get(selectStrategyType); } } // 전략 객체를 사용할 Context class Selector { private SelectStrategyFactory selectStrategyFactory; // @Autowired public Selector(SelectStrategyFactory selectStrategyFactory) { this.selectStrategyFactory = selectStrategyFactory; } public void selectNumber(List<Integer> numbers, SelectStrategyType strategyType) { SelectStrategy strategy = this.selectStrategyFactory.getSelectStrategy(strategyType); System.out.println("숫자를 하나 뽑겠습니다."); System.out.printf("뽑힌 숫자는 %d 입니다.", strategy.select(numbers)); System.out.println("감사합니다."); } } public class Main { public static void main(String[] args) { // 실제로는 Spring Framework 에서 지원하는 Dependency Injection 으로 해결될 부분 Selector selector = new Selector(new SelectStrategyFactory(Set.of(new MaxValueStrategy(), new MinAbsStrategy()))); // 숫자 후보군 리스트를 생성 List<Integer> numbers = List.of(-20, -7, 1, 15, 67); selector.selectNumber(numbers, SelectStrategyType.MAX_VALUE); selector.selectNumber(numbers, SelectStrategyType.MIN_ABS); } }SelectStrategyType 열거체의 값으로 전략을 구분
SelectStrategyFactory는 각 타입에 해당하는 전략 객체를 반환하는 getSelectStrategy 메소드를 구현
Selector 는 SelectStrategyType를 주입받아 Factory로부터 그에 해당하는 전략객체를 획득/사용
main에서 Selector를 생성하는 과정은 실제 Spring에서는 의존성 주입(DI) 방식으로 이루어짐
예시 코드는 Spring Framework를 적용하지 않은 상태이기에 편의상 위와 같이 작성
Spring에 적용된 Strategy Pattern
@GeneratedValue
주로 엔티티 작성시 Id값을 자동생성하기 위해서 사용되는 어노테이션
GeneratedValue(strategy = GenerationType.IDENTITY) 와 같이 값 생성을 위한 전략 주입
@ManytoOne, @OneToMany, @OnetoOne, @ManytoMany
- 엔티티간 관계 매핑을 위해 사용되는 어노테이션
- @ManytoOne(fetch = FetchType.LAZY) 와 같이 연관 엔티티를 불러올 타이밍에 대한 전략을 주입
'프레임워크 > Spring' 카테고리의 다른 글
| #5 Spring AOP(Aspect Oriented Programming) (0) | 2022.04.30 |
|---|---|
| #4 Spring 의존성 주입 방법 (0) | 2022.04.27 |
| #2 Spring Bean (0) | 2022.04.22 |
| #1 Spring Boot (0) | 2022.04.21 |
| #0 Spring (0) | 2022.04.20 |