olrlobt
[디자인 패턴] 전략 패턴(Strategy Pattern), 팩토리 패턴(Factory Pattern), 레지스트리 패턴(Registry Pattern) 본문
[디자인 패턴] 전략 패턴(Strategy Pattern), 팩토리 패턴(Factory Pattern), 레지스트리 패턴(Registry Pattern)
olrlobt 2024. 7. 30. 00:19
전략 패턴(Strategy Pattern)과 팩토리 패턴(Factory Pattern), 레지스트리 패턴(Registry Pattern)은 객체지향 설계에서 자주 사용되는 디자인 패턴이다. 해당 패턴 모두 객체 생성 및 행위 관련 문제를 해결하는데 도움을 주는데, 전략 패턴과 팩토리 패턴, 레지스트리 패턴을 결합하여 사용하는 경우 장점을 극대화하고, 더 유연하고 확장 가능한 시스템을 만들 수 있다. 오늘이 이 패턴들을 정리하면서, 개인 프로젝트에 어떻게 적용하였는지 알아보자.
전략 패턴(Strategy Pattern)
전략 패턴(Strategy Pattern)은 객체지향 디자인 패턴 중 하나로, 실행 중에 알고리즘을 선택할 수 있게 하는 행위 소프트웨어 디자인 패턴이다. 이 패턴은 동일한 문제를 해결하는 여러 방법이 있을 때, 이들 방법을 각각의 클래스(전략)로 캡슐화하고, 실행 시점에 필요에 따라 전략을 바꿀 수 있도록 해준다.
전략 패턴의 구조
- 전략 인터페이스(Strategy Interface):
- 알고리즘을 캡슐화하는 인터페이스. 다양한 알고리즘 클래스가 이 인터페이스를 구현하여 특정 작업을 수행한다.
- 구체적인 전략 클래스(Concrete Strategy Classes):
- 전략 인터페이스를 구현하여 실제 알고리즘을 정의하는 클래스들. 이 클래스들은 서로 다른 알고리즘을 구현하며, 클라이언트의 요청에 따라 교체될 수 있다.
- 컨텍스트 클래스(Context Class):
- 전략 인터페이스를 통해 알고리즘을 사용하는 클래스. 이 클래스는 클라이언트가 선택한 전략을 사용하여 작업을 수행한다.
전략 패턴 예제
예제 설명에는 간단하게 내 개인 프로젝트를 활용했다. 간단하게 블로그를 스크래핑하여, Posting 이미지를 만들어주는 예제이며, posting() 메서드는 포스팅 이미지를 만들고, link() 메서드는 link를 반환하며, blog() 메서드는 블로그 이미지를 반환해 준다.
1. 전략 인터페이스 정의
public interface Blog {
Posting posting(String blogName, int index, PostingType postingType) throws IOException;
RedirectView link(String blogName, int index) throws IOException;
Posting blog(String blogName) throws IOException;
}
먼저, 전략 패턴에서 사용할 인터페이스를 정의한다. 이 인터페이스는 알고리즘을 캡슐화하는 역할을 한다.
2. 구체적인 전략 클래스
public class Tistory implements Blog {
@Override
public Posting posting(String blogName, int index, PostingType postingType) throws IOException {
/** 비즈니스 로직 **/
}
@Override
public RedirectView link(String blogName, int index) throws IOException {
/** 비즈니스 로직 **/
}
@Override
public Posting blog(String blogName) throws IOException {
/** 비즈니스 로직 **/
}
}
public class Velog implements Blog {
@Override
public Posting posting(String blogName, int index, PostingType postingType) throws IOException {
/** 비즈니스 로직 **/
}
@Override
public RedirectView link(String blogName, int index) throws IOException {
/** 비즈니스 로직 **/
}
@Override
public Posting blog(String blogName) throws IOException {
/** 비즈니스 로직 **/
}
}
앞서 캡슐화한 인터페이스를 구현하는 구체적인 전략 클래스를 작성한다. 위 예제에서는 Blog 인터페이스를 각각 Tistory, Velog 클래스로 구현하여 각기 다른 비즈니스 로직을 수행하게 된다. 조금 더 자세히 설명하자면, Velog의 경우 Blog API를 지원하여 정보를 갖고 오기 위하여 API를 호출하는 로직을 수행하며, Tistory의 경우 API 지원이 종료되었기 때문에 자체적인 크롤링 데이터를 이용하여 로직을 수행한다.
3. 컨텍스트 클래스 구현
public class PostingService {
public Posting posting(String blogName, String platform, int index) throws IOException {
Blog blog;
if(platform.equals("tistory"){
blog = new Tistory();
}else if(platform.equals("velog"){
blog = new Velog();
}
return blog.posting(blogName, index, PostingType. BlogPosting);
}
public RedirectView link(String blogName, String platform, int index) throws IOException {
/** 비즈니스 로직 **/
}
public Posting blog(String blogName, String platform) throws IOException {
/** 비즈니스 로직 **/
}
}
컨텍스트 클래스는 전략 객체를 이용하는 클래스이다. 클라이언트가 설정한 전략에 따라 작업을 수행시키는 역할을 한다.
즉, 위 예제에서는 platform 변수의 값을 어떻게 설정하느냐에 따라 Tistory의 크롤링 로직을 수행하기도, Velog의 API호출 로직을 수행하기도 한다.
이렇듯 비즈니스 로직을 서비스 실행 중에 동적으로 변경할 수 있는 방법을 제공하는 디자인 패턴을 전략 패턴(Strategy Pattern)이라고 한다.
팩토리 패턴 (Factory Pattern)
팩토리 패턴(Factory Pattern)은 객체 생성의 인스턴스를 서브클래스에 위임하는 디자인 패턴이다. 이 패턴을 사용하면 객체 생성의 로직을 캡슐화하여 클라이언트 코드가 객체 생성의 구체적인 클래스를 몰라도 객체를 생성할 수 있다.
앞선 코드에서 마지막 부분에서 아래와 같이 인스턴스를 생성하였다.
public class PostingService {
public Posting posting(String blogName, String platform, int index) throws IOException {
Blog blog;
if(platform.equals("tistory"){
blog = new Tistory();
}else if(platform.equals("velog"){
blog = new Velog();
}
return blog.posting(blogName, index, PostingType. BlogPosting);
}
}
이 부분은 팩토리 패턴을 사용했다고 보기 어려운데, 객체 생성 부분의 로직을 분리하지 않아 posting() 메서드 안에 객체 생성 로직이 포함되어 있고, 새로운 블로그 플랫폼을 추가할 때마다 posting() 메서드에 조건문을 추가해야 하므로 확장성이 떨어지기 때문이다.
이제 이 부분에 팩토리 패턴을 적용해 보자.
팩토리 패턴은 크게 단순 팩토리/팩토리 메서드/ 추상 팩토리로 나눌 수 있다.
단순 팩토리 (Simple Factory)
단순 팩토리는 객체 생성 로직을 하나의 클래스나 메서드로 분리하여 제공한다.
BlogFactory 클래스를 선언하고, 해당 클래스 내부에서 객체를 생성하여 반환해 준다. 이렇게 구현하면서 팩토리 클래스에 객체를 생성하는 책임을 부여하게 되고, 확장을 할 때 해당 부분에서 객체 반환 로직을 추가하면 된다.
public class BlogFactory {
public static Blog getBlog(String platform) {
if (platform.equals("tistory")) {
return new Tistory();
} else if (platform.equals("velog")) {
return new Velog();
}
throw new IllegalArgumentException("Unsupported platform: " + platform);
}
}
public class PostingService {
public Posting posting(String blogName, String platform, int index) throws IOException {
Blog blog = BlogFactory.getBlog(platform);
return blog.posting(blogName, index, PostingType.BlogPosting);
}
}
팩토리 메서드 (Factory Method) / 추상 팩토리 (Abstract Factory)
팩토리 메서드는 객체 생성에 대한 인터페이스를 정의하고, 하위 클래스에서 인스턴스를 생성하도록 한다. 단순 팩토리를 조금 더 구조화한 방식으로 볼 수 있으며, 추상 팩토리 패턴의 경우에는 인터페이스를 추상 클래스로 변경만 하면 되므로 생략한다.
public interface BlogFactory {
Blog createBlog();
}
public class TistoryFactory implements BlogFactory {
public Blog createBlog() {
return new Tistory();
}
}
public class VelogFactory implements BlogFactory {
public Blog createBlog() {
return new Velog();
}
}
public class PostingService {
private final BlogFactory tistoryFactory = new TistoryFactory();
private final BlogFactory velogFactory = new VelogFactory();
public Posting posting(String blogName, String platform, int index) throws IOException {
BlogFactory blogFactory;
if (platform.equals("tistory")) {
blogFactory = tistoryFactory;
} else if (platform.equals("velog")) {
blogFactory = velogFactory;
} else {
throw new IllegalArgumentException("Unsupported platform: " + platform);
}
Blog blog = blogFactory.createBlog();
return blog.posting(blogName, index, PostingType.BlogPosting);
}
}
위 두 방법으로 팩토리 패턴을 사용할 수 있으며, 객체의 생성을 별도의 클래스로 분리한다는 점에서 단일 책임 원칙을 지키게 된다. 또한 새로운 기능이 추가되면, 해당 팩토리 내부에서 로직 수정이 이루어지므로 열린-폐쇄 원칙을 지킬 수 있다.
하지만 팩토리 패턴을 너무 남발하게 된다면 클래스가 많아지게 되므로 오히려 복잡해질 수 있고, 여러 클래스를 오가야 되는 불편함이 증가할 수 있으니 유의해서 사용하자.
레지스트리 패턴 (Registry Pattern)
레지스트리 패턴은 객체를 전역적으로 관리하고 제공하는 디자인 패턴이다. 중앙 저장소(레지스트리)를 통해 객체를 관리하고 필요할 때 조회하여 사용하는 방식이다. 이 패턴은 싱글톤 패턴과 유사하지만, 여러 객체를 관리할 수 있으며, 이 객체들은 전역적으로 접근 가능하게 한다.
레지스트리 패턴 역시, 팩토리 패턴과 같이 사용하면 효과적이다.
팩토리 패턴에서 기능이 추가될 때 객체를 조건문으로 처리하여 반환하는 방식은 확장성 측면에서 한계를 가질 수 있는데, 이러한 한계를 레지스트리 패턴을 사용하면서 어느 정도 보완이 가능하다.
레지스트리 패턴 예제
레지스트리 패턴을 사용하면 조건문을 사용하지 않고도 객체를 동적으로 관리할 수 있다. 객체를 미리 Map에 static을 통해 전역적으로 저장해 두고, 조회 메서드를 통해 조회하는 방식으로 사용한다.
아래 예제는 Spring 프레임워크에 Bean으로 등록하여 싱글톤 스코프를 사용하기 때문에, static을 사용하지 않고 구현하였다.
@Component
public class BlogFactory {
private final Map<String, Blog> blogStrategyMap;
public BlogFactory(Tistory tistory, Velog velog, Anything anything) {
blogStrategyMap = new HashMap<>();
blogStrategyMap.put("tistory", tistory);
blogStrategyMap.put("t", tistory);
blogStrategyMap.put("velog", velog);
blogStrategyMap.put("v", velog);
blogStrategyMap.put("else", anything);
}
public Blog getBlog(String platform) {
return blogStrategyMap.getOrDefault(platform, blogStrategyMap.get("else"));
}
}
앞선 팩토리 패턴에서의 조건문들에 비해 구조가 간단해 보이고 훨씬 가독성이 좋은 것을 알 수 있다. 여기서 객체를 조회할 때는 getBlog()를 통해 간단히 조회가 가능하다.
@Service
@RequiredArgsConstructor
public class PostingService {
private final BlogFactory blogFactory;
public Posting posting(String blogName, String platform, int index) throws IOException {
Blog blog = blogFactory.getBlog(platform);
return blog.posting(blogName, index, PostingType. BlogPosting);
}
public RedirectView link(String blogName, String platform, int index) throws IOException {
Blog blog = blogFactory.getBlog(platform);
return blog.link(blogName, index);
}
public Posting blog(String blogName, String platform) throws IOException {
Blog blog = blogFactory.getBlog(platform);
return blog.blog(blogName);
}
}
최종적으로는 비즈니스 로직을 위와 같이 간추릴 수 있었고, 팩토리 클래스를 의존성 주입받아 호출하는 방식으로 동적인 로직을 수행하게 구성하였다.
만약 추가적인 기능(해당 예제에서는 플랫폼)이 추가된다면, BlogFactory에 객체를 하나 추가해 주고, 해당 클래스의 비즈니스 로직을 구현하기만 하면 기능 추가가 완료된다.
'Spring > Project' 카테고리의 다른 글
[Java] Object pooling(오브젝트 풀링) - Apache Commons Pool2로 구현하기 (2) | 2024.10.05 |
---|---|
[Java] IntelliJ Profiler로 병목지점 찾아, Java ImageIO 성능 개선하기 (5) | 2024.10.03 |
[추천 알고리즘] Spotify ANNOY (Approximate Nearest Neighbors Oh Yeah) 알고리즘이란? (3) | 2024.04.20 |
[토이 프로젝트] 블로그를 깃허브에 효과적으로 노출시키는 방법 (2) | 2024.03.28 |
[Pose Estimation] Mediapipe 2배속 분석과 분석 결과 중간 부족한 프레임 채우기 (1) | 2023.04.21 |