WEB/Design Pattern

구조 관련 디자인 패턴

Tony Lim 2023. 1. 27. 10:14

어댑터 패턴

기존에는 Account는 security package(건드릴수없는 코드, library) 에 있었던 LoginHandler를 쓸 수 없었다. 
Login Handler는 UserDetailsService와 연관되어있고 UserDetailsService는 UserDetails를 return 하기 때문이다.

우리가 원하는 것은 AccountService - UserDetailService , Account - UserDetails 처럼 연결하고 싶다

하지만 AccountUserDetailsService , AccountUserDetails 가 중간에 adapter 역할을 하여 client가 LoginHandler를 원할하게 사용이 가능해졌다.

public static void main(String[] args) {
    // collections
    List<String> strings = Arrays.asList("a", "b", "c");
    Enumeration<String> enumeration = Collections.enumeration(strings);
    ArrayList<String> list = Collections.list(enumeration);

    // io
    try(InputStream is = new FileInputStream("input.txt");
        InputStreamReader isr = new InputStreamReader(is);
        BufferedReader reader = new BufferedReader(isr)) {
        while(reader.ready()) {
            System.out.println(reader.readLine());
        }
    } catch (IOException e) {
        throw new RuntimeException(e);
    }
}

java에서 FileInputStream은 String을 인자로 받았는데 InputStream 을 return 하는것을 보면 일종의 어댑터 패턴을 적용한것임을 알 수 있다.

public interface HandlerAdapter {
    boolean supports(Object var1);

    @Nullable
    ModelAndView handle(HttpServletRequest var1, HttpServletResponse var2, Object var3) throws Exception;

    long getLastModified(HttpServletRequest var1, Object var2);
}

스프링에서 HandlerAdapter 인터페이스를 통해  다양한 구현체를 DIspatcherServlet#doDispatch 메소드에서 사용할 수있도록 제공한다. 


브릿지 패턴

현 상황에서는 매번 신스킨, 신챔피언이 나올때마다. 매번 중복코드가 발생하게 된다. 

현재 2개의 관심사 (스킨, 챔피언) 이 존재하는 데 1개의 계층밖에 존재하지 않기 떄문이다. 

public class DefaultChampion implements Champion {

    private Skin skin;

    private String name;

    public DefaultChampion(Skin skin, String name) {
        this.skin = skin;
        this.name = name;
    }

중간에 브릿지 클래스(Skin)이 생겨서 DefaultChampion은 Skin을 따로 주입받는다.
여기서 Chapion 과 Skin 이 잘 분리된것을 알 수 있다.

위에서 챔피언을 생성할때는 DefaultChampion만 extend해서 생성하면 되고 신스킨을 추가할때면 Skin 인터페이스만 상속받아서 생성하면된다.

핵심은 서로 다른 계층에 영향을 주지 않는다.

public static void main(String[] args) {
    Champion kda아리 = new 아리(new KDA());
    kda아리.skillQ();
    kda아리.skillW();

    Champion poolParty아리 = new 아리(new PoolParty());
    poolParty아리.skillR();
    poolParty아리.skillW();
}

또한 client는 Champion 만 바라보면 된다.

    public static void main(String[] args) throws ClassNotFoundException {
        Class.forName ("org.h2.Driver");

        try (Connection conn = DriverManager.getConnection ("jdbc:h2:mem:~/test", "sa","")) {

            String sql =  "CREATE TABLE  ACCOUNT " +
                    "(id INTEGER not NULL, " +
                    " email VARCHAR(255), " +
                    " password VARCHAR(255), " +
                    " PRIMARY KEY ( id ))";

            Statement statement = conn.createStatement();
            statement.execute(sql);

//            PreparedStatement statement1 = conn.prepareStatement(sql);
//            ResultSet resultSet = statement.executeQuery(sql);
        } catch (SQLException e) {
            throw new RuntimeException(e);
        }
    }

jdbc 관련 코드에서도 찾아 볼 수 있다. DriverManager 와 Connection을 사용하는 PreparedStatement은 분리가 되어있는 것을 확인 할 수 있다.

아무리 db를 바꿔서 다른 Connection 구현체를 얻어오더라도 jdbc 코드는 바뀌지 않는다. 반대도 마찬가지이다.

public class Slf4jExample {

    private static Logger logger = LoggerFactory.getLogger(Slf4jExample.class);

    public static void main(String[] args) {
        logger.info("hello logger");
    }
}

Slf4j도 마찬기지이다. Logger와 LoggerFactory는 밑에 구현체들이 바뀌게 되더라도 실제 로깅하는 구현코드들은 서로 영향을 미치지 않는다. 
이를 Logging Facade라 부른다.

public class TransactionTemplate extends DefaultTransactionDefinition {
    protected final Log logger = LogFactory.getLog(this.getClass());
    @Nullable
    private PlatformTransactionManager transactionManager;

또다른 예시로는 PlatformTransactionManager(implementation) 와 TransactionTemplate(redefined abstraction)이 있다.


컴포짓 패턴

public class Client {

    public static void main(String[] args) {
        Item doranBlade = new Item("도란검", 450);
        Item healPotion = new Item("체력 물약", 50);

        Bag bag = new Bag();
        bag.add(doranBlade);
        bag.add(healPotion);

        Client client = new Client();
        client.printPrice(doranBlade);
        client.printPrice(bag);
    }

