오늘 다루어볼 내용은 java 14에서 도입된 record 타입의 클래스입니다. 

 

record란?

레코드(record)란 "데이터 클래스"이며 순수하게 데이터를 보유하기 위한 특수한 종류의 클래스이다. 코틀린의 데이터 클래스와 비슷한 느낌이라고 보면 된다. 밑에서 코드를 보겠지만, record 클래스를 정의할때, 그 모양은 정말 데이터의 유형만 딱 나타내는 듯한 느낌이다. 훨씬더 간결하고 가볍기 때문에 Entity 혹은 DTO 클래스를 생성할때 사용되면 굉장히 좋을 듯하다.

 

sample code

간단하게 샘플코드를 살펴보자.

 

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
public class SampleRecord {
   private final String name;
   private final Integer age;
   private final Address address;
 
   public SampleRecord(String name, Integer age, Address address) {
      this.name = name;
      this.age = age;
      this.address = address;
   }
 
   public String getName() {
      return name;
   }
 
   public Integer getAge() {
      return age;
   }
 
   public Address getAddress() {
      return address;
   }
}
cs

 

위와 같은 코드가 있다고 가정하자. 해당 클래스는 모든 인스턴스 필드를 초기화하는 생성자가 있고, 모든 필드는 final로 정의되어 있다. 그리고 각각 필드의 getter를 가지고 있다. 이러한 클래스 같은 경우는 record 타입의 클래스로 변경이 가능하다.

 

1
2
3
4
5
public record SampleRecord(
   String name,
   Integer age,
   Address address
) {}
cs

 

엄청 클래스가 간결해진 것을 볼 수 있다. 이 record 클래스에 대해 간단히 설명하면 아래와 같다.

 

  • 해당 record 클래스는 final 클래스이라 상속할 수 없다.
  • 각 필드는 private final 필드로 정의된다.
  • 모든 필드를 초기화하는 RequiredAllArgument 생성자가 생성된다.
  • 각 필드의 getter는 getXXX()가 아닌, 필드명을 딴 getter가 생성된다.(name(), age(), address())

 

만약 그런데 json serialize가 되기 위해서는 위와 같이 선언하면 안된다. 아래와 같이 jackson 어노테이션을 붙여줘야한다.

 

1
2
3
4
5
public record SampleRecord(
   @JsonProperty("name") String name,
   @JsonProperty("age") Integer age,
   @JsonProperty("address") Address address
) {}
cs

 

