소스 코드를 기록하는 남자

스프링의 삼각형 IoC/DI, AOP, PSA에 대하여

Spring

Java 백앤드 면접을 보러가면 여러 면접장에서 공통적으로 하는 질문들이 있는데, 그 중 빠지지 않고 나온 질문 중 하나가 스프링의 3대 특징에 대해서 이야기하라는 것이였다. 매번 면접 질문에 대한 대답을 하기 위해 정리했었던 부분을 다시 한번 보면서 공부했고, 이를 정리하여 포스팅하기로 마음을 먹었다.

 

면접이 급해서 찾아보았다면 다음과 같이 이해하라.

 

IoC/DI 의존 역전/의존성 주입은  @Autowired나 XML 설정을 통해서 강합 결합을 느슨한 결합으로 변경해주며, 코드를 유연하게 해준다.
AOP 관점 지향 프로그래밍으로서 공통된 로직을 추출하여 메소드의 다양한 시점에 실행할 수 있게 해줄수 있으며, 코드를 줄여주고, 개발자가 공통 로직을 배제하고 핵심 관심사에 집중할 수 있도록 해준다.
PSA Portable Service Abstraction으로 일관성 있는 서비스 추상화이다. 서비스 추상화의 대표적인 예를 JDBC로 들수 있으며, 어떠한 데이터 베이스를 사용하더라도 일관성있는 방식으로 제어할 수 있도록 공통의 인터페이스를 제공하는 것이 서비스 추상화라고 한다.

 

 

스프링 공부를 시작하게 되면 어떠한 개발자라도 반드시 이해하고 가야되는 부분이 스프링의 3대 프로그래밍 모델인 IoC/DI, AOP, PSA다. 따라서 이러한 부분을 모르고 따짜고짜 스프링으로 개발을 하겠다? 이것은 중학교 수학 공식도 모르는 친구가 고교 수학부터 시작하는 것일수도 있다. 물론 그 친구가 천재라면 가능하겠다만, 대다수는 천재가 아니지 않는가.

 

IoC/DI - 제어의 역전/의존성 주입

스프링에서는 제어의 역전을 의존성 주입이라고도 한다. 이를 더 잘 이해하기 위해서는 의존성을 이해해야 한다.

 

프로그래밍에서 의존성?

의존성은 어렵게 생각하지 않아도 단순하게 예를 들자면 다음과 같다.

 

 

운전자는 자동차를 생산한다. = new Car()

자동차는 내부적으로 타이어를 생산한다. = Car 객체 생성자에서 new Tire();

 

 

따라서 new 라는 키워드는 의존성이라 할 수 있다. Car 객체 생성자에서 new 를 실행함으로 Car가 Tire에 의존한다고 볼 수 있다. 이렇게 의존이라는 것은 전체가 부분에 의존하는 것을 표현하며, 좀 더 깊게 들어가서 의존 관계 사이를 집합 관계와 구성 관계로 구분할 수 있으며, 의존 관계를 어떻게 맺냐에 따라서 강합 결합이냐 느슨한 결합이냐를 이야기할 수 있게 된다.

 

집합 관계 부분이 전체와 다른 생성 주기를 가질 수 있다.
구성 관계 부분은 전체와 같은 생명 주기를 갖는다.

 

강한 결합

객체 내부에서 다른 객체를 생성하는 것은 강한 결합도를 가지는 구조이다. A 클래스 내부에서 B 라는 객체를 직접 생성하고 있다면, B 객체를 C 객체로 바꾸고 싶은 경우에 A 클래스도 수정해야 하는 방식이기 때문에 강한 결합이다. 위에서도 동일하게 자동차 내부에서 타이어를 생성하는 것은 다른 타이어를 생성하고자 해도 코드를 수정해야 되는 상황이 발생한다.

 

느슨한 결합

객체를 주입 받는다는 것은 외부에서 생성된 객체를 인터페이스를 통해서 넘겨받는 것이다. 이렇게 하면 결합도를 낮출 수 있고, 런타임시에 의존관계가 결정되기 때문에 유연한 구조를 가진다.

 

SOLID 원칙에서 O 에 해당하는 Open Closed Principle 을 지키기 위해서 디자인 패턴 중 전략패턴을 사용하게 되는데, 생성자 주입을 사용하게 되면 전략패턴을 사용하게 된다.

 

본격적으로 IoC/DI가 스프링에서 어떠한 역할을 하는지 알아보며 필요성을 느껴보도록 하자.

 

 

객체의 주입?

객체를 주입하는 방법은 생성자를 통한 주입, setter를 통한 주입이 있다. 각 방식에 대해서 살펴보고 어떠한 문제점들이 발생하는지 확인해보자.

 

