이 글을 쓰는 이유는 우테코 프리코스를 통해 얻은 배움과 깨달음을 정리하고, 스스로에게 의미 있는 기록을 남기기 위함입니다. 프리코스 이전의 저는 Java가 객체지향 언어라는 사실을 알고는 있었지만, 깊이 이해하지 못한 채 단순히 익숙하고 편리하다는 이유로 생각 없이 MVC 패턴을 사용하는 등 무책임한 방식으로 개발을 진행하곤 했습니다. 😅
그러나 프리코스를 통해 다양한 사람들의 코드를 접할 기회를 가졌고, 그들과 심도 있는 대화를 나누며 많은 것을 배우게 되었습니다. 특히 객체지향 프로그래밍에 대한 대화는 제 자신을 되돌아보게 만들었습니다.
1주차부터 4주차까지의 과정에서 저는 많은 발전과 변화를 경험했습니다. 이를 통해 얻은 중요한 배움들을 독백 형식으로 정리하며 자연스럽게 글을 풀어나가 보려 합니다.
1주차를 진행하면서 들었던 가장 큰 고민은 객체 생성을 어디서 하냐였습니다. 저는 아래의 코드와 같이 생성자 의존성 주입을 통해 해당 클래스의 기능을 사용하려고 했습니다. 하지만 이후 고민이 하나 생깁니다.
[InputParser.java]
public InputParser(DelimiterParser delimiterParser, NumberConverter numberConverter, Validator validator) {
this.delimiterParser = delimiterParser;
this.numberConverter = numberConverter;
this.validator = validator;
}
[Calculator.java]
public Calculator(InputParser inputParser, Validator validator) {
this.inputParser = inputParser;
this.validator = validator;
}
🙄어디서 new를 통해 객체를 생성하지?
저는 당시에 두가지 선택지 밖에 생각나지 않았습니다.
1. Application (main이 있는 클래스)
2. Controller
너무나 많은 의존성이 엉켜있었기도 하였고, 배움이 부족해 객체 생성의 책임을 어디에 부여해야할지 고민이 되었습니다.
main에 객체 생성의 책임을 부여하자니 보기에 좋지 않았고, 컨트롤러에 부여하자니 컨트롤러의 역할에 맞지 않다고 생각했고,
결국 컨트롤러에 new를 난사하여 미션을 끝냈습니다..😥
public CalculatorController() {
this.calculatorView = new CalculatorView();
Validator validator = new Validator();
DelimiterParser delimiterParser = new DelimiterParser();
NumberConverter numberConverter = new NumberConverter();
InputParser inputParser = new InputParser(delimiterParser, numberConverter, validator);
this.calculator = new Calculator(inputParser, validator);
}
저는 이 고민을 해결하기 위해 여러 방법을 찾아보았고, 팩토리 메서드 패턴의 원리를 통해 객체 생성의 책임을 전담하는 클래스를 만들어 관리하는 방법을 찾았습니다.
팩토리메서드와 저의 방식의 차이
는 원래 팩토리 메서드는 AppCofing 클래스에 객체생성메서드를 정의한 인터페이스
혹은 추상클래스
를 구현
또는 상속
해야 하지만 저는 러프하게 객체 생성 메서드를 가진 클래스를 만들었습니다.
나중에는 도메인 객체 또한 이러한 방식으로 생성하고 관리하니 책임을 명확히 분리할 수 있었습니다 👍
public class AppConfig {
public RacingGameController racingGameController() {
return new RacingGameController(racingGameService(), inputView(), outputView(), carSetupService());
}
private InputView inputView() {
return new InputView();
}
private OutputView outputView() {
return new OutputView();
}
private RacingGameService racingGameService() {
return new RacingGameService(outputView());
}
private CarSetupService carSetupService() {
return new CarSetupService(inputValidator());
}
private InputValidator inputValidator() {
return new InputValidator();
}
}
프리코스를 접하기 전까지 저는 도메인 클래스를 단순히 데이터를 저장하고 getter(), setter() 메서드를 구현하는 용도로만 사용했습니다. 이렇게 작성하다 보니, 자연스럽게 서비스 레이어에서 getter()를 마구 활용해 비즈니스 로직을 처리하게 되었습니다. 당시에는 getter()를 지양해야 한다는 사실조차 몰랐습니다. 😅
그러나 프리코스 3주 차 미션에서는 Lotto 도메인 클래스를 활용해 요구사항을 구현해야 했습니다. 이 과정에서 도메인 클래스가 단순히 데이터 저장소 역할만 하는 것이 아니라, 비즈니스 로직을 포함해야 한다는 점을 깨닫게 되었습니다.
[Lotto.java]
public class Lotto {
private final List<Integer> numbers;
public Lotto(List<Integer> numbers) {
validate(numbers);
this.numbers = numbers;
}
private void validate(List<Integer> numbers) {
if (numbers.size() != 6) {
throw new IllegalArgumentException("[ERROR] 로또 번호는 6개여야 합니다.");
}
}
// TODO: 추가 기능 구현
}
여기서 의문이 생겼습니다. 지금까지 도메인 클래스는 단순히 데이터를 저장하는 용도로만 사용했고, 검증이나 파싱과 같은 로직은 도메인 클래스 외부에서 처리해왔기 때문입니다. "이 방식이 잘못된 것일까?"라는 궁금증을 해소하기 위해 공부를 진행한 결과, 제가 사용했던 방식은 바로 스크립트 패턴이라는 것을 알게 되었습니다.
스크립트 패턴은 단순하고 빠르게 개발할 수 있다는 장점이 있지만, 여러 단점을 가지고 있어 지양해야 하는 방식으로 꼽힌다고 합니다.
스크립트 패턴에서는 도메인 클래스가 데이터를 단순히 저장하는 역할만 하고, 로직은 전부 외부(서비스 클래스 등)에서 처리되기 때문에 유지보수성과 확장성에서 큰 한계를 가지게 됩니다. 특히 시스템이 커질수록 복잡성이 급격히 증가하며, 코드의 응집도가 낮아져 가독성과 관리가 어려워질 수 있습니다.
그럼 여기서 어떤 방식으로 개발을 해야한다는 것인가?
이제 스크립트 패턴의 한계를 넘어서기 위해 DDD(Domain-Driven Design)와 같은 설계 방식을 학습하고 적용해 나가야 한다는 것을 느꼈습니다. 😊
DDD란 도메인 모델을 중심으로 시스템을 구축하는 방식입니다.
지금까지는 도메인 클래스를 단순히 데이터를 저장하고 필요할 때 꺼내는 용도로만 사용했지만, DDD를 적용하면 도메인 클래스 내부에서 데이터를 직접 다루고, 그 결과를 반환하도록 설계할 수 있습니다.
예를 들어, 어떤 물건의 총 가격을 구해야 한다고 가정해보겠습니다. 기존 방식에서는 아래와 같이 getter()를 사용하여 데이터를 가져온 후 외부에서 계산을 수행했을 것입니다:
int totalPrice = product.getPrice() * product.getQuantity()
이 방식은 데이터를 꺼내와 외부에서 처리한다는 점에서 객체가 자신의 책임을 다하지 못하게 만듭니다. 때문에 앞으로는 데이터를 직접 꺼내는 대신, 도메인 클래스 내부에 로직을 포함시켜 총 가격을 계산하도록 구현할 것입니다.
이런 방식으로 설계하면 아래와 같이 코드를 작성할 수 있습니다:
[Product.java]
public class Product{
private int price;
private int quantity;
//생성자 생략
//총가격 구하기
public int getTotalPrice(){
return price * quantity;
}
}
[사용하기]
int totalPrice = product.getTotalPrice();
차이점이 보이시나요??
기존 방식에서는 데이터를 꺼내서(getter 사용) 외부에서 직접 계산하는 방식으로 로직을 구현했습니다. 하지만 DDD를 적용한 방식에서는 도메인 클래스(Product)가 자신의 데이터를 스스로 처리하고, 결과를 반환합니다.
이 차이는 단순히 코드 작성 방식의 차원이 아니라, 책임
을 어디에 두느냐에 대한 설계 철학에서 비롯됩니다. DDD는 데이터를 사용하는 로직을 도메인 객체 내부로 이동시킴으로써, 객체가 자신의 상태와 행동을 책임지도록 설계합니다.
이 방식은 로직의 응집도를 높이고, 변경에 강한 코드를 작성할 수 있게 해줍니다. 데이터와 관련된 책임을 도메인 클래스에 두는 것이 더 객체 지향적
이고 유지보수
에 적합한 설계라는 것을 깨닫게 되었습니다. 😊
저희는 다양한 비즈니스 로직을 구현합니다.
하지만 때때로 너무 많은 비즈니스 로직이 서비스 레이어에 집중되어 있는 경우, 결합도가 높아지고 코드의 가독성이 떨어지는 문제를 경험하게 됩니다.
예를 들어, 쇼핑몰의 구매 로직을 구현하고 있다고 가정해봅시다. 하나의 서비스 레이어만 사용해 모든 작업(재고 확인, 결제 처리, 주문 생성 등)을 처리하려고 하면, 코드가 지나치게 길어지고 복잡해질 수 있습니다. 이는 서비스 레이어가 과도한 책임을 떠안게 되어 변경에 취약해지고, 새로운 기능을 추가하거나 기존 기능을 수정할 때 전체 코드를 이해해야 하는 부담을 가중시킵니다.
이러한 문제를 해결하기 위해 퍼사드(Facade) 패턴을 활용할 수 있습니다.
"퍼사드 패턴이란 쉽게 말하면 서비스 목록을 가진 서비스라 할 수 있습니다."
즉 재고 확인, 결제 처리, 주문 생성를 각각 담당하는 서비스 클래스가 3개 있다고 하면 이 서비스들을 관리하는 서비스 클래스를 만드는 것입니다.
[PurchaseFacade.java]
public class PurchaseFacade {
private final InventoryService inventoryService;
private final PaymentService paymentService;
private final OrderService orderService;
public PurchaseFacade(InventoryService inventoryService, PaymentService paymentService, OrderService orderService) {
this.inventoryService = inventoryService;
this.paymentService = paymentService;
this.orderService = orderService;
}
public void processPurchase(OrderRequest orderRequest) {
// 각 도메인 서비스 호출
inventoryService.checkStock(orderRequest);
paymentService.processPayment(orderRequest.getPaymentDetails());
orderService.createOrder(orderRequest);
}
}
단순화된 인터페이스 제공: 복잡한 로직을 퍼사드 내부에서 관리하여 클라이언트는 간단히 사용할 수 있습니다.
결합도 감소: 클라이언트와 하위 시스템 간의 결합을 줄이고, 시스템 변경에 유연하게 대처할 수 있습니다.
가독성 향상: 퍼사드가 전체 로직을 조율하므로 클라이언트 코드가 간결해집니다.
프리코스를 통해 저는 단순히 코드를 작성하는 개발자가 아니라, 더 나은 설계를 고민하고 실천하는 개발자로 성장할 수 있는 계기를 마련할 수 있었습니다. 객체 생성의 책임 분리, 도메인의 역할 강화, 그리고 비즈니스 로직을 체계적으로 정리하는 패턴 적용까지, 모든 과정이 저에게 새로운 통찰과 배움을 선사했습니다.
객체 지향 설계는 단순히 코드를 "잘 짜는 것"을 넘어, 코드의 의도를 명확히 하고, 유지보수와 확장성에 강한 시스템을 만드는 것이라는 사실을 알게 되었습니다.
앞으로도 이 과정을 통해 배운 원칙과 설계를 실천하며, 더 나은 코드를 작성하기 위해 고민을 멈추지 않을 것입니다. 😊
제가 작성한 이 글이 같은 고민을 가진 분들에게 작은 도움이 되기를 바랍니다! 🚀