Spring boot

[리팩토링] if 문 없이 객체 만들어보자 - 팩토리 패턴, 상속관계 엔티티 생성

개발중인 감자 2023. 10. 28. 23:12

이 게시글은 pjh3749님의 게시글을 참고하였습니다.

 

📌 문제 상황

 

프로젝트를 하던 중 .. 상속관계에 있는 엔티티들을 매번 빌더로 만들어야하는 상황이었다. 

방법이 없어 보여 switch 문 사용해서 썼지만,

프로젝트 팀원분이 조금 더 깔끔하게 만들면 보자 해서 리팩토링을 하기로 했다. 

 

 

📌 기존 코드

부모 클래스 : itinerary 
자식 클래스 :  Movement, Lodgement, Stay
부모 클래스의 type 필드에 따라 자식들을 구분해서 객체를 만들어야하는 상황.
참고로 밑의 코드는 만든 객체들을 jpa를 이용해 db에 저장하는 상황이다. 
    for (ItineraryRequest ir : itineraryRequests) {
        switch (ir.getType()) {
            case MOVEMENT:
                itinerary = itineraryRepository.save(
                        Movement.builder().trip(trip).itineraryName(ir.getMovementName())
                                .itineraryOrder(ir.getOrder()).departureDate(ir.getStartDate())
                                .arrivalDate(ir.getEndDate())
                                .departurePlace(ir.getDeparturePlace())
                                .arrivalPlace(ir.getArrivalPlace()).transportation(ir.getItem())
                                .build()
                );
                itineraryResponse = MovementResponse.fromEntity((Movement) itinerary);
                break;
            case LODGEMENT:
                itinerary = itineraryRepository.save(
                        Lodgement.builder().trip(trip).itineraryName(ir.getItem())
                                .itineraryOrder(ir.getOrder()).checkIn(ir.getStartDate())
                                .checkOut(ir.getEndDate()).build()
                );
                itineraryResponse = LodgementResponse.fromEntity((Lodgement) itinerary);
                break;
            case STAY:
                itinerary = itineraryRepository.save(
                        Stay.builder().trip(trip).itineraryName(ir.getItem())
                                .itineraryOrder(ir.getOrder()).departureDate(ir.getStartDate())
                                .arrivalDate(ir.getEndDate()).build()
                );
                itineraryResponse = StayResponse.fromEntity((Stay) itinerary);
                break;
        }
        itineraryResponseList.add(itineraryResponse);
    }

 

위의 코드를 보면 딱 느끼겠지만, 정말 지저분하다

자식 클래스에는 많은 필드들이 따로 존재하기 때문에, 빌더 패턴을 이용해 인스턴스를 생성해줄 때 많은 양의 코드를 요구한다. 

그래서 팩토리 패턴을 활용하여 정리를 해보았다. 

 

 

📌 팩토리 패턴?

팩토리 패턴은 객체를 생성하는 인터페이스를 미리 정의하지만,
인스턴스를 만들 클래스의 결정은 서브 클래스 쪽에서 결정하는 패턴이다.
팩토리 클래스란 들어오는 인자에 따라서 하나의 자식클래스의 인스턴스를 반환해주는 역할을 하는 클래스이다.

 

📝 팩토리 패턴의 장점

조건에 따른 객체의 생성을 서브 클래스에게 위임함으로써 객체간의 결합도가 낮아질 수 있다. 

그에 따라 코드가 훨씬 깔끔해지고 유지보수가 쉬워진다. 

 

📝 다음과 같을 때 활용

- if문을 많이 써서 객체를 생성해야할 때 (나같은 상황)

- 어떤 조건문을 써서 객체를 생성해야할지 모를때 

- 객체 생성 책임을 딴 클래스에게 미루고 싶을 때 

 

 

⭐️ Function을 활용하자 (함수형 인터페이스)

📝 Function은 함수형 인터페이스를 구현할 수 있는 라이브러리로 java.util.function에 있다.
java8부터 사용 가능하다.
매개변수가 1개일 때에는 Function<T, R>을,
매개변수가 2개일 때에는 BiFunction<T, U, R>을,
매개변수가 int형일 때에는 intFunction<R>을 사용하면 된다. 

 

✔️ 나같은 경우, trip 필드와 type 필드를 통해 객체를 생성해야했으므로 BiFunction을 사용했다. 

 

👩🏻‍💻 구현 과정

1. 일단 itineraryFactory를 만든다.

