본문 바로가기
Study

[스터디] 객체 지향 디자인 패턴

by DuncanKim 2022. 8. 4.
728x90

[스터디] 객체 지향 디자인 패턴

 

 

 

3주 차까지 공부했던 객체지향 4개 특성, SOLID에 이어 디자인 패턴을 알아보았다. SOLID 원칙의 특성을 담아 어떤 프로그램을 짜야할 것이다. 사람들이 SOLID라는 원칙에 따라서 프로그램을 설계하기 시작했는데, 마치 맛있는 음식을 만드는 방법처럼 하나의 패턴, 공식이 보이는 것이다. 그래서 이것들을 정리해보았고 디자인 패턴으로 불리게 된 것이다.

 

디자인 패턴은 하나만 있는 것은 아니다. 마치 백종원씨가 '김치찌개'를 여러 가지 재료와 조리 방식을 활용해서 만드는 것처럼. 7분 동안 돼지김치찌개를 만들면 새마을식당의 7분돼지김치찌개가 될 것이고, 자취생이 해 먹는 간단 김치찌개를 만들 수도 있다. 그렇지만, '맛'은 항상 평타 이상을 친다. 맛에도 공식이 있어서 그것을 따르면 '맛이 있는 김치찌개'가 되는 것이다.

 

여기서 김치찌개는 '프로그램', 각각의 김치찌개의 레시피는 프록시, 싱글턴 등등의 디자인 패턴이 될 것이다. 어쨌든 디자인 패턴은 결과물은 달리 내지만, 좋은 프로그램을 만들어내는 것에 필요한 것이다.

 

전에 한번 디자인 패턴에 대해 포스팅 한 적이 있었다. 

 

2022.07.07 - [IT 지식/CS] - [CS] 객체지향 디자인 패턴

 

[CS] 객체지향 디자인패턴

[CS] 객체지향 디자인패턴 Java의 객체 지향을 공부하다 보면, 어떻게 객체 지향 프로그램을 잘 '설계'하고 개발해나갈 것인가를 고민하게 된다. 이 때 우리에게 답을 주는 것이 "객체 지향 디자인

masterpiece-programming.tistory.com

이것에 이어 조금 더 자세하게 스터디를 진행해보았다.

 

 

1. 디자인 패턴을 대분류 하면?

 

목적에 따라 나누어볼 수 있다. 생성과 관련된 디자인 패턴, 구조와 관련된 디자인 패턴, 행위와 관련된 디자인 패턴이 있다.

 

생성은 인스턴스 생성과 관련이 깊다. 이 분류에 속하는 디자인 패턴은 어떤 하나의 클래스에서 객체를 생성하는 역할만 하고, 다른 구현 클래스에서 객체를 생성하는 클래스의 객체를 생성해서 메서드를 사용하는 방식을 사용한다. 

 

구조는 각각의 클래스를 더 추상화된 클래스, 인터페이스 밑에 정렬하는 방식으로 구조화하는 것과 관련이 있다. 이 분류에 속한 디자인 패턴은 SOLID의 개방 폐쇄 원칙(OCP), 의존 역전 원칙(DIP)이 적용되어 있는 패턴들이다.

 

행위는 어떤 객체가 여러 객체들을 갈아가면서 메서드를 호출해야 하는 상황 등을 정의한 것이다. 이 분류에 속한 디자인 패턴은 객체들이 어떻게 상호작용 할 것인지, 어떤 역할을 담당할 것인지를 명확하게 구분하기 위해서 존재하는 패턴들이라고 할 수 있다. 

 

 

 

2. 디자인 패턴 알아보기

 

이 책에서는 여덟 가지의 패턴들을 소개하고 있다.


어댑터 패턴 / 프록시 패턴 / 데코레이터 패턴 / 싱글턴 패턴 / 템플릿 메서드 패턴 / 팩토리 메서드 패턴 / 전략 패턴 / 템플릿 콜백 패턴


이전 포스팅과는 다르게 여덟 가지가 어떻게 실제로 '활용'이 되는지 알아볼 것이다.

