간단한 예시를 가지고 스프링에서 객체 지향 원리를 어떻게 적용하는지 알아보겠습니다. (아래 예시는 부족한점이 많으니 참고해주세요)
개발자 Soon은 프로젝트에서 초기 미팅 후 요구사항에 따라 회원 도메인과 회원 등급에 따른 포인트 적립율 적용을 개발했습니다.
Soon 은 객체지향을 조금 배웠기에 다음과 같이 개발했습니다.
@Entity
@Getter
@NoArgsConstructor
@Table(name = "`user`")
public class User {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
private String email;
@Enumerated(EnumType.STRING)
private Grade grade;
private Long point;
public User(Long id, String email, Grade grade, Long point) {
this.id = id;
this.email = email;
this.grade = grade;
this.point = point;
}
public void order(Long orderPrice, AccumulationPointPolicy pointPolicy) {
Long earnedPoint = pointPolicy.calculateAccumulationPointByGrade(orderPrice, this.grade);
this.point += earnedPoint;
}
}
@Getter
public enum Grade {
PLATINUM(0.10),
GOLD(0.07),
SILVER(0.05),
DEFAULT(0.03);
private final double accumulationRate;
Grade(double accumulationRate) {
this.accumulationRate = accumulationRate;
}
}
public interface AccumulationPointPolicy {
Long calculateAccumulationPointByGrade(Long orderPrice, Grade grade);
}
public class RegularAccumulationPointPolicy implements AccumulationPointPolicy {
@Override
public Long calculateAccumulationPointByGrade(Long orderPrice, Grade grade) {
return Math.round(orderPrice * grade.getAccumulationRate());
}
}
public class OrderService {
private final UserRepository userRepository;
private final AccumulationPointPolicy pointPolicy = new RegularAccumulationPointPolicy();
public OrderService(UserRepository userRepository) {
this.userRepository = userRepository;
}
public void order(Long userId, Long orderPrice) {
User user = userRepository.findById(userId).orElseThrow();
user.order(orderPrice, pointPolicy);
}
}
@Repository
public interface UserRepository extends JpaRepository<User, Long> {
}
테스트코드도 성공합니다.
@Test
void order() {
AccumulationPointPolicy pointPolicy = new RegularAccumulationPointPolicy();
User user = new User(1L, "abc@naver.com", Grade.PLATINUM, 10_000L);
user.order(50_000L, pointPolicy);
assertThat(user.getPoint()).isEqualTo(15_000L);
}
대략적으로 Soon은 흐름을 그려보았습니다.
이번에 특정 기간에 이벤트로 적립률을 변경해달라는 요구사항이 들어왔습니다.
public class EventAccumulationPolicy implements AccumulationPointPolicy{
@Override
public Long calculateAccumulationPointByGrade(Long orderPrice, Grade grade) {
LocalDateTime now = LocalDateTime.now();
if(now.isAfter(LocalDateTime.of(2023, Month.SEPTEMBER, 22, 23, 59))) {
throw new IllegalStateException("not event period");
}
return Math.round(orderPrice * (grade.getAccumulationRate() + 0.05));
}
}
테스트코드도 성공합니다.
@Test
void orderWithEventPeriod() {
AccumulationPointPolicy pointPolicy = new EventAccumulationPolicy();
User user = new User(1L, "abc@naver.com", Grade.PLATINUM, 10_000L);
user.order(50_000L, pointPolicy);
assertThat(user.getPoint()).isEqualTo(17_500L);
}
그림도 다시한번 그려봅니다.
새로 개발한 이벤트 적립률 정책을 적용하려고 하니 클라이언트 코드인 주문 서비스 구현체도 함께 변경해주어야합니다.
적립률 정책 코드가 수천개 서비스에서 사용하고 있다고 극단적으로 가정해봅니다…. 수천개를 다 바꿔줘야하네요?! 🥲
public class OrderService {
private final UserRepository userRepository;
// private final AccumulationPointPolicy pointPolicy = new RegularAccumulationPointPolicy();
private final AccumulationPointPolicy pointPolicy = new EventAccumulationPolicy();
public OrderService(UserRepository userRepository) {
this.userRepository = userRepository;
}
public void order(Long userId, Long orderPrice) {
User user = userRepository.findById(userId).orElseThrow();
user.order(orderPrice, pointPolicy);
}
}
주문 서비스 클라이언트가 인터페이스인 AccumulationPointPolicy 뿐만 아니라, 구체 클래스인 RegularPointPolicy 도 함께 의존하고 있습니다. → DIP 위반
어떻게해야되지?
@Configuration
public class AppConfig {
private final UserRepository userRepository;
public AppConfig(UserRepository userRepository) {
this.userRepository = userRepository;
}
@Bean
public AccumulationPointPolicy accumulationPointPolicy() {
return new EventAccumulationPolicy();
}
@Bean
public OrderService OrderService() {
return new OrderService(userRepository, accumulationPointPolicy());
}
}
public class OrderService {
private final UserRepository userRepository;
private final AccumulationPointPolicy pointPolicy;
public OrderService(UserRepository userRepository, AccumulationPointPolicy pointPolicy) {
this.userRepository = userRepository;
this.pointPolicy = pointPolicy;
}
@Transactional
public void order(Long userId, Long orderPrice) {
User user = userRepository.findById(userId).orElseThrow();
user.order(orderPrice, pointPolicy);
}
}
테스트도 성공적으로 통과합니다
@SpringBootTest
class OrderServiceTest {
@Autowired OrderService orderService;
@Autowired
UserRepository userRepository;
@Test
void order() {
User user = new User(1L, "abc@naver.com", Grade.PLATINUM, 10_000L);
userRepository.save(user);
orderService.order(1L, 50_000L);
User res = userRepository.findById(1L).get();
assertThat(res.getPoint()).isEqualTo(17_500L);
}
}
위의 내용들은 김영한님의 스프링 기초 핵심원리를 듣고 정리 및 개인적으로 학습한 내용들입니다. 물론 애노테이션으로 빈을 등록해서 사용하면 AppConfig 는 필요없지만 스프링이 기본적으로 뒤에서 수행하고 있는 것들을 이해하는데 도움이 될 것입니다.