먼저 객체 내부에서 생성하는 코드부터 확인하자.

 

package 의존성주입;

public interface Dress {
    String getSeason();
}

public class FWSeasonDress implements Dress{
    @Override
    public String getSeason() {
        return "F/W 신상 드레스";
    }
}

public class SSSeasonDress implements Dress{
    @Override
    public String getSeason() {
        return "S/S 신상 드레스";
    }
}

public class Person {
    Dress dress;

    public Person() {
        dress = new FWSeasonDress();
    }

    public String getDress() {
        return "입은 옷은 " + dress.getSeason();
    }
}

 

사람은 옷을 입게 된다. 이 코드에서 확인해보면 사람이 생성될 때 입을 옷이 결정이 된다. 따라서 코드가 유연하지 못하다는 의미이다. 이제 생성자 주입과 수정자 주입을 확인해보자.

 

생성자를 통한 의존성 주입

생성자를 통한 주입 방법은 매우 간단하다. 기존의 생성자 코드를 아래와 같이 변경해주면 된다.

package 의존성주입;

public class Person {
    Dress dress;

    public Person(Dress dress) {
        this.dress = dress;
    }

    public String getDress() {
        return "입은 옷은 " + dress.getSeason();
    }
}

Person의 생성자 부분이 변경되었다. 이렇게 되면 외부에서 Person 객체가 생성될 때 입을 옷을 결정해줄 수 있게 되었다. 

코드가 유연해졌다는 의미이다. 기존의 Person 클래스 코드 변경없이 새로 추가되는 시즌의 옷들을 갈아입혀 줄 수 있게 되었다. 좀 더 실무적인 이야기를 해보자면, 새로운 W/W 시즌의 옷이 나왔을 때 코드의 변경은 Person 객체를 변경해주는 부분과 새롭게 추가되는 W/W 객체만 컴파일해서 배포하면 된다는 의미이다. 

 

Setter (수정자)를 통한 주입

생성자를 통한 의존성 주입을 현실 세계로 예를 들어보면 한번 사람이 태어나면 태어나서 입을수 있는 옷은 고정되어 변경할 방법이 없다는 것이다. 이러한 부분에서 유연성이 떨어지기 때문에 유연성 높은 코드를 작성하고자 한다면 이를 생성자가 아닌 속성을 통해서 의존성 주입을 해야 한다.

 

사실 이러한 부분에서 프로그래밍 진형에서는 생성자를 통한 것이 좋은가? 아니면 속성을 통한 주입이 좋은가에 대한 이야기가 많다. 따라서 사실 무엇이 더 좋다기보단 상황에 맞는 DI를 선택하는 것이 올바르다고 볼 수 있다.

 

아래 글을 참조하면 생성자 주입을 강제하는 것처럼 보일지 모르지만 읽어보면 좋기에 링크를 남긴다.

 

yaboong.github.io/spring/2019/08/29/why-field-injection-is-bad/

 

스프링 - 생성자 주입을 사용해야 하는 이유, 필드인젝션이 좋지 않은 이유

개요 Dependency Injection (의존관계 주입) 이란 Setter Based Injection (수정자를 통한 주입) Constructor based Injection (생성자를 통한 주입) 스프링에서 사용할 수 있는 DI 방법 세가지 생성자 주입을 이용한 순

yaboong.github.io

코드로 변경해보면 아래와 같이 바뀐다.

package 의존성주입;

public class Person {
    Dress dress;

    public void setDress(Dress dress) {
        this.dress = dress;
    }

    public Dress getDress() {
        return dress;
    }

    public String getDressSeason() {
        return "입은 옷은 " + dress.getSeason();
    }
}

public class Driver {
    public static void main(String[] args) {
        Dress dress = new FWSeasonDress();
        Person person = new Person();
        person.setDress(dress);
    }
}

 

생성자가 사라졌고, dress 속성에 대한 접근자와 설정자 메소드가 생겼다. 따라서 언제든지 사람의 옷을 변경 가능하도록 유연한 코드로 변경하게 되었다.

 

이제 스프링에서는 어떻게 의존성을 주입하는 것인가?

 

두가지 방법이 있다.

XML 파일 사용 , @Autowired

 

이 포스팅에서는 깊게 보기보단 간단하게 보겠다. 스프링에는 XML 설정 파일들이 존재한다. 뿐만 아니라 스프링은 Bean을 관리하게 되는데, 내가 코드에서 생성할 Bean을 XML 파일에 등록하여 의존성을 주입할 수 있다. XML 관련 정보는 이 포스팅보다 아래 게시글을 참조하길 바란다.

 