들어가기 전에 디자인 패턴은 상속, 인터페이스, 합성(객체를 속성으로 사용) 이 세 가지를 이용한다는 것을 알고 가면 좋을 것 같다.

 

 

1) 어댑터 패턴(Adapter Pattern)

(구조와 관련)

 

충전기를 생각해보면 쉽다. 핸드폰을 콘센트에 직접 연결할 수 없고 충전기를 통해 전기를 공급받는 것을 생각해보자. 마찬가지로, 직접 데이터를 구현 클래스에서 받지 않고, 어떤 인터페이스 또는 상위 클래스에 '어댑터'와 같은 역할을 부여하거나, 동일한 메서드를 가진 클래스를 생성해서 '일정한 메서드명만 쓰면서 각각의 특징에 맞는 행위들을 하게 만든다.'

 

('메서드 명의 통일과 관련이 깊다.')

 

class ServiceA {
	void runServiceA() {
		System.out.println("ServiceA");
	}
}

class ServiceB {
	void runServiceB() {
		System.out.println("ServiceB");
	}
}

class AdapterServicA {
	ServiceA sa1 = new ServiceA();

	void runService() {
		sa1.runServiceA();
	}
}

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() 메서드를 구현하지 않았다면, 각각의 AdapterServiceA, B에서 runServiceA() runServiceB() 이런 식으로 메서드를 만들어서 사용했을 것이다. 물론, 이름을 같게 해서 생성을 할 수 있겠지만 코드의 가독성이 떨어질 수 있다. 

 

그래서 저런 식으로 클래스 안에서 ServiceB라는 객체를 만들어 놓고,  메서드명을 통일해 놓으면, 각각의 ServiceA, B 클래스에 있는 runServeiceA, B() 메서드가 불러와질 것이다. 구현부에서 runServiceA()인지 runServiceB()인지 생각하지 않고 코드를 구현할 수 있는 이점이 생겼다. 

 

결론적으로 말하자면, 호출당하는 쪽의 메서드를 호출하는 쪽의 코드에 대응하도록, 중간에 변환기를 통해 호출하는 패턴이라고 정리할 수 있겠다. 앞의 글에서 들었던 '핸드폰' 사례가 이에 어울리는 패턴이라고 할 수 있겠다.

 

 

2) 프록시 패턴(Proxy Pattern)

(구조와 관련)

 

프록시는 대리인을 의미한다. 원래 구현하고자 하는 객체와 실행하고자 하는 클래스 사이에서 무엇인가를 전달해주는 역할을 하는 클래스가 껴서, 대리인 클래스의 객체가 모든 일을 해준다. 대리인 클래스는 실제 서비스 객체가 가진 변수, 메서드들을 모두 가진다. 그리고 실제 서비스 객체의 같은 이름을 가진 메서드를 호출하고 클라이언트에게 돌려준다.

(프록시는 실제 서비스 메서드 호출 전후에 별도의 메서드를 호출할 수도 있긴 하다.)

 

대변인이 대표자의 말을 가감 없이 전달하듯 프록시는 '전달'에만 초점을 둔다. 프록시 패턴은 실제 서비스 메서드의 반환 값에 가감하는 것을 목적으로 하지 않고 제어의 흐름을 변경하거나 다른 로직을 수행하기 위해 사용하는 것이다.

 

package com.ll.exam.article;

import com.ll.exam.article.dto.ArticleDto;
import java.util.List;
import java.util.ArrayList;
import java.util.stream.Collectors;
import java.util.stream.IntStream;

public class ArticleService { // 프록시를 담당하는 서비스 클래스
    private ArticleRepository articleRepository;

    public ArticleService() {
        articleRepository = new ArticleRepository();
    }

    public long write(String title, String body) {
        return articleRepository.write(title, body);
    }

    public List<ArticleDto> findAll() {
        return articleRepository.findAll();
    }

    public ArticleDto findById(long id) {
        return articleRepository.findById(id);
    }

    public void delete(long id) {
        articleRepository.delete(id);
    }

    public void modify(long id, String title, String body) {
        articleRepository.modify(id, title, body);
    }

    public List<ArticleDto> findIdGreaterThan(long fromId) {
        return articleRepository.findAllIdGreaterThan(fromId);
    }
}

