객체 생성 관련 대표적인 디자인 패턴
- 생성자 패턴: 생성자를 통해 객체를 생성
- 자바 빈 패턴: 객체 상태의 초기화 없이 객체 생성 후, setter를 통해 상태를 초기화/변경
- 프로토타입 패턴: 기존 객체를 복제해 새로운 객체를 생성
- 빌더 패턴: 객체 생성을 한 번에 하지 않고 순차적으로 진행
- 팩토리 메서드 패턴: 객체 생성 전용 인터페이스를 통해 새로운 객체를 생성
- 추상 팩토리 패턴: 여러 팩토리 메서드들을 모아, 여러 객체 간 연관된 객체 생성을 관리
객체 상태 관리 방법
객체의 상태를 직접 변경하는 방식
- field의 접근제한자를 public으로 선언
- 매우 편리하게 원하는 값을 초기화/변경/조회 가능
- 의도치 않은 객체 상태 변경에 매우 취약함
- 유효성 검사를 수행할 수 없음 —> 객체가 유효하지 않은 상태를 가질 위험성 내포
자바 빈 패턴(JavaBeans Pattern)
- 가장 기본이 되는 패턴이자, 대부분의 프레임워크 작동 시 개발자가 사용할 것으로 가정되는 패턴(자바 빈 패턴을 기반으로 작동)
- 다루기 편리하고 상황 변화에 유연함
- 객체의 불변성을 유지하기 어려움
- 객체 상태의 예측 가능성이 떨어짐
- 멀티 스레드 환경에서 특히 취약
- 상태 변경의 파급효과가 큰 Entity에 부적합
- 데이터 전송용 객체(DTO)에 적합
불변 객체(Immutable Object)
불변 객체의 정의
- 생성 후 상태를 변경할 수 없는 객체
불변 객체가 필요한 이유
- 같은 프로젝트를 진행하는 10명의 개발자가 각자의 코드에서 객체를 생성하고, 상태를 변경하고, 그 상태를 조회하고, 조회된 상태를 기반으로 다른 객체의 작업을 수행한다면?
불변 객체의 조건
- 모든 field를 final로 선언
- JPA를 사용할 경우, 실행 시간에 동적으로 Entity 클래스의 proxy를 생성해야 하기 때문에 적용 불가
- 클래스의 모든 field의 접근 제한자를 private으로 선언하고, getter를 제외한 모든 method에서 새로운 객체를 반환하는 경우 적용 예외 —> 캡슐화로 해결
- 클래스를 final로 선언
- 클래스의 모든 field의 접근 제한자를 private으로 선언하고, getter를 제외한 모든 method에서 새로운 객체를 반환하는 경우 적용 예외 —> 캡슐화로 해결
- setter를 제공하지 않음
- 객체가 가진 field의 참조 객체가 변경되지 않도록 관리(가변 객체의 주소를 field로 가질 수 없음) —> 현실적으로 어려운 이유
- 해당 클래스의 모든 method는 기존 상태를 변경하지 않고 항상 새로운 객체를 반환
불변 객체를 생성하는 대표적인 디자인 패턴
- 빌더 패턴
- 팩토리 메서드 패턴
빌더 패턴(Builder Pattern)
특징
- 객체를 한 번에 생성하지 않고 순차적으로 생성함
- 대상 객체의 초기화할 field가 많은 경우, 객체 생성 과정이 복잡한 경우, 객체의 각 field마다 추가 로직이 필요한 경우 적합
- 주로 메서드 체이닝을 이용 —> 코드가 깔끔하고 가독성이 좋음
- 초기화할 필드명과 그에 대한 대입값을 명시함으로써 객체 생성 과정의 이해가 쉽고 명확함
- 자동 오버로딩을 통해 매개변수를 필요한 만큼 자유롭게 선택해서 객체 생성 가능
- 매개변수의 순서에 구애받지 않기 때문에 대입 순서를 지킬 필요가 없음
- 생성 대상 객체의 구조를 파악할 필요가 없기 때문에, 객체 간 결합도를 낮추고 객체의 자율성을 높임
- 불변성을 유지하기 어려운 PATCH 메서드의 데이터 일부 속성 수정 과정에서 강점을 보임
- 객체 생성 과정이 장황하기 때문에, 간단한 객체 생성에 적용하면 오버 엔지니어링의 가능성이 있음
예시 코드
- Lombok에서 제공하는 @Builder 어노테이션을 통해 Builder 클래스 자동 생성
@Builder
private Menu(Long id, Shop shop, String name, Integer price, Integer salesRate, Integer stock,
String flavor, Integer portions, Integer cookingTime, String menuType,
String foodType, Boolean isPopularMenu, List<String> menuOptions,
LocalDateTime registrationTime, LocalDateTime updateTime) {
this.id = id;
this.shop = shop;
this.name = name;
this.price = price;
this.salesRate = salesRate;
this.stock = stock;
adjustStockState();
this.flavor = flavor;
this.portions = portions;
this.cookingTime = cookingTime;
this.menuType = menuType;
this.foodType = foodType;
this.isPopularMenu = isPopularMenu;
this.menuOptions = menuOptions;
this.registrationTime = registrationTime;
this.updateTime = updateTime;
}
- 데이터의 일부 속성 수정 시 적용
@Override
public MenuResponseDto changeMenu(Long id, MenuRequestDto menuRequestDto) throws Exception {
Menu menu = menuRepository.findById(id)
.orElseThrow(() -> new EntityNotFoundException("해당 메뉴를 찾을 수 없습니다. (menuId: " + id + ")"));
int modifiedStock = menu.getStock() + menuRequestDto.getStock();
if (modifiedStock < 0) {
throw new OutOfStockException("재료 최소 수량: 0 (현재 재고: " + menu.getStock() + ")");
}
Menu changingMenu = Menu.builder()
.id(id)
.shop(menu.getShop())
.name(menuRequestDto.getName() != null ? menuRequestDto.getName() : menu.getName())
.price(menuRequestDto.getPrice() > 0 ? menuRequestDto.getPrice() : menu.getPrice())
.salesRate(menuRequestDto.getSalesRate() == -1 ? 0 : menu.getSalesRate())
.stock(modifiedStock)
.flavor(menuRequestDto.getFlavor() != null ? menuRequestDto.getFlavor() : menu.getFlavor())
.portions(menuRequestDto.getPortions() > 0 ? menuRequestDto.getPortions() : menu.getPortions())
.cookingTime(menuRequestDto.getCookingTime() > 0 ? menuRequestDto.getCookingTime() : menu.getCookingTime())
.menuType(menuRequestDto.getMenuType() != null ? menuRequestDto.getMenuType() : menu.getMenuType())
.foodType(menuRequestDto.getFoodType() != null ? menuRequestDto.getFoodType() : menu.getFoodType())
.menuOptions(menuRequestDto.getMenuOptions() != null ? menuRequestDto.getMenuOptions() : menu.getMenuOptions())
.registrationTime(menu.getRegistrationTime())
.updateTime(LocalDateTime.now())
.build();
Menu changedMenu = menuRepository.save(changingMenu);
MenuResponseDto menuResponseDto = beanConfiguration.modelMapper()
.map(changedMenu, MenuResponseDto.class);
return menuResponseDto;
}
- 객체 생성 도중 추가 로직 삽입
@Override
public MenuResponseDto changeMenu(Long id, MenuRequestDto menuRequestDto) throws Exception {
Menu menu = menuRepository.findById(id)
.orElseThrow(() -> new EntityNotFoundException("해당 메뉴를 찾을 수 없습니다. (menuId: " + id + ")"));
MenuBuilder menuBuilder = Menu.builder()
.id(id)
.shop(menu.getShop())
.name(menuRequestDto.getName() != null ? menuRequestDto.getName() : menu.getName())
.price(menuRequestDto.getPrice() > 0 ? menuRequestDto.getPrice() : menu.getPrice())
.salesRate(menuRequestDto.getSalesRate() == -1 ? 0 : menu.getSalesRate());
int modifiedStock = menu.getStock() + menuRequestDto.getStock();
if (modifiedStock < 0) {
throw new OutOfStockException("재료 최소 수량: 0 (현재 재고: " + menu.getStock() + ")");
}
menuBuilder.stock(modifiedStock);
Menu changingMenu = menuBuilder
.flavor(menuRequestDto.getFlavor() != null ? menuRequestDto.getFlavor() : menu.getFlavor())
.portions(menuRequestDto.getPortions() > 0 ? menuRequestDto.getPortions() : menu.getPortions())
.cookingTime(menuRequestDto.getCookingTime() > 0 ? menuRequestDto.getCookingTime() : menu.getCookingTime())
.menuType(menuRequestDto.getMenuType() != null ? menuRequestDto.getMenuType() : menu.getMenuType())
.foodType(menuRequestDto.getFoodType() != null ? menuRequestDto.getFoodType() : menu.getFoodType())
.menuOptions(menuRequestDto.getMenuOptions() != null ? menuRequestDto.getMenuOptions() : menu.getMenuOptions())
.registrationTime(menu.getRegistrationTime())
.updateTime(LocalDateTime.now())
.build();
Menu changedMenu = menuRepository.save(changingMenu);
MenuResponseDto menuResponseDto = beanConfiguration.modelMapper()
.map(changedMenu, MenuResponseDto.class);
return menuResponseDto;
}
정적 팩토리 메서드 패턴(Static Factory Method Pattern)
특징
- 팩토리 클래스를 통해 팩토리 메서드들을 관리하는 팩토리 메서드 패턴에 비해 객체 생성 로직이 단순하고, 오버로딩이 적은 경우에 적합
- 객체 생성 로직이 복잡하거나 오버로딩이 많다면, 빌더 패턴을 사용하거나 팩토리 메서드 패턴을 통해 별도의 팩토리 클래스에 모아서 관리
- 대체로 생성자를 통해 객체의 상태를 변경하지 못하도록 생성자를 private으로 선언(싱글톤 패턴과 유사)
- 생성자를 private으로 제한할 수 없는 팩토리 메서드 패턴에 비해 강점을 가짐(무분별한 객체 생성 방지)
- 객체의 생성 권한을 클라이언트에게 맡기지 않고 해당 클래스 스스로 제어 —> 캡슐화를 통해 객체 간 결합도를 낮추고 객체의 자율성을 높임
- 객체를 생성할 서브타입을 실행시간에 결정할 수 있으므로, 객체 생성의 유연성 증가
- 메서드의 명칭에서 역할이 명확히 드러나기 때문에, 생성자 패턴에 비해 가독성이 좋고 메서드의 역할 이해가 명확해짐
- 빌더 패턴에 비해 일부 속성 변경 로직에 취약하고, 객체 초기화에 필요한 매개변수가 많아지면 가독성이 급격히 떨어짐(읽는 사람마다 다를 수 있으나, 개발자들의 경험 통계적으로 매개변수가 4개 이하인 경우에 대체로 적합하다는 평)
예시 코드
- 객체 생성을 위해 createOrderMenu 메서드 호출
public List<OrderMenu> createOrderMenu(Map<Menu, Integer> menuToOrderMap, Order order) {
List<OrderMenu> orderMenuList = new ArrayList<>();
for (Menu menuToOrder : menuToOrderMap.keySet()) {
OrderMenu orderMenu = OrderMenu.createOrderMenu
(menuToOrder, order, menuToOrder.getPrice(), menuToOrderMap.get(menuToOrder));
orderMenuList.add(orderMenu);
menuToOrder.useStockForSale(orderMenu.getOrderQuantity());
order.computeTotalOrderPrice(orderMenu.getTotalMenuPrice());
}
return orderMenuList;
}
- 직접 객체를 생성 후 반환
private OrderMenu(Menu menu, Order order, Integer menuPrice, Integer orderQuantity) {
this.menu = menu;
this.order = order;
this.menuPrice = menuPrice;
this.orderQuantity = orderQuantity;
totalMenuPrice = orderPrice * orderQuantity;
}
public static OrderMenu createOrderMenu(Menu menu, Order order, Integer menuPrice, Integer orderQuantity) {
return new OrderMenu(menu, order, menuPrice, orderQuantity);
}
'Study' 카테고리의 다른 글
개발지식 스터디 발표 자료(MSA) (0) | 2023.08.01 |
---|---|
객체지향의 사실과 오해 부록A 발표 자료 (2) | 2023.07.12 |
객체지향의 사실과 오해 스터디 7장 발표 자료 (0) | 2023.07.11 |
객체지향의 사실과 오해 스터디 5장 발표 자료 (0) | 2023.07.03 |
커뮤니케이션 스터디 발표 자료(재귀함수) (0) | 2023.06.23 |