챌린지 배틀 회고 근데 이제 OOP와 도메인 주도 설계를 곁들인
1. 들어가며
작년 하반기에 헥사고날 아키텍처, 클린 아키텍처, TDD 등을 공부하면서 느낀 가장 큰 깨달음은:"이 모든 것이 결국 객체지향 프로그래밍(OOP)에 기반한다" 는 사실이었습니다.
기존에 데이터 중심, 절차지향 프로그래밍으로 프로젝트를 진행해오다가, 최근 챌린지 배틀 기능을 구현하면서 객체지향 관점으로 접근해보았습니다. 그 과정을 통해 느낀 점과 얻은 교훈을 공유하고자 합니다.
2. 절차지향 vs. 객체지향: 이론적 특징
2.1 절차지향 프로그래밍(Procedural)
- 장점
- 구현이 단순하고 직관적
- 빠른 프로토타이핑 및 생산성 확보
- 로직 흐름이 명확하게 보임
- 단점
- 코드 중복이 늘어나고 재사용성이 낮아짐
- 요구사항 변경 시 유지보수 어려움
- 복잡해질수록 가독성이 급격히 떨어짐
2.2 객체지향 프로그래밍(OOP)
- 장점
- 유지보수와 확장성이 우수
- 응집도가 높아지고, 도메인 모델과 직관적으로 매핑 가능
- 캡슐화를 통해 변경에 유연하게 대응 가능
- 단점
- 초기 설계와 학습에 상대적으로 많은 시간 투자
- 설계자의 역량에 따라 품질이 크게 달라질 수 있음
- 잘못된 객체 분리 시 오히려 복잡도가 증가
3. 왜 우리는 절차지향 프로그래밍을 하게 될까?
아래 예시는 Order와 LineItem 엔티티를 JPA로 정의한 코드입니다.
@Entity
public class OrderEntity {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
@OneToMany(mappedBy = "order", cascade = CascadeType.ALL)
private List<LineItemEntity> lineItems = new ArrayList<>();
private double total;
}
@Entity
public class LineItemEntity {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
private double price;
private int quantity;
}
3.1 절차지향 코드 예시
@Service
public class OrderService {
private final OrderRepository orderRepository;
public double applyDiscountProcedural(Long orderId) {
// 1. OrderEntity 조회
OrderEntity orderEntity = orderRepository.findById(orderId)
.orElseThrow(() -> new IllegalArgumentException("해당 주문이 존재하지 않습니다."));
// 2. 할인율
double discountRate = 0.05; // 5% 할인
// 3. 주문 총액 계산
double originalTotal = 0.0;
for (LineItemEntity lineItem : orderEntity.getLineItems()) {
double itemTotal = lineItem.getPrice() * lineItem.getQuantity();
originalTotal += itemTotal;
}
// 4. 할인가 계산
double discountAmount = originalTotal * discountRate;
double finalTotal = originalTotal - discountAmount;
// 5. 엔티티에 최종 값 set
orderEntity.setTotal(finalTotal);
// 6. 저장
orderRepository.save(orderEntity);
return finalTotal;
}
}
이처럼 DB 설계를 먼저 진행하고, 엔티티를 통해 데이터를 꺼내와( get ) 서비스 계층에서 로직을 구현합니다.주요 특징은 다음과 같습니다.
- 데이터 중심 사고 에 의해 자연스레 절차지향적 흐름이 형성됨
- 단순하고 빠르게 결과물을 만들 수 있지만, 비즈니스 로직이 서비스 계층에 몰림
- 변경 및 확장 시(예: 할인정책 변경) 수정 범위가 커지고, 테스트도 어려워짐
3.2 객체지향 코드 예시
// 도메인 모델: Order
public class Order {
private List<LineItem> lineItems;
private double total;
public Order(List<LineItem> lineItems) {
this.lineItems = lineItems;
}
public double calculateFinalPrice(double discountRate) {
double sum = 0.0;
for (LineItem item : lineItems) {
sum += item.calculateItemTotal();
}
double discount = sum * discountRate;
return sum - discount;
}
public void updateTotal(double newTotal) {
this.total = newTotal;
}
public double getTotal() {
return total;
}
}
// 도메인 모델: LineItem
public class LineItem {
private double price;
private int quantity;
public LineItem(double price, int quantity) {
this.price = price;
this.quantity = quantity;
}
public double calculateItemTotal() {
return price * quantity;
}
}
@Service
public class OrderService {
private final OrderRepository orderRepository;
public double applyDiscountUsingDomain(Long orderId) {
// 1. 엔티티(OrderEntity) 조회
OrderEntity orderEntity = orderRepository.findById(orderId)
.orElseThrow(() -> new IllegalArgumentException("해당 주문이 존재하지 않습니다."));
// 2. 엔티티를 도메인 객체로 변환
List<LineItem> domainLineItems = orderEntity.getLineItems().stream()
.map(item -> new LineItem(item.getPrice(), item.getQuantity()))
.collect(Collectors.toList());
// 3. 도메인 객체 활용
Order order = new Order(domainLineItems);
double discountRate = 0.05; // 5% 할인
double finalPrice = order.calculateFinalPrice(discountRate);
order.updateTotal(finalPrice);
// 4. 결과를 엔티티에 반영 후 저장
orderEntity.setTotal(order.getTotal());
orderRepository.save(orderEntity);
return finalPrice;
}
}
- 비즈니스 로직이 도메인(Order, LineItem) 내부로 옮겨짐
- 객체 스스로 계산하고 상태를 변경
- 테스트 시 도메인 객체만 테스트할 수 있어 단위 테스트가 훨씬 수월함
4. 챌린지 배틀: 객체지향 프로그래밍 사례
최근 진행한 "챌린지 배틀" 기능을 OOP로 구현하며 느낀 점을 공유합니다.
4.1 객체지향적 개발 프로세스
- 기획서 분석 및 도메인 설계
- 요구사항을 바탕으로 도메인과 그 행위(비즈니스 로직)를 설계
- 테스트 코드 작성
- 도메인의 핵심 로직이 의도대로 동작하는지 먼저 검증
- 도메인을 사용하여 구현(조합)
- 애플리케이션 계층에서 도메인 객체를 조합하여 시나리오 구현
- DB 설계
- 도메인 모델을 확정한 후, 필요한 테이블 및 스키마 설계
이 과정을 따르면 도메인 중심으로 사고하게 되므로,
DB 의존성이 낮아지고, 변경 및 확장에 더욱 유연해집니다.
4.2 챌린지 배틀 시나리오 예시: “스크래치 구매 로직”
- 현재 챌린지 게임 상태 확인
challengeGame도메인 객체가 스스로 실행 가능 여부 판단
- 유저 정보 검증
challengeUser도메인 객체에서 사용자 자격(뱃지, 재화 등) 체크
- 스크래치 사용 가능 여부 검증
challengeGameScratch도메인이 특정 스크래치(실버/프리미엄 등) 사용 가능 여부 판단
- 보상 지급 처리
- 스크래치 보상 계산, 교환 크레딧 잔액 확인, 골드 지급, 아바타 지급 등
- 데이터 업데이트 (DB 어댑터)
- 유저 스크래치 사용 기록, 잔액 업데이트, 통계 반영
- 결과 전송 (Protocol)
- 최종 결과 정보를 응답 패킷으로 전달
이렇게 도메인들이 각자의 책임을 갖고 메시지를 주고받도록 설계하면,
테스트가 용이하고, 새로운 스크래치 타입을 추가하거나 로직을 변경하는 일이 훨씬 간단해집니다.
5. 직접 OOP를 하며 느낀점
- 변경에 유연
- 과거에는 DB 구조 수정이 큰 부담이었지만, 이제는 도메인 객체 변경으로 대응이 가능
- 확장성이 뛰어남
- 예: 실버, 프리미엄 스크래치의 경우, 인터페이스 기반으로 확장 가능
- 협업에 용이
- OOP 방식은 비즈니스 로직이 분리되어 있어, 여러 명이 동시 개발해도 충돌이 적고 이해가 쉬움
- 테스트 코드 작성 편의
- 도메인 로직 자체를 단위 테스트로 검증하므로, 테스트 코드가 늘어나도 가독성과 유지보수가 용이
- 가독성 개선
- 중첩
if-else가 크게 줄어들고, “메시지 전송” 중심으로 코드가 간결해짐
- 중첩
- 도메인 중심 사고
- “너가 할 수 있는 일을 스스로 해결해줘”라고 각 도메인에 메시지를 보내는 방식이 재미있고 유의미했음
- 다만, 초반 설계가 다소 어렵고 시간이 소요될 수 있음
추가로, 순수 자바로 만들어진 도메인 코드는 프레임워크에 의존되어있지 않기 때문에, 어디서나 사용가능
ex) 프레임워크가 사라진다면? , 특정 모듈이 fade-out 된다면? , 스프링 프레임워크보다 좋은 게 나왔다면?
기타
객체지향을 몸에 익히기 위한 2가지 규칙
이론적으로 객체지향을 이해했어도, 어느 순간 다시 절차지향적 습관으로 되돌아가는 경우가 많습니다.다음 2가지 규칙을 일상적으로 적용하면 자연스럽게 OOP 사고방식을 유지할 수 있습니다.
6.1 규칙 1: get, set을 절대 사용하지 않는다
- Getter: 객체 내부 데이터를 직접 꺼내 쓰는 행위
- Setter: 객체 상태를 강제로 바꾸는 행위
이는 객체의 캡슐화를 약화시키고, 로직이 외부로 분산되는 원인이 됩니다.그 대신 “객체에게 메시지를 보내기” 방식을 사용하세요.
// Bad (절차적)
order.setTotal(order.getTotal() - (order.getTotal() * discountRate));
// Good (객체지향적)
order.applyDiscount(discountRate);
6.2 규칙 2: 일급 컬렉션(First-Class Collection)을 적극 활용한다
- 일급 컬렉션: 컬렉션(List, Set 등)을 감싼 클래스가 해당 컬렉션에 대한 로직을 스스로 책임지는 구조
- 컬렉션 이외의 멤버 변수가 없고, 로직이 클래스 내부에 집중됨
예: 주문 항목(LineItem) 리스트를 직접 순회하며 로직을 만드는 대신,일급 컬렉션(LineItems)이 스스로 합계 계산 등을 담당합니다.
// 절차적 (Bad)
List<LineItem> lineItems = order.getLineItems();
double total = 0;
for (LineItem item : lineItems) {
total += item.getPrice() * item.getQuantity();
}
// 일급 컬렉션 (Good)
public class LineItems {
private List<LineItem> items;
public LineItems(List<LineItem> items) {
this.items = items;
}
public double calculateTotal() {
return items.stream()
.mapToDouble(LineItem::calculateItemTotal)
.sum();
}
}
- 조영호 님의 발표: “객체지향은 여전히 유용한가”
감사합니다.
'개발자로서 살아남기' 카테고리의 다른 글
| 서버개발자로서 살아남기 - MCP 실무에서 활용하기 with spring ai (2) (0) | 2025.08.01 |
|---|---|
| 서버개발자로서 살아남기 - MCP 실무에서 활용하기 with 옵시디언, mysql, 파일시스템 (1) (9) | 2025.08.01 |
| 개발자로서 살아남기 - 루아스크립트를 실무에서 사용한 이유 (0) | 2025.08.01 |
| 개발자로서 살아남기 - JVM GC 동작 원리 (0) | 2025.08.01 |
| JAVA - Synchronized와 ThreadLocal 을 이용한 동시성 문제 해결 (0) | 2024.05.03 |