class ArticleRepository { // 실제 서비스 메서드가 담겨있는 레포지토리 클래스
    private static List<ArticleDto> datum;
    private static long lastId;

    static {
        datum = new ArrayList<>();
        lastId = 0;

        makeTestData();
    }

    private static void makeTestData() {
        IntStream.rangeClosed(1, 10).forEach(id -> {
            String title = "제목%d".formatted(id);
            String body = "내용%d".formatted(id);
            write(title, body);
        });
    }

    public static long write(String title, String body) {
        long id = ++lastId;
        ArticleDto newArticleDto = new ArticleDto(id, title, body);

        datum.add(newArticleDto);

        return id;
    }

    public static List<ArticleDto> findAll() {
        return datum;
    }

    public static ArticleDto findById(long id) {
        for (ArticleDto articleDto : datum) {
            if (articleDto.getId() == id) {
                return articleDto;
            }
        }

        return null;
    }

    public void delete(long id) {
        ArticleDto articleDto = findById(id);

        if (articleDto == null) return;

        datum.remove(articleDto);
    }

    public void modify(long id, String title, String body) {
        ArticleDto articleDto = findById(id);

        if (articleDto == null) return;

        articleDto.setTitle(title);
        articleDto.setBody(body);
    }

    public List<ArticleDto> findAllIdGreaterThan(long fromId) {
        return datum
                .stream()
                .filter(articleDto -> articleDto.getId() > fromId)
                .collect(Collectors.toList());
    }
}

 

'제어의 흐름을 조정하기 위한 목적으로 중간에 대리자를 두는 패턴'이라고 정리할 수 있겠다.

 

 

3) 데코레이터 패턴(Decorator Pattern)

(구조와 관련)

 

데코레이터 패턴은 전체적으로 프록시 패턴과 비슷하다. 다만, 프록시 패턴은 실제 서비스 객체의 값들을 변경할 수 없지만(별도의 메서드를 추가하는 것과는 다르다), 데코레이터 페턴은 반환값 자체를 꾸며낼 수 있다. 데코레이터 객체가 내부에 실제 서비스의 메서드의 반환 값, 즉 리턴 값에 어떤 값, 예를 들어 "결과 : "를 추가한다던지 하는 방식으로 값 자체를 바꾸는 것이다.

 

/* 클라이언트 */
public class Client {
  public static void main(String[] args) {
      Display road = new RoadDisplay();
      road.draw(); // 기본 도로 표시
      Display roadWithLane = new LaneDecorator(new RoadDisplay());
      roadWithLane.draw(); // 기본 도로 표시 + 차선 표시
      Display roadWithTraffic = new TrafficDecorator(new RoadDisplay());
      roadWithTraffic.draw(); // 기본 도로 표시 + 교통량 표시
  }
}



/* 다양한 추가 기능에 대한 공통 클래스 */
public abstract class DisplayDecorator extends Display {
  private Display decoratedDisplay;
  
  // '합성(composition) 관계'로 RoadDisplay 객체를 참조
  public DisplayDecorator(Display decoratedDisplay) {
      this.decoratedDisplay = decoratedDisplay;
  }
  @Override
  public void draw() { decoratedDisplay.draw(); }
}

/* 차선 표시를 추가하는 클래스 */
class LaneDecorator extends DisplayDecorator {
  // 기존 표시 클래스의 설정
  public LaneDecorator(Display decoratedDisplay) { super(decoratedDisplay); }
  @Override
  public void draw() {
      super.draw(); // 설정된 기존 표시 기능을 수행
      drawLane(); // 추가적으로 차선을 표시
  }
  // 차선 표시 기능만 직접 제공
  private void drawLane() { System.out.println("\t차선 표시"); }
}

/* 교통량 표시를 추가하는 클래스 */
public class TrafficDecorator extends DisplayDecorator {
  // 기존 표시 클래스의 설정
  public TrafficDecorator(Display decoratedDisplay) { super(decoratedDisplay); }
  @Override
  public void draw() {
      super.draw(); // 설정된 기존 표시 기능을 수행
      drawTraffic(); // 추가적으로 교통량을 표시
  }
  // 교통량 표시 기능만 직접 제공
  private void drawTraffic() { System.out.println("\t교통량 표시"); }
}