2. BiFunction을 담는 HashMap을 선언한다. private로 하고, 반환할 수 있는 메소드를 따로 만들 것이다. 

public class ItineraryFactory {
    private final static Map<ItineraryType, BiFunction<Trip,
            ItineraryRequest, Itinerary>> map = new HashMap<>();

 

3. 다음과 같이 값을 put 해주자. 이 때 빌더 패턴을 사용한 객체를 생성할 수 있는 함수형 인터페이스를 만드는 것이다. 

static {
        map.put(ItineraryType.MOVEMENT, (trip, ir) ->
            Movement.builder()
                    .trip(trip).itineraryName(ir.getMovementName()).itineraryOrder(ir.getOrder())
                    .itineraryType(ir.getType())
                    .departureDate(ir.getStartDate()).arrivalDate(ir.getEndDate())
                    .departurePlace(ir.getDeparturePlace()).arrivalPlace(ir.getArrivalPlace())
                    .transportation(ir.getItem())
                    .build()
        );

        map.put(ItineraryType.LODGEMENT, (trip, ir) ->
            Lodgement.builder()
                    .trip(trip).itineraryName(ir.getItem())
                    .itineraryOrder(ir.getOrder()).itineraryType(ir.getType())
                    .checkIn(ir.getStartDate()).checkOut(ir.getEndDate())
                    .build()
        );

        map.put(ItineraryType.STAY, (trip, ir) ->
            Stay.builder()
                    .trip(trip).itineraryName(ir.getItem())
                    .itineraryOrder(ir.getOrder()).itineraryType(ir.getType())
                    .departureDate(ir.getStartDate()).arrivalDate(ir.getEndDate())
                    .build()
        );
    }

 

4. 생성된 객체를 반환해줄 수 있는 메소드를 만들자. 객체 생성 메소드는 public으로 선언한다. 

public static Itinerary getItineraryEntity(Trip trip, ItineraryRequest ir) {
        BiFunction<Trip, ItineraryRequest, Itinerary> function = map.get(ir.getType());
        return function.apply(trip, ir);
    }

 

5. 다음과 같이 사용할 수 있다. 위의 기존코드와 비교했을 때 훨씬 로직이 깔끔해졌다. 😃 (약 30개의 줄 -> 6개의 줄)

for (ItineraryRequest ir : itineraryRequests) {
	Itinerary itinerary = itineraryRepository.save(
		ItineraryFactory.getItineraryEntity(trip, ir)
	);

	itineraryResponseList.add(
 		ItineraryResponseFactory.getItineraryResponse(itinerary)
	);
}

 

 

🙋🏻‍♀️ 전체 코드 

/**
 * request의 여정 타입이 들어오면 그에 따라 entity를 반환해주는
 * 팩토리 패턴을 적용한 클래스
 */
public class ItineraryFactory {
    private final static Map<ItineraryType, BiFunction<Trip,
            ItineraryRequest, Itinerary>> map = new HashMap<>();

    static {
        map.put(ItineraryType.MOVEMENT, (trip, ir) ->
            Movement.builder()
                    .trip(trip).itineraryName(ir.getMovementName()).itineraryOrder(ir.getOrder())
                    .itineraryType(ir.getType())
                    .departureDate(ir.getStartDate()).arrivalDate(ir.getEndDate())
                    .departurePlace(ir.getDeparturePlace()).arrivalPlace(ir.getArrivalPlace())
                    .transportation(ir.getItem())
                    .build()
        );

        map.put(ItineraryType.LODGEMENT, (trip, ir) ->
            Lodgement.builder()
                    .trip(trip).itineraryName(ir.getItem())
                    .itineraryOrder(ir.getOrder()).itineraryType(ir.getType())
                    .checkIn(ir.getStartDate()).checkOut(ir.getEndDate())
                    .build()
        );

        map.put(ItineraryType.STAY, (trip, ir) ->
            Stay.builder()
                    .trip(trip).itineraryName(ir.getItem())
                    .itineraryOrder(ir.getOrder()).itineraryType(ir.getType())
                    .departureDate(ir.getStartDate()).arrivalDate(ir.getEndDate())
                    .build()
        );
    }

    public static Itinerary getItineraryEntity(Trip trip, ItineraryRequest ir) {
        BiFunction<Trip, ItineraryRequest, Itinerary> function = map.get(ir.getType());
        return function.apply(trip, ir);
    }
}