📌 TIL
1) 디자인 패턴
디자인 (Design) → 설계
패턴 (Pattern) → 자주 반복되는 것
즉, 디자인 패턴이란 설계 과정에서 자주 반복되는 형태의 코드를 발견하여 각각의 패턴으로 정의해놓은 것 입니다.
그렇다면, 디자인 패턴이 우리에게 주는 이점은 무엇이 있을까요 ?
(1) 검증된 해결 방안 💡
디자인 패턴은 설계 과정에서 자주 반복되는 형태의 코드 중 누군가 효과적이라고 생각되는 패턴들을 발견하여 정의한 것으로 실무에서 자주 겪는 문제들을 해결할 수 있는 검증된 해결방법 입니다.
(2) 효율적인 커뮤니케이션 💡
많은 개발자들이 디자인 패턴을 학습하기 때문에 디자인 패턴이라는 용어에 대해 범용적으로 사용함으로써 소통적인 부분에 있어 도움을 줍니다. (ex. 코드리뷰, 회의 등)
[ 디자인 패턴의 종류 ]
(1) 생성을 위한 패턴
: 추상 팩토리, 빌더 패턴, 팩토리 메서드 패턴, 프로토타입 패턴, 싱글턴 패턴
(2) 구조를 위한 패턴
: 퍼싸드 패턴, 플라이웨이트 패턴, 데코레이터 패턴, 브릿지 패턴, 컴포지트 패턴, 어댑더 패턴, 프록시 패턴
(3) 행위를 위한 패턴
: 방문자, 템플릿 메서드, 전략, 상태, 옵저버, 중재자, 메멘토, 인터프리터, 책임 연쇄, 커맨드 패턴
이 중, 우리가 직접 구현해서 사용하는 패턴과 이미 구현된 것을 사용하는 패턴으로 나누어 중요한 패턴들을 몇 가지 살펴보겠습니다.
그 전에! 안티 패턴을 먼저 살펴볼게요.
2) 안티 패턴
안티패턴이란, 실제 디자인 패턴과 비슷하게 많이 사용되는 패턴이지만 비효율적이거나 비생산적인 패턴을 의미합니다.
실제 습관적으로 많이 사용하는 패턴이지만 성능, 디버깅, 유지보수, 가독성 등의 측면에서 서비스에 부정적인 영향을 줄 수 있어 ㅅ사용을 지양하는 패턴이라고 해요.
(1) if문을 작성할 때 한 줄 이라는 이유로 { } 를 생략하는 습관 .... (좋지 않아요)
(2) 배열 순회 시 array.lengthg는 캐시하고 사용하는 것이 좋습니다. for문을 사용할 때 반복이 100번 ~ 1000번이라면 array.length 가 100번 ~ 1000번 수행되게 됩니다.
(3) 예외에 기반한 코딩으로, if-else로 간단하게 할 수 있는 로직을 예외처리로 로직을 짜면 안됩니다. 예외는 정말 나쁜 상황에..
(4) 바퀴의 재발명 : 이미 있는 라이브러리나 프레임워크를 다시 만들어 사용하는 것으로, 유용한 프레임워크나 라이브러리의 존재를 잘 몰라 이를 구현하는 것이에요.
그럼 이제, 우리가 직접 구현해서 사용하는 패턴들에 대해 알아보겠습니다.
(3) 어댑터 패턴 (Adapter Pattern)
어댑터 패턴은 호환되지 않는 인터페이스를 가진 객체들이 협업할 수 있도록 하는 구조적 디자인 패턴이에요.
한 클래스의 인터페이스를 클라이언트에서 사용하고자 하는 다른 인터페이스로 변환합니다. 어댑터를 이용하면 인터페이스 호환성 문제 때문에 같이 쓸 수 없는 클래스들을 연결해서 사용할 수 있어요 !
어댑터 패턴은 전기 콘센트를 보면 이해하기 수월합니다.
한국의 표준 플러그를 일본에 전원 소켓에 바로 끼워줄 수 없어 동그란 모양을 일자로 바꿔주는 어댑터를 끼워줘야 합니다. 이와 같이 어댑터는 소켓의 인터페이스를 플러그에서 필요로 하는 인터페이스로 바꿔준다고 할 수 있겠죠 ?
자세한 내용은 해당 포스팅을 통해 어댑터 패턴에 대해 정리해놓았습니다 ! 🤓
(4) 퍼싸드 패턴
"Facade"라는 건물의 (출입구가 있는) 정면을 뜻하는 프랑스로부터 유래 되었습니다.
--> 건물 내부의 복잡함을 감추고, 정면만을 보게 한다는 의미로 정면을 인터페이스로 생각하면 이해가 쉽습니다. (클라이언트는 정면에 보이는 인터페이스만 사용하는 셈)
ex) "세탁"이라는 행위에 Facade Pattern 적용해보기
세탁을 하기 위해서는 크게 Wasing, Rinsing, Spinning 과 같은 동작들이 필요한데요, 이들을 Facade Object(= WashingMachine)를 통해 "세탁"이라는 행위에 필요한 공통 기능들을 정의할 수 있어요.
[ 기존의 코드 ]
class Rinsing{
void rinse(){
System.out.println("do Rinsing")
}
}
class Spinning{
void spin(){
System.out.println("do Spinning")
}
}
class Washing{
void wash(){
System.out.println("do Washing")
}
}
class Client{
Washing washing = new Washing();
Rinsing rinsing = new Rinsing();
Spinning spinning = new Spinning();
washing.wash();
rinsing.rinse();
spinning.spin();
}
읽기가 힘들고, 유지보수에 용이하지 않으며 Client가 여러 객체들에 강하게 연결되어 있는 문제가 있어요.
[ Facade Pattern 적용 ]
class WashingMachine{
Washing washing = new Washing();
Rinsing rinsing = new Rinsing();
Spinning spinning = new Spinning();
void startWashing(){
washing.wash();
rinsing.rinse();
spinning.spin();
}
}
class Client{
WashingMachine washingMachine = new WashingMachine();
washingMachine.strartWahsing();
}
Client에서 Facade Object(=WashingMachine)만을 호출하여 "세탁"이라는 동작을 수행할 수 있으며, 메서드의 의미 또한 명확하게 알 수 있습니다.
[ Facade Pattern 의 장점 ]
- 낮은 결합도 보장 (로직의 변경 --> Facade Object 까지만 영향을 받습니다.)
- 가독성 상승
(5) 전략 패턴
전략 패턴은 실행(런타임)중에 알고리즘 전략을 선택해 객체 동작을 실시간으로 바뀌도록 할 수 있게 하는 행위 디자인 패턴입니다.
예제를 통해 이해해볼까요 ?
ex) 과일 매장에서 할인 정책을 적용하고 있습니다.
- 제일 먼저 온 손님 : 10%
- 마지막 손님 : 20%
- 신선도가 떨어지는 과일 : 20%
[ 전략 패턴이 적용되지 않은 코드 ]
public class Calculator {
public double calculate(boolean isFirstGuest, boolean isLastGuest, List<Item> items) {
double sum = 0;
for (Item item : items) {
if (isFirstGuest) {
sum += item.getPrice() * 0.9;
} else if (!item.isFresh()) {
sum += item.getPrice() * 0.8;
} else if (isFirstGuest) {
sum += item.getPrice() * 0.8;
} else {
sum += item.getPrice();
}
}
return sum;
}
}
public class Item {
private final String name;
private final int price;
public Item(String name, int price) {
this.name = name;
this.price = price;
}
public int getPrice() {
return price;
}
public boolean isFresh() {
return true;
}
}
- 할인 정책을 사용하기 위해 할인 조건이 충족하는지를 if-else 문을 통해 해결하고 있습니다.
- 변경에 유연하지 않은 코드입니다.
- 새로운 가격 정책이 추가되면 변경이 번거롭고 시간이 지날수록 코드 분석이 어려워집니다.
[ 전략 패턴이 적용된 알고리즘 ]
public interface DiscountPolicy {
double calculateWithDisCountRate(Item item);
}
public class FirstCustomerDiscount implements DiscountPolicy{
@Override
public double calculateWithDisCountRate(Item item) {
return item.getPrice() * 0.9;
}
}
public class LastCustomerDiscount implements DiscountPolicy{
@Override
public double calculateWithDisCountRate(Item item) {
return item.getPrice() * 0.8;
}
}
public class UnFreshFruitDiscount implements DiscountPolicy{
@Override
public double calculateWithDisCountRate(Item item) {
return item.getPrice() * 0.8;
}
}
그리고 이를, 기존의 Calculator 클래스에서 생성자를 통해 필요한 하위 타입을 주입받아 사용하겠습니다.
public class Calculator {
private final DiscountPolicy discountPolicy;
public Calculator(DiscountPolicy discountPolicy) {
this.discountPolicy = discountPolicy;
}
public double calculate(List<Item> items) {
double sum = 0;
for (Item item : items) {
sum += discountPolicy.calculateWithDisCountRate(item);
}
return sum;
}
}
할인 정책에 따른 할인 조건을 생성자를 통해 전달해줄 수 있습니다. 요청에 맞는 객체를 Calculator 에 주입해주는 방식을 통해 전략패턴을 구현한 것이죠 !
[ 전략 패턴의 장점 ]
- 런타임에 어떤 동작을 수행할 지 전략을 선택하기 때문에 유지보수 측면에서 좋습니다.
- 새로운 전략을 추가하기 용이합니다.
여기까지가 직접 구현하여 사용하는 디자인 패턴중 자주 사용하는 것들을 알아보았는데요, 이어서 이미 구현되어 있는 디자인 패턴들을 살펴볼게요.
(6) 싱글턴 패턴
해당 클래스의 인스턴스를 오직 하나만 만들 수 있도록 제한하는 패턴입니다. 인스턴스를 새로 생성한다는 것은 자원(메모리, 시간)을 소모한다는 것과 같죠 ?
참조 변수를 가지고 있다고 하더라도 각각의 로직에서 활용되지 않는다면 이런 상태에서는 동시성 문제를 일으키지 않기 때문에 무상태성 Bean 이라고 합니다.
즉, 싱글턴은 공유되어 사용해도 문제가 없는 메서드만이 존재해야 합니다.
public class Singleton {
// 단 1개만 존재해야 하는 객체의 인스턴스로 static 으로 선언
private static Singleton instance;
// private 생성자로 외부에서 객체 생성을 막아야 한다.
private Singleton() {
}
// 외부에서는 getInstance() 로 instance 를 반환
public static Singleton getInstance() {
// instance 가 null 일 때만 생성
if (instance == null) {
instance = new Singleton();
}
return instance;
}
}
(7) 옵저버 패턴
어떤 대상의 상태변화에 관심 있어하는 대상들에게 상태가 변화되었다고 전파할 수 있는 패턴으로 어떤 일에 대한 구독과 발행을 해준다고 생각하면 됩니다.
ex) 뉴스가 발행되면 구독한 사용자에게 알림이 가는 서비스
1. 뉴스가 발행되면 구독한 사람에게 알림이 가도록 하자.
2. 그런데 만약, 뉴스 뿐만 아니라 잡지를 구독했을 때에도 알림이 가도록 기능을 추가한다면 ?
이런 요구사항들이 발생할 때, 우리는 옵저버 패턴을 통해 효과적으로 코드를 처리할 수 있습니다.
class AlarmPublisher{
private List<Subscriber> subscribers = new ArrayList<>();
void registerSubscriber(){}
void removeSubscriber(){}
void notifyAlarm(){}
}
- 실행 시점에 이벤트 구독 대상을 추가하거나 제거할 수 있습니다.
- 의존성을 제거할 수 있습니다.
(8) 프록시 패턴
Proxy는 "대리자"라는 뜻을 가지고 있습니다. 정의로는 원래 객체를 대신하여 요청을 받아 원래 객체를 호출하기 전이나 후에 특정
로직을 실행하는 패턴입니다.
ex) 용량이 큰 이미지와 글이 같이 있는 문서를 화면에 띄울 때 텍스트는 용량이 작아 빠르게 나타나지만, 이미지는 용량이 크기 때문에 느리게 로딩된다. 텍스트와 이미지 로딩이 모두 끝난 후 화면이 나오게 되면 사용자는 로딩이 끝날때까지 기다려야 한다.
따라서, 로딩이 먼저 끝난 텍스트를 먼저 나오게 하는 것이 좋다. 이와 같은 방식을 가지려면 텍스트 처리용 프로세스, 이미지 처리용 프로세스를 별도로 운영하면 된다.
[ Image - Interface ]
public interface Image {
public void displayImage();
}
[ RealImage - implements Image ]
// Real_Image.java
public class Real_Image implements Image {
private String fileName;
public Real_Image(String fileName) {
this.fileName = fileName;
}
private void loadFromDisk(String fileName) {
System.out.println("로딩: " + fileName);
}
@Override
public void displayImage() {
System.out.println("보여주기: " + fileName);
}
}
/
[ Proxy_Image ]
// Proxy_Image.java
public class Proxy_Image implements Image {
private String fileName;
private Real_Image realImage;
public Proxy_Image(String fileName) {
this.fileName = fileName;
}
@Override
public void displayImage() {
if (realImage == null) {
realImage = new Real_Image(fileName);
}
realImage.displayImage();
}
}
// Proxy_Pattern.javva
public class Proxy_Pattern {
public static void main(String args[]) {
Image image1 = new Proxy_Image("test1.jpg);
Image image2 = new Proxy_Image("test2.jpg);
image1.displayImage();
image2.displayImage();
}
}
Proxy_Pattern 클래스에서 Real_Image 클래스에 직접 접근하지 않고, Proxy_Image 클래스에서 객체를 생성하여 대신 일을 수행하는 것을 확인할 수 있다.
[ 프록시 패턴의 장점 ]
- 사이즈가 큰 객체가 로딩되기 전에도 프록시를 통해 참조할 수 있다.
- 실제 객체의 public, protected 메서드를 숨기고 인터페이스를 통해 노출시킬 수 있다.
[ AOP - 관점지향 프로그래밍]
: AOP와 프록시 패턴은 연관이 있습니다. AOP는 어떤 로직을 핵심적인 로직과 부가적인 로직으로 나누고 반복되는 부가적인 로직을 분리하여 감추는 것입니다.
이는, 부가적인 로직이 없어지고 핵심적인 로직만 남기 때문에 핵심적인 로직의 가독성이 올라가요. AOP를 적용하기 위해서는 AOP를 적용하고 싶은 메서드에 정의한 AOP 어노테이션을 붙이면 됩니다.
@Service
public class MyService {
@LogBeforeExecution
public void performAction() {
System.out.println("Performing the action.");
}
}
@LogBeforeExecution이라는 어노테이션을 붙여 AOP를 적용하였습니다. 해당 어노테이션을 정의한 내용에 따라 performAction() 이전이나 이후에 실행할 수 있습니다.
[ Self Invocation ]
: 진짜 객체 내부에서 this 를 통해 메서드를 호출하게 되면 AOP를 적용하더라도 제대로 동작하지 않을 가능성이 발생합니다.
< 참고 자료 >