/* 상위 추상 클래스 */
abstract class Display{
	public abstract void draw();
}

/* 기본 도로 표시 클래스 */
class RoadDisplay extends Display {
  @Override
  public void draw() { System.out.println("기본 도로 표시"); }
}

 

위의 코드를 보면 디스플레이 클래스를 통해 road, roadWithLane, roadWithTraffic 객체에 접근하고 있다. 어떤 기능을 추가하느냐에 관계없이 클라이언트는 동일한 Display 클래스에 의존하여 일관성 있는 방식으로 도로 정보를 표시할 수 있다. 다양한 장식자들을 붙이는 방법으로 클라이언트가 원하는 방식에 따라 장식을 붙여주는 로직을 구현한 것이다.

 

데코레이터 패턴을 다시 한번 정리하면 '메서드 호출의 반환 값에 변화를 주기 위해 중간에 장식자를 두는 패턴'이라고 할 수 있겠다.

 

 

4) 싱글턴 패턴(Singleton Pattern)

(생성과 관련)

 

하나의 객체만 생성해서 그것만 재사용하는 패턴이다. 왜 필요할까? 일정하게 바뀌어야 하는 어떤 '세팅값' 같은 것이 있을 수 있다. 그것을 위해서 사용하기도 하고, 설정과 관련된 객체의 경우 여러 개가 생성되면 자원을 낭비하게 되어 성능 저하와 예기치 않은 오류를 발생시킬 수 있어서 그냥 객체 하나만을 생성하게 하는 것이 필요하다. 그래서 사용하는 것이다.

 

생성자 자체에 private를 사용하고, 내부적으로 new를 사용해서 getInstance()라는 메서드로 외부에 객체를 리턴한다. 외부에서는 생성자에 private가 걸려있으므로, 새로운 객체를 생성할 수 없다. 외부에서는 이 클래스의 객체를 getInstance()로 받아올 수밖에 없고, 다른 변수가 이를 참조한다고 해도, 결국 '하나'의 객체만 변수들이 참조하게 되는 것이다.

 

public class Main {
	public static void main(String[] args) {
		사람[] 사람들 = new 사람[5];
		사람들[0] = 사람.get사람();
		사람들[1] = 사람.get사람();
		사람들[2] = 사람.get사람();
		사람들[3] = 사람.get사람();
		사람들[4] = 사람.get사람();
		
		for ( int i = 0; i < 사람들.length; i++ ) {
			사람들[i].자기소개();
		}
		
		/*
		// 출력
		저는 1번째 사람입니다.
		저는 2번째 사람입니다.
		저는 3번째 사람입니다.
		저는 4번째 사람입니다.
		저는 5번째 사람입니다.
		*/
	}
}


class 사람 {
	static private int 사람수;
	private int 번호;
	
	// static 요소 전용 생성자. 따로 뭔가를 호출하지 않아도 프로그램이 실행되면 가장먼저 실행된다.
	static {
		사람수 = 0;
	}
	
	private 사람(int 번호) {
		this.번호 = 번호;
	}
	
	static 사람 get사람() { //사람 객체를 반환하는 메서드
		사람 a사람 = new 사람(사람수 + 1);
		사람수++;
		return a사람;
	}
	
	public void 자기소개() {
		System.out.println("저는 " + 번호 + "번째 사람입니다.");
	}
}

 

 

 

싱글턴의 경우 멀티스레드 환경에서 오류가 발생할 수도 있는데, holder를 활용해서 초기화를 해주면 된다.

public class Something {
    private Something() {
    }     

    private static class LazyHolder {
    	public static final Something INSTANCE = new Something();    
    }     

    public static Something getInstance() {        
    	return LazyHolder.INSTANCE;    
    }
}

