File I/O를 이용한 Repository의 저장 및 로드 메소드 리팩토링
☑️ 기존 FileRepository 저장 및 로드 메소드
@Repository
@ConditionalOnProperty(name = "discodeit.repository.type", havingValue = "file")
public class FileChannelRepository implements ChannelRepository {
private final Map<UUID, Channel> channels;
public Map<UUID, Channel> loadFromFile() {
File file = new File(filePath);
if (!file.exists()) return new HashMap<>();
try (ObjectInputStream ois = new ObjectInputStream(new FileInputStream(file))) {
return (Map<UUID, Channel>) ois.readObject();
} catch (IOException e) {
throw new RuntimeException("파일 읽기 실패: " + filePath, e);
} catch (ClassNotFoundException e) {
throw new RuntimeException("데이터 형식 오류: " + filePath, e);
}
}
public void saveToFile(Map<UUID, Channel> data) {
try (ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream(filePath))) {
oos.writeObject(data);
} catch (IOException e) {
throw new RuntimeException("파일 저장 실패 : ", e);
}
}
}
- 각 FileRepository마다 loadFromFile(), saveToFile()의 메소드가 중복되어 선언되어 있음
- 만약, 파일 저장 방식이 바뀌거나 예외 처리 로직이 변경되면 모든 Repository를 하나하나 수정해야 함
- 유지보수가 어렵고, 코드 중복
✅ AbstractFileRepository 추상 클래스로 리팩토링
public abstract class AbstractFileRepository<K, V> {
private final String filePath;
protected AbstractFileRepository(@Value("${discodeit.repository.file-directory}")String basePath, String path) {
this.filePath = basePath + path;
}
public Map<K, V> loadFromFile() {
File file = new File(filePath);
if (!file.exists()) return new HashMap<>();
try (ObjectInputStream ois = new ObjectInputStream(new FileInputStream(file))) {
return (Map<K, V>) ois.readObject();
} catch (IOException e) {
throw new RuntimeException("파일 읽기 실패: " + filePath, e);
} catch (ClassNotFoundException e) {
throw new RuntimeException("데이터 형식 오류: " + filePath, e);
}
}
public void saveToFile(Map<K, V> data) {
try (ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream(filePath))) {
oos.writeObject(data);
} catch (IOException e) {
throw new RuntimeException("파일 저장 실패 : ", e);
}
}
}
- 다양한 타입에 대응 가능한 제너릭 K, V 구조 설계
- 생성자를 통해 저장 path를 파라미터로 전달 받아 filePath 초기화
- loadFromFile(), saveToFile() 메소드 정의
- 추후 save, delete, find 등 추상 메소드로 정의 후 클래스에서 확장 가능
👍 기존 FileRepository 수정
@Repository
@ConditionalOnProperty(name = "discodeit.repository.type", havingValue = "file")
public class FileChannelRepository extends AbstractFileRepository<UUID, Channel> implements ChannelRepository {
public FileChannelRepository(@Value("${discodeit.repository.file-directory}") String filePath) {
super(filePath, "/channel.ser");
}
@Override
public Channel createChannel(ChannelCreateDto channelCreateDto) {
try {
Channel channel = new Channel(
channelCreateDto.getAdmin(),
channelCreateDto.getName(),
channelCreateDto.getDescription()
);
return save(channel.getId(), channel);
} catch (RuntimeException e) {
throw new RuntimeException("채널 생성 실패: " + e.getMessage(), e);
}
}
@Override
public Optional<Channel> getChannel(GetPublicChannelRequestDto getPublicChannelRequestDto) {
Map<UUID, Channel> channels = loadFromFile();
return channels.get(getPublicChannelRequestDto.getChannelId()) == null ? Optional.empty() : Optional.of(channels.get(getPublicChannelRequestDto.getChannelId()));
}
}
- 더이상 중복된 I/O 메소드를 작성할 필요 없이 상속받은 loadFromFile(), saveToFile() 메소드를 사용하면 됨
💡 리팩토링을 통해 느낀 점
- 공통 기능은 최대한 추상화하여 중복을 제거하자
- 제너릭을 통해 유연한 설계 방법
- 미션을 진행하면서 작은 기능 하나를 추가했을 뿐인데도, 관련된 여러 부분을 함께 수정해야 하는 상황이 반복됐고, 이 과정에서 유연한 설계의 중요성과 유지보수의 부담을 체감했고, 초기 설계 단계의 중요성을 뼈저리게 느꼈음
- 앞으로 미션, 프로젝트를 진행할 때에는 기능 구현만 급하게 생각할 것이 아닌, 지금보다 나중을 염두에 두고 설계할 수 있도록 많은 연습이 필요해 보임