atoz-develop.tistory.com/entry/Spring-%EC%8A%A4%ED%94%84%EB%A7%81-XML-%EC%84%A4%EC%A0%95-%ED%8C%8C%EC%9D%BC-%EC%9E%91%EC%84%B1-%EB%B0%A9%EB%B2%95-%EC%A0%95%EB%A6%AC

 

[Spring] 스프링 XML 설정 파일 작성 방법 정리

[Spring] 스프링 XML 설정 파일 작성 방법 정리 📄 목차 1. 스프링 XML 설정 파일 포맷 - 기본 포맷 - 애노테이션 설정을 사용하기 위한 포맷 2. 빈(Bean) 설정 예시 - 자동 주입 설정 - autowire 속성 3.

atoz-develop.tistory.com

위 링크에 대한 내용을 이해하고 스프링 의존성 주입이 이루어지는 메커니즘을 이해하게 된다면 스프링을 도입해서 얻고자 하는 목적을 이해할 것이다. 스프링을 도입한다면 코드의 수정없이 XML 파일만 수정하면 프로그램의 실행 결과를 변경할 수 있다는 것이다. 재컴파일이 필요없다는 것은 개발자로서 얻는 강점이 어마어마하다.

 

AOP (Aspect-Oriented Programming) : 관점 지향 프로그래밍

스프링의 DI가 의존성의 주입이라면 AOP는 코드 주입이라고 할 수 있다. 여러 모듈을 개발하다보면 모듈들에서 공통적으로 등장하는 로직이 존재한다. 예를 들어서 입금 출금 이체와 같은 부분에서 보안적인 부분이나 트랜잭션 로그를 남기고자 하는 코드 부분들이 분명히 공통적으로 등장할 것이다.

 

이렇게 공통적으로 등장하는 부분은 횡단 관심사(cross-cutting concern)이라고 한다. 일반적으로 코드는 핵심 관심사 + 횡단 관심사로 구성된다. 따라서 이러한 부분을 @Aspect 어노테이션을 통해서 추출하여 특정 메소드가 호출될 때 특정 시점에 동작하도록 할 수 있는 것이다.

 

이를 통해서 스프링이 얻고자 하는 부분은 어떤 것인가? 공통적으로 등장하는 횡단 관심사를 어느 한 사람이 잘 정의하여 코드를 작성했다면, 다른 개발자들은 이를 재사용할 수 있을 것이다. 이를 통해서 기존의 횡단 관심사를 계속해서 코딩해야 되는 불편함이 사라지고 오직 개발자들은 핵심 관심사에만 집중하여 개발을 할 수 잇게 되는 것이다. 또한 핵심 관심사에만 집중함으로 자연스럽게 SRP을 적용할 수 있게 된다.

 

AOP 개발에 대해서 궁금한 부분이 있다면 아래 링크에서 참조하여 공부하길 바란다.

engkimbs.tistory.com/746

 

[Spring] 스프링 AOP (Spring AOP) 총정리 : 개념, 프록시 기반 AOP, @AOP

| 스프링 AOP ( Aspect Oriented Programming ) AOP는 Aspect Oriented Programming의 약자로 관점 지향 프로그래밍이라고 불린다. 관점 지향은 쉽게 말해 어떤 로직을 기준으로 핵심적인 관점, 부가적인 관점으..

engkimbs.tistory.com

 

PSA (Portable Service Abstraction) : 일관성 있는 서비스 추상화

PSA는 일관성있는 추상화이다. 서비스 추상화의 대표적인 예로 JDBC를 두는데 이러한 표준 스펙 덕분에 개발자는 오라클을 사용하든, MySQL을 사용하든, MS-SQL을 사용하던 어떠한 데이터베이스를 사용하던 공통된 방식으로 코드를 작성할 수 있다. 데이터베이스 종류에 관계없이 같은 방식으로 제어할 수 있는 이유는 디자인 패턴에서 설명했던 어댑터 패턴을 활용했기 때문이다. 이처럼 어댑터 패턴을 적용해 같은 일을 하는 다수의 기술을 공통의 인터페이스로 제어할 수 있게 한 것을 서비스 추상화라고 한다.

 

실질적으로 스프링은 서비스 추상화를 위해 다양한 어댑터를 제공하는데 이러한 부분이 궁금하다면 직접 찾아보는 것을 권한다. 스프링 고수가 되는 그날까지 다들 화이팅이다.

 

 

 

 

 

 

 

 

 

 

 

 

 

 

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

디자인 패턴