    private void printPrice(Item item) {
        System.out.println(item.getPrice());
    }

    private void printPrice(Bag bag) {
        int sum = bag.getItems().stream().mapToInt(Item::getPrice).sum();
        System.out.println(sum);
    }
}

현재 printPrice같은 일들을 client단에서 처리하고 있다.

public static void main(String[] args) {
    Item doranBlade = new Item("도란검", 450);
    Item healPotion = new Item("체력 물약", 50);

    Bag bag = new Bag();
    bag.add(doranBlade);
    bag.add(healPotion);

    Client client = new Client();
    client.printPrice(doranBlade);
    client.printPrice(bag);
}

private void printPrice(Component component) {
    System.out.println(component.getPrice());
}

Component interface는 getPrice를 가지고 있고 이것을 상속하는 클래스들(Item, Bag)이
각자 price에 대한 역할을 맡게 된다.


데코레이터 패턴

CommentDecorator 의 하위 클래스들은 addComment를 override해서 자신들이 하고싶은 처리를 한후에 super.addComment를 호출하게 된다.

또한 CommentDecorator는 CommentService를 인자로 가지고 있고 주입받은 CommentService의 addComment를 호출할 뿐이다. 이때 주입받은것이 DefaultCommentService, 일수도 아니면 Spam -> Trimming 을 다거친 2개의 Decorator를 지닌 CommentService일 수도 있다. 

Decorator구현체가 추가되면 차례로 랩핑하게 되면 모든 Decorator가 적용이 되게 된다. 

재귀함수처럼 바깥것부터 차례대로 호출되게 되는 구조다. 

이 패턴은 동적으로 여러가지 조합을 런타임에 조절할 수 있는데 그렇게 하지 않으면 정적으로 모든 조합들이 컴파일타임에 이미 다 만들어져있어야 사용이 가능할 것이다.

 

List books = Collections.checkedList(list, Book.class);
List unmodifiableList = Collections.unmodifiableList(list);

자바에서 볼수있는 decorator 패턴이다. 


퍼사드 패턴

이메일을 보내는 기능을 따로 뺴놓아서 해당 Client말고 다른 곳에서 이메일 기능을 쓸때 유용하게 될것이다. 또한 해당 email 관련 api를 깊이 있게 알필요없이 추상화해놓은 메소드만 호출하면 된다.

기본적으로 안으로 숨겨서 interface만 노출시키는 느낌이랑 비슷하다. 의존성은 퍼사드 클래스가 물고 있게 된다.


플라이웨이트 패턴

public static void main(String[] args) {
    Character c1 = new Character('h', "white", "Nanum", 12);
    Character c2 = new Character('e', "white", "Nanum", 12);
    Character c3 = new Character('l', "white", "Nanum", 12);
    Character c4 = new Character('l', "white", "Nanum", 12);
    Character c5 = new Character('o', "white", "Nanum", 12);
}

어떤 편집기가 char하나하나를 객체로 만들어서 인식하는 방식이라면 글이 길어질수록 기하급수적으로 메모리 사용량이 늘어날 것이다.

자주변하는 속성(extrinsit) = h,e,l 알파벳

자주 변하지 않는속성(intrinsit) = white, nanum , 12 같은 속성들

이것들을 분리하고 재사용하여서 메모리 사용을 줄일 수 있다.

public static void main(String[] args) {
    FontFactory fontFactory = new FontFactory();
    Character c1 = new Character('h', "white", fontFactory.getFont("nanum:12"));
    Character c2 = new Character('e', "white", fontFactory.getFont("nanum:12"));
    Character c3 = new Character('l', "white", fontFactory.getFont("nanum:12"));
}

FontFactory에서 만드는 Font는 immutable 하고 이것이 flightweight 의 포인트이다.

public static void main(String[] args) {
    Integer i1 = Integer.valueOf(10);
    Integer i2 = Integer.valueOf(10);
    System.out.println(i1 == i2);
}

내부적으로 캐싱하고 있기때문에 같은 객체가 튀어나오게 되어 print true가 되게 된다.


프록시 패턴

client는 프록시를 먼저 거쳐가게 하여 여러 부가적인 기능을 추가할 수 있다.

private GameService getGameServiceProxy(GameService target) {
    return  (GameService) Proxy.newProxyInstance(this.getClass().getClassLoader(),
            new Class[]{GameService.class}, (proxy, method, args) -> {
                System.out.println("O");
                method.invoke(target, args);
                System.out.println("ㅁ");
                return null;
            });
}

자바에서 런타임에 프록시를 객체를 생성하는 api를 제공해준다.  

@Aspect
@Component
public class PerfAspect {

    @Around("bean(gameService)")
    public void timestamp(ProceedingJoinPoint point) throws Throwable {
        long before = System.currentTimeMillis();
        point.proceed();
        System.out.println(System.currentTimeMillis() - before);
    }
}

스프링에서 제공하는 프록시 = aspect (advice + pointcut)

 

'WEB > Design Pattern' 카테고리의 다른 글

행동 관련 디자인 패턴  (1) 2023.01.29
객체 생성 관련 디자인 패턴  (0) 2023.01.26