스프링 부트 프로젝트에 대한 경험이 누적되며 단순히 기능을 구현하는 것뿐만이 아닌 더 좋은 코드를 작성하기 위해 고민할 수 있었습니다.
인터페이스를 코드에 적용하는 것은 구체 클래스에 의존하지 않는 느슨한 결합을 통해 유연한 확장과 수정을 가능하게 해 줍니다.
이러한 장점을 가지고 있지만 코드에 인터페이스를 적용하는 것은 복잡도를 높일 수 있고 이에 프로젝트에 따라 인터페이스가 필요한 클래스를 판단하고 적용하는 것이 중요하다 생각합니다.
해당 글에서는 프로젝트를 수행 간 코드에 인터페이스를 적용하면서 느낄 수 있었던 "스프링 부트를 사용하며 인터페이스가 필요한 순간"에 대한 저의 생각을 공유하려 합니다.
제어가 필요한 순간
제가 생각하는 스프링 부트를 사용하며 인터페이스가 필요한 순간은 "제어가 필요한 순간"입니다.
public class SignUpUserUseCase {
private final UserDao userDao;
private final RandomNickNameGeneraterImpl nickNameGenerater;
private final GoogleOauthClient oauthClient;
public Response execute(Request request) {
...
// OauthClient에서 정보를 조회합니다.
OauthResponse oauthResponse = oauthClient.execute();
// 정보에서 닉네임이 없다면 중복되지 않는 랜덤한 닉네임을 생성합니다.
if(!oauthResponse.hasNickName()) {
String nickName;
while(true) {
nickName = nickNameGenerater.execute();
// 닉네임의 중복 여부를 확인합니다.
bool isExist = userDao.existByNickName(nickName);
if(isExist) {
break;
}
}
oauthResponse.setNickName(nickName);
}
...
}
}
위는 Oauth 클라이언트에서 정보를 조회하고 해당 정보에 닉네임이 존재하지 않으면 중복되지 않는 랜덤 한 닉네임을 생성하여 회원가입을 하는 기능 구현의 일부를 나타낸 것입니다.
RandomNickNameGeneraterImpl
가 어떤 닉네임을 생성하는지, GoogleOauthClient
가 어떤 응답을 반환하는지 제어하지 못해도 코드는 동작하고 어떠한 문제도 느끼지 못할 수 있습니다.
하지만 보다 기능이 복잡해지고 테스트가 필요해진다면 우리는 제어하지 못했던 것들을 제어할 필요가 생깁니다.
이러한 상황에서 인터페이스와 구현체의 프로필(@Profile
) 설정은 상황에 따라 코드를 제어할 수 있도록 도와줍니다.
@Profile("!test")
@Component
public class RandomNickNameGeneraterImpl implement RandomNickNameGenerater {
public string execute() {
...
return nickName;
}
}
@TestComponent
public class TestRandomNickNameGeneraterImpl implement RandomNickNameGenerater {
public string execute() {
return nickName;
}
}
@Profile("!test")
: 프로필이 test가 아닌 상황에서 빈으로 등록된다.RandomNickNameGeneraterImpl
: 랜덤 하게 닉네임을 생성한다.@TestComponent
: 프로필이 test인 상황에서 빈으로 등록된다.TestRandomNickNameGeneraterImpl
: 미리 설정한 닉네임을 생성한다.
public class SignUpUserUseCase {
...
private final RandomNickNameGenerater nickNameGenerater;
...
}
RandomNickNameGenerater
: 구체 클래스가 아닌 인터페이스에 의존한다.
구체 클래스가 아닌 인터페이스에 의존하고 프로필에 따라 등록되는 빈을 다르게 설정하여 제어할 수 없던 코드를 상황에 따라 제어할 수 있는 코드로 대체할 수 있게 됩니다.
SignUpUserUseCase
를 테스트한다면 RandomNickNameGenerater
와 GoogleOauthClient
를 제어하여 원하는 값을 미리 설정해 두어 회원 가입을 위한 비즈니스 코드를 검증하는 테스트를 작성하는 것에 집중할 수 있습니다.
테스트 코드뿐 아니라 시나리오 테스트, 부하 테스트와 같은 상황에서도 필요에 따라 구현체를 생성한다면 원하는 방식으로 코드를 제어할 수 있을 것입니다.
Mockito
Java 오픈소스 라이브러리인 Mockito 역시 위와 같이 제어하지 못했던 것들을 제어할 수 있도록 도와줍니다.
하지만 그 제어의 범위가 테스트 코드로 한정됩니다.
시나리오 테스트, 부하 테스트와 같이 보다 다양한 상황에서 코드에 대한 제어가 필요하다면 Mockito 보다는 인터페이스를 활용하는 것이 더 적절할 것이라 생각합니다.
빈을 그룹으로 관리할 필요가 있는 순간
제가 생각하는 또 다른 스프링 부트를 사용하며 인터페이스가 필요한 순간은 "빈을 그룹으로 관리할 필요가 있는 순간"입니다.
위의 SignUpUserUseCase
에서 요구사항이 추가되어 GoogleOauthClient
와 같이 특정 클라이언트가 아닌 여러 클라이언트를 지원해야 한다면 OauthClient
와 같이 인터페이스를 선언하고 구현체를 생성하여 구현체를 그룹으로 관리하는 것이 편리합니다.
SignUpUserUseCase
의 수정하는 과정을 통해 인터페이스가 어떻게 구현체를 그룹으로 설정할 수 있게 도와주는지 알아봅시다.
우선 OauthClient
라는 인터페이스를 선언하고 구현체를 생성합니다.
public interface OauthClient {
...
String execute();
}
@Component
public class GoogleOauthClient implement OauthClient {
...
public String execute() {
...
}
}
@Component
public class FaceBookOauthClient implement OauthClient {
...
public String execute() {
...
}
}
이 구현체들을 관리할 수 있는 매니저 클래스를 생성합니다.
@Component
@RequiredArgsConstructor
public class OauthClientManager {
Map<String, OauthClient> clients;
public OauthClient get(String key) {
// key를 통해 적절한 OauthClient를 선택한다.
Collection<String> clients = clients.keySet();
String clientName =
clients.stream()
.filter(client -> client.contains(key.name().toLowerCase()))
.findFirst()
.orElseThrow(NoSuchFieldError::new);
return clients.get(clientName);
}
}
Map<String, OauthClient> clients
@Autowired
애너테이션은 배열, 컬렉션, 맵을 해당 컬랙션의 값 타입에서 파생된 대상 빈 타입을 가져와 처리합니다.
OauthClientManager
에서는 스프링이 Map<String, OauthClient>
에 빈 등록 이름을 키 값으로 빈을 값으로 가지는 맵을 주입(DI)해 줍니다.
이러한 맵의 주입을 통해 OauthClientManager
는 구현체들을 그룹으로 관리할 수 있게 됩니다.
public class SignUpUserUseCase {
private final UserDao userDao;
private final RandomNickNameGeneraterImpl nickNameGenerater;
private final OauthClientManager oauthClientManager;
public Response execute(Request request) {
...
// OauthClientManager에서 key에 해당하는 OauthClient를 선택하고 실행한다.
OauthResponse oauthResponse = oauthClientManager.get(key).execute();
...
}
}
인터페이스 구현체를 그룹으로 관리하는 OauthClientManager
를 통해 SignUpUserUseCase
에서는 key
의 값에 따라 동적으로 적절한 OauthClient
를 선택할 수 있게 됩니다.
'개발' 카테고리의 다른 글
네임드 락을 활용한 동시성 제어 (2) | 2024.09.08 |
---|---|
Pinpoint 도입 이모저모 (0) | 2024.08.22 |
jOOQ 사용기 (1) | 2024.07.23 |
FEW MVP 기능을 구현하며 (0) | 2024.07.14 |
이미지를 지원하는 Swagger 만들기 - multipart/form-data 요청을 직접 추가 (0) | 2024.06.28 |