템플릿 콜백 패턴은 전략 패턴의 변형이며 스프링 3대 프로그래밍 모델 중 하나인 DI (의존성 주입)에서 사용하는 특별한 형태의 전략 패턴이다. 템플릿 콜백 패턴은 전략 패턴과 모든 것이 동일하나 전략을 익명 내부 클래스로 정의해서 사용하는 것이 차이이다. 앞에서 봤던 내용을 템플릿 콜백 패턴으로 변경해보자. 

 

package 전략패턴;

public class Client {
    public static void main(String[] args) {
        전략 strategy = null;
        군인 rambo = new 군인();

        strategy = new 총();
        rambo.runContext(strategy);

        strategy = new 칼();
        rambo.runContext(strategy);

        strategy = new 활();
        rambo.runContext(strategy);

    }
}

 

위의 코드는 기존의 코드이다. 익명 내부 클래스를 사용하여 변경해보자.

 

 

package 전략패턴;

public class Client {
    public static void main(String[] args) {
        군인 rambo = new 군인();

        rambo.runContext(new 전략() {
        	@Override
            public void runStrategy() {
            	System.out.println("총 : 빵야");
            }
        });

        rambo.runContext(new 전략() {
        	@Override
            public void runStrategy() {
            	System.out.println("칼 : 슈욱");
            }
        });
        
                rambo.runContext(new 전략() {
        	@Override
            public void runStrategy() {
            	System.out.println("활 : 슈슉");
            }
        });

    }
}

 

위와 같이 익명 내부 클래스로 변경했다. 코드를 보자하니 중복되는 부분이 많다. 이를 리팩토링 해보자.

 

package 전략패턴;

public class 군인 {
    void runContext(String weaponSound) {
        System.out.println("전투 시작");
        executeWeapon(weaponSound).runStrategy();
        System.out.println("전투 종료");
    }
    
    private 전략 executeWeapon(final String weaponSound) {
        return new 전략() {
            @Override
            public void runStrategy() {
                System.out.println(weaponSound);
            }
        };
    }
}

public class Client {
    public static void main(String[] args) {
        군인 rambo = new 군인();

        rambo.runContext("총");

        rambo.runContext("칼");

        rambo.runContext("활");

    }
}

아래와 같이 리팩토링이 가능하다. 리팩토링의 예제를 보면 볼수록 신기하고 즐겁다.

이렇게 리팩토링하여 보니, 전략이 군인의 내부로 들어왔다. 스프링은 이와 같은 형식으로 템플릿 콜백 패턴을 DI에 적극 활용하고 있다. 따라서 전략 패턴과 템플릿 콜백 패턴, 리팩터링된 템플릿 콜백 패턴은 꼭 기억해둘 필요성이 있다.

 

마지막 한마디로 정리해보자.

 

"전략을 익명 내부 클래스로 구현한 전략 패턴"

 

guy-who-writes-sourcecode.tistory.com/30

 

스프링에 녹아있는 아름다운 디자인 패턴

프로그램을 개발하다 보면 많은 상황에 직면하게 되는데, 프로그래밍의 역사가 꽤 길지 않은가? 따라서 이와 비슷한 사례를 이미 경험한 선배들이 정리해 둔 표준 설계 패턴이 있다. 이를 디자

guy-who-writes-sourcecode.tistory.com

 

전략 패턴(Strategy Pattern)

디자인 패턴

전략 패턴은 디자인 패턴의 꽃이라 할 수 있다. 따라서 스프링을 공부하고자 하는 사람은 꼭 이해하고 넘어가야 할 부분이다. 주의깊게 보도록 해보자.

 

전략 패턴을 구성하는 요소는 세 가지다.

  1. 전략 메소드를 가진 전략 객체
  2. 전략 객체를 사용하는 컨텍스트 (전략 객체의 사용자/소비자)
  3. 전략 객체를 생성해 컨텍스트에 주입하는 클라이언트

전략 패턴의 맹점은 다음과 같다. 

 

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

 

예를 들어 군인이 있다고 가정해보자. 군인은 다양한 무기중 주어진 환경에 적합한 무기를 사용할 것이다. 또한 이를 보급해주는 보급 장교가 있다고 해보자. 그럼 여기서 무기는 전략이 되고, 군인은 컨텍스트, 보급 장교는 제 3자인 클라이언트가 된다.

 

다음은 코드를 볼 예정인데, 다양한 전략, 무기를 공통된 방법으로 사용하기 위해서 인터페이스를 정의할 것이다.

 

package 전략패턴;

public interface 전략 {
    public abstract void runStrategy();
}

public class 총 implements 전략{

    @Override
    public void runStrategy() {
        System.out.println("총 : 탕탕탕");
    }
}

public class 칼 implements 전략{
    @Override
    public void runStrategy() {
        System.out.println("칼 : 슝슝");
    }
}

public class 활 implements 전략{
    @Override
    public void runStrategy() {
        System.out.println("활 : 슉 슉 슈슝");
    }
}

public class 군인 {
    void runContext(전략 strategy) {
        System.out.println("전투 시작");
        strategy.runStrategy();
        System.out.println("전투 종료");
    }
}

public class Client {
    public static void main(String[] args) {
        전략 strategy = null;
        군인 rambo = new 군인();

        strategy = new 총();
        rambo.runContext(strategy);

        strategy = new 칼();
        rambo.runContext(strategy);

        strategy = new 활();
        rambo.runContext(strategy);

    }
}

 

위의 코드의 흐름은 클라이언트(=보급장교)가 군인인 람보에게 총, 칼, 활을 주고 전투가 시작되고 전투가 끝나는 흐름이 된다. 위에 보면 전략인 무기가 다양하게 변경되면서 컨텍스트를 실행할 수 있게 된 것이다. 전략 패턴은 디자인 패턴의 꽃이라 했던 것처럼, 다양한 문제 상황의 해결책으로 사용된다.

 

혹여라도 SOLID에 대한 부분이나 이전에 디자인 패턴이 공부한 기억이 있다면, 템플릿 메소드 패턴과 유사하다는 생각이 들 수도 있다. 같은 문제에 해결책으로 템플릿 메소드 패턴과 전략 패턴을 선택하여 사용할 수 있는데 상속이라는 제한이 있는 템플릿 메소드 패턴보다는 전략 패턴을 Java 진형에서 더 많이 사용한다고 한다.

 

마지막 한마디로 정리하자.

 

"클라이언트가 전략을 생성해 전략을 실행할 컨텍스트에 주입하는 패턴"

 

 

 

guy-who-writes-sourcecode.tistory.com/

 

소스 코드를 기록하는 남자

 

guy-who-writes-sourcecode.tistory.com

 

팩터리 메소드 패턴(Factory Method Pattern)

디자인 패턴

팩터리의 의미는 공장이다. 공장은 무언가를 생산하는 장소이다. 객체 지향에서의 팩터리는 객체를 생성하고, 팩터리 메소드는 객체를 생성 반환하는 메소드를 말한다.

 

팩터리 메소드 패턴은 무엇을 의미하는 것인가? 하위 클래스에서 팩터리 메소드를 오버라이딩해서 객체를 반환하는 것을 의미한다. 코드를 살펴보며, 이해를 해보자.

package 팩토리메소드패턴;

public abstract class 동물 {
    abstract 동물장난감 getToy();
}

public abstract class 동물장난감 {
    abstract void identify();
}

public class 강아지 extends 동물 {
    @Override
    동물장난감 getToy() {
    	return new 강아지장난감();
    }
}

public class 강아지장난감 extends 동물장난감 {
    public void identify() {
    	System.out.println("강아지 장난감입니다");
    }
}

public class 고양이 extends 동물 {
    @Override
    동물장난감 getToy() {
    	return new 고양이장난감();
    }
}

public class 고양이장난감 extends 동물장난감 {
    @Override
    public void identify() {
    	System.out.println("고양이 장난감입니다");
     }
}


 

위 코드는 매우 억지처럼 보일 수 있지만, 그래도 중점에 대해서 파악해보자.

여기서 동물의 팩토리 메소드를 구현하여 각 동물이 각자의 장난감을 반환하도록 구현되는 모습을 볼 수 있다.

 

위와 같은 코드 방식으로 구성된다. 아직 미비된 부분이 많은 것 같다. 내가 공부한 책에서 다루는 팩토리 메소드 패턴은 매우 간단하다. 한 마디로 정의된다

"오버라이드된 메소드가 객체를 반환하는 패턴"

 

guy-who-writes-sourcecode.tistory.com/30

 

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

디자인 패턴

템플릿 메소드는 템플릿을 제공하는 메소드, 하위 클래스에게 구현을 강제하는 추상 메소드, 하위 클래스가 선택적으로 오버라이딩할 수 있는 Hook 메소드를 두는 패턴을 템플릿 메소드 패턴이라 한다.

 

이해가 되지 않는다면, 코드를 보는 것이 좋다.

 

억지스럽게 코드를 한번 작성해보겠다. 너무 불편해하지 않았으면 좋겠다.

 

public class Bmw {
    public void driveOnRoad() {
    	System.out.println("자동차 시동 부릉");
        System.out.println("수동 기어로 시작");
        System.out.println("정지");
        System.out.println("자동차 시동 끄기");
    }
 }
 
 public class Audi {
    public void driveOnRoad() {
    	System.out.println("자동차 시동 부릉");
        System.out.println("자동 기어로 시작");
        System.out.println("정지");
        System.out.println("자동차 시동 끄기");
    }
 }

