일 | 월 | 화 | 수 | 목 | 금 | 토 |
---|---|---|---|---|---|---|
1 | 2 | 3 | 4 | 5 | ||
6 | 7 | 8 | 9 | 10 | 11 | 12 |
13 | 14 | 15 | 16 | 17 | 18 | 19 |
20 | 21 | 22 | 23 | 24 | 25 | 26 |
27 | 28 | 29 | 30 |
- spring boot
- java
- 인터셉터
- aspect
- RestControllerAdvice
- SQL
- Interceptor
- 객체지향프로그래밍
- Spring Security
- proxy pattern
- mybatis
- 스프링부트
- 자바
- aop
- git
- Spring
- 디자인패턴
- exception
- 트랜잭션
- 관점지향프로그래밍
- 스프링
- OOP
- http
- network
- response
- Redis
- MYSQL
- Filter
- request
- 스프링 시큐리티
- Today
- Total
장쫄깃 기술블로그
[Design Pattern] 험블 객체 패턴 본문
험블 객체 패턴이란
험블 객체 패턴은 테스트하기 어려운 행위와 테스트하기 쉬운 행위를 단위 테스트 작성자가 분리하기 쉽게 하는 방법으로 고안된 디자인 패턴이다.
이 패턴에서는 핵심 로직을 담당하는 객체와 외부 시스템과 상호작용하는 험블 객체를 분리한다. 만약 험블 객체가 핵심 로직 안에 포함되면 테스트가 어려워지므로, 이를 분리하여 테스트하기 쉬운 형태로 만든다.
이를 통해 핵심 로직은 단위 테스트가 용이해지고, 험블 객체는 단순해져 유지보수가 쉬워진다.
예제로 보는 험블 객체 패턴
(1) 문제 상황: 외부 API 호출이 포함된 서비스
다음과 같은 UserService가 있다고 가정해보겠다.
@Service
public class UserService {
public String getUserInfo(String userId) {
// 외부 API 호출 (테스트하기 어려운 부분)
String response = callExternalAPI(userId);
return parseResponse(response);
}
private String callExternalAPI(String userId) {
// 외부 API 호출 (예제이므로 단순한 형태)
return "userData:" + userId;
}
private String parseResponse(String response) {
return response.split(":")[1];
}
}
이 서비스 클래스는 외부 API를 호출하기 때문에 단위 테스트가 어렵다.
(2) 험블 객체 패턴 적용
외부 API 호출을 담당하는 로직을 험블 객체(UserApiClient)를 분리하고 비즈니스 로직을 UserService에서 집중적으로 관리하도록 코드를 수정해보자.
UserApiClient
@Component
public class UserApiClient {
public String callExternalAPI(String userId) {
// 실제 외부 API 호출 (여기서는 간단한 예제)
return "userData:" + userId;
}
}
UserService
@Service
@RequiredArgsConstructor
public class UserService {
private final UserApiClient userApiClient;
public String getUserInfo(String userId) {
String response = userApiClient.callExternalAPI(userId);
return parseResponse(response);
}
private String parseResponse(String response) {
return response.split(":")[1];
}
}
(3) 단위 테스트 작성
UserService를 테스트할 때, UserApiClient를 Mock 객체로 대체하여 API 호출 없이 테스트할 수 있다.
@ExtendWith(MockitoExtension.class)
class UserServiceTest {
@Mock
private UserApiClient userApiClient;
@InjectMocks
private UserService userService;
@Test
void testGetUserInfo() {
// given
String userId = "123";
Mockito.when(userApiClient.callExternalAPI(userId)).thenReturn("userData:123");
// when
String result = userService.getUserInfo(userId);
// then
assertEquals("123", result);
}
}
험블 객체 패턴의 장점
1. 단위 테스트 가능
UserService는 UserApiClient를 분리했기 때문에 UserApiClient를 Mock으로 대체하여 테스트할 수 있다.
2. 유지보수성 증가
API 변경이 발생해도 UserApiClient만 수정하면 되므로 UserService의 비즈니스 로직에는 영향이 적다.
3. 객체 지향 원칙 준수
- 단일 책임 원칙(SRP, Single Responsibility Principle): UserService는 비즈니스 로직을 담당하고, UserApiClient는 API 호출을 담당하여 각 클래스의 역할이 명확히 분리된다.
- 개방-폐쇄 원칙(OCP, Open-Closed Principle): API 호출 방식이 변경되더라도 UserApiClient만 수정하면 되므로 기존 UserService의 코드를 수정할 필요 없이 확장할 수 있다.
- 의존성 역전 원칙(DIP, Dependency Inversion Principle): UserService가 UserApiClient의 구체적인 구현이 아닌 인터페이스에 의존하도록 하면, API 호출 방식 변경 시에도 유연한 대응이 가능하다.
험블 객체 패턴과 DIP 원칙 적용
기존 코드에서는 험블 객체 패턴을 적용하여 UserApiClient를 분리했지만, 더 나아가 DIP 원칙을 지키기 위해 인터페이스를 적극 활용하여 UserService가 구체적인 구현이 아닌 인터페이스에 의존하도록 개선할 수 있다.
1. 기존 코드의 문제점
UserService
@Service
@RequiredArgsConstructor
public class UserService {
private final UserApiClient userApiClient;
public String getUserInfo(String userId) {
String response = userApiClient.callExternalAPI(userId);
return parseResponse(response);
}
private String parseResponse(String response) {
return response.split(":")[1];
}
}
이 코드에서 UserApiClient가 인터페이스가 아니라 클래스라면, API 호출 방식이 변경될 때 UserService도 함께 수정해야 한다. 이는 단위 테스트에서도 마찬가지다.
2. 개선된 객체 지향 설계
(1) UserApiClient 인터페이스 정의
public interface UserApiClient {
String callExternalAPI(String userId);
}
(2) UserApiClientImpl: 실제 API 호출하는 구현체
@Component("externalApiClient")
public class UserApiClientImpl implements UserApiClient {
@Override
public String callExternalAPI(String userId) {
// 실제 외부 API 호출 로직
return "userData:" + userId;
}
}
(3) CachedUserApiClient: 캐싱된 데이터를 활용하는 구현체
@Component("cachedApiClient")
public class CachedUserApiClient implements UserApiClient {
private final Map<String, String> cache = new HashMap<>();
public CachedUserApiClient() {
// 예제용 캐시 데이터
cache.put("123", "userData:cached-123");
cache.put("456", "userData:cached-456");
}
@Override
public String callExternalAPI(String userId) {
return cache.getOrDefault(userId, "userData:unknown");
}
}
(4) UserService가 인터페이스를 의존하도록 변경
@Service
@RequiredArgsConstructor
public class UserService {
@Qualifier("externalApiClient")
private final UserApiClient userApiClient;
public String getUserInfo(String userId) {
String response = userApiClient.callExternalAPI(userId);
return parseResponse(response);
}
private String parseResponse(String response) {
return response.split(":")[1];
}
}
DIP를 적용함으로써 더욱 객체지향적인 설계를 가지게 되었다.
이를 통해 테스트 코드에서도 장점을 가질 수 있다.
- 구체적인 구현체에 의존하지 않음
- UserApiClientImpl 같은 특정 클래스가 아니라 인터페이스(UserApiClient) 에 의존하므로, 테스트 코드가 특정 구현체 변경에 영향을 받지 않음
- 테스트 코드 유지보수성 향상
- 새로운 구현체가 추가되거나 기존 구현이 변경되더라도 테스트 코드 수정이 불필요
- 테스트의 관심사가 오로지 UserService의 동작 검증으로 명확해짐
이와 같이 테스트 코드가 특정 구현체에 종속되지 않아 빠르고 안정적인 테스트가 가능하다.
정리하며
단위 테스트를 쉽게 하려면 비즈니스 로직을 명확하게 분리해야 한다. 이는 곧 객체 지향적인 설계를 의미하며, 유지보수성과 확장성이 향상되어 서비스의 품질이 좋아지고 안정적인 운영이 가능해진다.
하지만 현실에서는 많은 개발자들이 쏟아지는 요구사항을 빠르게 처리하기 위해 설계를 뒤로 미루고 기능 구현에 집중하는 경우가 많다. “시간이 없어서”, “일단 급한 것부터 처리하고” 같은 이유로 설계를 소홀히 하게 되지만, 이런 방식이 반복되면 결국 코드가 점점 복잡해지고 유지보수가 어려워진다.
코드 한 줄을 수정하기 위해 여러 레이어를 분석해야 하고, 예상치 못한 오류에 직면하게 되며, 점점 코드 수정이 부담스러워지는 악순환이 생긴다. 그러다 보면 결국 "설계가 이래서 안돼", "코드가 저래서 안돼"라며 새로운 요구사항을 거부하거나 비효율적인 방식으로 개발을 하게 된다.
처음에는 좋은 설계가 시간이 더 걸리는 것처럼 보일 수 있지만, 장기적으로 보면 유지보수 비용을 줄이고 개발 생산성을 높이는 데 큰 도움이 된다. "지금 바쁘다", "이렇게까지 할 필요가 있나?"라는 생각으로 설계를 미루면, 결국 더 큰 문제와 높은 유지보수 비용을 감당해야 할 것이다.
참고
'ETC > Design Pattern' 카테고리의 다른 글
[Design Pattern] 파사드 패턴 (Facade Pattern) (0) | 2023.04.16 |
---|---|
[Design Pattern] 컴포지트 패턴 (Composite Pattern) (0) | 2023.04.13 |
[Design Pattern] 프록시 패턴 (Proxy Pattern) (0) | 2023.04.13 |