record 클래스는 static 변수를 가질 수 있고, static&public method를 가질 수 있다.

 

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
public record SampleRecord(
   @JsonProperty("name"String name,
   @JsonProperty("age") Integer age,
   @JsonProperty("address") Address address
) {
   
   static String STATIC_VARIABLE = "static variable";
   
   @JsonIgnore
   public String getInfo() {
      return this.name + " " + this.age;
   }
 
   public static String get() {
      return STATIC_VARIABLE;
   }
}
cs

 

또한 record 클래스의 생성자를 명시적으로 만들어서 생성자 매개변수의 validation 로직등을 넣을 수도 있다.

 

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
public record SampleRecord(
   @JsonProperty("name"String name,
   @JsonProperty("age") Integer age,
   @JsonProperty("address") Address address
) {
 
   public SampleRecord {
      if (name == null || age == null || address == null) {
         throw new IllegalArgumentException();
      }
   }
 
   static String STATIC_VARIABLE = "static variable";
 
   @JsonIgnore
   public String getInfo() {
      return this.name + " " + this.age;
   }
 
   public static String get() {
      return STATIC_VARIABLE;
   }
}
cs

 

이러한 record 클래스를 spring의 controller와 연계해서 사용하면 더 간결한 injection이 가능해지기 때문에 훨씬 깔끔한 컨트롤러 클래스작성이 가능하다.

 

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
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
======================================================================================
 
public record SampleRecord(
   @JsonProperty("name"String name,
   @JsonProperty("age") Integer age,
   @JsonProperty("address") Address address
) {
   
   static String STATIC_VARIABLE = "static variable";
   
   @JsonIgnore
   public String getInfo() {
      return this.name + " " + this.age;
   }
 
   public static String get() {
      return STATIC_VARIABLE;
   }
}
 
======================================================================================
 
public record Address(
    @JsonProperty("si"String si,
    @JsonProperty("gu"String gu,
    @JsonProperty("dong"String dong
) {}
 
======================================================================================
 
@Service
public class SampleRecordService {
    public Mono<SampleRecord> sampleRecordMono(SampleRecord sampleRecord) {
        return Mono.just(sampleRecord);
    }
}
 
======================================================================================
 
@RestController
public record SampleController(SampleRecordService sampleRecordService) {
    @PostMapping("/")
    public Mono<SampleRecord> sampleRecord(@RequestBody SampleRecord sampleRecord) {
        System.out.println(sampleRecord.getInfo());
        return sampleRecordService.sampleRecordMono(sampleRecord);
    }
}
 
======================================================================================
cs

 

여기까지 간단하게 jdk 14의 new feature인 record 클래스에 대해 간단하게 다루어 보았다. 모든 코드는 아래 깃헙을 참고하자.

 

 

yoonyeoseong/jdk_14_record_sample

Contribute to yoonyeoseong/jdk_14_record_sample development by creating an account on GitHub.

github.com

 

참조 : https://dzone.com/articles/jdk-14-records-for-spring-devs

 

JDK 14 Records for Spring - DZone Java

In this article, we'll discuss several use cases for JDK 14 Records to write cleaner and more efficient code.

dzone.com

 

posted by 여성게
:

오늘 다루어볼 내용은 Model mapping을 아주 쉽게 해주는 Mapstruct라는 라이브러리를 다루어볼 것이다. 그 전에 Model mapping에 많이 사용되는 ModelMapper와의 간단한 차이를 이야기해보면, Model Mapper는 Runtime 시점에 reflection으로 모델 매핑을 하게 되는데, 이렇게 런타임시에 리플렉션이 자주 이루어진다면 앱성능에 아주 좋지 않다. 하지만 Mapstruct는 컴파일 타임에 매핑 클래스를 생성해줘 그 구현체를 런타임에 사용하는 것이기 때문에 앱 사이즈는 조금 커질수 있지만 성능상 크게 이슈가 없어서 ModelMapper보다 더 부담없이 사용하기 좋다. 그리고 아주 다양한 기능을 제공하기 때문에 조금더 세밀한 매핑이 가능해진다.

 

바로 간단하게 Mapstruct를 맛보자.

 

build.gradle

build.gradle를 채워넣어보자 !

buildscript {
	ext {
		mapstructVersion = '1.3.1.Final'
	}
}

...

// Mapstruct
implementation "org.mapstruct:mapstruct:${mapstructVersion}"
annotationProcessor "org.mapstruct:mapstruct-processor:${mapstructVersion}"
testAnnotationProcessor "org.mapstruct:mapstruct-processor:${mapstructVersion}"

...

compileJava {
	options.compilerArgs = [
			'-Amapstruct.suppressGeneratorTimestamp=true',
			'-Amapstruct.suppressGeneratorVersionInfoComment=true'
	]
}

 

Java
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
@Data
@AllArgsConstructor(staticName = "of")
@NoArgsConstructor
public class CarDto {
    private String name;
    private String color;
}
 
@Data
@AllArgsConstructor(staticName = "of")
@NoArgsConstructor
public class Car {
    private String modelName;
    private String modelColor;
}
 
@Mapper
public interface CarMapper {
 
    CarMapper INSTANCE = Mappers.getMapper(CarMapper.class);
 
    @Mapping(source = "name", target = "modelName")
    @Mapping(source = "color", target = "modelColor")
    Car to(CarDto carDto);
}
cs

 

사실 간단한 것이라, 어노테이션만 보아도 어떠한 기능을 제공하는지 확실하다. 하지만 각 클래스에 getter/setter 및 기본생성자는 꼭 생성해주자.(필드명이 동일할 일은 많이 없겠지만, 각 객체의 필드명이 동일하면 @Mapping 어노테이션은 생략가능하다.) 해당 코드로 테스트코드를 간단하게 작성하자.

 

1
2
3
4
5
6
7
8
9
10
11
public class MapstructTest {
 
    @Test
    public void test() {
        CarDto carDto = CarDto.of("bmw x4""black");
        Car car = CarMapper.INSTANCE.to(carDto);
 
        assertEquals(carDto.getName(), car.getModelName());
        assertEquals(carDto.getColor(), car.getModelColor());
    }
}
cs

 

테스트가 성공하는 것을 볼 수 있다. 아주 간단하면서도 파워풀한 라이브러리인 것 같다. 그러면 실제로 생성된 코드를 한번 볼까?

 

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
@Generated(
    value = "org.mapstruct.ap.MappingProcessor"
)
public class CarMapperImpl implements CarMapper {
 
    @Override
    public Car to(CarDto carDto) {
        if ( carDto == null ) {
            return null;
        }
 
        Car car = new Car();
 
        car.setModelName( carDto.getName() );
        car.setModelColor( carDto.getColor() );
 
        return car;
    }
}
cs

 

우리가 작성하지 않았지만, 코드가 생성되었다. 리플렉션과 같이 비용이 큰 기술이 사용되지 않았다. 간단한 validation 코드와 생성자, getter, setter 코드 뿐이다. 이제 더 많은 Mapstruct 기능들을 살펴보자.

 

하나의 객체로 합치기

여러 객체의 필드값을 하나의 객체로 합치기가 가능하다. 특별히 다른 옵션을 넣는 것은 아니다.

 

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
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
@Data
@AllArgsConstructor(staticName = "of")
@NoArgsConstructor
public class UserDto {
    private String name;
}
 
@Data
@AllArgsConstructor(staticName = "of")
@NoArgsConstructor
public class AddressDto {
    private String si;
    private String dong;
}
 
@Mapper
public interface UserInfoMapper {
 
    UserInfoMapper INSTANCE = Mappers.getMapper(UserInfoMapper.class);
 
    @Mapping(source = "user.name", target = "userName")
    @Mapping(source = "address.si", target = "si")
    @Mapping(source = "address.dong", target = "dong")
    UserInfo to(UserDto user, AddressDto address);
}
 
@Generated(
    value = "org.mapstruct.ap.MappingProcessor"
)
public class UserInfoMapperImpl implements UserInfoMapper {
 
    @Override
    public UserInfo to(UserDto user, AddressDto address) {
        if ( user == null && address == null ) {
            return null;
        }
 
        UserInfo userInfo = new UserInfo();
 
        if ( user != null ) {
            userInfo.setUserName( user.getName() );
        }
        if ( address != null ) {
            userInfo.setDong( address.getDong() );
            userInfo.setSi( address.getSi() );
        }
 
        return userInfo;
    }
}
cs

 

이미 생성된 객체에 매핑

새로운 인스턴스를 생성하는 것이 아니라, 기존에 이미 생성되어 있는 객체에 매핑이 필요한 경우이다.

 

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
32
@Mapper
public interface UserInfoMapper {
 
    UserInfoMapper INSTANCE = Mappers.getMapper(UserInfoMapper.class);
 
    @Mapping(source = "user.name", target = "userName")
    @Mapping(source = "address.si", target = "si")
    @Mapping(source = "address.dong", target = "dong")
    void write(UserDto user, AddressDto address, @MappingTarget UserInfo userInfo);
}
 
@Generated(
    value = "org.mapstruct.ap.MappingProcessor"
)
public class UserInfoMapperImpl implements UserInfoMapper {
 
    @Override
    public void write(UserDto user, AddressDto address, UserInfo userInfo) {
        if ( user == null && address == null ) {
            return;
        }
 
        if ( user != null ) {
            userInfo.setUserName( user.getName() );
        }
        if ( address != null ) {
            userInfo.setDong( address.getDong() );
            userInfo.setSi( address.getSi() );
        }
    }
}
 
cs

 

타입 변환

대부분의 암시적인 자동 형변환이 가능하다. (Integer -> String ...) 다음은 조금 유용한 기능이다.

1
2
3
4
5
6
7
8
9
@Mapper
public interface CarMapper {
 
    @Mapping(source = "price", numberFormat = "$#.00")
    CarDto carToCarDto(Car car);
 
    @IterableMapping(numberFormat = "$#.00")
    List<String> prices(List<Integer> prices);
}
cs

 

만약 클래스에 있는 List<Integer> 필드값을 List<String>의 다른 포맷으로 변경하고 싶다면 @IterableMapping 어노테이션을 이용하자. 위와 비슷하게 날짜 데이터를 문자열로 변환하는 dateFormat이라는 옵션도 존재한다.

 

Source, Target mapping policy

매핑될 필드가 존재하지 않을 때, 엄격한 정책을 가져가기 위한 기능을 제공한다.

 

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
ja@Data
@AllArgsConstructor(staticName = "of")
@NoArgsConstructor
public class Car {
    private String modelName;
    private String modelColor;
    private String modelPrice;
    private String description;
}
 
@Data
@AllArgsConstructor(staticName = "of")
@NoArgsConstructor
public class CarDto {
    private String name;
    private String color;
    private Integer price;
}
 
@Mapper(unmappedTargetPolicy = ReportingPolicy.ERROR)
public interface CarMapper {
 
    CarMapper INSTANCE = Mappers.getMapper(CarMapper.class);
 
    @Mapping(source = "name", target = "modelName")
    @Mapping(source = "color", target = "modelColor")
    @Mapping(source = "price", target = "modelPrice", numberFormat = "$#.00")
    Car to(CarDto carDto);
 
}
cs

 

위 코드는 타겟이 되는 오브젝트 필드에 대한 정책을 가져간다. Car 클래스에는 description 필드가 있는데, CarDto 클래스에는 해당 필드가 존재하지 않기때문에 컴파일시 컴파일에러가 발생한다.(ERROR, IGNORE, WARN 정책존재) 만약 특정 필드는 해당 정책을 피하고 싶다면 아래와 같이 어노테이션하나를 달아준다.

 

1
2
3
4
5
6
7
8
9
10
11
12
@Mapper(unmappedTargetPolicy = ReportingPolicy.ERROR)
public interface CarMapper {
 
    CarMapper INSTANCE = Mappers.getMapper(CarMapper.class);
 
    @Mapping(source = "name", target = "modelName")
    @Mapping(source = "color", target = "modelColor")
    @Mapping(source = "price", target = "modelPrice", numberFormat = "$#.00")
    @Mapping(target = "description", ignore = true)
    Car to(CarDto carDto);
 
}
cs

 

null 정책

Source가 null이거나 혹은 Source의 특정 필드가 null일때 적용가능한 정책이 존재한다.

 

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
@Mapper(
        unmappedTargetPolicy = ReportingPolicy.ERROR,
        nullValueMappingStrategy = NullValueMappingStrategy.RETURN_DEFAULT
)
public interface CarMapper {
 
    CarMapper INSTANCE = Mappers.getMapper(CarMapper.class);
 
    @Mapping(source = "name", target = "modelName")
    @Mapping(source = "color", target = "modelColor")
    @Mapping(source = "price", target = "modelPrice", numberFormat = "$#.00")
    @Mapping(target = "description", ignore = true)
    Car to(CarDto carDto);
 
}
cs

 

위 코드는 Source 오브젝트가 null일때, 기본생성자로 필드가 비어있는 Target 오브젝트를 반환해준다.

 

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
@Mapper(
        unmappedTargetPolicy = ReportingPolicy.ERROR,
        nullValueMappingStrategy = NullValueMappingStrategy.RETURN_NULL
)
public interface CarMapper {
 
    CarMapper INSTANCE = Mappers.getMapper(CarMapper.class);
 
    @Mapping(source = "name", target = "modelName")
    @Mapping(source = "color", target = "modelColor")
    @Mapping(source = "price", target = "modelPrice", numberFormat = "$#.00")
    @Mapping(
            source = "description"
            target = "description"
            ignore = true,
            nullValuePropertyMappingStrategy = NullValuePropertyMappingStrategy.SET_TO_DEFAULT
    )
    Car to(CarDto carDto);
 
}
cs

 

위 코드는 각 필드에 대해 null 정책을 부여한다. 만약 SET_TO_DEFAULT로 설정하면, List 일때는 빈 ArrayList를 생성해주고, String은 빈문자열, 특정 오브젝트라면 해당 오브젝트의 기본 생성자 등으로 기본값을 생성해준다.

 

특정 필드 매핑 무시

특정 필드는 매핑되지 않길 원한다면 @Mapping 어노테이션에 ignore = true 속성을 넣어준다.

 

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
32
33
@Mapper(
        unmappedTargetPolicy = ReportingPolicy.ERROR,
        nullValueMappingStrategy = NullValueMappingStrategy.RETURN_NULL
)
public interface CarMapper {
 
    CarMapper INSTANCE = Mappers.getMapper(CarMapper.class);
 
    @Mapping(source = "name", target = "modelName")
    @Mapping(source = "color", target = "modelColor")
    @Mapping(source = "price", target = "modelPrice", numberFormat = "$#.00")
    @Mapping(target = "description", ignore = true)
    Car to(CarDto carDto);
 
}
 
public class MapstructTest {
 
    @Test
    public void test() {
        CarDto carDto = CarDto.of(
                "bmw x4",
                "black",
                10000,
                "description");
        Car car = CarMapper.INSTANCE.to(carDto);
        System.out.println(car.toString());
    }
}
 
result =>
 
Car(modelName=bmw x4, modelColor=black, modelPrice=$10000.00, description=null)
cs

 

매핑 전처리, 후처리

매핑하기 이전과 매핑 이후 특정 로직을 주입시킬 수 있다.

 

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
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
@Mapper(
        unmappedTargetPolicy = ReportingPolicy.ERROR,
        nullValueMappingStrategy = NullValueMappingStrategy.RETURN_NULL,
        componentModel = "spring"
)
public abstract class CarMapper {
 
    @BeforeMapping
    protected void setColor(CarDto carDto, @MappingTarget Car car) {
        if (carDto.getName().equals("bmw x4")) {
            car.setModelColor("red");
        } else {
            car.setModelColor("black");
        }
 
    }
 
    @Mapping(source = "name", target = "modelName")
    @Mapping(target = "modelColor", ignore = true)
    @Mapping(source = "price", target = "modelPrice", numberFormat = "$#.00")
    public abstract Car to(CarDto carDto);
 
    @AfterMapping
    protected void setDescription(@MappingTarget Car car) {
        car.setDescription(car.getModelName() + " " + car.getModelColor());
    }
}
 
<Generate Code>
 
@Generated(
    value = "org.mapstruct.ap.MappingProcessor"
)
@Component
public class CarMapperImpl extends CarMapper {
 
    @Override
    public Car to(CarDto carDto) {
        if ( carDto == null ) {
            return null;
        }
 
        Car car = new Car();
 
        setColor( carDto, car );
 
        car.setModelName( carDto.getName() );
        if ( carDto.getPrice() != null ) {
            car.setModelPrice( new DecimalFormat( "$#.00" ).format( carDto.getPrice() ) );
        }
        car.setDescription( carDto.getDescription() );
 
        setDescription( car );
 
        return car;
    }
}
cs

 

전처리와 후처리를 위한 메서드는 private을 사용해서는 안된다. 그 이유는 generate된 코드에 전,후 처리 메서드가 들어가는 것이 아니라 추상 클래스에 있는 메서드를 그대로 사용하기 때문이다.

 

이외에도 지원하는 기능이 너무 많다.. 다 못쓰겠다. 아래 공식 레퍼런스를 살펴보자..

 

 

MapStruct 1.3.1.Final Reference Guide

The mapping of collection types (List, Set etc.) is done in the same way as mapping bean types, i.e. by defining mapping methods with the required source and target types in a mapper interface. MapStruct supports a wide range of iterable types from the Jav

mapstruct.org

 

posted by 여성게
:

오늘 다루어볼 내용은 자바에서 Stream(java 8 stream, reactor ...)을 사용할때 유용한 팁이다. 많은 사람들이 아는 해결법일 수도 있고, 혹은 필자와 같은 스타일을 선호하지 않는 사람들도 있을 것이다. 하지만 필자가 개발할때 이러한 상황에서 조금 유용했던 Stream pipeline Tip을 간단히 소개한다.

 

중첩이 많고, 이전 스트림보다 더 이전의 스트림의 결과 값을 사용해야 할때

상황은 아래와 같은데, 간단히 바로 이전 스트림의 결과가 아닌, 더 전의 스트림 원자를 로직에서 사용하려면 대게 아래와 같이 스트림 파이프 라인을 이어나간다.

 

Mono.just("id")
	.flatMap(id -> 
		return Mono.just(service.getById(id))
				.map(entity -> {
					...
				})
	)

 

파이프라인의 시작인 id 값을 파이프라인의 중간에서 사용하려면 Stream의 pipeline을 점점 안으로 중첩해 나가면서 사용해야한다. 이렇게 된다면 복잡한 로직일 수록 점점 안쪽으로 파고드는 파이프라인이 될 것이다. 그렇다면 훨씬 가독성 좋은 코드는 어떻게 작성해 볼 수 있을까?

 

Tuple을 사용해서 넘겨주자.

reactor에 있는 Tuple을 사용해서 이전 파이프라인의 값을 뒤로 넘겨줘보자.

 

void stream() {
	Mono.just("id")
		.flatMap(str -> {
			...
			return Mono.just(Tuples.of(str, "str2"));
		})
		.flatMap(tuple -> {
			final String str = tuple.getT1();
			...
			return Mono.just("result"); 
		})
}

 

위처럼 튜플을 이용하여 해당 스트림의 결과와 이전 스트림의 결과가 나중에 쓰일 수 있도록 튜플에 값을 넣어서 넘겨준다. 이렇게 파이프라이닝하면 같은 depth로 파이프라인을 이어나갈 수 있기 때문에 가독성이 훨씬 좋다. 만약 튜플을 사용하지 않았다면 점점 depth가 깊어지는 중첩 파이프라인을 이어나가야하기 때문에 가독성도 훨씬 떨어지게 될 것이다.

 

여기까지 중첩이 많고, 이전 스트림의 결과를 미래 파이프라인에서 사용할 때 이용할 수 있는 팁이었다.

posted by 여성게
:

 

크기가 일반적으로 작고 읽기 전용 작업이 변경 작업보다 훨씬 많을 때 사용하면 좋은 라이브러리이다. iteration 중, 스레드 간의 간섭이 없어야 할때 사용하기 좋다. 즉, 스레드 안전하다.

하지만 변경 작업 같은 경우(add, set, remove) snapshot(복제본)을 이용하여 변경작업을 하기 때문에 비용이 비싸다. 내부적으로 object lock, synchronized 등이 사용되기 때문에 읽기 작업이 많고 변경작업이 적은 경우에 사용하는 것이 좋다. 그리고 해당 라이브러리는 iteration 중 remove를 지원하지 않는다.

 

해당 라이브러리가 스레드 안전한 이유는 iteration을 사용할때, iteration을 새로 생성하지 않는 이상 내부적으로 가지고 있는 List의 스냅샷에 의존하기 때문에 여러 스레드에 안전하다.

 

즉, 변경 작업과 읽기 작업에 사용되는 오브젝트가 서로 다르다.(복제본)

posted by 여성게
:

adapter의 의미는 '접속 소켓', '확장 카드', '(물건을 다른 것에) 맞추어 붙이다', '맞추다'이다. 그림처럼 전원에서 나오는 전기는 대개 교류 200V이지만 노트북은 직류 120V이다. 그러나 우리는 노트북을 사용할 때 아무런 불편 없이 노트북 선을 전원에 그대로 꽃아 사용한다. 이는 중간에 교류 200V를 직류 120V로 바꾸어 주는 무엇인가가 존재하기 때문이다. 이것이 노트북 선 중간에 붙어 있는 어댑터이다.

 

 

  1. 클래스 adapter 패턴 : 상속을 이용한 어댑터 패턴
  2. 인스턴스 adapter 패턴 : 구성(위임)을 이용한 어댑터 패턴

그림에서 client target 인터페이스를 사용하여 메서드를 호출한다. adapter에서는 adaptee 인터페이스를 사용하여 concreteMethod 호출로 변경한다. 이때 client는 중간에 adapter가 존재한다는 것을 인식하지 못한다.

 

<클래스 Adapter 패턴>

<인스턴스(객체) Adapter 패턴>

 

클래스 어댑터 패턴은 다중 상속을 허용하는 프로그래밍 언어에서만 가능한 패턴이다.

 

콘센트를 예를 들어 사용해보자. 우리나라는 기본적으로 220V 플러그인을 사용하는 나라이다. 하지만 외국은 대부분 110V를 사용하는 나라가 많기 때문에 중간에 어댑터를 이용하여 사용한다. 이것을 객체로 디자인하여 사용해보자.

 

인스턴스(객체) 어댑터 패턴

 

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
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
public interface Plugin {
    
    public void connect();
    public void disconnect();
    
}
 
public class Plugin_220V implements Plugin {
    @Override
    public void connect() {
        System.out.println("220V connect");
    }
    @Override
    public void disconnect() {
        System.out.println("220V disconnect");
    }
}
 
public interface PluginAdapter {
    public void connect();
    public void disconnect();
}
 
public class Adapter_110V implements PluginAdapter {
    
    private Plugin plugin;
    
    public Adapter_110V(Plugin plugin) {
        this.plugin=plugin;
    }
    @Override
    public void connect() {
        System.out.println("110V->220V convert");
        this.plugin.connect();
    }
    @Override
    public void disconnect() {
        System.out.println("110V->220V convert");
        this.plugin.disconnect();
    }
}
 
public class AdapterMain {
    public static void main(String[] args) {
        PluginAdapter plugin = new Adapter_110V(new Plugin_220V());
        plugin.connect();
        plugin.disconnect();
    }
}
=>result
110V->220V convert
220V connect
110V->220V convert
220V disconnect
 
cs

 

객체 어댑터 패턴은 위와 같이 구성을 이용하여 어댑터 클래스를 구성한다. 클라이언트는 어댑터 인터페이스로만 의존하고 실제적인 어댑티는 알지 못한다.

posted by 여성게
:

composite의 의미는 '합성의', '합성물', '혼합 양식'이다. 이를 통해 composite 패턴이 뭔가 합쳐진 형태임을 짐작할 수 있다. 또 composite 패턴의 구성을 보면 일반적인 트리 구조를 하고 있는데, [그림 5-34]처럼 부분-전체의 상속 구조이다. 이와 같이 표현되는 조립 객체를 컴포지트 객체(composite object)라고 한다.

composite 패턴은 사용자가 단일 객체와 복합 객체 모두 동일하게 다루도록 한 것이다. 이런 형태는 재귀적인 구조로서, 마치 파일 구조에서 디렉토리 안에 파일이 존재할 수도 있고, 또 다른 디렉토리(서브 디렉토리)가 존재할 수 있는 것과 같다. 즉 composite 패턴은 그릇(디렉토리)과 내용물(파일)을 동일시해서 재귀적인 구조를 만들기 위한 설계 패턴이다.

 

 

컴포지트 패턴을 가장 잘 설명할 수 있는 예제는 파일과 디렉토리 관계이다. 가장 간단하게 파일과 디렉토리를 구현해보자.

 

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
32
33
34
public class File {
    
    private String name;
 
    public String getName() {
        return name;
    }
 
    public void setName(String name) {
        this.name = name;
    }
    
    
}
 
public class Directory {
    
    private String name;
    private List<File> files;
    
    public String getName() {
        return name;
    }
    
    public void setName(String name) {
        this.name = name;
    }
    
    public void addFile(File file) {
        if(!files.contains(file)) files.add(file);
        else System.out.println("동일한 파일이 존재합니다.");
    }
    
}
cs

 

위와 같이 구현했다고 생각해보자. 얼핏보면 괜찮은 것 같다. 하지만 만약 디렉토리 밑에 디렉토리를 넣고 싶다면? 어떻게 해야할까? 여기서 생각해보자. 디렉토리 안에는 파일도 들어갈 수 있고 또 다른 디렉토리가 들어갈 수도 있다. 여기서 파일하나가 단수 객체라면 디렉토리는 복수 객체가 될 수 있다. 또한 최상위는 하나의 디렉토리로 이루어지고 그 밑으로 디렉토리 및 파일이 들어간다. 즉, 전체 관계가 하나의 디렉토리이고 그 밑에 부분이 디렉토리 또는 파일이 될 수 있다. 이렇게 단수 혹은 복수를 동일한 인터페이스로 다룰 수 있게 하는 전체-부분관계를 구현할때 가장 유용한 것이 컴포지트 패턴이다. 

 

컴포지트 패턴을 다시 클래스다이어그램으로 나타내면 아래와 같다.

 

 

Component - 구체적인 부분, 즉 Leaf 클래스와 전체에 해당하는 Composite 클래스에 공통 인터페이스를 제공한다.

Leaf - 구체적인 부분 클래스로 Composite 객체의 부품으로 설정한다.

Composite - 전체 클래스로 복수 개의 Component를 갖도록 정의한다. 그러므로 복수 개의 Leaf, 심지어 복수 개의 Composite 객체를 부분으로 가질 수 있다.

 

우리는 맨 처음의 그림과는 다르게 파일 시스템을 설계할 것이다. 파일 시스템은 추상적으로 보면 트리와 같은 구조를 가지고 있다. 즉, Component를 Node로 표현하고 Leaf로 File Composite로 Directory를 표현할 것이다. 바로 구현해보자.

 

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
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
public abstract class Node {
    
    private String name;
    private int depth=0;
    
    public Node(String name) {
        this.name=name;
    }
 
    public String getName() {
        return name;
    }
 
    public void setName(String name) {
        this.name = name;
    }
 
    public int getDepth() {
        return depth;
    }
 
    public void setDepth(int depth) {
        this.depth = depth;
    }
    
    public abstract int getSize();
    public abstract void print();
}
 
public class File extends Node{
    
    private int size;
    
    public File(String name, int size) {
        super(name);
        this.size=size;
    }
 
    @Override
    public int getSize() {
        return this.size;
    }
 
    @Override
    public void print() {
        System.out.println("File - "+getName());
    }
    
}
 
public class Directory extends Node{
    
    private List<Node> nodes = new ArrayList<>();
    
    public Directory(String name) {
        super(name);
    }
    
    public void addNode(Node node) {
        nodes.add(node);
    }
    
    public void removeNode(Node node) {
        nodes.remove(node);
    }
    
    public int getSize() {
        int size = nodes.size();
        System.out.println("Directory size - "+size);
        return size;
    }
    
    public void print() {
        
        for(Node node : nodes) {
            node.print();
        }
        
    }
    
}
cs

 

완벽한 구현은 아니지만 파일 시스템을 컴포지트 패턴으로 구현 가능하다라는 정도만 알자!

posted by 여성게
:

abstract factory는 '추상적인 공장'이란 뜻으로, 밑의 그림과 같이 여러 개의 concrete Product 추상화시킨 것이다. 따라서 구체적인 구현은 concrete Product 클래스에서 이루어진다. abstract factory에서는 사용자에게 인터페이스(API)를 제공하고, 인터페이스만 사용해서 부품을 조립하여 만든다. 즉 추상적인 부품을 조합해서 추상적인 제품을 만든다.

 

 

2019/08/19 - [디자인패턴] - 디자인패턴 - 팩토리 메소드 패턴(Factory Method pattern)

 

디자인패턴 - 팩토리 메소드 패턴(Factory Method pattern)

factory는 '공장'이란 뜻이고, 공장은 물건을 만드는 곳이다. 여기서 물건에 해당되는 것이 바로 인스턴스이다. factory method 패턴은 상위 클래스에서 객체를 생성하는 인터페이스를 정의하고, 하위 클래스에서..

coding-start.tistory.com

 

이전 포스팅에서 다루어봤던 팩토리 메소드 패턴의 확장판이라고 볼수 있는 것이 추상 팩토리 패턴이다. 팩토리 메소드 패턴을 다시 떠올려보자. 엘리베이터는 특정 스케쥴링 방식에 따라 다른 방식으로 동작한다. 즉, 스케쥴러 클래스의 선택에 따라 엘리베이터의 동작이 달라지는 것이다. 하지만 이런 경우를 생각해보자 !

 

엘리베이터 제조업체가 여러개가 있다. LG,Samsung,Hundai 등의 많은 제조업체가 있고 해당 업체의 엘리베이터를 사용하려면 각각 제조업체의 모터,문,렘프 등을 사용해야한다.(이번 예제에서는 각 업체마다 모터, 문만 다룬다.) 경우에 따라 부품의 수가 더 많아 질 수도 있고 제조업체도 더욱 많아 질 수도 있다. 

 

만약 기존에 사용했던 팩토리 메소드 패턴을 사용한다면 어떻게 될까? Motor,Door라는 인터페이스가 있고 각각을 구현한 LGMotor,LGDoor,SamsungMotor,SamsungDoor 등 업체마다 다른 부품에 해당하는 클래스를 생성해주어야 하고 각각을 생성해주는 팩토리 클래스를 만들어주어야 한다.(LGMotorFactor,SamsungMotorFactory,HundaiFactory...) 3개의 업체라면 객체 생성을 담당하는 팩토리 클래스가 6개가 생성된다.(MotorFactory - LG,Samsung,Hundai / DoorFactory - LG,Samsung,Hundai) 딱 봐도 비효율적일 것같다라는 느낌이 든다.

 

여기서 핵심은 특정 제조업체의 엘리베이터를 이용하면 그들의 부품만 사용한다는 뜻이다. 즉, 관련있는 객체(모터,문)들이 제조업에 따라 같이 생성이 된다는 것이다. 팩토리 메소드 패턴은 한종류의 객체를 생성하는 롤을 갖고 있다면 추상 팩토리 패턴은 다수의 연관있는 객체들의 일련의 생성을 담당하고 있다고 보면 된다. 바로 구현해보자.

 

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
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
public abstract class AbstractFactory {
    
    public abstract Motor createMotor();
    public abstract Door createDoor();
    
    enum Vendor{
        LG,SAMSUNG,HUNDAI;
    }
    
    public static AbstractFactory getFactory(Vendor vendor) {
        
        AbstractFactory factory = null ;
        
        switch (vendor) {
        case LG:
            factory = LGElevatorFactory.getInstance();
            break;
        case SAMSUNG:
            factory = SamsungElevatorFactory.getInstance();
            break;
        case HUNDAI:
            factory = HundaiElevatorFactory.getInstance();
            break;
        default:
            break;
        }
        
        return factory;
        
    }
    
}
 
public class LGElevatorFactory extends AbstractFactory{
    
    private static final LGElevatorFactory FACTORY = new LGElevatorFactory();
    
    public static AbstractFactory getInstance() {
        return FACTORY;
    }
 
    @Override
    public Motor createMotor() {
        return new LGMotor();
    }
 
    @Override
    public Door createDoor() {
        return new LGDoor();
    }
    
}
cs

 

추상 팩토리 클래스를 하나 만들어주고 각 제조사에 해당하는 팩토리클래스를 생성해준다. 이전 팩토리메소드 패턴과 같이 부품마다 별도의 팩토리 클래스를 두는 것이 아니라 각 제조사마다 팩토리 메소드를 만들어서 관련있는 모든 부품에 대한 생성을 제조사 팩토리 클래스에 위임한다. 또한 모든 제조사의 팩토리 메소드는 동일한 타입의 부품을 생성하는 팩토리 메소드를 가지니, 추상 클래스도 추상 팩토리 클래스를 만들어 상속받게 한다.

 

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
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
public abstract class Door {
    
    enum DoorStatus{
        OPEN,CLOSE;
    }
    
    public Door() {
        this.status=DoorStatus.CLOSE;
    }
    
    private DoorStatus status;
 
    public DoorStatus getStatus() {
        return status;
    }
 
    public void setStatus(DoorStatus status) {
        this.status = status;
    }
    
    public void open() {
        if(this.status.equals(DoorStatus.OPEN)) return;
        this.status=DoorStatus.OPEN;
        System.out.println("문을 엽니다.");
    }
    
    public void close() {
        if(this.status.equals(DoorStatus.CLOSE)) return;
        this.status=DoorStatus.CLOSE;
        System.out.println("문을 닫습니다.");
    }
    
}
 
public abstract class Motor {
    
    private Door door;
    private MotorStatus status;
    
    public Motor() {
        this.status=MotorStatus.STOP;
    }
    
    enum MotorStatus{
        MOVING,STOP;
    }
    
//템플릿 메소드 패턴 적용.
    public void move() {
        //모터가 이미 움직이고 있다면 아무런 행동을 하지 않는다.
        if(getStatus().equals(MotorStatus.MOVING)) return;
        //문이 열려있다면 닫는다.
        if(door.getStatus().equals(DoorStatus.OPEN)) door.close();
        doMove();
        //모터를 이동중으로 설정한다.
        setStatus(MotorStatus.MOVING);
    }
    
    /*
     * 업체마다 doMove의 행동은 다르다.
     */
    public abstract void doMove();
 
    public MotorStatus getStatus() {
        return status;
    }
 
    public void setStatus(MotorStatus status) {
        this.status = status;
    }
 
    public Door getDoor() {
        return door;
    }
 
    public void setDoor(Door door) {
        this.door = door;
    }
    
    
    
}
cs

 

각 부품에 대한 추상클래스이다. 각 제조사 부품마다 동일한 행동을 하지만 자세한 구동 프로세스가 다를 수 있으므로 모터 추상클래스는 템플릿 메소드 패턴을 이용하여 상속받게 하였다.

 

1
2
3
4
5
6
7
8
9
10
11
12
13
public class LGMotor extends Motor{
 
    @Override
    public void doMove() {
        System.out.println("LG motor 가동");
    }
    
    
}
 
public class LGDoor extends Door{
 
}
cs

 

LG모터와 문 코드이다.

 

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
public class AbstractFactoryMain {
 
    public static void main(String[] args) {
        
        AbstractFactory factory = AbstractFactory.getFactory(Vendor.LG);
        
        Motor motor = factory.createMotor();
        Door door = factory.createDoor();
        motor.setDoor(door);
        
        door.open();
        motor.move();
        door.open();
    }
 
}
cs

 

모터와 문에 대한 클래스의 구현체를 생각할 필요가 없어졌다. 단순히 추상 팩토리 클래스에 벤더값만 전달하면 모든 연관있는 부품에 대한 생성은 각 업체마다의 팩토리 클래스가 담당하게 된다.

 

어떻게 보면 팩토리 메소드 패턴의 확장판이라고 볼수 있다. 추상 팩토리 패턴은 일련의 관련있는 객체의 생성을 캡슐화 혹은 추상화 하기 위해 사용하는 패턴이다. 이제는 클라이언트 코드에서 업체가 바뀌어도 코드의 변경을 필요 없게 되었다. 단순히 어떠한 업체인지 벤더 값만 전달해주고 팩토리 클래스가 생성해주는 모터와 문의 구현체를 몰라도되고 단순히 사용만 하면 된다.

posted by 여성게
:

factory는 '공장'이란 뜻이고, 공장은 물건을 만드는 곳이다. 여기서 물건에 해당되는 것이 바로 인스턴스이다. factory method 패턴은 상위 클래스에서 객체를 생성하는 인터페이스를 정의하고, 하위 클래스에서 인스턴스를 생성하도록 하는 방식이다. 즉 상위 클래스에서는 인스턴스를 만드는 방법만 결정하고, 구체적인 클래스 이름은 뒤로 미룬다. 따라서 객체를 생성하는 인터페이스와 실제 객체를 생성하는 클래스를 분리할 수 있다. 예를 들어 그림처럼 추상 단계에서는 생성하려는 객체의 클래스를 정확히 지정하지 않고, concreteCreator 클래스에서 인스턴스를 생성한다.

 

 

다시 쉽게 정리하면 특정 클래스에서 사용할 클래스를 직접 new로 생성하면 강한 결합이 생기게 된다. 즉, 무엇인가 전략이 바뀔때마다 코드의 변경이 생기게 된다. 여기서 핵심은 사용할 인스턴스를 생성하는 코드를 특정 클래스가 생성하도록 함으로써 클래스 간의 결합도를 낮추게 된다는 것이다. 위의 그림에서 전략은 Product로 볼 수 있고, concreteCreator가 적절한 Product를 생성하는 역할을 담당하게 된다.

 

하나 예를 들어보자. 건물 내의 엘리베이터는 시간대에 따라 작동 방식이 달라져야한다. 오전 출근시간과 점심시간에는 많은 사람들이 엘리베이터를 사용하게 된다. 그렇다면 이 시간대에는 처리량이 극대화될 수 있는 엘리베이터 스케쥴링방식을 사용해야한다. 그 이후 시간대에는 단순히 기다리는 시간이 최소화되도록 하는 스케쥴링방식을 사용한다. 여기서 또 하나의 조건은 특정 스케쥴링 방식을 직접 선택할 수도 있어야한다.

 

아래와 같이 단순한 구현이 가능하다.

 

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
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
public class ElevatorManager {
    
    List<Elevator> elevators;
    
    public ElevatorManager(int count) {
        elevators = new ArrayList<>(count);
        
        IntStream.range(0, count).forEach(i->elevators.add(new Elevator()));
        
    }
    
    public void requestElevator(int destination, Scheduler schedule) {
        
        ElevatorScheduler scheduler = null ;
        
        int hour = LocalDateTime.now().getHour();
        
        if(schedule.equals(Scheduler.DYNAMIC)) {
            System.out.println("스케줄링방식 - "+schedule);
            if( (9<=hour && hour<=10|| (12<=hour && hour<=1)) {
                scheduler = new ThroughputScheduler();
            }else {
                scheduler = new ResponseTimeScheduler();
            }
        }else if(schedule.equals(Scheduler.THROUGHPUT)) {
            System.out.println("스케줄링방식 - "+schedule);
            scheduler = new ThroughputScheduler();
        }else if(schedule.equals(Scheduler.RESPONSETIME)) {
            System.out.println("스케줄링방식 - "+schedule);
            scheduler = new ResponseTimeScheduler();
        }
        
        int elevatorIndex = scheduler.selectElevator(elevatorCount());
        
        elevators.get(elevatorIndex).gotoElevator(destination, elevatorIndex);
    }
    
    private int elevatorCount() {
        return this.elevators.size();
    }
    
    enum Scheduler{
        THROUGHPUT,RESPONSETIME,DYNAMIC;
    }
}
 
public class Elevator {
    
    public void gotoElevator(int destination, int eleNum) {
        System.out.println(eleNum+"번 엘리베이터가 "+destination+"층으로 가는 중입니다.");
    }
}
 
public interface ElevatorScheduler {
    
    public int selectElevator(int count);
    
}
 
public class ThroughputScheduler implements ElevatorScheduler {
 
    @Override
    public int selectElevator(int count) {
        return (int)(Math.random()*count)+1;
    }
 
}
 
public class ResponseTimeScheduler implements ElevatorScheduler {
 
    @Override
    public int selectElevator(int count) {
        return (int)(Math.random()*count)+1;
    }
 
}
cs

 

문제가 무엇일까? 만약에 스케쥴링방식이 추가되거나 혹은 스케쥴링 선택 정책이 변경되었다면 ElevatorManager의 requestEleveator 메소드가 변경되어야 할 것이다. 즉, 특정 상황에 따라 알고리즘을 수행하게 되는 객체(스케쥴러)가 달라지기 때문에 혹은 정책이 바뀌면서 알고리즘을 수행하는 객체가 바뀌게 된다. 그렇다면 이러한 상황에서는 어떻게 하면 좋을까? 바로 팩토리 메소드 패턴이다. 팩토리 메소드 패턴으로 객체 생성을 담당하도록 별도 클래스 및 메소드로 분리하는 것이다. 그렇다면 ElevatorManager는 객체 생성을 직접적으로 신경쓸 필요가 없고 책임을 팩토리 클래스로 위임해버리는 것이다. 

 

팩토리 메소드 패턴으로 변경해보자.

 

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
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
public class ElevatorManager {
    
    List<Elevator> elevators;
    
    public ElevatorManager(int count) {
        elevators = new ArrayList<>(count);
        
        IntStream.range(0, count).forEach(i->elevators.add(new Elevator()));
        
    }
    
    public void requestElevator(int destination, Scheduler schedule) {
        
        Factory factory = new SchedulerFactory();
        
        ElevatorScheduler scheduler = factory.createScheduler(schedule);
        
        int elevatorIndex = scheduler.selectElevator(elevatorCount());
        
        elevators.get(elevatorIndex).gotoElevator(destination, elevatorIndex);
    }
    
    private int elevatorCount() {
        return this.elevators.size();
    }
    
    enum Scheduler{
        THROUGHPUT,RESPONSETIME,DYNAMIC;
    }
}
 
public abstract class Factory {
    
    public abstract ElevatorScheduler createScheduler(Scheduler scheduler);
    
}
 
public class SchedulerFactory extends Factory{
 
    @Override
    public ElevatorScheduler createScheduler(Scheduler schedule) {
        
        ElevatorScheduler scheduler = null ;
        
        int hour = LocalDateTime.now().getHour();
        
        if(schedule.equals(Scheduler.DYNAMIC)) {
            System.out.println("스케줄링방식 - "+schedule);
            if( (9<=hour && hour<=10|| (12<=hour && hour<=1)) {
                scheduler = new ThroughputScheduler();
            }else {
                scheduler = new ResponseTimeScheduler();
            }
        }else if(schedule.equals(Scheduler.THROUGHPUT)) {
            System.out.println("스케줄링방식 - "+schedule);
            scheduler = new ThroughputScheduler();
        }else if(schedule.equals(Scheduler.RESPONSETIME)) {
            System.out.println("스케줄링방식 - "+schedule);
            scheduler = new ResponseTimeScheduler();
        }
        
        return scheduler;
        
    }
    
    
}
cs

 

위와 같이 코드를 변경하였다. 이제 ElevatorManager는 자신의 일만 신경쓰면 된다. 스케쥴러 선택은 팩토리 클래스에게 맡겨버리면 되기 때문이다. 혹시나 스케쥴링 정책 혹은 새로운 스케쥴링 방식이 생겨나도 다른 곳은 아무곳도 신경쓸 필요가 없다. 단순히 팩토리 클래스에만 변경해주면 되기 때문이다. 

 

다시 한번 팩토리 메서드 패턴의 정의를 보자. 

 

객체를 생성하는 코드를 별도의 클래스/메서드로 분리함으로써 객체 생성 방식의 변화에 대비하는데 유용한 패턴이다.
posted by 여성게
: