반응형

1) 점층적 생서자 패턴도 쓸 수는 있지만, 매개변수 개수가 많아지면 클라이언트 코드를 작성하거나 읽기 어렵다.

점층적 생성자 패턴 - 확장하기 어렵다!

public class NutritionFacts{
    private final int servingSize;
    private final int servings;
    private final int calories;
    private final int fat; 
    private final int sodium;
    private final int carbohydrate;

    public NutritionFacts(int servingSize, int servings){
        this(servingSize, servings, 0);
    }

    public NutritionFacts(int servingSize, int servings, inst calories){
        this(servingSize, servings, calories, 0);
    }
}

점층적 생성자 패턴도 쓸 수 있지만, 매개변수 개수가 많아지면 클라이언트 코드를 작성하거나 읽기 어렵다.

자바빈즈 패턴 - 일관성이 깨지고 불변으로 만들 수 없다.

public class NutritionFacts {
    private int servingSize = -1;
    private int servings = -1;
    private final int calories = 0;
    private final int fat = 0;
    private final int sodium = 0;
    private final int carbohydrate = 0;

    public NutritionFacts(){}

    public void setServingSize(int val){servingSize = val;}
    public void setServings(int val){servings = val;}
    public void setCalories(int val){calories = val;}
    public void setFat(int val){fat = val;}
    public void setSodium(int val){sodium = val;}
    public void setCarbohydrate(int val){ carbohydrate = val;}

}

자바빈즈 패턴에서는 객체 하나를 만들려면 메서드를 여러 개 호출해야 하고, 객체가 완전히 생성되기 전까지는 일관성이 무너진 상태에 놓이게 됨

자바빈즈 패턴에서는 클래스를 불변으로 만들 수 없음(아이템 17_변경 가능성을 최소화하라)

final 등 불변을 보장, 스레드 안정하도록 구현이 필요함

위 두가지를 아우리는게 세번째 대안인 빌터 패턴이다

빌더 패턴 - 점층적 생성자 패턴과 자바빈즈 패턴의 장점만 취함

public class NutritionFacts{
    private final int servingSize; 
    private final int servings;
    private final int calories;
    private final int fat; 
    private final int sodium;
    private final int carbohydrate;

    public static class Builder {

        private final int servingSize; 
        private final int servings;
        private int calories = 0; 
        private int sodium = 0; 
        private int carbohydrate = 0;

        public Builder(int servingSize, int servings){
            this.servingSize = servingSize;
            this.servings = servings;
        }

        public Builder calories(int val){
            calories = val; return this;
        }
        public Builder fat(int val){
            fat = val; return this;
        }
        public Builder sodium(int val){
            sodium = val; return this;
        }
        public Builder carbohydrate(int val){
            carbohydrate = val; return this;
        }

        public NutritionFacts build(){
            return new NutritionFacts(this);
        }

        public NutritionFacts build(){
            return new NutritionFacts(this);
        }

    }

    private NutritionFacts(Builder builder){
        servingSize = builder.servingSize;
        servings = builder.servings;
        calories = builder.calories;
        fat = builder.fat;
        sodium = builder.sodium;
        carbohydrate = builder.carbohydrate;
    }

}

//클라이언트 후출시
NutritionFacts cocaCola = new NutritionFacts.Builder(240, 8)
        .calories(100).sodium(35).carbohydrate(27).build();

메서드 호출이 흐르듯이 연결된다는 뜻으로 플루언트 API 혹은 메서드 연쇄라 한다.

계층적으로 설계된 클래스와 잘 어울리는 빌더 패턴

public abstract class Pizza{
    public enum Topping{HAM, MUSHROOM, ONION, PEPPER, SAUSAGE}
    final Set<Topping> toppings;

    abstract static class Builder<T extends Builder<T>> {
        EnumSet<Topping> toppings = EnumSet.noneOf(Topping.class);
        public T addTopping(Topping topping){
            toppings.add(Objects.requiredNonNull(topping));
            return self();
        }

        abstract Pizza build();

        protected abstract T self();
    }

    Pizza(Builder<?> builder){
        toppings = builder.toppings.clone();
    }
}

재귀적 타입 하전을 이용하는 제네릭 타입
추상 메서드인 self를 더해 하위 클래스에서 형변환하지 않고도 메서드 연쇄를 지원

뉴욕피자

public class NyPizza extends Pizza {
    public enum Size {SMALL, MEDIUM, LARGE}
    private final Size size; 

    public static class Builder extends NyPizza.Builder<Builder>{
        private final Size size; 

        public Builder(Size size){
            this.size = Objects.requiredNonNull(size);
        }

        @Override public NyPizza build() {
            return new NyPizza(this);
        }

        @Override protected Builder self(){return this;}

    }

    private NyPizza(Builder builder){
        super(builder);
        size = builder.size;
    }

}

@Override public NyPizza build() {
return new NyPizza(this);
}
다음 구체 하위 클래스는 하위 클래스 메서드가 상위 클래스의 메서드가 정의한 반환 타입이 아닌, 그 하위 타입을 반환하는 기능을 공병환 타이핑이라 한다.
클라이언트가 형변환에 신경 쓰지 않고도 빌더를 사용할 수 있음

NyPizza pizza = new NyPizza.Builder(SMALL)
        .addTopping(SAUSAGE).addTopping(ONION).build();

생성자나 정적 팩터리가 처리해야 할 매개변수가 많다면 빌더 패턴을 선택하는 게 더 낫다.

실무에서는 롬복에서 제공하는 Builder 어노테이션을 잘 활용하면 될 듯하다.

728x90
반응형

정적 팩터리 메서드

class ExampleItem() {

    public static Boolean valueOf(boolean b) {
        return b ? Boolean.TRUE : Boolean.FALSE;
    }

}

정적 팩터리 메서드 장점

1) 이름을 가질 수 있다.

BitInteger(int, int, Random)과 정적 팩터리 메서드인 BigInteger.probablePrime 비교

[값이 소수인 BigInteger를 반환한다]는 의미를 더 잘 설명함

2) 호출될 때마다 인스턴스를 새로 생성하지 않아도 된다.

인스턴스를 캐싱하여 재활용하는 식으로 불필요한 객체를 아예 생성을 피할 수 있음

이는 디자인 패턴 중에서 플라이웨이트 패턴*(Flyweight pattern)과 비슷한 기법

  • 플라이웨이트 패턴이란?
    어떤 클래스의 인스턴스 한 개만 가지고 여러 개의 "가상 인스턴스"를 제공하고 싶을 때 사용하는 패턴
    즉, 인스턴스를 가능한 대로 공유시켜 쓸데없이 new 연산자를 통한 메모릴 낭비를 줄이는 방식
    자주 사용하는 속성과 자주 사용하지 않는 속성을 분리해서 재사용하는 방식

public class Font {
    final String family;

    final int size;

    public Font(String family, int size) {
        this.family = family;
        this.size = size;
    }

    public int getSize() {
        return size;
    }

    public String getFamily() {
        return family;
    }
}
public class FontFactory {

    private Map<String, Font> cache = new HashMap<>();

    public Font getFont(String font) {
        if (cache.containsKey(font)) {
            return cache.get(font);
        } else {
            String[] split = font.split(":");
            Font newFont = new Font(split[0], Integer.parseInt(split[1]));
            cache.put(font, newFont);
            return newFont;
        }

    }
}
public class Client {

    public static void main(String[] args) {
        FontFactory fontFactory = new FontFactory();
        Character c1 = new Character('h', "white", fontFactory.getFont("nanum:12"));
        Character c2 = new Character('e', "white", fontFactory.getFont("nanum:12"));
        Character c3 = new Character('l', "white", fontFactory.getFont("nanum:12"));
    }
}

3) 반환 타입의 하위 타입 객체를 반환할 수 있는 능력이 있다.

API를 만들때 정적 팩터리 메서드의 반환 타입으로 사용하는 인터페이스를 사용하면 API를 작게 유지함
아이템 20. 추상 클래스보다는 인터페이스를 우선하라

public interface Singer{
    AudioClip sing(Song song);
}

public interface Songwriter{
    Song compose(int chartPosition);
}
public interface SingerSongwriter extends Singer, Songwriter{
    AudioClip strum();

    void actSensitive();
}

자바 8 이전에는 인터페이스에 정적 메서드를 선언할 수 없어서 java.util.Collections에서 정적 팩터리 메서드를 통해 얻도록 했음
하지만 이후에 인터페이스가 제공되면서 컬렉션 프레임워크는 인터페이스만으로 다루게 되어 API가 작아지고 개념적인 무게와 개념의 수와 난이도가 낮아짐
인터페이스가 정적 메서드를 가질 수 없다는 제한이 풀렸기 때문에 인스턴스화 불가 동반 클래스를 둘 이유가 없음

4) 입력 매개변수에 따라 매번 다른 클래스의 객체를 반환할 수 있다.

EnumSet 클래스
원소가 64개 이하면 원소들을 long 변수로 관리하는 RegularEnumSet의 인스턴스
원소가 65개 이상이면 long 배열로 관리하는 JumboEnumSet의 인스턴스를 반환
하지만 클라이언트는 두 클래스의 존재를 모름

내부적으로 위 기능이 필요 없어서 변경하더라도 클라이언트의 코드는 변화 없음
(일종의 캡슐화, OCP개념을 설명)

5) 정적 팩터리 메서드를 작성하는 시점에는 반환할 객체의 클래스가 존재하지 않아도 된다.

서비스 제공자 프레임워크를 만드는 근간 ex) JDBC
구현체들을 클라이언트에 제공하는 역할을 프레임워크가 통제, 클라이언트 구현체로 부터 분리
(IoC 개념에 대한 설명과 같음)

리플렉션

  • 구체적인 클래스 타입을 알지 못하더라도 그 클래스의 메서드, 타입, 변수들에 접근할 수 있도록 해주는 자바 API
  • 컴파일 시간이 아닌 실행 시간(런타임)에 동적으로 특정 클래스의 정보를 추출 할 수 있는 프로그래밍 기법
  • Intellij 자동완성, 스프링 어노테이션
Class clazz = Child.class;
System.out.println("Class name: " + clazz.getName());
//출력
Class name: test.Child
Class clazz2 = Class.forName("test.Child");
System.out.println("Class name: " + clazz2.getName());

//출력
Class name: test.Child
Class clazz = Class.forName("test.Child");
Constructor constructors2[] = clazz.getConstructors();
for (Constructor cons : constructors2) {
    System.out.println("Get public constructors in Child: " + cons);
}

//출력
Get public constructors in both Parent and Child: public test.Child()

브리지 패턴
추상적인 것과 구체적인 것을 분리하여 연결하는 패턴

의존 객체 주입 프레임워크(DI) -> 결국 스프링 프레임워크

정적 팩터리 메서드 단점

1) 상속을 하려면 public이나 protected 생성자가 필요하닌 정적 팩터리 메서드만 제공하면 하위 클래스를 만들 수 없다.

컬렉션 프레임워크의 유틸리티 구현 클래스를 상속할 수 없음을 나타냄
상속(구현상속에 한정적)은 재사용하는 강력한 수단이지만, 항상 최선을 아님, 잘못 사용하면 오류를 내기 쉬운 소프트웨어(아이템 18 내용 참고)
결국 상속을 하지 못하는게 일종의 장점이 될 수 있다는 의미

2) 정적 팩터리 메서드는 프로그래머가 찾기 어렵다.

API 설명에 명확히 드러나지 않으니 사용자는 정적 패턱터리 메서드 방식 클래스를 인스턴스화할 방법을 알아내야함

정리

정적 팩터리를 사용하는게 유리한 경우가 더 많으므로 무작적 public 생성자를 제공하던 습관이 있다면 고치자.

728x90
반응형

전략 패턴이란?

하나의 메시지와 책임을 정의하고, 이를 수행할 수 있는 다양한 전략을 만든 후, 다형성을 통해 전략을 선택해 구현을 실행하는 패턴.

전략 패턴은 GoF의 디자인패턴 중에서 행위 패턴 중에 하나에 해당함

전략 패턴 UML

Spring에서 사용하는 전략패턴 예시

package com.ji.behavioral_patterns.strategy.java;

import org.springframework.beans.factory.xml.BeanDefinitionParser;
import org.springframework.cache.CacheManager;
import org.springframework.context.ApplicationContext;
import org.springframework.context.annotation.AnnotationConfigApplicationContext;
import org.springframework.context.support.ClassPathXmlApplicationContext;
import org.springframework.context.support.FileSystemXmlApplicationContext;
import org.springframework.transaction.PlatformTransactionManager;

public class StrategyInSpring {

    public static void main(String[] args) {
        ApplicationContext applicationContext = new ClassPathXmlApplicationContext();
        ApplicationContext applicationContext1 = new FileSystemXmlApplicationContext();
        ApplicationContext applicationContext2 = new AnnotationConfigApplicationContext();

        BeanDefinitionParser parser;

        PlatformTransactionManager platformTransactionManager;

        CacheManager cacheManager;

    }
}