클래스 안에 클래스(holder)를 두고, JVM의 Class loader 메커니즘과 class가 로드되는 시점을 이용한 방법이다. JVM의 클래스 초기화 과정에서 보장되는 원자적 특성을 이용해서 싱글턴 초기화 문제에 대한 책임을 JVM에게 넘기는 것이다. holder안에 선언된 INSTANCE가 static이기 때문에 클래스 로딩 시점에 한 번만 호출되고, final을 사용해 다시 값이 할당되지 않도록 만들어놓은 방법이다.

 

현재 가장 많이 사용하고 일반적인 singleton 패턴 사용방법이다.

 

 

5) 템플릿 메서드 패턴(Template Method Pattern)

(행위와 관련)

 

상속을 활용해서 상위 클래스에 반드시 하위 메서드에서 구현해야 하는 것들을 추상 메서드로 두고, 하위 클래스들이 그것을 오버라이딩해서 쓰는 패턴이다. 상속과 별반 다를 것이 없다. 하지만, 이것을 왜 패턴이라고 부를까?  

 

템플릿 메서드에서 상속은 일정한 형식이 있다. 부모 클래스에 전반 과정을 수행하는 메인 메서드가 있고, 그 과정 가운데 세부 메소드가 있다. 메인 메서드를 호출하면 실행 중에 세부 메서드들이 호출되는 형태이다. 자식 과정에서는 그 세부 메서드를 오버라이딩 하는 것이다. 작업 패턴을 강제하고, 상속을 통한 확장 개발 공식으로 '패턴'이라고 이름 지어놓은 것이다.

 

public class CoffeeMain {
  public static void main(String[] args) {
    IceAmericano americano = new IceAmericano();
    IceLatte latte = new IceLatte();
    
    americano.makeCoffee();
    System.out.println("===");
    latte.makeCoffee();
  }
}

/* 추상 클래스(커피) */
abstract class Coffee {
  final void makeCoffee() {
    boilWater();
    putEspresso();
    putIce();
    putExtra();
  }
  
  // SubClass에게 확장/변화가 필요한 코드만 작성하도록 한다.
  abstract void putExtra();
  
  // 공통된 부분은 상위 클래스에서 해결하여 코드 중복을 최소화
  private void boilWater() {
    System.out.println("물을 끓인다.");		
  }
  private void putEspresso() {
    System.out.println("끓는 물에 에스프레소를 넣는다.");		
  }
  private void putIce() {
    System.out.println("얼음을 넣는다.");		
  }
}


class IceAmericano extends Coffee{
  @Override
  void putExtra() {
    System.out.println("시럽을 넣는다.");		
  }
}

class IceLatte extends Coffee{
  @Override 
  void putExtra() {
    System.out.println("우유를 넣는다.");
  }
}

 

상위 클래스를 이용해서 최대한 오버라이딩을 줄이는 방식. 상속을 활용한 것이라고 보면 된다. 익숙한 패턴일 것이다.

 

결론적으로 템플릿 메서드 패턴은 어떤 일을 수행하는 몇 가지 방법이 있는데, 전반적으로 공통된 절차가 있고, 공통 절차 내부는 일부 변경할 수 있는 경우, 코드를 효율적으로 짜기 위해 만들어진 패턴인 것이다. 

 

 

6) 팩토리 메서드 패턴(Factory Method Pattern)

(생성과 관련)

 

팩터리는팩토리는 공장을 의미한다. 객체지향에서 팩토리는 객체를 '생성'한다. 팩토리 메서드는 객체를 생성해서 반환하는 메서드를 의미한다. 여기에 패턴이 붙으면 하위 클래스에서 팩토리 메서드를 오버라이딩해서 객체를 반환하게 하는 것을 의미한다.

 

기존에 만들어진 라이브러리 클래스들을 활용할 때, 다른 클래스를 고치지 않고 새로운 클래스를 개발하는 것이 좋을 때가 있다. 특정 종류의 기능들에 사용될 수 있는 클래스들의 종류가 많고 복잡할 때, 개발자 측에서는 이를 다 알 필요 없이 사용할 객체의 조건들만 인자로 넘겨주면 이에 적절한 클래스를 찾아 객체로 생성해 주는 ‘중간’ 단계의 클래스(팩토리)를 구현해놓는 것이다. 

 

