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

+ Recent posts