Search
Duplicate

SOLID

수업
DesignPattern
주제
5 more properties
전략패턴
옵저버 패턴
데코레이터 패턴
팩토리 패턴
싱글톤 패턴
커맨드 패턴
어댑터 패턴
파사드 패턴
템플릿 메소드 패턴
반복자 패턴
컴포지트 패턴
상태 패턴
프록시 패턴
복합 패턴 - MVC

SOLID

단일책임

public class UserController { //Store used by controller private Store store = new Store(); //Create a new user public String createUser(String userJson) throws IOException { ObjectMapper mapper = new ObjectMapper(); User user = mapper.readValue(userJson, User.class); if(!isValidUser(user)) { return "ERROR"; } store.store(user); return "SUCCESS"; } //Validates the user object private boolean isValidUser(User user) { if(!isPresent(user.getName())) { return false; } user.setName(user.getName().trim()); if(!isValidAlphaNumeric(user.getName())) { return false; } if(user.getEmail() == null || user.getEmail().trim().length() == 0) { return false; } user.setEmail(user.getEmail().trim()); if(!isValidEmail(user.getEmail())) { return false; } return true; } //Simply checks if value is null or empty.. private boolean isPresent(String value) { return value != null && value.trim().length() > 0; } //check string for special characters private boolean isValidAlphaNumeric(String value) { Pattern pattern = Pattern.compile("[^A-Za-z0-9]"); Matcher matcher = pattern.matcher(value); return !matcher.find(); } //check string for valid email address - this is not for prod. //Just for demo. This fails for lots of valid emails. private boolean isValidEmail(String value) { Pattern pattern = Pattern.compile("^[_A-Za-z0-9-\\+]+(\\.[_A-Za-z0-9-]+)*@[A-Za-z0-9-]+(\\.[A-Za-z0-9]+)*(\\.[A-Za-z]{2,})$"); Matcher matcher = pattern.matcher(value); return matcher.find(); } }
Java
복사
컨트롤러 클래스는 기본적으로 요청을 수신하고 반환한다.
컨트롤러 자체에는 비즈니스 로직이 없어야 하지만 현재에는 유저를 생성하고, 검증하고, 저장하는 많은 비즈니스 로직을 포함하고 있습니다.
public class UserController { //Store used by controller private UserPersistenceService persistenceService = new UserPersistenceService(); //Create a new user public String createUser(String userJson) throws IOException { ObjectMapper mapper = new ObjectMapper(); User user = mapper.readValue(userJson, User.class); UserValidator validator = new UserValidator(); boolean valid = validator.validateUser(user); if(!valid) { return "ERROR"; } persistenceService.saveUser(user); return "SUCCESS"; } }
Java
복사
public class UserValidator { public boolean validateUser(User user) { return isValidUser(user); } private boolean isValidUser(User user) { if(!isPresent(user.getName())) { return false; } user.setName(user.getName().trim()); if(!isValidAlphaNumeric(user.getName())) { return false; } if(user.getEmail() == null || user.getEmail().trim().length() == 0) { return false; } user.setEmail(user.getEmail().trim()); if(!isValidEmail(user.getEmail())) { return false; } return true; } //Simply checks if value is null or empty.. private boolean isPresent(String value) { return value != null && value.trim().length() > 0; } //check string for special characters private boolean isValidAlphaNumeric(String value) { Pattern pattern = Pattern.compile("[^A-Za-z0-9]"); Matcher matcher = pattern.matcher(value); return !matcher.find(); } //check string for valid email address - this is not for prod. //Just for demo. This fails for lots of valid emails. private boolean isValidEmail(String value) { Pattern pattern = Pattern.compile("^[_A-Za-z0-9-\\+]+(\\.[_A-Za-z0-9-]+)*@[A-Za-z0-9-]+(\\.[A-Za-z0-9]+)*(\\.[A-Za-z]{2,})$"); Matcher matcher = pattern.matcher(value); return matcher.find(); } }
Java
복사
public class UserPersistenceService { private Store store = new Store(); public void saveUser(User user) { store.store(user); } }
Java
복사

OCP(개방 폐쇄)

확장을 위해 열려 있어야 하지만 수정에는 닫혀야 있어야 한다.
확장: 행위가 늘어난다.
수정에 닫혀있다: 기존 코드를 바꿔서는 안된다.
@Getter @Setter public class PhoneSubscriber { private Long subscriberId; private String address; private Long phoneNumber; private int baseRate; public double calculateBill() { List<CallHistory.Call> sessions = CallHistory.getCurrentCalls(subscriberId); long totalDuration = sessions.stream().mapToLong(CallHistory.Call::getDuration).sum(); // 계산 결과 return (double) (totalDuration * baseRate) /100; } }
Java
복사
public class ISPSubscriber { private Long subscriberId; private String address; private Long phoneNumber; private int baseRate; private long freeUsage; // 추가 속성 public ISPSubscriber() { } public double calculateBill() { List<InternetSessionHistory.InternetSession> sessions = InternetSessionHistory.getCurrentSessions(subscriberId); long totalData = sessions.stream().mapToLong(InternetSessionHistory.InternetSession::getDataUsed).sum(); long chargeableData = totalData - freeUsage; // 추가 연산 return chargeableData*baseRate/100; } }
Java
복사
public class CallHistory { @Getter public static class Call { private LocalDateTime begin; private long duration; private Long subscriberId; public Call(Long subscriberId, LocalDateTime begin, long duration) { this.begin = begin; this.duration = duration; this.subscriberId = subscriberId; } } private static final Map<Long, List<Call>> CALLS = new HashMap<>(); public synchronized static List<Call> getCurrentCalls(Long subscriberId) { if(!CALLS.containsKey(subscriberId)) { return Collections.emptyList(); } return CALLS.get(subscriberId); } public synchronized static void addSession(Long subscriberId, LocalDateTime begin, long duration) { List<Call> calls; if(!CALLS.containsKey(subscriberId)) { calls = new LinkedList<>(); CALLS.put(subscriberId, calls); } else { calls = CALLS.get(subscriberId); } calls.add(new Call(subscriberId, begin, duration)); } }
Java
복사
public class CallHistory { @Getter public static class Call { private LocalDateTime begin; private long duration; private Long subscriberId; public Call(Long subscriberId, LocalDateTime begin, long duration) { this.begin = begin; this.duration = duration; this.subscriberId = subscriberId; } } private static final Map<Long, List<Call>> CALLS = new HashMap<>(); public synchronized static List<Call> getCurrentCalls(Long subscriberId) { if(!CALLS.containsKey(subscriberId)) { return Collections.emptyList(); } return CALLS.get(subscriberId); } public synchronized static void addSession(Long subscriberId, LocalDateTime begin, long duration) { List<Call> calls; if(!CALLS.containsKey(subscriberId)) { calls = new LinkedList<>(); CALLS.put(subscriberId, calls); } else { calls = CALLS.get(subscriberId); } calls.add(new Call(subscriberId, begin, duration)); } }
Java
복사
public class InternetSessionHistory { @Getter public static class InternetSession { private LocalDateTime begin; private Long subscriberId; private Long dataUsed; public InternetSession(Long subscriberId, LocalDateTime begin, long dataUsed) { this.begin = begin; this.dataUsed = dataUsed; this.subscriberId = subscriberId; } } private static final Map<Long, List<InternetSession>> SESSIONS = new HashMap<>(); public synchronized static List<InternetSession> getCurrentSessions(Long subscriberId) { if(!SESSIONS.containsKey(subscriberId)) { return Collections.emptyList(); } return SESSIONS.get(subscriberId); } public synchronized static void addSession(Long subscriberId, LocalDateTime begin, long dataUsed) { List<InternetSession> sessions; if(!SESSIONS.containsKey(subscriberId)) { sessions = new LinkedList<>(); SESSIONS.put(subscriberId, sessions); } else { sessions = SESSIONS.get(subscriberId); } sessions.add(new InternetSession(subscriberId, begin, dataUsed)); } }
Java
복사
@Getter @Setter public abstract class Subscriber { protected Long subscriberId; protected String address; protected Long phoneNumber; protected int baseRate; public abstract double calculateBill(); // 확장 가능 }
Java
복사
@Getter @Setter public class PhoneSubscriber extends Subscriber{ // 언제든 수정이 가능하다. @Override public double calculateBill() { List<CallHistory.Call> sessions = CallHistory.getCurrentCalls(subscriberId); long totalDuration = sessions.stream().mapToLong(CallHistory.Call::getDuration).sum(); // 계산 결과 return (double) (totalDuration * baseRate) /100; } }
Java
복사
public class ISPSubscriber extends Subscriber { private long freeUsage; // 추가 속성 @Override public double calculateBill() { List<InternetSessionHistory.InternetSession> sessions = InternetSessionHistory.getCurrentSessions(subscriberId); long totalData = sessions.stream().mapToLong(InternetSessionHistory.InternetSession::getDataUsed).sum(); long chargeableData = totalData - freeUsage; // 추가 연산 return (double) (chargeableData * baseRate) /100; } }
Java
복사

리스코프

최상위 클래스 객체가 예상되는 곳에서 서브 클래스와 클래스 객체를 사용할 수 있어야 한다.
public class Main { public static void main(String[] args) { Rectangle rectangle = new Rectangle(10, 20); System.out.println(rectangle.computeArea()); Square square = new Square(10); System.out.println(square.computeArea()); useRectangle(rectangle); useRectangle(square); } private static void useRectangle(Rectangle rectangle) { rectangle.setHeight(20); rectangle.setWidth(30); assert rectangle.getHeight() == 20 : "Height Not equal to 20"; assert rectangle.getWidth() == 30 : "Width Not equal to 30"; } }
Java
복사
예외발생 ⇒ Square객체는 set을 할경우 높이/너비가 둘다 변한다.
@Getter @Setter public class Rectangle { private int width; private int height; public Rectangle(int width, int height) { this.width = width; this.height = height; } public int computeArea() { return width * height; } }
Java
복사
사각형
public class Square extends Rectangle { public Square(int side) { super(side, side); } @Override public void setWidth(int width) { setSide(width); } @Override public void setHeight(int height) { setSide(height); } public void setSide(int side) { super.setWidth(side); super.setHeight(side); } }
Java
복사
정사각형
public class Main { public static void main(String[] args) { Rectangle rectangle = new Rectangle(10, 20); System.out.println(rectangle.computeArea()); Square square = new Square(10); System.out.println(square.computeArea()); useRectangle(rectangle); } private static void useRectangle(Rectangle rectangle) { rectangle.setHeight(20); rectangle.setWidth(30); assert rectangle.getHeight() == 20 : "Height Not equal to 20"; assert rectangle.getWidth() == 30 : "Width Not equal to 30"; } }
Java
복사
public interface Shape { public int computeArea(); }
Java
복사
@Getter @Setter public class Square implements Shape { private int side; public Square(int side) { this.side = side; } @Override public int computeArea() { return side * side; } }
Java
복사

인터페이스 분리

Interface Pollution
거대한 인터페이스
관련없는 메소드 ⇒ 구현만하고 아무것도 안하는 메서드를 만들게됨
public interface PersistenceService<T extends Entity> { public void save(T entity); public void delete(T entity); public T findById(Long id); public List<T> findByName(String name); }
Java
복사
public class UserPersistenceService implements PersistenceService<User>{ private static final Map<Long, User> USERS = new HashMap<>(); @Override public void save(User entity) { synchronized (USERS) { USERS.put(entity.getId(), entity); } } @Override public void delete(User entity) { synchronized (USERS) { USERS.remove(entity.getId()); } } @Override public User findById(Long id) { synchronized (USERS) { return USERS.get(id); } } @Override public List<User> findByName(String name) { synchronized (USERS) { return USERS.values().stream().filter(u->u.getName().equalsIgnoreCase(name)).collect(Collectors.toList()); } } }
Java
복사
public interface PersistenceService<T extends Entity> { public void save(T entity); public void delete(T entity); public T findById(Long id); }
Java
복사
public class UserPersistenceService implements PersistenceService<User>{ private static final Map<Long, User> USERS = new HashMap<>(); @Override public void save(User entity) { synchronized (USERS) { USERS.put(entity.getId(), entity); } } @Override public void delete(User entity) { synchronized (USERS) { USERS.remove(entity.getId()); } } @Override public User findById(Long id) { synchronized (USERS) { return USERS.get(id); } } }
Java
복사
public class OrderPersistenceService implements PersistenceService<Order> { private static final Map<Long, Order> ORDERS = new HashMap<>(); @Override public void save(Order entity) { synchronized (ORDERS) { ORDERS.put(entity.getId(), entity); } } @Override public void delete(Order entity) { synchronized (ORDERS) { ORDERS.remove(entity.getId()); } } @Override public Order findById(Long id) { synchronized (ORDERS) { return ORDERS.get(id); } } }
Java
복사

의존성 역전 원칙

1.
높은 수준의 모듈은 저 수준의 모듈에 의존하면 안된다. (특정 클래스와 긴밀하게 연결되어서는 안된다. 추상화에 의존해야 한다.S)
2.
추상화된 것이 세부사항애 의존해서는 안된다.
의존성

디자인 패턴

빌더

불변 클래스 - 생성이후 변경할 수 없다.
불변 클래스는 생성자에게 모든 변수를 초기화 해주어야 한다.
하지만 인자에 따른 생성자를 모두 만들어주기에는 코드가 너무 길어진다.
public class Client { public static void main(String[] args) { // 유저 생성 & 빌더 생성 User user = createUser(); UserDTOBuilder builder = new UserWebDTOBuilder(); // DTO를 빌더로 생성 UserDTO dto = directBuild(builder, user); System.out.println("dto = " + dto); } // 빌더 사용 private static UserDTO directBuild(UserDTOBuilder builder, User user) { return builder.withFirstName(user.getFirstName()) .withLastName(user.getLastName()) .withBirthday(user.getBirthday()) .withAddress(user.getAddress()) .build(); } /** * Returns a sample user. */ public static User createUser() { User user = new User(); user.setBirthday(LocalDate.of(1960, 5, 6)); user.setFirstName("Ron"); user.setLastName("Swanson"); Address address = new Address(); address.setHouseNumber("100"); address.setStreet("State Street"); address.setCity("Pawnee"); address.setState("Indiana"); address.setZipcode("47998"); user.setAddress(address); return user; } }
Java
복사
@Getter @Setter public class User { private String firstName; private String lastName; private LocalDate birthday; private Address address; }
Java
복사
@Getter @Setter public class Address { private String houseNumber; private String street; private String city; private String zipcode; private String state; }
Java
복사
@Getter public class UserWebDTO implements UserDTO { private String name; private String address; private String age; public UserWebDTO(String name, String address, String age) { this.name = name; this.address = address; this.age = age; } @Override public String toString() { return "name=" + name + "\nage=" + age + "\naddress=" + address ; } }
Java
복사
setter가 없는 불변객체
public class UserWebDTOBuilder implements UserDTOBuilder { private String firstName; private String lastName; private String age; private String address; private UserWebDTO dto; @Override public UserDTOBuilder withFirstName(String fname) { firstName = fname; return this; } @Override public UserDTOBuilder withLastName(String lname) { lastName = lname; return this; } @Override public UserDTOBuilder withBirthday(LocalDate date) { Period ageInYears = Period.between(date, LocalDate.now()); age = Integer.toString(ageInYears.getYears()); return this; } @Override public UserDTOBuilder withAddress(Address address) { this.address = address.getHouseNumber() + ", " + address.getStreet() + "\n " + address.getCity() + "\n " + address.getState() + " " + address.getZipcode(); return this; } @Override public UserDTO build() { dto = new UserWebDTO(firstName + " " + lastName, address, age); return dto; } @Override public UserDTO getUserDTO() { return null; } }
Java
복사
public class Client { public static void main(String[] args) { User user = createUser(); // Client has to provide director with concrete builder UserDTO dto = directBuild(UserDTO.getBuilder(), user); System.out.println(dto); } /** * This method serves the role of director in builder pattern. */ private static UserDTO directBuild(UserDTOBuilder builder, User user) { return builder .withFirstName(user.getFirstName()) .withLastName(user.getLastName()) .withBirthday(user.getBirthday()) .withAddress(user.getAddress()) .build(); } /** * Returns a sample user. */ public static User createUser() { User user = new User(); user.setBirthday(LocalDate.of(1960, 5, 6)); user.setFirstName("Ron"); user.setLastName("Swanson"); Address address = new Address(); address.setHouseNumber("100"); address.setStreet("State Street"); address.setCity("Pawnee"); address.setState("Indiana"); address.setZipcode("47998"); user.setAddress(address); return user; } }
Java
복사
@Getter @Setter public class UserDTO { private String name; private String address; private String age; @Override public String toString() { return "name=" + name + "\nage=" + age + "\naddress=" + address ; } //Get builder instance public static UserDTOBuilder getBuilder() { return new UserDTOBuilder(); } //Builder public static class UserDTOBuilder { private String firstName; private String lastName; private String age; private String address; private UserDTO userDTO; public UserDTOBuilder withFirstName(String fname) { this.firstName = fname; return this; } public UserDTOBuilder withLastName(String lname) { this.lastName = lname; return this; } public UserDTOBuilder withBirthday(LocalDate date) { age = Integer.toString(Period.between(date, LocalDate.now()).getYears()); return this; } public UserDTOBuilder withAddress(Address address) { this.address = address.getHouseNumber() + " " +address.getStreet() + "\n"+address.getCity()+", "+address.getState()+" "+address.getZipcode(); return this; } public UserDTO build() { this.userDTO = new UserDTO(); userDTO.setName(firstName+" " + lastName); userDTO.setAddress(address); userDTO.setAge(age); return this.userDTO; } } }
Java
복사
빌더 패턴의 주의점
빌더 패턴은 손쉽게 불변 객체를 만들 수 있으며, 불필요한 생성자를 방지할 수 있습니다.
불변성이 필요하지 않아도 객체 생성에 직관적인 코드가 만들어집니다.
객체 생성과 초기화는 같이 이루어져야 한다,
추상 빌더는 크게 의미가 없으나, 여러가지 클래스에대한 공통 빌더를 만들 수는 있다. (클레스 계층구조 잘보기)
생성자의 인수가 너무 많을때 고려한다.
실제 예시
java.lang.StringBuilder 클래스와 java.nio 패키지의 ByteBuffer, CharBuffer 클래스들이 빌더 패턴의 예시로 자주 언급됩니다.
제 의견으로는, 이 클래스들이 빌더 패턴의 예로 제시될 수 있지만, 완벽하게 맞지는 않습니다. 이 클래스들은 여러 단계로 객체를 만들 수 있게 해주지만, 최종 객체의 일부분만을 한 번에 만듭니다. 이런 점에서 빌더 패턴을 어느 정도 구현했다고 볼 수 있습니다.
StringBuilder는 빌더 패턴의 의도와 목적을 충족시킵니다. 하지만 StringBuilder의 구조를 자세히 보면, 문제가 있습니다. 빌더 패턴의 정의에 따르면, 빌더는 동일한 단계를 통해 다양한 형태의 객체를 만들 수 있어야 합니다.
빌더와 프로토타입 차이

빌더 (Builder)

// 피자 클래스 public class Pizza { private String dough; private String sauce; private String topping; public static class Builder { private String dough; private String sauce; private String topping; public Builder dough(String dough) { this.dough = dough; return this; } public Builder sauce(String sauce) { this.sauce = sauce; return this; } public Builder topping(String topping) { this.topping = topping; return this; } public Pizza build() { return new Pizza(this); } } private Pizza(Builder builder) { this.dough = builder.dough; this.sauce = builder.sauce; this.topping = builder.topping; } @Override public String toString() { return "Pizza with " + dough + ", " + sauce + ", " + topping; } public static void main(String[] args) { Pizza pizza = new Pizza.Builder() .dough("cross") .sauce("tomato") .topping("mozzarella") .build(); System.out.println(pizza); } }
Java
복사
복잡한 생성자가 있을 때, 빌더는 이를 다루는 방법을 제공합니다.
우리는 빌더를 별도의 클래스로 만들어 기존 코드(레거시 코드)와 함께 사용할 수 있습니다.

프로토타입 (Prototype)

프로토타입 패턴은 객체를 복제하여 새로운 객체를 생성합니다. 여기서는 클론 가능한 직원을 생성하는 예제를 보겠습니다.
// 직원 클래스 public class Employee implements Cloneable { private String name; private String position; public Employee(String name, String position) { this.name = name; this.position = position; } public String getName() { return name; } public void setName(String name) { this.name = name; } public String getPosition() { return position; } public void setPosition(String position) { this.position = position; } @Override protected Object clone() throws CloneNotSupportedException { return super.clone(); } @Override public String toString() { return "Employee{name='" + name + "', position='" + position + "'}"; } public static void main(String[] args) { try { Employee original = new Employee("John Doe", "Manager"); Employee clone = (Employee) original.clone(); clone.setName("Jane Doe"); clone.setPosition("Developer"); System.out.println("Original: " + original); System.out.println("Clone: " + clone); } catch (CloneNotSupportedException e) { e.printStackTrace(); } } }
Java
복사
프로토타입은 생성자를 완전히 생략할 수 있게 합니다.
자바에서는 이 패턴이 clone 메서드를 사용하여 작동하며, 기존 코드를 수정해야 하기 때문에 레거시 코드와 함께 사용할 수 없을 수도 있습니다.

Pitfalls (함정)

초보자에게 복잡할 수 있음: 주로 '메서드 체이닝' 사용으로 인해 빌더 메서드가 빌더 객체 자체를 반환하기 때문에 초보자에게 다소 복잡할 수 있습니다.
부분적으로 초기화된 객체의 가능성: 사용자 코드가 withXXX 메서드를 사용하여 일부 속성만 설정하거나 전혀 설정하지 않고 build()를 호출할 수 있습니다. 필요한 속성이 누락된 경우, build 메서드는 적절한 기본값을 제공하거나 예외를 던져야 합니다.

심플 팩토리

Simple Factory란?

정의: 간단한 팩토리 패턴에서는 객체 생성 로직을 별도의 클래스로 이동시키고, 이 클래스의 정적 메서드로 객체를 생성합니다.
디자인 패턴 여부: 일부 사람들은 간단한 팩토리를 디자인 패턴으로 보지 않습니다. 이는 단순히 객체 생성을 캡슐화하는 메서드이기 때문입니다. 복잡한 작업이 필요하지 않습니다.
사용 이유: 객체를 인스턴스화할 때 여러 옵션이 있는 경우, 올바른 클래스를 선택하는 간단한 로직을 사용합니다.

UML 설명

SimpleFactory 클래스: 정적 메서드를 제공하여 하위 클래스의 인스턴스를 얻습니다.
Product 클래스: 이 클래스와 그 하위 클래스들의 객체가 필요합니다.
Client: SimpleFactory를 사용하여 필요한 객체를 생성합니다.

간단한 팩토리 구현

1.
별도의 클래스 생성: SimpleFactory라는 클래스를 만들고, 필요한 객체를 반환하는 메서드를 추가합니다.
2.
정적 메서드 추가: 이 메서드는 객체를 인스턴스화할 클래스를 결정하는 인자를 받습니다.
3.
추가 인자 제공: 객체를 인스턴스화할 때 사용할 추가 인자를 제공할 수 있습니다.

구현 고려사항 (Implementation Considerations)

단순 팩토리는 기존 클래스의 메서드로도 구현 가능: 별도의 클래스를 추가하면 다른 코드에서도 간단한 팩토리를 쉽게 사용할 수 있습니다.
상태 추적이 필요하지 않음: 단순 팩토리는 상태를 추적할 필요가 없기 때문에 정적 메서드로 유지하는 것이 좋습니다.

디자인 고려사항 (Design Considerations)

다른 디자인 패턴과 함께 사용: 간단한 팩토리는 객체를 생성하기 위해 빌더 패턴과 같은 다른 디자인 패턴을 사용할 수 있습니다.
특정 하위 클래스에서 팩토리를 특수화해야 하는 경우: 이 경우 팩토리 메서드 디자인 패턴이 필요합니다.

단순 팩토리 예제 (Example of a Simple Factory)

자바의 java.text.NumberFormat 클래스는 getInstance 메서드를 가지고 있으며, 이는 단순 팩토리의 예입니다.
java코드 복사 private static NumberFormat getInstance(LocaleProviderAdapter adapter, Locale locale, int choice) { NumberFormatProvider provider = adapter.getNumberFormatProvider(); NumberFormat numberFormat = null; switch (choice) { case NUMBERSTYLE: numberFormat = provider.getNumberInstance(locale); break; case PERCENTSTYLE: numberFormat = provider.getPercentInstance(locale); break; case CURRENCYSTYLE: numberFormat = provider.getCurrencyInstance(locale); break; case INTEGERSTYLE: numberFormat = provider.getIntegerInstance(locale); break; } return numberFormat; }
Java
복사

비교 및 대조: 팩토리 메서드 패턴 (Compare & Contrast with Factory Method Pattern)

단순 팩토리(Simple Factory):
인스턴스화 로직을 클라이언트 코드에서 분리하여 보통 정적 메서드로 구현.
생성할 수 있는 모든 클래스에 대해 알고 있음.
팩토리 메서드(Factory Method):
객체 생성을 하위 클래스에 위임할 때 유용.
생성할 모든 제품 하위 클래스에 대해 미리 알 필요가 없음.

단점 (Pitfalls)

복잡해질 수 있는 인스턴스화 기준: 시간이 지나면서 인스턴스화할 객체를 결정하는 기준이 복잡해질 수 있음. 이런 상황이 발생하면 팩토리 메서드 디자인 패턴을 사용하는 것이 좋습니다.

팩토리 메소드

UML 설명

역할(Role)

Product: 팩토리 메서드가 생성하는 제품의 인터페이스 또는 기본 클래스입니다.
Concrete Product: Product 인터페이스나 클래스를 구현합니다.
Creator: 추상 팩토리 메서드를 선언합니다. 이 클래스는 팩토리 메서드를 사용하여 제품을 생성합니다.
Concrete Creator: 팩토리 메서드를 구현하고, 구체적인 제품 인스턴스를 반환합니다.

팩토리 메서드 구현 (Implement a Factory Method)

1.
Creator 클래스 생성: 구체적인 객체를 제공할 수 있는 경우, Creator 클래스는 구체적일 수 있으며, 기본 객체를 제공할 수 있는 경우 추상적일 수도 있습니다.
2.
구현 클래스에서 메서드 재정의: 구현 클래스는 메서드를 재정의하여 객체를 반환합니다.

구현 고려사항 (Implementation Considerations)

구체 클래스인 Creator: Creator는 구체 클래스가 될 수 있으며, 기본 객체를 제공하는 팩토리 메서드의 기본 구현을 제공할 수 있습니다. 이러한 경우 기본 Creator에서 "기본" 객체를 생성합니다.
추가 인자 사용: 단순 팩토리 방식으로 추가 인자를 받아서 서로 다른 객체 유형을 선택할 수 있습니다. 하위 클래스는 팩토리 메서드를 재정의하여 특정 기준에 따라 서로 다른 객체를 선택적으로 생성할 수 있습니다.

디자인 고려사항 (Design Considerations)

Creator 계층 구조: 팩토리 메서드 패턴에서 Creator 계층 구조는 제품 계층 구조를 반영합니다. 일반적으로 객체 유형별로 구체적인 Creator를 가지게 됩니다.
템플릿 메서드 패턴: 템플릿 메서드 디자인 패턴은 종종 팩토리 메서드를 사용합니다.
추상 팩토리 패턴: 또 다른 생성 패턴인 추상 팩토리 패턴도 팩토리 메서드 패턴을 활용합니다.

팩토리 메서드 예제 (Examples of a Factory Method)

java.util.Collection 또는 java.util.AbstractCollection 클래스는 팩토리 메서드의 예로, 추상 메서드 iterator()를 가지고 있습니다.
java코드 복사 public abstract class AbstractCollection<E> implements Collection<E> { // 유일한 생성자 protected AbstractCollection() {} // 쿼리 작업 public abstract Iterator<E> iterator(); }
Java
복사

단점 (Pitfalls)

구현이 더 복잡함: 더 많은 클래스가 필요하고 단위 테스트가 필요합니다.
초기부터 사용해야 함: 팩토리 메서드 디자인 패턴은 처음부터 사용해야 하며, 기존 코드를 리팩토링하기 쉽지 않습니다.
하위 클래스를 강제 생성: 적절한 인스턴스를 생성하기 위해 하위 클래스를 생성해야 합니다.

요약 (In-A-Hurry Summary)

팩토리 메서드 패턴은 객체 인스턴스화를 하위 클래스에 위임하려는 경우에 사용합니다. 제품 상속 계층 구조가 있고, 이를 확장할 가능성이 있을 때 유용합니다.

예제 코드

예제 UML 설명

Message: 메시지의 추상 클래스입니다.
TextMessage, JSONMessage: Message 클래스를 구현한 구체적인 클래스들입니다.
MessageCreator: 팩토리 메서드를 선언한 추상 클래스입니다.
JSONMessageCreator, TextMessageCreator: MessageCreator를 구현한 클래스들로, 구체적인 메시지 객체를 생성합니다.

Product 인터페이스 및 구현 클래스들

// Message 추상 클래스 public abstract class Message { public abstract void send(); } // TextMessage 클래스 public class TextMessage extends Message { @Override public void send() { System.out.println("Sending Text Message"); } } // JSONMessage 클래스 public class JSONMessage extends Message { @Override public void send() { System.out.println("Sending JSON Message"); } }
Java
복사

Creator 클래스 및 구현 클래스들

// MessageCreator 추상 클래스 public abstract class MessageCreator { // 팩토리 메서드 protected abstract Message createMessage(); // 메시지를 보내는 메서드 public void sendMessage() { Message msg = createMessage(); msg.send(); } } // TextMessageCreator 클래스 public class TextMessageCreator extends MessageCreator { @Override protected Message createMessage() { return new TextMessage(); } } // JSONMessageCreator 클래스 public class JSONMessageCreator extends MessageCreator { @Override protected Message createMessage() { return new JSONMessage(); } }
Java
복사

클라이언트 코드

public class Client { public static void main(String[] args) { MessageCreator creator; // 텍스트 메시지를 생성하는 팩토리 메서드를 사용 creator = new TextMessageCreator(); creator.sendMessage(); // Output: Sending Text Message // JSON 메시지를 생성하는 팩토리 메서드를 사용 creator = new JSONMessageCreator(); creator.sendMessage(); // Output: Sending JSON Message } }
Java
복사
이 예제는 팩토리 메서드 패턴을 사용하여 다양한 메시지 유형을 생성하고 사용하는 방법을 보여줍니다. MessageCreator 클래스는 팩토리 메서드를 정의하고, TextMessageCreatorJSONMessageCreator 클래스는 이 메서드를 구현하여 구체적인 메시지 객체를 생성합니다.

추상 팩토리

UML 설명

역할(Role)

Abstract Factory: 제품을 생성하는 연산을 정의하는 인터페이스입니다.
Concrete Factory: 특정 제품군을 생성하는 팩토리를 구현합니다.
Abstract Product: 특정 제품 유형을 위한 인터페이스입니다.
Concrete Product: 제품 인터페이스나 클래스를 구현합니다.
Client: 추상 팩토리와 추상 제품을 사용합니다.

추상 팩토리 구현 (Implement Abstract Factory)

1.
제품 "세트" 연구: 제품군을 식별합니다.
2.
추상 팩토리 생성: 추상 클래스나 인터페이스로 정의합니다.
3.
구체 팩토리 구현: 각 제품군에 대한 구체 팩토리를 제공합니다.
4.
팩토리 메서드 패턴 사용: 추상 팩토리는 여러 팩토리 메서드를 가진 객체로 생각할 수 있습니다.

예제 코드

Abstract Product 및 Concrete Product

java코드 복사 // AbstractProductA 인터페이스 public interface AbstractProductA { void performActionA(); } // ConcreteProductA1 클래스 public class ConcreteProductA1 implements AbstractProductA { @Override public void performActionA() { System.out.println("Action A1 performed."); } } // ConcreteProductA2 클래스 public class ConcreteProductA2 implements AbstractProductA { @Override public void performActionA() { System.out.println("Action A2 performed."); } } // AbstractProductB 인터페이스 public interface AbstractProductB { void performActionB(); } // ConcreteProductB1 클래스 public class ConcreteProductB1 implements AbstractProductB { @Override public void performActionB() { System.out.println("Action B1 performed."); } } // ConcreteProductB2 클래스 public class ConcreteProductB2 implements AbstractProductB { @Override public void performActionB() { System.out.println("Action B2 performed."); } }
Java
복사

Abstract Factory 및 Concrete Factory

java코드 복사 // AbstractFactory 인터페이스 public interface AbstractFactory { AbstractProductA createProductA(); AbstractProductB createProductB(); } // ConcreteFactory1 클래스 public class ConcreteFactory1 implements AbstractFactory { @Override public AbstractProductA createProductA() { return new ConcreteProductA1(); } @Override public AbstractProductB createProductB() { return new ConcreteProductB1(); } } // ConcreteFactory2 클래스 public class ConcreteFactory2 implements AbstractFactory { @Override public AbstractProductA createProductA() { return new ConcreteProductA2(); } @Override public AbstractProductB createProductB() { return new ConcreteProductB2(); } }
Java
복사

클라이언트 코드

java코드 복사 public class Client { private AbstractProductA productA; private AbstractProductB productB; public Client(AbstractFactory factory) { productA = factory.createProductA(); productB = factory.createProductB(); } public void performActions() { productA.performActionA(); productB.performActionB(); } public static void main(String[] args) { AbstractFactory factory1 = new ConcreteFactory1(); Client client1 = new Client(factory1); client1.performActions(); // Output: Action A1 performed. Action B1 performed. AbstractFactory factory2 = new ConcreteFactory2(); Client client2 = new Client(factory2); client2.performActions(); // Output: Action A2 performed. Action B2 performed. } }
Java
복사
이 예제는 추상 팩토리 패턴을 사용하여 두 개의 서로 다른 제품군을 생성하고 사용하는 방법을 보여줍니다. AbstractFactory 인터페이스는 제품을 생성하는 메서드를 정의하고, ConcreteFactory1ConcreteFactory2 클래스는 각기 다른 제품군을 생성하는 구현을 제공합니다. 클라이언트 코드는 팩토리를 사용하여 제품을 생성하고, 각 제품의 동작을 실행합니다.
개념 설명:
추상 팩토리 패턴(Abstract Factory Pattern)은 관련된 객체들의 그룹을 생성하는 인터페이스를 제공하는 패턴입니다. 이 패턴을 사용하면 클라이언트가 구체적인 클래스들을 지정하지 않고도 객체를 생성할 수 있습니다.
구성 요소:
1.
추상 팩토리(AbstractFactory): 제품을 생성하기 위한 인터페이스를 정의합니다.
2.
구체 팩토리(ConcreteFactory): 추상 팩토리를 구현하여 구체적인 제품을 생성합니다.
3.
추상 제품(AbstractProduct): 제품 객체에 대한 인터페이스를 정의합니다.
4.
구체 제품(ConcreteProduct): 추상 제품 인터페이스를 구현한 클래스들입니다.
5.
클라이언트(Client): 추상 팩토리 및 추상 제품을 사용하여 작업을 수행합니다.
UML 다이어그램 설명:
AbstractFactory: 추상 팩토리 클래스는 createProductA(), createProductB() 등의 메서드를 정의합니다.
ConcreteFactoryA, ConcreteFactoryB: 각 팩토리는 추상 팩토리 메서드를 구현하여 ProductAOne, ProductATwo, ProductBOne, ProductBTwo 등의 객체를 생성합니다.
AbstractProductA, AbstractProductB: 각 제품군의 인터페이스를 정의합니다.
ConcreteProductAOne, ConcreteProductATwo, ConcreteProductBOne, ConcreteProductBTwo: 실제 제품 클래스로, 추상 제품 인터페이스를 구현합니다.
Client: 추상 팩토리와 추상 제품을 사용하여 객체를 생성하고 사용합니다.
구현 고려 사항:
1.
기본 객체 제공: 기본 구현을 제공할 수 있는 구체 클래스를 사용하거나, 추상 클래스로 정의하여 필요에 따라 서브클래스에서 구현하도록 합니다.
2.
추가 인자 사용: 팩토리 메서드에서 추가 인자를 받아 다양한 객체를 선택적으로 생성할 수 있습니다.
디자인 고려 사항:
1.
제품 계층 구조 반영: 팩토리 메서드 패턴의 생성자 계층 구조가 제품 계층 구조를 반영합니다.
2.
템플릿 메서드 패턴 활용: 템플릿 메서드 디자인 패턴을 자주 사용하여 팩토리 메서드를 구성할 수 있습니다.
3.
추상 팩토리와의 연계: 추상 팩토리 디자인 패턴은 팩토리 메서드 패턴을 활용하여 다수의 팩토리 메서드를 포함할 수 있습니다.
예제 코드:
java코드 복사 // AbstractProductA interface AbstractProductA { void use(); } // ConcreteProductA1 class ProductAOne implements AbstractProductA { public void use() { System.out.println("Using Product A1"); } } // ConcreteProductA2 class ProductATwo implements AbstractProductA { public void use() { System.out.println("Using Product A2"); } } // AbstractProductB interface AbstractProductB { void consume(); } // ConcreteProductB1 class ProductBOne implements AbstractProductB { public void consume() { System.out.println("Consuming Product B1"); } } // ConcreteProductB2 class ProductBTwo implements AbstractProductB { public void consume() { System.out.println("Consuming Product B2"); } } // AbstractFactory interface AbstractFactory { AbstractProductA createProductA(); AbstractProductB createProductB(); } // ConcreteFactory1 class ConcreteFactoryA implements AbstractFactory { public AbstractProductA createProductA() { return new ProductAOne(); } public AbstractProductB createProductB() { return new ProductBOne(); } } // ConcreteFactory2 class ConcreteFactoryB implements AbstractFactory { public AbstractProductA createProductA() { return new ProductATwo(); } public AbstractProductB createProductB() { return new ProductBTwo(); } } // Client public class Client { public static void main(String[] args) { AbstractFactory factoryA = new ConcreteFactoryA(); AbstractProductA productA1 = factoryA.createProductA(); AbstractProductB productB1 = factoryA.createProductB(); productA1.use(); productB1.consume(); AbstractFactory factoryB = new ConcreteFactoryB(); AbstractProductA productA2 = factoryB.createProductA(); AbstractProductB productB2 = factoryB.createProductB(); productA2.use(); productB2.consume(); } }
Java
복사
추가 설명:
추상 팩토리 패턴은 서로 관련이 있거나 의존성이 있는 객체들을 생성하는데 유용합니다.
팩토리 메서드 패턴을 사용하여 구체적인 객체 생성을 서브클래스에 위임할 수 있으며, 이는 코드의 유연성과 확장성을 높여줍니다.
추상 팩토리 패턴을 적용하면 클라이언트 코드는 구체적인 클래스를 알 필요가 없으며, 이는 유지보수성과 코드의 가독성을 향상시킵니다.

개체 풀

오브젝트 풀(Object Pool)이란 무엇인가?

정의: 오브젝트 풀은 클래스 인스턴스를 생성하는 비용이 높고, 짧은 시간 동안 많은 수의 객체가 필요한 경우에 사용됩니다. 사용법: 객체를 미리 생성하거나 인메모리 캐시에 수집해 둡니다. 코드에서 객체가 필요할 때, 이 캐시에서 객체를 제공합니다. 복잡성: 효율적으로 결함 없이 구현하기 가장 복잡한 패턴 중 하나입니다.

UML 다이어그램 설명

ObjectPool: 재사용 가능한 객체 인스턴스를 캐시하고, 재사용 가능한 객체를 얻고 반납하는 메서드를 제공합니다.
Client: 객체 풀을 사용하여 재사용 가능한 객체 인스턴스를 얻습니다.
AbstractReusable: 재사용 가능한 객체의 작업을 정의합니다.
ConcreteReusable: 상태, 작업 및 초기화 메서드를 구현하여 재사용 가능한 객체를 구현합니다.

구현 단계

오브젝트 풀 클래스 생성:
1.
객체의 스레드 안전 캐싱을 구현합니다.
2.
객체를 획득하고 반납하는 메서드를 제공합니다.
3.
객체를 풀에 제공하기 전에 캐시된 객체를 초기화합니다.
재사용 가능한 객체:
코드에서 반납할 때 객체의 상태를 초기화하는 메서드를 제공해야 합니다.
결정 사항:
풀에 객체가 없을 때 새 객체를 생성할지 아니면 객체가 사용 가능해질 때까지 대기할지를 결정합니다.
이 선택은 객체가 고정된 수의 외부 자원에 연결되어 있는지 여부에 따라 달라집니다.

예제 코드

다음은 Java로 구현한 예제입니다:
java코드 복사 import java.util.concurrent.ConcurrentLinkedQueue; abstract class AbstractReusable { public abstract void operation(); public abstract void reset(); } class ConcreteReusable extends AbstractReusable { private String state; public void operation() { System.out.println("Performing operation with state: " + state); } public void reset() { state = null; } public void setState(String state) { this.state = state; } } class ObjectPool { private ConcurrentLinkedQueue<ConcreteReusable> pool = new ConcurrentLinkedQueue<>(); public ObjectPool(int initialSize) { for (int i = 0; i < initialSize; i++) { pool.add(new ConcreteReusable()); } } public ConcreteReusable getReusable() { ConcreteReusable reusable = pool.poll(); if (reusable == null) { reusable = new ConcreteReusable(); } return reusable; } public void releaseReusable(ConcreteReusable reusable) { reusable.reset(); pool.add(reusable); } } public class Client { public static void main(String[] args) { ObjectPool pool = new ObjectPool(2); ConcreteReusable reusable1 = pool.getReusable(); reusable1.setState("First Use"); reusable1.operation(); pool.releaseReusable(reusable1); ConcreteReusable reusable2 = pool.getReusable(); reusable2.setState("Second Use"); reusable2.operation(); pool.releaseReusable(reusable2); } }
Java
복사

요약

오브젝트 풀: 객체 생성을 최소화하기 위해 재사용 가능한 객체 집합을 효율적으로 관리합니다.
디자인 고려사항:
스레드 안전성: 풀을 스레드 안전하게 만들어 동시성 문제를 피해야 합니다.
객체 상태 초기화: 재사용 전에 객체의 상태를 초기화하여 부작용을 방지해야 합니다.
객체 수명 주기 관리: 풀에 객체가 없을 때 새 객체를 생성할지, 아니면 사용할 수 있을 때까지 대기할지 결정해야 합니다.

Object Pool이란?

Object Pool(객체 풀)은 객체 생성 비용이 높고, 짧은 시간 동안 많은 객체가 필요할 때 사용하는 디자인 패턴입니다.
정의: 객체 풀은 생성 비용이 높은 객체를 관리하고 재사용할 수 있도록 합니다.
용도: 객체를 미리 생성하거나 메모리 캐시에 수집합니다. 코드에서 객체가 필요할 때, 이 캐시에서 제공됩니다.
복잡성: 효율적으로 구현하고 결함 없이 사용하기 어려운 패턴 중 하나입니다.

UML 다이어그램 설명

ObjectPool: 재사용 가능한 객체 인스턴스를 캐시하고 이를 가져오고 반환하는 메서드를 제공합니다.
Client: 객체 풀을 사용하여 재사용 가능한 객체 인스턴스를 가져옵니다.
AbstractReusable: 재사용 가능한 객체의 작업을 정의합니다.
ConcreteReusable: 상태, 작업 및 리셋 메서드를 가진 재사용 가능한 객체를 구현합니다.

구현 단계

1.
Object Pool 클래스 생성:
객체의 스레드 안전한 캐싱을 구현합니다.
객체를 획득하고 반환하는 메서드를 제공합니다.
캐시된 객체를 제공하기 전에 리셋합니다.
2.
재사용 가능한 객체:
코드에 의해 반환될 때 상태를 리셋하는 메서드를 제공해야 합니다.
3.
결정 포인트:
풀이 비었을 때 새 객체를 생성할지 아니면 객체가 사용 가능해질 때까지 기다릴지 결정합니다.
이 선택은 객체가 고정된 수의 외부 리소스에 연결되어 있는지에 따라 달라집니다.

Java 예제 코드

java코드 복사 import java.util.concurrent.ConcurrentLinkedQueue; abstract class AbstractReusable { public abstract void operation(); public abstract void reset(); } class ConcreteReusable extends AbstractReusable { private String state; public void operation() { System.out.println("Performing operation with state: " + state); } public void reset() { state = null; } public void setState(String state) { this.state = state; } } class ObjectPool { private ConcurrentLinkedQueue<ConcreteReusable> pool = new ConcurrentLinkedQueue<>(); public ObjectPool(int initialSize) { for (int i = 0; i < initialSize; i++) { pool.add(new ConcreteReusable()); } } public ConcreteReusable getReusable() { ConcreteReusable reusable = pool.poll(); if (reusable == null) { reusable = new ConcreteReusable(); } return reusable; } public void releaseReusable(ConcreteReusable reusable) { reusable.reset(); pool.add(reusable); } } public class Client { public static void main(String[] args) { ObjectPool pool = new ObjectPool(2); ConcreteReusable reusable1 = pool.getReusable(); reusable1.setState("First Use"); reusable1.operation(); pool.releaseReusable(reusable1); ConcreteReusable reusable2 = pool.getReusable(); reusable2.setState("Second Use"); reusable2.operation(); pool.releaseReusable(reusable2); } }
Java
복사

요약

Object Pool: 객체 생성을 최소화하여 재사용 가능한 객체를 효율적으로 관리합니다.
디자인 고려사항:
스레드 안전성: 풀이 스레드 안전하도록 하여 동시성 문제를 방지합니다.
객체 상태 리셋: 객체를 재사용하기 전에 상태를 리셋하여 부작용을 방지합니다.
객체 수명 주기 관리: 풀이 비었을 때 새 객체를 생성할지 또는 사용 가능한 객체를 기다릴지 결정합니다.

구현 고려사항

1.
객체 상태 리셋: 리셋 작업이 비용이 많이 들지 않아야 성능 절감을 유지할 수 있습니다.
2.
객체 미리 캐싱: 객체를 미리 생성하면 사용 시점에 속도를 높일 수 있지만, 시작 시간과 메모리 소비가 증가할 수 있습니다.
3.
동기화: 객체 풀의 동기화는 리셋 시간과 동기화된 컨텍스트에서 리셋을 피해야 할 필요성을 고려해야 합니다.

디자인 고려사항

1.
매개변수화된 풀: 풀을 매개변수화하여 여러 객체를 캐싱하고 반환하는 방법을 제공할 수 있습니다.
2.
객체 풀링의 장점: 외부 리소스 초기화 비용이 높은 경우에만 유용합니다.
3.
장기 객체 풀링: 장기적으로 사용되는 객체를 풀링하지 않거나, 자주 객체를 생성하는 것을 피하기 위해 풀링하지 않아야 합니다.

Object Pool의 예

1.
Thread Pool Executor: java.util.concurrent.ThreadPoolExecutor는 스레드를 풀링합니다. 주로 ExecutorService를 통해 사용됩니다.
2.
데이터베이스 연결 풀링: Apache Commons DBCP 라이브러리는 데이터베이스 연결 풀링에 사용되며, 일반적으로 JNDI를 통해 생성되고 노출됩니다.

Prototype 패턴과의 비교

Object Pool:
프로그램 실행 동안 자주 사용되는 캐시된 객체를 가집니다.
객체를 명시적으로 풀로 반환해야 합니다. 반환하지 않으면 메모리/리소스 누수가 발생할 수 있습니다.
Prototype:
필요할 때 객체를 생성하며, 캐싱은 없습니다.
객체가 클론되면 특별한 처리가 필요 없으며, 일반 객체처럼 사용할 수 있습니다.

단점

1.
올바른 사용: 클라이언트 코드가 객체를 풀로 반환하는 것이 중요합니다.
2.
상태 리셋: 재사용 가능한 객체는 상태를 효율적으로 리셋할 수 있어야 합니다.
3.
레거시 코드: 기존 코드를 리팩토링하여 객체 풀을 사용하는 것은 어려울 수 있습니다.
4.
풀 사이즈 관리: 풀이 비었을 때 새 객체를 생성할지 또는 객체가 사용 가능해질 때까지 기다릴지 결정해야 합니다.