자바에서 사용하는 전략 패턴

https://stackoverflow.com/questions/1673841/examples-of-gof-design-patterns-in-javas-core-libraries/2707195#2707195

 

Examples of GoF Design Patterns in Java's core libraries

I am learning GoF Java Design Patterns and I want to see some real life examples of them. What are some good examples of these Design Patterns in Java's core libraries?

stackoverflow.com

 

배경

전략 패턴의 핵심은 Context를 기반으로 해당 개발자가 필요에 따라서 원하는 기능을 다형성에 의해서 원하는 전략을 선택한 것에 있음

현재 코드에서 Context가 로그 조회 기능이고 요청에 따라서 구현체가 선택되어 기능이 동작하게 구현하면 되기 때문에 전략 패턴 적용하기에 용이하다고 판단함

전략 패턴 적용 전 예시

전략 패턴 적용 전 레거시 코드

각 API 별로 각 요청에 해당하는 메서드를 가져와서 return하는 구조였음

@ApiOperation(value = "계정 관련 로그")
@PostMapping("/list/account")
public ResultVO getAccountLog(@Valid @RequestBody SearchListVO vo) {
    log.info("[/list/account] :" + vo.toString());
    SearchResultVO result;

    try {

        result = logService.getAccountLogList(vo);

    } catch (Exception e) {
        e.printStackTrace();
        return APIUtil.resResult(ErrorCode.SERVER_ERR.getErrorCode(), "계정 로그 조회가 실패되었습니다.", null);
    }

    return APIUtil.resResult(ErrorCode.SUCCESS.getErrorCode(), "계정 로그 조회가 완료되었습니다.", result);

}

@ApiOperation(value = "파일 보호 이벤트 로그")
@PostMapping("/list/file-protect")
public ResultVO getFileProtect(@Valid @RequestBody SearchListVO vo) {
    log.info("[/list/file-protect] :" + vo.toString());
    SearchResultVO result;

    try {
        result = logService.getClientFileProtectLog(vo);

    } catch (Exception e) {
        e.printStackTrace();
        return APIUtil.resResult(ErrorCode.SERVER_ERR.getErrorCode(), "파일 보호 이벤트 로그 조회가 실패되었습니다.", null);
    }

    return APIUtil.resResult(ErrorCode.SUCCESS.getErrorCode(), "파일 보호 이벤트 로그 조회가 완료되었습니다..", result);

}

@ApiOperation(value = "서버 상태 로그")
@PostMapping("/list/server-status")
public ResultVO getServerStatusLog(@Valid @RequestBody SearchListVO vo) {
    log.info("[/list/server-status] :" + vo.toString());
    SearchResultVO result;

    try {

        result = logService.getServerStatusLog(vo);

    } catch (Exception e) {
        e.printStackTrace();
        return APIUtil.resResult(ErrorCode.SERVER_ERR.getErrorCode(), "서버 상태 로그 조회가 실패되었습니다.", null);
    }

    return APIUtil.resResult(ErrorCode.SUCCESS.getErrorCode(), "서버 상태 로그 조회가 완료되었습니다..", result);

}
public interface LogService {

	public SearchResultVO getAccountLogList(SearchListVO vo) throws Exception;

	public SearchResultVO getClientFileProtectLog(SearchListVO vo) throws Exception;

	public SearchResultVO getServerStatusLog(SearchListVO vo) throws Exception;

}
@Override
public SearchResultVO getAccountLogList(SearchListVO vo) throws Exception {
	List<AccountLogDto> result = new ArrayList<AccountLogDto>();
	
	SearchResultVO daoVO = logDao.getAccoutLogList(vo);
	
	//date 포맷 변경
	List<AccountLogDto> searchedList = (List<AccountLogDto>) daoVO.getSearchedList();
	for(AccountLogDto dto : searchedList) {
		dto.set_logTime(DateUtils.parseDateFormatHHMMSSss(dto.getLogTime()));
		result.add(dto);
	}
	
	return new SearchResultVO(daoVO.getTotal(), result);
}

 

위 구조의 레거시 코드를 전략패턴을 적용하여 리팩터링 하였음.

전략 패턴 적용  예시

다음 UML과 같의 LogStrategyService라는 strategy 클래스를 선언하였고, 각 로그 유형에 해당하는 cocreate class가 동일한 인터페이스를 상속하여 구현하고 있음

전략 패턴 적용 후 UML

