[Study] 우아한스터디 2주차 회고

박진우·2023년 11월 18일
0

우아한스터디

목록 보기
2/3
post-thumbnail

두번째 스터디 활동

2주차 활동은 '내 코드가 그렇게 이상한가요?'책의 4장~8장 내용을 바탕으로 진행되었습니다!
지금부터 스터디에서 알아보았던 내용들에 대해 회고해보는 시간을 갖도록 하겠습니다🤗

1. 응집도가 낮은 컬렉션 처리 : 일급 컬렉션을 통한 응집도 향상

일급 컬렉션 : 컬렉션과 관련된 로직을 캡슐화하는 디자인 패턴

응집도가 낮은 컬렉션이 좋지 못한 이유는 다음과 같습니다.

  • 코드의 중복이 발생한다
  • 변경에 취약하다
  • 유지보수가 어려워진다

응집도가 낮은 컬렉션은 그 내부의 변경이 발생할 경우, 그 영향을 받는 모든 클래스를 찾아 수정해야하는 번거로움을 수반합니다.

[좋지 못한 예시]

public class OrderService {
    private List<Order> orders;

    public boolean isEligibleOrders() {
        return orders.stream().anyMatch(order -> 
            "processing".equals(order.getStatus()) &&
            order.getAmount() > 1000 &&
            order.getOrderDate().isEqual(LocalDate.now())
        );
    }
}

public class ReportService {
    private List<Order> orders;

    public List<Order> findEligibleOrders() {
        return orders.stream().filter(order -> 
            "processing".equals(order.getStatus()) &&
            order.getAmount() > 1000 &&
            order.getOrderDate().isEqual(LocalDate.now())
        ).collect(Collectors.toList());
    }
}

위 코드로 보아 알 수 있는 부분은 다음과 같습니다.

  • 중복 코드 : 동일한 조건 검사 로직이 여러 클래스에 걸쳐 나타남
  • 낮은 응집도 : 서로 관련된 작업이 여러 클래스에 흩어져 관리가 힘들어짐
  • 낮은 재사용성 : 같은 조건의 로직이 다른 곳에서 필요할 때 재사용하기 힘들어짐

이러한 컬렉션의 '응집도가 낮아지는 문제'일급 컬렉션 패턴을 사용하여 해결할 수 있습니다.

[일급 컬렉션의 예시]

public class Orders {
    private List<Order> orders;

    public Orders(List<Order> orders) {
        this.orders = orders;
    }

    public boolean isEligibleOrders() {
        return orders.stream().anyMatch(this::matchesCriteria);
    }

    public List<Order> findEligibleOrders() {
        return orders.stream().filter(this::matchesCriteria).collect(Collectors.toList());
    }

    private boolean matchesCriteria(Order order) {
        return "processing".equals(order.getStatus()) &&
               order.getAmount() > 1000 &&
               order.getOrderDate().isEqual(LocalDate.now());
    }
}

public class OrderService {
    private Orders orders;

    public boolean isEligibleOrders() {
        return orders.isEligibleOrders();
    }
}

public class ReportService {
    private Orders orders;

    public List<Order> findEligibleOrders() {
        return orders.findEligibleOrders();
    }
}

위 코드와 같이 일급 컬렉션 패턴을 위한 Order 클래스 활용을 통해 알 수 있는 개선점은 다음과 같습니다.

  • matchesCriteria() 메서드를 통해 필터링 조건을 한 곳에서 관리하여 유지보수가 편리해짐
  • Order 클래스 내에 주문 컬렉션과 관련된 로직을 캡슐화함으로써 응집도가 높아짐
  • Order 클래스를 통해 코드의 재사용성이 향상됨

위 내용만을 보게된다면, 일급 컬렉션 패턴코드의 재사용성을 향상시키고 응집도를 높이는 좋은 효과를 가져온다고 볼 수 있습니다.

하지만 우리가 모든 상황에서 일급 컬렉션을 사용하지 않는다는 것은 곧 부작용의 가능성도 배제할 수는 없습니다.
스터디에서 알아보았던 내용을 후술해보겠습니다.

[그렇다면, 일급 컬렉션을 왜 자주 사용하지 못하는가?]

결론부터 말하자면, 이유는 다음과 같습니다.

