개선 전: 단일 메서드로 직접 처리
/** 1:1 상품 채팅방 생성 or 조회 */
@Transactional
public ChatRoomResponse createOrGetProductChat(Long productId, User buyer) {
ChatRoom room = chatRoomRepository
.findByTypeAndProductIdAndBuyer(ChatRoomType.DIRECT, productId, buyer.getId())
.orElseGet(() -> {
Product product = productService.findById(productId);
ChatRoom newRoom = ChatRoom.builder()
.name(product.getName())
.type(ChatRoomType.DIRECT)
.productId(productId)
.owner(product.getSeller())
.build();
newRoom.registerOwner(product.getSeller());
newRoom.addOrActivateMember(buyer);
return chatRoomRepository.save(newRoom);
});
return ChatRoomResponse.from(room);
}
왜 전략 패턴(인터페이스)로 바꿀까?
- 단일 책임 원칙(SRP)
각 채팅방 생성 로직을 별도 클래스로 분리하면,
ChatRoomService가 “상품용”·“그룹용”·“이벤트용” 등 여러 책임을 갖지 않음.
- 개방-폐쇄 원칙(OCP)
새로운 채팅방 유형이 추가될 때마다 기존 코드를 수정할 필요 없이,
ChatRoomCreator 인터페이스 구현체만 새로 추가하면 끝.
- 유지보수·테스트 용이
각 Creator를 독립적으로 단위 테스트할 수 있고,
로직이 커질수록 if-else·switch문이 사라져 가독성이 좋아짐.
단점
- 초기 복잡도 증가
인터페이스·구현체가 많아지면서 파일 수와 설정이 늘어나고, 학습 곡선이 가파를 수 있음.
- 과도한 추상화
채팅방 유형이 2~3개 이하로 적으면 전략 패턴이 오히려 코드 구조를 불필요하게 복잡하게 만들 수 있음.
- 런타임 오버헤드
요청 시 매번 Map에서 전략을 조회하는 추가 연산이 발생.
- 테스트 비용 증가
각 전략 구현체마다 별도의 단위 테스트가 필요해져, 테스트 코드 양이 늘어남.
- 한마디로 조금 귀찮아짐..ㅎㅎ
개선 후: 인터페이스 기반 구조
전략 인터페이스 정의
public interface ChatRoomCreator {
ChatRoomType type();
ChatRoomResponse createOrGet(Object identifier, User user);
}
상품 1:1 채팅 Creator
@Component
@RequiredArgsConstructor
public class ProductChatRoomCreator implements ChatRoomCreator {
private final ChatRoomRepository repo;
private final ProductService productService;
@Override
public ChatRoomType type() {
return ChatRoomType.DIRECT;
}
@Override
@Transactional
public ChatRoomResponse createOrGet(Object id, User buyer) {
Long productId = (Long) id;
return repo.findByTypeAndProductIdAndBuyer(type(), productId, buyer.getId())
.map(ChatRoomResponse::from)
.orElseGet(() -> {
Product product = productService.findById(productId);
ChatRoom room = ChatRoom.builder()
.name(product.getName())
.type(type())
.productId(productId)
.owner(product.getSeller())
.build();
room.registerOwner(product.getSeller());
room.addOrActivateMember(buyer);
return ChatRoomResponse.from(repo.save(room));
});
}
}
그룹 채팅 Creator
@Component
@RequiredArgsConstructor
public class GroupChatRoomCreator implements ChatRoomCreator {
private final ChatRoomRepository repo;
@Override
public ChatRoomType type() {
return ChatRoomType.GROUP;
}
@Override
@Transactional
public ChatRoomResponse createOrGet(Object dtoObj, User user) {
ChatRoomCreateDTO dto = (ChatRoomCreateDTO) dtoObj;
ChatRoom room = ChatRoom.builder()
.name(dto.chatRoomName())
.type(type())
.owner(user)
.build();
room.registerOwner(user);
return ChatRoomResponse.from(repo.save(room));
}
}
ChatRoomService 변경
@Service
@RequiredArgsConstructor
public class ChatRoomService {
private final Map<ChatRoomType, ChatRoomCreator> creators;
@Transactional
public ChatRoomResponse createOrGetChatRoom(
ChatRoomType type, Object identifier, User user) {
ChatRoomCreator creator = creators.get(type);
if (creator == null) {
throw new IllegalArgumentException("지원하지 않는 타입: " + type);
}
return creator.createOrGet(identifier, user);
}
}
요약
- SRP: 각 구현체가 “상품”, “그룹” 방만 책임
- OCP: 새 유형 추가 시 서비스 코드 불변
- 테스트·유지보수성 대폭 향상