일 | 월 | 화 | 수 | 목 | 금 | 토 |
---|---|---|---|---|---|---|
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 |
31 |
- Volatile
- 일급 객체
- Dependency Injection
- middleware
- synchronized
- Spring
- nestjs
- factory
- lombok
- java
- spring security
- OAuth 2.0
- builder
- 일급 컬렉션
- Google OAuth
- Today
- Total
HJW's IT Blog
Spring 없이 의존성 관리와 팩토리 패턴 구현하기 본문
들어가며..
이 글은 Spring Framework 없이 순수 Java로 프로젝트를 진행하며, 나의 사고 과정을 기록하고, 어떻게 이런 설계에 도달했는지를 설명하기 위해 작성된 글이다. 특히 User, Message, Channel과 관련된 Service Layer, Repository Layer 그리고 이들 간의 의존성 관리를 어떻게 설계했는지에 대한 고민과 해결 과정을 담고 있다.
개요
User, Message, Channel 에 대한 Service Layer, Repository Layer 와 이들 사이의 의존성 관리를 다루는 미션을 수행하는 과정에서의 일이다.
1. 도메인별 팩토리 + 하드코딩된 의존성
1.1 초기 구조
처음 설계 단계에서는 SOLID 원칙을 준수하기 위해 Factory Pattern을 도입했다. 각 도메인의 Service와 Repository를 각각의 팩토리로 분리하여 책임을 명확히 하려 했다. 아래와 같은 구조였다.
ChannelServiceFactory
,MessageServiceFactory
,UserServiceFactory
ChannelRepositoryFactory
,MessageRepositoryFactory
,UserRepositoryFactory
각 도메인의 Service와 Repository는 인터페이스로 정의하여 다양한 구현체를 가질 수 있도록 설계했다. 예를 들어:
public class UserServiceFactory {
public UserService createUserService() {
// 어떤 구현체를 쓸지 직접 하드코딩
UserRepository repository = new JCFUserRepository();
return new BasicUserService(repository);
}
}
당시에는 각 도메인이 독립적이어야 한다는 생각이 강했고, 이렇게 도메인별 팩토리를 분리하는 것이 책임이 명확하다고 판단했다. 하지만 시간이 지나면서 다음과 같은 문제들이 눈에 들어오기 시작했다.
유연성 부족
JCF
기반 저장소를File
기반으로 변경할 때마다 팩토리 코드를 직접 수정해야 했다.
중복된 로직
- 팩토리의 스위치-케이스 분기 구조가 거의 동일한 형태로 반복되었다.
2 ApplicationConfig 도입
위 문제들을 해결 하기 위해, 어떤 Service 타입을 쓸지, 어떤 Repository 타입을 쓸지를 한곳에서 정의해 일관성 있게 만들고자 했다. 이러한 설정을 여러 Factory 에서 공유할 수 있는 구조가 필요했다. 이러한 고민 끝에 ApplicationConfig
객체를 만들기로 했다.
2.1 ApplicationConfig 구현
ApplicationConfig
는 빌더 패턴을 사용하여 가독성을 높였고, 한 번에 초기화하여 사용할 수 있도록 설계했다.
@Getter
public class ApplicationConfig {
private final ServiceType channelServiceType;
private final StorageType channelStorageType;
private final ServiceType userServiceType;
private final StorageType userStorageType;
private final ServiceType messageServiceType;
private final StorageType messageStorageType;
private ApplicationConfig(ApplicationConfigBuilder builder) {
this.channelServiceType = builder.channelServiceType;
this.channelStorageType = builder.channelStorageType;
this.userServiceType = builder.userServiceType;
this.userStorageType = builder.userStorageType;
this.messageServiceType = builder.messageServiceType;
this.messageStorageType = builder.messageStorageType;
}
public static class ApplicationConfigBuilder {
/...
}
}
빌더 패턴으로 가독성 좋게 만들었고, 한번에 초기화 하여 사용할 수 있게 만들었다. 이제 어떤 Service/Storage 를 쓸 것인지는 오직 ApplicationConfig 생성 방식만 바꾸면 되었다.
3. ApplicationConfig와 Factory 연동
3.1 Factory 리팩토링
ApplicationConfig
를 사용하며, 더이상 팩토리는 구현채를 변경할때, 직접 하드코딩할 필요가 없어졌다.
public class UserServiceFactory {
private UserServiceFactory() { }
public static UserService createUserService(ApplicationConfig ac) {
// userServiceType 에 따라 분기
return switch (ac.getUserServiceType()) {
case BASIC -> BasicUserService.getInstance(
RepositoryFactory.createUserRepository(ac.getUserStorageType())
);
case FILE -> FileUserService.getInstance();
case JCF -> JCFUserService.getInstance();
};
}
// ChannelService, MessageService 등도 동일한 패턴
}
이제 Factory 는 어떤 인스턴스의 생성에만 집중할 수 있고, 어떤 타입을 생성할지는 ApplicationConfig 가 결정하게 된다.
4. 새로운 고민
지금 구조에서 팩토리 생성 코드를 보면, new 키워드를 통해 생성할 때 마다 새로운 객체를 생성하여 반환했다. 그렇다면, UserService 에서 UserRepository 에 의존하고, MessageService 에서도 UserRepository 에 의존한다면, 이 둘은 서로 다른 UserRepository 객체를 주입받게 되고, 결과적으로 저장소를 공유하지 못하는 문제가 발생했다.
단순히 Service 를 생성할때 이전에 생성한 Repository 를 넘겨 줄 수도 있었지만, 그렇게 되면 모든 과정을 ApplicationConfig 에 위임할 수 없기 때문에 최대한 책임과 관심사의 분리를 위해 ApplicationConfig 에 모든것을 위임하고 싶었다.
4.1 Factory 들, Service 들 합치기
우선 선택한 방식은 ServiceFactory, RepositoryFactory 단 두개의 Factory 로 컨트롤 하는 것이었다.
Service 가 각 Repository 를 멤버 변수로 가지게 하여, 상태를 가지게 하는 것이다.
RepositoryFactory
:UserRepository
,ChannelRepository
,MessageRepository
를 생성ServiceFactory
:UserService
,ChannelService
,MessageService
를 생성
public class ServiceFactory {
private UserRepository userRepo;
private ChannelRepository channelRepo;
private MessageRepository messageRepo;
private ApplicationConfig ac;
public init(ApplicationConfig ac){
this.ac = ac;
channelRepo = RepositoryFactory.createChannelRepository(ac.getChannelRepositoryType());
//... 다른 초기화
}
public static createUserService(){
return switch(ac.getUserServiceType){
case BASIC -> BasicUserService.getInstance(userRepo, messageRepo);
// ... 다른 분기
}
}
}
이렇게 구현하고 보니 여전히 코드가 깔끔하지 않았다. 팩토리를 정적 팩토리로 만들고 싶었다. 하지만 지금 상태의 팩토리는 멤버 변수를 가지기에 동시성 문제가 있을 것 같았다.. (RaceCondition)
5. 해결 방안들
고민끝에 다음과 같은 해결 방안들을 생각해 보았다.
- ApplicationConfig 집착 버리고 같이 repository 객체도 넘겨서 주입
- service 는 싱글톤으로 구현해 놨으니, repository 대신 service 를 주입하기
- 팩토리를 인스턴스화 해서 사용하기
- repository를 singleton으로 구현
사실 위의 고민들은 Spring 을 사용하면 의존성 주입도 깔끔하게 주입도 하고 지금의 문제도 해결될 수 있다. 그래서 Spring 이라면 이라는 생각을 하고 repository 를 싱글톤으로 구현했다.
public class ServiceFactory {
private ServiceFactory() {
}
public static UserService createUserService(ApplicationConfig ac) {
return switch (ac.getUserServiceType()) {
case JCF -> JCFUserService.getInstance();
case FILE -> FileUserService.getInstance();
case BASIC -> BasicUserService.getInstance(RepositoryFactory.createUserRepository(ac.getUserStorageType()));
};
}
public static ChannelService createChannelService(ApplicationConfig ac) {
return switch (ac.getChannelServiceType()) {
case JCF -> JCFChannelService.getInstance();
case FILE -> FileChannelService.getInstance();
case BASIC ->
BasicChannelService.getInstance(RepositoryFactory.createChannelRepository(ac.getChannelStorageType()));
};
}
public static MessageServiceV2<ChatChannel> createMessageService(ApplicationConfig ac, UserService userService, ChannelService channelService) {
return switch (ac.getMessageServiceType()) {
//case JCF -> JCFMessageServiceV2.getInstance(userService, channelService);
case JCF -> JCFMessageServiceV2.getInstance(
ServiceFactory.createUserService(ac),
ServiceFactory.createChannelService(ac)
);
// case FILE -> FileMessageService.getInstance(userService, channelService);
case FILE -> FileMessageService.getInstance(
ServiceFactory.createUserService(ac),
ServiceFactory.createChannelService(ac)
);
case BASIC -> BasicMessageService.getInstance(
RepositoryFactory.createMessageRepository(ac.getMessageStorageType()),
RepositoryFactory.createUserRepository(ac.getUserStorageType()),
RepositoryFactory.createChannelRepository(ac.getChannelStorageType())
);
};
}
}
마무리
아직 설계의 개선점이 많이 보이지만 이번 설계 과정을 통해 Spring 없이도 의존성 관리와 팩토리 패턴을 활용한 객체 생성을 어떻게 하면 효율적으로 할 수 있을지 고민할 수 있었다. 다만 Spring을 사용했다면 DI 컨테이너와 Bean Scope를 활용해 훨씬 간단하게 해결할 수 있었을 것이다. 그래도 순수 Java로 이러한 문제를 풀어낸 경험은 설계 능력을 키우는 데 큰 도움이 되었다.
'JAVA' 카테고리의 다른 글
JPA - EntityManager와 영속성 컨텍스트 이해하기 (0) | 2025.02.25 |
---|---|
DTO ↔ 엔티티 변환, MapStruct로 자동화하기 (0) | 2025.02.18 |
JVM 은 어떻게 동작하는가 (1) | 2025.01.24 |
JAVA Volatile 키워드와 멀티쓰레드 (0) | 2025.01.13 |
[JAVA] Collections Framework (Linked List, Stack, Queue, Set) (1) | 2024.10.13 |