위와 같은 코드가 있다고 하자. 객체 지향의 특징을 조금이나마 이해하고 있다면, 위에서 반복적하는 코드에 대한 리팩토링 의지가 불타오를 것인다. 따라서 이를 템플릿 메소드 패턴을 적용하여 개선해보자.

 

package 템플릿메소드패턴;

public abstract class Car {
    public void driveOnRoad() {
        System.out.println("자동차 시동 부릉");
        drive();
        stop();
        System.out.println("자동차 시동 끄기");
    }
    
    abstract drive();
    
    void stop() {
    	System.out.println("정지");
    }
}
        
        
package 템플릿메소드패턴;

public class Bmw extends Car {
    @Override
    void drive() {
    	System.out.println("자동 주행");
    }
    
    @Override
    void stop() {
    	System.out.println("Bmw 정지");
    }
 }
 
 
package 템플릿메소드패턴;

public class Audi extends Car {
    @Override
    void drive() {
    	System.out.println("수동 주행");
    }
    
    @Override
    void stop() {
    	System.out.println("Audi 정지");
    }
 }

 

코드만 봐서 이해가 안될 것인다. 하나 하나 짚어서 설명해보겠다.

 

템플릿 메소드 패턴 구성 요소 상위 클래스 Car 하위 클래스 (Bmw, Audi)
템플릿 메소드는 공통 로직을 수행하는 부분, 공통 로직 안에서 하위 클래스에서 오버라이딩한 추상 메소드/훅 메소드를 호출 driveOnRoad()  
템플릿 메소드에서 호출하는 추상 메소드, 하위 클래스가 반드시 오버라이딩하도록 만든다. drive() 오버라이딩 필수
템플릿 메소드에서 호출하는 훅 메소드를 하위 클래스에서 선택적으로 오버라이딩합니다. stop() 오버라이딩 선택

 

"상위 클래스의 견본 메서드에서 하위 클래스가 오버라이딩한 메소드를 호출하는 패턴"

 

템플릿 메소드 패턴이 의존 역전 법칙(DIP)을 활용하고 있음을 알 수 있다. 이 패턴을 통해서 중복되는 공통 로직을 리팩토링하고, 개별로 다르게 진행되는 로직은 추상 메소드와 훅 메소드를 사용하여 강제로 오버라이딩하거나 선택적으로 오버라이딩할 수 있습니다.

 

guy-who-writes-sourcecode.tistory.com/30

 

싱글톤 패턴 (Singleton Pattern)

디자인 패턴

싱글톤 패턴이 무엇인가?

  • 싱글톤 패턴은 인스턴스를 딱 하나만 만들어 사용하기 위한 패턴이다.
  • 커넥션 풀, 스레드 풀, 디바이스 설정 객체 등과 같이 여러 개를 만들었을 때 불필요한 자원 낭비가 일어나는 부분을 방지할 수 있다.

싱글톤 패턴을 어떻게 적용해야 하나?

  • new를 실행할 수 없도록 생성자에 private 접근 제한자를 사용한다.
  • 유일한 단일 객체를 반환할 수 있는 정적 메소드를 생성한다.
  • 유일한 단일 객체를 참조할 정적 참조 변수를 사용한다.

코드를 보면서 어떻게 사용하는 것인가 다시 확인하자.

package 싱글톤패턴;

public class Singleton {

    static Singleton sigletonObject;
    
    private Singleton() {};
    
    public static Singleton getInstance() {
    	if (singtonObject == null) {
        	singletonObject = new Singleton();
        }
    	return singletonObject;
    }
}

 

new를 실행할 수 없도록 Singleton 생성자에 private을 사용했다. 이렇게 한다면 외부에서 new Singleton() 메소드를 호출할 수가 없다. 

 

유일한 단일 객체를 참조할 정적 참조 변수인 singletonObject를 사용한다. 추후 getInstance 메소드를 호출하게 되면 이 참조 객체를 반환한다.

 

유일한 단일 객체를 반환할 수 있는 정적 메소드가 필요하며 여기서는 getInstance라는 메소드를 사용했다. 일반적으로 싱글톤 패턴을 사용할 때 이름은 getInstance로 하는 듯하니 싱글톤 패턴을 사용할 때 고민하지말고 getInstance를 사용해도 무방하다.

 

마지막으로, 싱글톤 패턴 특징을 간략하게 설명하고, 마무리한다.

 

싱글톤 패턴

기본적으로 쓰기 가능한 속성을 갖지 않는 것을 대상으로 적용하며, private 생성자를 가지며, static 키워드 단일 객체 참조 변수를 사용하고, 단일 객체를 반환할 수 있는 getInstance 메소드를 사용한다.

 

