일 | 월 | 화 | 수 | 목 | 금 | 토 |
---|---|---|---|---|---|---|
1 | 2 | 3 | ||||
4 | 5 | 6 | 7 | 8 | 9 | 10 |
11 | 12 | 13 | 14 | 15 | 16 | 17 |
18 | 19 | 20 | 21 | 22 | 23 | 24 |
25 | 26 | 27 | 28 | 29 | 30 | 31 |
- NoSQL
- Redis
- SQL
- 깃허브
- MongoDB
- 분할정복
- 이벤트루프
- in-memory
- 게시판
- 정보처리기사
- VMware
- 다이나믹프로그래밍
- Spring Boot
- 캐시
- 실행 컨텍스트
- 정처기
- 영속성 컨텍스트
- document database
- 호이스팅
- JPA
- spring security
- github
- 레디스
- 스프링 시큐리티
- 스프링부트
- 자바의 정석
- sqld
- 가상 면접 사례로 배우는 대규모 시스템 설계 기초
- 동적계획법
- 스프링 부트
- Today
- Total
FreeHand
자바로 구현한 디자인 패턴 본문
시작하며
예전에 공부하다가 어려워서 그만뒀던 이펙티브 자바를 다시 펼쳐 보려고 한다. 물론 지금 바로 다시 시작하는 건 예전과 다를 게 없다. 당시 어렵다고 느꼈던 이유는 디자인 패턴과 OOP에 대한 이해도가 낮았기 때문이라고 생각한다.
그래서 정보처리기사 실기도 준비하고 프로젝트 리팩토링도 할겸 겸사겸사 디자인 패턴을 먼저 가볍게 정리해 볼 생각이다.
디자인 패턴의 종류
디자인 패턴은 크게 세 가지로 분류할 수 있다.
- 생성패턴: 객체의 생성에 관여하는 패턴
- 구조패턴: 클래스나 객체의 합성에 관한 패턴
- 행위패턴: 클래스나 객체의 상호작용과 책임을 분산하는 패턴
생성 | 구조 | 행위 |
- Factory Method - Singleton - Builder - Prototype - Abstraction Factory |
- Adapter - Facade - Bridge - Proxy - Decorator - Composite - Flyweight |
- Strategy - Observer - Template Method - Visitor - Command - Iterator ... |
위 패턴들 중에서 몇 가지 패턴들만 정리했다.
생성 패턴
Singleton
싱글톤 패턴은 하나의 클래스가 하나의 인스턴스만 생성하는 것을 보장하는 패턴이다.
단 하나의 인스턴스만 생성하므로 메모리를 절약하고 인스턴스 생성 비용을 줄일 수 있다. 또한 객체의 일관된 상태를 유지할 수 있다는 장점이 있다.
그러나 전역에서 사용되기 때문에 결합도가 증가하고 그로인해 독립적으로 실행되어야 하는 단위 테스트에 어려움이 있을 수 있다는 단점이 있다.
public class DBConnection {
private static Connection conn;
public static Connection getConnection(String driver, String user, String password) throws Exception {
if (conn == null) {
conn = DriverManager.getConnection(driver, user, password);
}
return conn;
}
}
jdbc Connection을 싱글톤으로 생성하는 코드이다. conn이 null일 경우에만 즉, 처음에만 DriverManager로 생성하게 된다.
이후에는 이미 생성된 Connection 인스턴스를 반환하게 된다. 이처럼 객체 생성이 필요할 때 생성할 수도 있고, 미리 객체를 생성해 둘 수도 있다.
Factory Method
객체를 사용하는 코드에서 객체 생성 부분을 분리하여 추상화한 패턴이다.
이 패턴 역시 많이 사용되는 패턴이고 JPA의 EntityManagerFactory 그리고 slf4j의 LoggerFactory 등이 Factory Method 패턴의 예이다.
class DriverManager {
public static Connection getConnection(String driver, String user, String password) {
if (driver.contains("oracle")) {
return new OracleConnection(driver, user, password);
} else if (driver.contains("mysql")) {
return new MysqlConnection(driver, user, password);
}
}
}
public static void main (String[] args) {
String driver = "jdbc:mysql...";
String user = "root";
String password = "1234";
// Factory를 통해 객체 생성
Connection conn = DriverManager.getConnection(driver, user, password);
}
이렇게 객체 생성 부분을 분리하여 더 유연하고 유지 보수에 용이하다는 장점이 있다.
구조 패턴
Proxy
프록시 패턴은 대상 객체에 접근하기 전에 흐름을 가로채 대상 객체 앞단의 인터페이스 역할을 하는 패턴이다.
이를 통해 앞단에서 보안, 캐싱, 로깅 등을 할 수 있다.
프록시는 프록시 서버 개념에서도 활용된다. 대표적인 웹 서버 nginx는 프록시 서버로 많이 사용된다.
실제 서버의 앞단에서 로드밸런싱, gzip압축, 캐싱, https구축, cors해결, DDOS방어, CDN 등 여러 역할을 수행한다.
public class User {
private Long id;
private String name;
private int age;
public User(Long id, String name, int age) {
this.id = id;
this.name = name;
this.age = age;
}
public User getId() {
return this.id;
}
public String getName() {
return this.name;
}
}
class UserService {
private List<User> users = new ArrayList<>();
public void addUser(User user) {
users.add(user);
}
public User findById(Long id) {
for (User user : users) {
if (user.getId() == id) {
return user;
}
}
return null;
}
}
public class Main {
public static void main(String[] args) {
UserService userService = new UserService();
User user = new User(1, "Jin", 26);
userService.addUser(user);
User foundUser = userService.findById(1);
System.out.println(foundUser.getName());
}
}
위 코드는 프록시를 사용하지 않은 코드이다. main(클라이언트)에서 직접 UserService를 사용한다.
public interface UserService {
void addUser(User user);
User findById(Long id);
}
public class UserServiceReal implements UserService {
private List<User> users = new ArrayList<>();
@Override
public void addUser(User user) {
users.add(user);
}
@Override
public User findById(Long id) {
for (User user : users) {
if (user.getId() == id) {
return user;
}
}
return null;
}
}
public class UserServiceProxy implements UserService {
private final Logger log = LoggerFactory.getLogger(this.getClass());
private UserServiceReal userService = new UserServiceReal();
@Override
public void addUser(User user) {
log.info("id: {} 추가", user.getId()); // 로깅
userService.addUser(user);
}
@Override
public User findById(Long id) {
for (User user : users) {
if (user.getId() == id) {
log.info("id: {} 조회", id); // 로깅
return user;
}
}
return null;
}
}
public class ProxyMain {
public static void main(String[] args) {
private UserServiceProxy = userService = new UserServiceProxy();
User user = new User(1, "Jin", 26);
userService.addUser(user);
User foundUser = userService.findById(1);
System.out.println(foundUser.getName());
}
}
ProxyMain은 위에서 본 Main과 같다. 한 가지 다른 점은 UserService를 사용하지 않고 UserServiceProxy를 사용한다는 점이다. 이렇게 ProxyMain(클라이언트)에서는 UserService를 사용하는지 UserServiceProxy를 사용하는지 알 수 없다.
그러나 ProxyMain에서는 UserServiceProxy에서 실행하는 로깅이 남게 된다. UserServiceProxy가 ProxyMain과 UserServiceReal 사이에서 프록시 역할을 하며 여기서는 로깅을 하고 있는 것이다.
이 작업은 지금처럼 로깅이 될 수도 있고 다른 무언가가 될 수도 있다.
이러한 프록시 패턴으로 스프링은 AOP를 구현하고 있다.
행위 패턴
Strategy
전략 패턴은 객체의 행위를 수정할 때 직접 수정하지 않고 '전략'이라는 캡슐화된 객체를 통해 수정하는 패턴이다.
@Getter
@AllArgsContructor
public class Item {
private String name;
private int price;
}
public class ShoppingCart {
List<Item> items;
public ShoppingCart() {
this.items = new ArrayList<>();
}
public void addItem(Item item) {
this.items.add(item);
}
public void removeItem(Item item) {
this.items.remove(item);
}
public int calculateTotal() {
int sum = 0;
for (Item item : items) {
sum += item.getPrice();
}
return sum;
}
public void pay(PaymentStrategy paymentMethod) {
int amount = calculateTotal();
paymentMethod.pay(amount);
}
}
public interface PaymentStrategy {
public void pay(int amount);
}
@AllArgsConstructor
public class KakaoPay implements PaymentStrategy {
private String name;
private String cardNumber;
private LocalDateTime ExpirationDate;
@Override
public void pay(int amount) {
System.out.println(amount + " won was paid by KakaoPay");
}
}
@AllArgsConstructor
public class NaverPay implements PaymentStrategy {
private String name;
private String cardNumber;
private LocalDateTime ExpirationDate;
@Override
public void pay(int amount) {
System.out.println(amount + " won was paid by NaverPay");
}
}
public class Main {
public static void main(String[] args) {
ShoppingCart cart = new ShoppingCart();
Item a = new Item("A", 100);
Item b = new Item("B", 300);
cart.addItem(a);
cart.addItem(b);
cart.pay(new KakaoPay(...)); // 카카오페이 결제
cart.pay(new NaverPay(...)); // 네이버페이 결제
}
}
// 400 won was paid by KakaoPay
// 400 won was paid by NaverPay
위 코드는 카카오페이와 네이버페이로 결제하는 코드이다.
어떤 결제 방식을 택하는지에 따라 cart.pay(PaymentStrategy paymentMethod)의 파라미터인 결제 전략만 변경하면 된다.
Observer
마치며
디자인 패턴은 프로그래밍을 하면서 자주 마주한 문제점들을 해결하기 위해 만들어진 패턴이다. 여러 패턴이 있지만 결국 핵심은 변경이 필요할 때 수정을 최소화하는 것 그리고 반복된 코드를 줄이는 것. 즉 유연하고 확장성을 고려한 설계인 것 같다. 이 패턴들을 내 프로젝트에는 어떻게 적용할지 생각하고 리팩토링 해보면 탄탄한 기본기를 만드는데 도움이 될 것이라 생각한다.
추가 정리 중...