"많은 책들이 일급 컬렉션을 추천하지만, 매끄럽고 깔끔하게 사용하는 코드를 만들기가 어렵다"

일급 컬렉션을 사용하면서, 의도하지 않은 n+1 또는 Lazy를 일으키는 경우가 있으므로,
POJO Entity라면 크게 상관없지만 JPA Entity라면 일급 컬렉션을 조심히 다루어야 한다는 점입니다.

반면, 자주 사용하는 로직이 일급 컬렉션을 사용함으로써 재사용 및 관리가 용이해진다는 이점을 바탕으로
Response값을 DTO(Data Transfer Object)로 변환할 때 사용하는 것은 고민해볼만한 점인 것 같습니다.


2. 결합도와 책무 : 인터페이스를 통한 느슨한 결합

책에서는 다음과 같은 내용을 바탕으로 느슨한 결합을 설명합니다.

관심사에 따라 분리해서 독립되어 있는 구조를 느슨한 결합이라고 부릅니다.

interface DiscountPolicy {
    int getDiscountPrice(int price);
}


class RegularDiscountPolicy {
    private static final int MIN_AMOUNT = 0;
    private static final int DISCOUNT_AMOUNT = 4000;
    final int amount;

    RegularDiscountPolicy(int price) {
        int discountedAmount = price - DISCOUNT_AMOUNT;
        if (discountedAmount < MIN_AMOUNT) {
            discountedAmount = MIN_AMOUNT;
        }

        amount = discountedAmount;
    }
}


class SummerDiscountPolicy {
    private static final int MIN_AMOUNT = 0;
    private static final int DISCOUNT_AMOUNT = 3000;
    final int amount;

    SummerDiscountPolicy(int price) {
        int discountedAmount = price - DISCOUNT_AMOUNT;
        if (discountedAmount < MIN_AMOUNT) {
            discountedAmount = MIN_AMOUNT;
        }

        amount = discountedAmount;
    }
}

위의 예시 코드는 클래스가 일반 할인 가격, 여름 할인 가격으로 구분되어 있습니다.
책에서는 이렇게 관심사를 바탕으로 독립된 구조를 갖춤으로써, 할인과 관련된 사양이 변경되어도 서로 영향을 주지 않는 상태가 느슨한 결합이라고 설명하고 있습니다.

이와 관련하여 스터디 멤버 중 한분께서 이러한 질문을 남기셨습니다.

제 생각에는 클라이언트가 일반 할인 클래스나 여름 할인 클래스를 직접 의존하지 않고, 할인 정책 인터페이스만 의존하고 런타임 때 실제 할인 정책에 따라 구현 클래스가 정해져야 느슨한 결합이라고 생각합니다.

interface DiscountPolicy {
    int getDiscountPrice(int price);
}


class RegularDiscountPolicy implements DiscountPolicy {
    private static final int MIN_AMOUNT = 0;
    private static final int DISCOUNT_AMOUNT = 4000;

    @Override
    public int getDiscountPrice(int price) {
        int discountedAmount = price - DISCOUNT_AMOUNT;
        if (discountedAmount < MIN_AMOUNT) {
            discountedAmount = MIN_AMOUNT;
        }

        return discountedAmount;
    }
}


class SummerDiscountPolicy implements DiscountPolicy {
    private static final int MIN_AMOUNT = 0;
    private static final int DISCOUNT_AMOUNT = 3000;

    @Override
    public int getDiscountPrice(int price) {
        int discountedAmount = price - DISCOUNT_AMOUNT;
        if (discountedAmount < MIN_AMOUNT) {
            discountedAmount = MIN_AMOUNT;
        }

        return discountedAmount;
    }
}

이에 대해 토론을 진행하였고, 내용을 후술해보도록 하겠습니다.

[정말 느슨한 결합이란 무엇일까?]

위의 코드를 보았을 때, 결과적으로 의존 관계가 존재하는 것은 맞으므로 느슨한 결합으로 보기엔 조금이나마 모호한 부분이 있습니다.
따라서, 클래스 자체를 분리하는 행위가 곧 느슨한 결합을 만든다고 볼 수 있는 것입니다.