/* 클라이언트 */
// 메인 내부에 new 키워드가 없다. 팩토리 클래스에 객체 생성을 위임했기 때문이다.
// 이런 이유때문에 새로운 로봇이 추가된다 해도, 메인은 변경할 것이 별로 없다.
public class FactoryMain {
	public static void main(String[] args) {

		RobotFactory rf = new SuperRobotFactory();
		Robot r = rf.createRobot("super");
		Robot r2 = rf.createRobot("power");

		System.out.println(r.getName());
		System.out.println(r2.getName());
	}
}

/* 팩토리 */
// 로봇팩토리를 상속받아 super로봇팩토리를 만든다.
abstract class RobotFactory {
	abstract Robot createRobot(String name);
}

class SuperRobotFactory extends RobotFactory {
	@Override
	Robot createRobot(String name) {
		switch( name ){
			case "super": return new SuperRobot();
			case "power": return new PowerRobot();
		}
		return null;
	}
}


/* 로봇 */

abstract class Robot {
	public abstract String getName();
}

class SuperRobot extends Robot {
	@Override
	public String getName() {
		return "SuperRobot";
	}
}

class PowerRobot extends Robot {
	@Override
	public String getName() {
		return "PowerRobot";
	}
}

 

정리하자면, '팩터리 메서드 패턴은 오버라이드 된 메서드가 객체를 반환하는 패턴'이라고 할 수 있겠다. 의존 역전 원칙(DIP)을 활용하고 있다.

 

 

7) 전략 패턴(Strategy Pattern)

(행위와 관련)

 

디자인 패턴의 꽃이다.

 

전략 패턴의 세 요소는 다음과 같다.

- 전략 메서드를 가진 전략 객체
- 전략 객체를 사용하는 컨텍스트(전략 객체의 사용자/소비자)
- 전략 객체를 생성해 컨텍스트에 주입하는 클라이언트(제3자, 전략 객체의 공급자)

클라이언트는 다양한 전략 중 하나를 선택해서 객체를 생성한 후 컨텍스트에 주입한다.

 

검색을 할 수 있는 창에 어떤 버튼들이 있다고 하자. 그 버튼들은 선택된 모드를 지정할 수 있고, 선택된 버튼에 따라서 그 모드에 맞게 검색이 되도록 만드는 것을 구현하고 싶다면? 프로그램 실행 중 모드가 바뀔 때마다 검색이 이루어지는 방식, 즉 전략이 수정되는  것이다. 클릭하는 것에 따라 메서드가 어떤 것이 구현되는 지를 연결시키는 방법도 있겠지만, 프로젝트의 사이즈가 커지면 유지보수가 어려워지고 개발이 힘들어질 것이다. 그래서 모듈로 따로 분리해서 이 버튼들을 누를 때마다 검색 버튼을 누를 때 실행될 검색 ‘모듈’을 갈아 끼워주는 방식으로 코드를 짜는 것이다. 옵션들마다의 행동들을 모듈화 해서 독립적이고 상호 교체 가능하게 만드는 것이다.

 

/* 클라이언트 */
// 전략 객체를 통해서 마음대로 새로운 객체를 인자로 '주입'하고 각각 다른 결과를 얻을 수 있다.
public class Main {
    public static void main(String[] args) {
        People people = new People();
 
        // 숫자 설정
        people.changeNumber(1,2);
 
        // Calculator 설정
        people.setCalculator(new PlusCalculator());
        double result1 = people.operate();
        System.out.println(result1);
 
        // 새로운 Calculator 설정
        people.setCalculator(new MinusCalculator());
        double result2 = people.operate();
        System.out.println(result2);
    }
}

/* 전략 컨텍스트. 계산기 객체(전략 객체)를 모두 가질 수 있다. */
public class People {
 
    private Calculator calculator;
    private double n1;
    private double n2;
 
    public double operate(){
        return calculator.execute(n1,n2);
    }
 
    public void setCalculator(Calculator calculator){
        this.calculator=calculator;
    }
 
    void changeNumber(double n1, double n2) {
        this.n1 = n1;
        this.n2 = n2;
    }
 
}