guy-who-writes-sourcecode.tistory.com/30

 

스프링에 녹아있는 아름다운 디자인 패턴

프로그램을 개발하다 보면 많은 상황에 직면하게 되는데, 프로그래밍의 역사가 꽤 길지 않은가? 따라서 이와 비슷한 사례를 이미 경험한 선배들이 정리해 둔 표준 설계 패턴이 있다. 이를 디자

guy-who-writes-sourcecode.tistory.com

 

백준 9376번 : 탈옥 [Java]

Algorithm/백준

www.acmicpc.net/problem/9376

 

9376번: 탈옥

상근이는 감옥에서 죄수 두 명을 탈옥시켜야 한다. 이 감옥은 1층짜리 건물이고, 상근이는 방금 평면도를 얻었다. 평면도에는 모든 벽과 문이 나타나있고, 탈옥시켜야 하는 죄수의 위치도 나타

www.acmicpc.net

package 백준;

import java.io.*;
import java.util.Arrays;
import java.util.LinkedList;
import java.util.PriorityQueue;
import java.util.StringTokenizer;

public class 백준9376_탈옥 {
    static BufferedReader br = new BufferedReader(new InputStreamReader(System.in));
    static BufferedWriter bw = new BufferedWriter(new OutputStreamWriter(System.out));
    static StringTokenizer st = null;
    //빈 공간은 '.', 지나갈 수 없는 벽은 '*', 문은 '#', 죄수의 위치는 '$'이다.

    static final char EMPTY = '.';
    static final char WALL = '*';
    static final char DOOR = '#';
    static final char PRISONER = '$';

    public static void main(String[] args)  throws IOException {
        int t;
        char[][] map;

        t = Integer.parseInt(br.readLine());

        for (int testCase = 0; testCase < t; testCase++)
        {
            int h, w, prisonerIdx, minimumOpenDoor;
            int[][] prisonerOne, prisonerTwo, sanggeun;

            st = new StringTokenizer(br.readLine());
            h = Integer.parseInt(st.nextToken());
            w = Integer.parseInt(st.nextToken());

            // h + 2 , w + 2 외부 출입라인 생성
            map = new char[h + 2][w + 2];
            Prisoner[] prisoners = new Prisoner[2];
            prisonerIdx = 0;

            String line = null;
            for (int i = 0; i < h; i++)
            {
                line = br.readLine();
                for (int j = 0; j < w; j++)
                {
                    map[i + 1][j + 1] = line.charAt(j);
                    if (line.charAt(j) == PRISONER)
                    {
                        prisoners[prisonerIdx++] = new Prisoner(i + 1, j + 1);
                    }
                }
            }

            prisonerOne = bfs(map, prisoners[0], h, w);
            prisonerTwo = bfs(map, prisoners[1], h, w);
            sanggeun = bfs(map, new Prisoner(0, 0), h, w);

            System.out.println();
            printMap(prisonerOne);
            printMap(prisonerTwo);
            printMap(sanggeun);

            minimumOpenDoor = getMinimumSum(prisonerOne, prisonerTwo, sanggeun, map);
            System.out.println(minimumOpenDoor);
        }
    }

    private static void printMap(int[][] arr)
    {
        for (int[] a: arr)
        {
            System.out.println(Arrays.toString(a));
        }
        System.out.println();
    }

    private static int getMinimumSum(int[][] prisonerOne, int[][] prisonerTwo, int[][] sanggeun, char[][] map) {
        int minSum;

        minSum = Integer.MAX_VALUE;

        for (int i = 0; i < prisonerOne.length; i++)
        {
            for (int j = 0; j < prisonerOne[i].length; j++)
            {
                if (map[i][j] == '*')
                    continue;

                int sum = prisonerOne[i][j] + prisonerTwo[i][j] + sanggeun[i][j];
                if (map[i][j] == '#')
                {
                    sum -= 2;
                }
                if (minSum > sum)
                {
                    minSum = sum;
                }
            }
        }


        return (minSum);
    }

    private static int[][] bfs(char[][] map, Prisoner prisoner, int h, int w) {
        PriorityQueue<Prisoner> queue = new PriorityQueue<>();
        boolean[][] visited = new boolean[h + 2][w + 2];
        int[][] doorHistory = new int[h + 2][w + 2];

        queue.add(prisoner);
        visited[prisoner.x][prisoner.y] = true;

        while (!queue.isEmpty())
        {
            Prisoner temp = queue.poll();
            doorHistory[temp.x][temp.y] = temp.openDoor;

            for (int i = 0; i < 4; i++)
            {
                int nx, ny;

                nx = temp.x + dx[i];
                ny = temp.y + dy[i];
                if (0 <= nx && nx < h + 2 && 0 <= ny && ny < w + 2 && !visited[nx][ny]
                && map[nx][ny] != '*')
                {
                    visited[nx][ny] = true;
                    queue.add(new Prisoner(nx, ny, map[nx][ny] == '#' ? temp.openDoor + 1 : temp.openDoor));
                }
            }
        }
        return (doorHistory);
    }

    static int[] dx = {0, 0 ,1, -1};
    static int[] dy = {1, -1 ,0, 0};


    public static class Prisoner implements Comparable<Prisoner>{
        int x, y, openDoor;

        public Prisoner(int x, int y) {
            this.x = x;
            this.y = y;
            this.openDoor = 0;
        }

        public Prisoner(int x, int y, int openDoor) {
            this.x = x;
            this.y = y;
            this.openDoor = openDoor;
        }

        @Override
        public int compareTo(Prisoner o) {
            return Integer.compare(this.openDoor, o.openDoor);
        }
    }
}

 

빠르게 문제의 요지를 파악하고 넘어가자.

 

고려할 것은 문을 어떻게 문제에서 제시하는 두 명의 죄수, 한 명의 외부인이 문을 어떻게 열 것인가? 이다.

 

풀이를 찾아보는 많은 이들이 분명히 BFS를 사용한다는 개념은 이해했을 것이라 생각한다.

 

하지만 이들을 한번에 움직일 것인가? 아니면 각자 움직일 것인가? 고려해보아야 한다.

 

해설은 다음과 같다.

 

  1. 죄수1이 움직인다.
  2. 죄수2이 움직인다.
  3. 외부인이 움직인다.

자, 그럼 각자 움직이면서 BFS로 탐색을 하게 된다면, 최소 거리로 문을 여는 개수를 배열에 저장할 수 있을 것인다.

 

[출처] https://rebas.kr/770
[출처] https://rebas.kr/770

위와 같이, 각자가 움직이면서 각 지점에서 최소로 열어온 문의 개수를 기록할 수 있다.

 

그럼 모든 배열의 각 위치에서 합산을 한다.

 

여기서 합산의 의미는 특정 위치까지 세 명이 문을 열어온 개수를 확인한다는 것이다.

 

따라서, 전체 배열을 순회하면서 비교한다면, 만났을 때 문을 열어온 최소 개수를 구할 수 있을 것인다.

 

여기서 두 가지 주의해야 한다.

 

첫 번째, 합산하는 위치에 문이 있다면 3명 중에 한명만 열면 되기 때문에 -2를 해준다.

두 번째, BFS로 각 인원들을 움직여 줄 때, 우선 순위 큐로 문을 가장 적게 연 사람을 우선적으로 탐색시켜야 한다.

 

데코레이터 패턴(Decorator Pattern)

디자인 패턴

장식하는 사람이란 입장에서 접근해보자. 데코레이터 패턴의 구현 방법은 프록시 패턴과 동일하다. 다만 프록시 패턴과 다른 점은 프록시 패턴이 반환값을 조작하지 않고 그대로 전달하는 것과 다르게 데코레이션을 한다.

 

바로 코드를 확인해보자.

 

[IService]

package decoratorPattern;

public interface IService {
    public abstract String runProcess();
}

[Service]

package decoratorPattern;

public class Service implements IService {
    @Override
    public String runProcess() {
        return "process";
    }
}

[Decorator]

package decoratorPattern;

public class Decorator implements IService {
    IService service;

    @Override
    public String runProcess() {
        System.out.println("호출에 대한 장식이 주목적이며, 클라이언트에게 장식이 달린 반환 결과를 전달");
        service = new Service();
        return "장식" + service.runProcess();
    }
}

 

기존의 프록시 패턴과 다르게 Decorator의 runProcess가 반환하는 문자열에 "장식" 이 추가되지 않았는가?

이제 데코레이터 패턴의 핵심들을 살펴보자.

 

  • 장식자는 실제 서비스와 같은 이름의 메소드를 사용한다.

  • 장식자는 실제 서비스에 대한 참조 변수를 갖는다 (합성).

  • 장식자는 실제 서비스와 같은 이름을 가진 메소드를 호출하고, 반환값에 장식을 붙여서
    클라이언트에게 전달한다.

  • 장식자는 실제 서비스의 메소드 호출 전후에 별도의 로직을 수행할 수 있다.

 

하나만 기억하자!

메소드 호출 반환값에 변화를 주기 위해 중간에 데코레이터를 두는 패턴!

 

스프링에 녹아있는 디자인 패턴들

guy-who-writes-sourcecode.tistory.com/30