굳이 말하자면 인터페이스 분리하는 것은 결합도와는 다른 영역으로 볼 수 있습니다.
물론 인터페이스를 사용함으로써 확장의 편리성을 높이는 부분도 고려해볼만 합니다.
그러나 결합도만을 기준으로 보았을 때, 인터페이스를 사용해서 구현체를 감추는 것과 클래스를 나눠서 결합도를 낮추는게 영역이 다르므로 관심사별로 클래스를 분리하여 결합도를 낮추는 것이 더 효과적인 방법이 될 것이라고 보았습니다.


3. 정적 팩토리 메서드의 활용

책에서는 초기화 로직의 분산을 막으려면 생성자를 private으로 만들고, 목적에 따라 팩토리 메서드를 만들어야 한다고 설명합니다.

예시를 함께 살펴보도록 하겠습니다.

class GiftPoint {
	private static final int MIN_POINT = 0;
	private static final int STANDARD_MEMBERSHIP_POINT = 3000;
	private static final int PREMIUM_MEMBERSHIP_POINT = 10000;
	final int value;

	// 외부에서는 인스턴스를 생성할 수 없습니다.
	// 클래스 내부에서만 생성할 수 있습니다.
	private GiftPoint(final int point) {
		if (point < MIN_POINT) {
			throw new IllegalArgumentException("포인트를 0 이상 입력해야 합니다.");
		}

		value = point;
	}

	// 표쥰 가입 기프트 포인트
	static GiftPoint forStandardMembership() {
		return new GiftPOint(STANDARD_MEMBERSHIP_POINT);
	}

	// 프리미엄 가입 기프트 포인트
	static GiftPoint forPremiumMembership() {
		return new GiftPOint(PREMIUM_MEMBERSHIP_POINT);
	}

	GiftPoint add(final GiftPoint other) {
		return new GiftPoint(value + other.value);
	}

	boolean isEnough(final ConsumptionPoint point) {
		return point.value <= value;
	}

	GiftPoint consume(final ConsumptionPoint point) {
		if (!isEnough(point)) {
			throw new IllegalArgumentException("포인트가 부족합니다.");
		}

		return new GiftPoint(value - point.value);
	}
}

위의 코드를 보았을 때, 표준 기프트 포인트와 프리미엄 기프트 포인트를 각각 팩토리 메서드로 구현하였습니다.

하지만 이런 상황에서 기프트 포인트의 종류가 프리미엄 이외에 더 많이 생긴다면 생성 로직 또한 많아질 수 있고,
생성 로직이 많아질 경우 생성 전용 팩토리 클래스를 분리하여 구현할 수 있습니다.

이 내용에 대해 스터디 멤버 한 분께서 질문을 남기셨습니다.

"팩토리 클래스를 구현하게 되면 외부에서 기프트 포인트 클래스의 생성자를 호출하기 위해 생성자의 접근 제한자를 public으로 해야할텐데,
이렇게 되면 의도치 않은 기프트 포인트가 생성될 수 있는 문제가 있을 것 같습니다."

해당 질문 사항을 반영하여 해결책이 될 수 있는 생성 전용 팩토리 클래스를 생성해보았습니다.
우리는 JPA Entity를 사용한다는 기준 아래에서 해당 코드를 작성할 수 있었습니다.

@AllArgsConstructor(access = AccessLevel.PACKAGED)
public GiftCard {
    public static GiftCard ofFirstLevel()
    public static GiftCard ofSecondLevel()
    public static GiftCard ofThirdLevel()
    
    public static GiftCard of(MemberLevel level) {
        return GiftCard(level.reward)
    }
}

이렇게 public으로 선언한 생성 팩토리 메서드일지라도,
생성자 생성 어노테이션의 AccessLevelPackage로 지정한다면, 의도치 않은 부수현상을 방지할 수 있을 것으로 보았습니다.


2주차 스터디를 마치며...

저번 주에 진행한 내용이지만 몸살 감기가 낫질 않아 계속 늦어졌습니다... 😭
하지만 뜻깊었던 내용이 많았던 2주차 내용을 다시 한 번 회고하는 시간을 가질 수 있었다는 점에 가치를 느껴 어느정도 만족스럽습니다 :)

다음 3주차도 알찬 내용 복기하도록 하겠습니다!

profile
꾸준히 한 발씩 발전해나가는 모습을 기록합니다.

0개의 댓글