코드를 자세이 보도록 하자

strategy 클래스에 해당하는 LogStrategyService 인터페이스

public interface LogStrategyService {

    SearchResultVO getLogList(SearchListVO searchListVO) throws Exception;

    SearchResultVO getLogList(SearchListVO searchListVO, String lang) throws Exception;

    boolean isTarget(String logType);

}

ConcreateStrategy1~ 클래스에 해당하는 AccountLogServiceImpl 클래스 구현체

package com.smt.service.log;

import com.smt.dao.log.LogDao;
import com.smt.dto.AccountLogDto;
import com.smt.util.DateUtils;
import com.smt.util.enums.LogType;
import com.smt.vo.common.SearchListVO;
import com.smt.vo.common.SearchResultVO;
import lombok.RequiredArgsConstructor;
import org.springframework.stereotype.Service;

import java.util.ArrayList;
import java.util.List;

@Service
@RequiredArgsConstructor
public class AccountLogServiceImpl implements LogStrategyService {

   private final LogDao logDao;

    @Override
    public SearchResultVO getLogList(SearchListVO searchListVO) throws Exception {
        List<AccountLogDto> result = new ArrayList<AccountLogDto>();

        SearchResultVO daoVO = logDao.getAccoutLogList(searchListVO);

        //date 포맷 변경
        List<AccountLogDto> searchedList = (List<AccountLogDto>) daoVO.getSearchedList();
        for (AccountLogDto dto : searchedList) {
            dto.set_logTime(DateUtils.parseDateFormatHHMMSSss(dto.getLogTime()));
            result.add(dto);
        }

        return new SearchResultVO(daoVO.getTotal(), result);
    }

    @Override
    public SearchResultVO getLogList(SearchListVO searchListVO, String lang) throws Exception {
        return null;
    }

    @Override
    public boolean isTarget(String logTypeUrl) {
        return logTypeUrl.equals(LogType.ACCOUNT.getUrl());
    }
}

Context에 해당하는 Controller 클래스

또한 다음 Controller에서 @PathVariable를 활용해서 기존에 URL에 요청하는 Controller 메서드가 동작하는 구조였지만, 하나는 Controller 메서드에서 로그 type에 해당하는 상속 클래스를 Spring에 singleton으로 주입된 해당 클래스를 찾아서 초기화 하도록 구현함에 따라서 코드를 간략하게 리팩토링 하였음.

참고로, 현재는 List를 활용해서 해당 하위 클래스를 접근하는 방식이지만, 현재는 Map과 Spring에서 제공하는 @Service("account") 속성을 활용해서 클래스를 더욱 빠른 속도로 찾을 수 있게 개선해 놓은 상태임

@Slf4j
@RestController
@RequiredArgsConstructor
@RequestMapping("/log")
@Validated
@Api(value = "LogController", description = "로그 관련(참고문서 : 구글 공유 문서 독스토리 서버 ReturnCode)")
public class LogController {

  private final LogService logService;

  private final List<LogStrategyService> logStrategyServices;

  @ApiOperation(value = "통합 로그 조회 API-정책 설정 로그 제외")
  @PostMapping("/list/{type}")
  public ResultVO getLogStrategy(@PathVariable("type") String logType, @Valid @RequestBody SearchListVO searchListVO) throws Exception {
      log.info("[/list/{type}] :" + searchListVO.toString() + "type : " + logType);

      SearchResultVO searchResult =  logStrategyServices.stream()
                                                                .filter(logService -> logService.isTarget(logType))
                                                                .findFirst()
                                                                .get()
                                                                .getLogList(searchListVO);

      return APIUtil.resResult(ErrorCode.SUCCESS.getErrorCode(), "통합 로그 조회가 완료되었습니다.", searchResult);
  }

결론

만약 PathVariable을 이용하고 전략 패턴을 적용하지 않았다면 path Type에 따라서 if문을 통해서 해당 메서드를 호출되었을 되었을 것임

하지만, 전략 패턴을 적용함으로써 if문이 아닌 반복문(다형성)을 이용하게됨

다른 유형의 로그 유형을 구현(추가)해야할 경우 Strategy 인터페이스를 상속 받아서 ConcreateStrategy~ 클래스를 구현하면됨

즉, 전략패턴을 적용함에 따라서 SOLID의 원칙 중 OCP(Open-Closed Principle)를 준수하게 됨

(확장에 열려있고, 변경에는 닫힘)

728x90

+ Recent posts