/* 계산기 클래스들(전략 객체) */
interface Calculator {
    double execute(double n1, double n2);
}

class PlusCalculator implements Calculator{
    @Override
    public double execute(double n1, double n2) {
        return n1+n2;
    }
}

class MinusCalculator implements Calculator{
 
    @Override
    public double execute(double n1, double n2) {
        return n1-n2;
    }
}

 

정리하자면 클라이언트가 전략을 '생성'해 전략을 실행할 컨텍스트에 주입하는 패턴이라고 할 수 있다.

 

 

8) 템플릿 콜백 패턴(Template Callback Pattern)

(행위와 관련)

 

 

전략 패턴의 변형으로 스프링의 3대 프로그래밍 모델 중 하나인 DI(의존성 주입)에서 사용하는 특별한 형태의 전략 패턴이다. 전략 패턴과 거의 비슷한데, 전략을 익명 내부 클래스로 정의해서 사용한다는 특징이 있다.

 

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("도끼! 독독..도도독 독끼!");
	}
}

/* 전략 컨텍스트 내부에 익명 클래스가 있다. */
// 만약 전략 패턴이었다면 내부 익명 클래스가 별도로 존재했을 것이다.
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);
			}
		};
	}
}

/* 전략 객체. 여러 가지 무기를 바꿔가면서 클라이언트에게 전략을 전달할 수 있다. */
interface Strategy {
	public abstract void runStrategy();
}

class StrategyBow implements Strategy {
	@Override
	public void runStrategy() {
		System.out.println("슝.. 쐐액.. 쇅, 최종 병기");
	}
}

class StrategyGun implements Strategy {
	@Override
	public void runStrategy() {
		System.out.println("탕, 타당, 타다당");
	}
}

class StrategySword implements Strategy {
	@Override
	public void runStrategy() {
		System.out.println("챙.. 채쟁챙 챙챙");
	}
}

 

중복을 줄이면서 코드가 깔끔해지는 장점이 있다. 또한 전략 패턴은 팩토리 객체가 필요하지만, 콜백 패턴은 팩토리 객체 없이 해당 객체를 사용하는 메서드에서 인터페이스의 전략을 선택할 수 있다.

 

 

3. 정리

 

요즘 스프링 부트의 전신이 되는 코드들을 배우고 있는데, 컨테이너건, 컨트롤러, 서비스 등의 클래스 개념들이 모두 이 패턴을 가지고 있는 것을 어렴풋이 알 수 있었다. 여러 번 더 보고 해야 그 맛을 진정하게 느낄 수 있겠지만, 일단 한 달 전에 이해한 디자인 패턴보다는 더 익숙하게 정리할 수 있었다. 더 많은 디자인 패턴들이 있지만, 그것들은 천천히 알아나가 보기로 한다...

 

하나의 원리라는 것, 하나의 공식이라는 것을 잘 기억하고, 앞으로의 스프링 부트 학습과정에서 이 디자인 패턴이 어떻게 적용되고 있는지, 어떤 결과로 나타나는 지를 잘 체감하면서 학습을 해 나가 볼 것이다. 교조적으로 디자인 패턴만을 생각하는 것은 아니고, 그냥 아, 이런 것들이 적용이 되어 있구나, 이러면 좋구나를 느껴나가볼 생각이다.

 

이번 주 공부를 하면서 깊이 느낀 것은 다음과 같다.

 

잘 된 프로그래밍의 '맛'을 느끼기 위한 레시피. 음식을 많이 만들어봐야 일단 좋은 음식을 만들 수 있겠지만, 맛을 생각하지 않고 백날 무지성으로 음식을 만든다면 음식물 쓰레기만 양산할 뿐이다. 쓰레기 같은 코드도 많이 만들어보고, 좋은 코드도 많이 만들어봐야 한다. 그 속에서 어떻게 해야 좋은 냄새를 풍기고 좋은 맛을 내는 코드를 만들 수 있는지는 우리가 코드를 만드는 과정에서 어떤 생각을 하느냐에 따라 달려있는 것 같다. 나는 어떤 코드를 요리하는 사람이 될 것인가.

 

 

 

728x90

댓글