전 글을 보시면 아시겠지만 1월부터 계약직 인턴 생활을 시작했습니다.
출근을 하고 일주일 정도는 도메인 지식 파악과, 기존 코드 분석만 하는데 시간을 사용했습니다.
여러 단계로 중첩된 연관관계를 가진 도메인과 비즈니스에 대한 이해를 해야했고, 처음 보는 DDD-Lite를 사용한 코드를 이해하기 위해 일주일 동안 인텔리제이를 붙잡고 있었습니다. 그러던 와중 어느날 갑자기 프론트 쪽 사무실에서 나와선 안되는 소리가 나왔습니다.
"…어? 뭐야 이거 왜 안돼”
어느날 파일 업로드가 안된다는 소식이 들려왔다.
프론트 쪽에서 갑자기 txt 파일이 요청에 들어가면 파일 업로드 API가 제대로 동작하지 않는다는 소식이 전해졌습니다.
현재 회사에선 파일 업로드를 위해 AWS-S3와 Presiengd-URL을 이용해 파일 업로드를 처리 중입니다.
https://docs.aws.amazon.com/ko_kr/AmazonS3/latest/userguide/using-presigned-url.html
미리 서명된 URL을 통해 객체 다운로드 및 업로드 - Amazon Simple Storage Service
임시 보안 인증 정보를 사용하여 미리 서명된 URL을 생성하면 보안 인증 정보가 만료되면 이 URL도 만료됩니다. 일반적으로 미리 서명된 URL은 URL을 만드는 데 사용한 자격 증명이 취소, 삭제 또는
docs.aws.amazon.com
(정확하게는 AWS S3가 아닌 회사 내부 NAS를 스토리지로 이용하고 있지만, 이 글에선 S3라고 생각해도 문제는 없습니다)
파일 업로드를 위해선 크게 다음과 같은 과정을 거칩니다.
- 프론트에서 업로드할 파일 정보를 넣어 백엔드 API에 요청
- 백엔드에선 해당 파일 정보를 DB에 저장한 후, 파일 정보에 맞는 presigned url 발급
- 프론트에서 해당 url을 이용해 S3 파일 업로드
문제는 2번째 과정인 presigned-url을 발급하는 과정에서 올바른 요청임에도 API가 에러를 뱉는 문제가 발생했다는 겁니다.
txt 확장자를 사용한 파일만 말이죠.
프론트에선 S3에 파일 업로드를 시도도 하기 전에 문제가 발생했으니 명백한 백엔드의 문제였습니다.
기존 코드에서 파일 업로드와 관련된 로직을 찾아 보기 시작했습니다. 그리고 얼마 안 가 바로 그 원인을 알 수 있었습니다.
하드코딩된 조건문에서 확장자가 누락됐다.
그 전에 파일 업로드 관련 비즈니스 로직에 대해 조금 더 자세히 살펴봅시다.
우리는 파일에 대한 정보를 DB에 저장하고 관리할 필요가 있습니다.
그래서 백엔드에선 presigned-url을 발급해주기 전, 해당 파일에 대한 정보를 DB에 저장해야 합니다.
이때 업로드하는 파일의 종류는 두 가지인데 이미지 파일과 문서 파일 이고, 상세한 요구사항은 다음과 같습니다.
- 모든 파일은 공통적으로 (이름, 확장자) 정보를 가진다.
- 이미지 파일의 경우 추가적으로 (해상도, 가로, 세로 길이) 정보를 가진다.
- 두 파일은 확장자를 이용해 구분한다. jpg 확장자는 이미지 파일, 나머지 확장자 (txt, pdf, xml)들은 문서 파일로 구분한다.
이제 문제가 된 로직을 살펴봅시다. 파일 생성에 관련된 어플리케이션 서비스 로직입니다.
public class FileService {
public File createFile(FileCreateRequest request) {
FileExtension extension = request.extension();
if(isFileTypeDocument(extension)){
createDocumentFile(request);
} else {
createImageFile(request);
}
}
// 문제의 로직
private boolean isFileTypeDocument(FileExtension extension) {
return extension == PDF || extension == XML;
}
}
사용자 요청으로 들어온 파일 정보 중 확장자를 이용해 요청 파일이 이미지 파일 인지, 문서 파일 인지 구분합니다.
이때 확장자를 확인하는 로직은 isFileTypeDocument()를 통해서 진행됩니다.
그 이후 파일 타입에 알맞은 생성 로직을 호출합니다.
여러분들은 혹시 해당 함수가 반환하는 조건식에 TXT 타입 확장자가 빠져있다는 사실을 눈치채셨나요..?
isFileTypeDocument()에 들어있어야 할 TXT 확장자가 빠져있어 올바른 파일 타입으로 구분을 못해서 문제가 발생했던 것이었습니다.
문제는 이것뿐만이 아니다.
txt 파일 업로드 문제는 그렇게 위 함수 조건식 마지막에 TXT 조건을 추가하는 것으로 쉽게 해결했습니다.
그런데 이대로 넘어가기엔 뭔가 찜찜한 생각이 듭니다..
- 왜 저런 하드코딩된 조건식이 등장하게 된 걸까요?
- 그리고 왜 어플리케이션 서비스에 저 코드가 존재하는것이며, 저 로직 말고 비슷한 문제가 발생하는 로직이 또 없을까요?
(당연히 있었습니다) - 만약 새로운 확장자 타입이나 파일 종류가 추가되어 지원해야 한다면? (ex : mp4 동영상 파일 등)
위에서 말한 문제가 생길 수 있는 함수들을 모두 찾아다니며 변경해야 하지 않을까요?
이 시점에서 저를 회사에 소개시켜준 A씨가 저에게 첫 임무를 줬습니다.
저 문제를 해결한 후, 파일 도메인 코드들을 알잘딱 하게 리팩토링해봐라.
기존 구현 뜯어보기 - JPA 상속 관계 매핑
이번에 문제가 된 로직을 리팩토링 하기 전에 파일 도메인이 어떻게 구현되어 있는지 확인해봅시다.
domain
├── file
│ ├── controller
│ ├── application
│ ├── dao
│ └── domain
│ ├── File.java
│ ├── ImageFile.java
│ ├── DocumentFile.java
│ ├── FileExtension.java
│ └── ...
└── ...
파일 도메인에 들어가 domain 패키지를 열자 DDD 특유의 작은 애그리거트 루트가 아닌, 여러 종류의 파일들이 저를 반겼습니다.
한 번 자세한 구현을 보겠습니다.
@Entity @Getter
@Inheritance(strategy = InheritanceType.SINGLE_TABLE)
public class File {
@Id @Column(name = "file_id")
private Long id;
private String name;
@Enumerated(EnumType.STRING)
private FileExtension extension;
}
public enum FileExtension {
JPG(".jpg"), TXT("txt"), PDF("pdf"), XML("xml");
private String value;
}
@Entity @Getter
public class ImageFile extends File {
private double resolution;
private int width;
private int height;
}
@Entity
public class DocumentFile extends File {}
파일 도메인은 JPA 싱글 테이블 상속 전략을 이용해 구현하고 있었습니다.
파일 도메인의 애그리거트 루트는 File 이었고, File을 상속 받은 ImageFIle, DocumentFile 가 존재했습니다.
요구사항을 다시 한 번 생각해보면
- 두 종류의 파일 중 한 파일만 가지는 정보가 존재합니다. (이미지 파일은 해상도, 가로, 세로 길이를 저장할 필요가 있음)
- 하지만 두 파일을 시스템 내부에서만 구분하고 사용자는 구분하지 않습니다. (요청과 응답 양식은 이미지 파일, 문서 파일 모두 동일함)
위의 두 가지 요구사항 모두를 만족하기 위해 JPA 상속 관계 매핑을 사용해 구현한 것으로 보입니다.
파일과 이미지 파일, 문서 파일의 관계가 정확히 (is a / is a kind of) 관계를 만족하기 때문에
상속 관계 매핑을 이용하기로 한 선택은 괜찮은 것처럼 보입니다.
굳이 상속 관계 매핑을 사용하지 않고 구현할 방법은 없을까 싶지만
- 파일 처리와 관련된 비즈니스 로직이 두 파일 모두 동일한데 둘을 다른 애그리거트로 보고 분리하는 것은 오히려 복잡성을 늘리고 중복 코드를 유발할 것으로 보이며
- 상속 없이 단일 테이블과 단일 엔티티로 제작시 문서 파일에서 getWidth() 함수를 사용할 수 있다는 점이 어색하게 느껴지기도 합니다.
그러나 상속 관계를 활용하지 못하는 서비스 로직들
그러나 지금 어플리케이션 서비스 로직엔 앞서 설명한 문제를 일으켰던 조건문 외에도 상속 관계의 이점을 사용하지 못하는 코드들이 존재했습니다.
public class FileService {
// 파일 정보를 수정하는 메서드
public void updateFile(File file, FileRequest reqeust){
if (file instanceOf ImageFIle imageFile) {
imageFile.update(request.name(),
request.extension(),
request.resolution(),
request.weight(),
request.height());
}
if (file instanceOf DocumentFile documentFile) {
documentFile.update(request.name(), request.extension());
}
}
}
instanceOf 를 이용해 해당 파일이 이미지 파일인지, 문서 파일인지 구분한 후 파일 타입별로 알맞은 함수를 호출하는 조건문입니다.
이러한 instanceOf를 활용하는 로직이 문제가 되는 이유는 많은 분들이 이미 아실거라고 생각합니다.
- 객체지향 원칙을 정면으로 위배하는 코드 (OCP, SRP 등)
- 외부 객체가 자식 객체들의 구현을 알고 있음
- 또한 앞서 문제가 됐던 생성 로직에선, 확장자를 이용해 생성할 파일 타입을 구분했는데 여기선 파일 객체 타입으로 구분.
즉 같은 역할을 하는 코드가 여기저기 분리되어 다른 방식으로 구현되어 있음 등등
사실 구글에 “instanceOf 사용을 지양하라” 라는 키워드로 검색을 해보면 다양한 블로그 글들을 볼 수 있습니다.
그리고 그러한 블로그 글들은 모두 위와 비슷한 이유를 근거로 대면서 객체지향적인 프로그래밍을 위해 instanceOf 사용을 지양하고 다형성을 사용하라고 이야기 합니다.
다형성을 사용해보려했더니.. 어?
instanceOf 대신 다형성을 활용하기 위해 수정 로직을 다음과 같이 수정해보겠습니다.
// 파일 정보를 수정하는 메서드
public void updateFile(File file, FileRequest reqeust){
file.update(request.name(), request.extension(), ???); //문제 발생
}
“어 근데 다형성을 사용할 수가 없는데?”
생각해보면 파일 타입 별로 응답 객체 생성과 수정 메서드에 들어가는 값이 다르다는 것을 알 수 있습니다.
이미지 파일은 문서 파일과 다르게, 수정 시에 가로, 세로, 해상도 값을 필요로 합니다.
이러한 상황에서 다형성을 활용하려면 다음과 같이 부모 클래스에서
들어올 수 있는 모든 값을 파라미터로 하는 update() 추상 메서드를 작성해야 합니다.
public class File {
private Long id;
private String name;
private FileExtension extension;
// 추상 메서드 사용
public abstract void update(String name,
FileExtension extension,
double resolution,
int width,
int height) {}
}
public class DocumentFile extends File {
@Override // 사용하지도 않을 width, heidhgt를 파라미터로 받는 문제
public void update(String name, FileExtension extension, double resolution, int width, int height){
this.name = name;
this.extension = extension;
}
}
public class ImageFile extends File {
private double resolution;
private int weight;
private int height;
@Override
public void update(String name, FileExtension extension, double resolution, int width, int height){
this.name = name;
this.extension = extension;
this.resolution = resolution;
this.weight = width;
this.height = height;
}
}
그러나 이러한 로직은 향후 다양한 문제를 발생시킬 수 있습니다.
- 부모 클래스가 자식 클래스의 구현에 의존합니다. 새로운 변수를 가지는 파일 타입이 추가된다면 부모 클래스의 update 메서드와, 이를 구현한 자식 클래스의 메서드들도 모두 찾아가서 바꿔야만 합니다.
- 사용자는 인자로 들어간 값들로 파일 내부 정보를 바꿀 것을 예상하는데, Document 파일 구현을 들어가봐야지만 인자로 받은 값을 무시한다는 사실을 알 수 있습니다. 메서드 이름만 보고는 사용자가 어떻게 동작할지 예측할 수 없는 상황이 오게 됩니다.
결국 해답은, 서브타입 별로 update 함수를 구현한 후, 서비스 코드에서 타입별 캐스팅을 한 후 사용해야 합니다. 이 말은 즉 서비스에서 instanceOf를 완전히 제거하기는 것이 불가능하다는 것입니다.
사실 파일 도메인 내부에 isDocumentFile()과 같이 파일 타입을 체크하는 함수를 만듦으로서 표면적으로 instnaceOf를 제거하는 것은 가능합니다.
그러나 이는 파일 타입 조회를 instanceOf로 하냐, 내부 속성을 이용하냐의 차이일 뿐, 서비스에서 파일의 서브타입과 구현을 알아야 한다는 instanceOf의 단점을 해결할 수 없습니다.
다형성은 만능 해결책이 될 수 없다.
앞서 이야기한 대부분의 블로그 글에서 instanceOf 에 대해 다루는 이야기를 보면,
코드를 보다가 instnaceOf를 보자마자 속이 뒤집어지고, 알 수 없는 무언가가 끓어오르며, 당장 저 코드를 작성한 놈을 잡아다 멍석에 말아 두들겨 패고 소금을 뿌려 쫓아낸 다음, 다형성을 사용하라는 식으로 이야기합니다. (사실 그 정도는 아니다)
그런데 instanceOf를 무슨일이 있어도 사용하면 안되는 걸까요?
제임스 고슬링이 바보도 아니고, 의미 없는 기능을 우리를 괴롭히려고 만들었을리가 없습니다.
우리 요구사항을 충족하기 위해선 instanceOf나 그와 같은 역할을 하는 로직이 반드시 서비스에 존재해야 합니다.
그러나 instanceOf를 사용을 지양해야 하는 이유 역시 앞서 설명했듯이 명확합니다.
잠깐.. 그럼 애초에 이 상황에서 상속을 쓰는게 맞는 걸까요?
“instanceOf를 반드시 사용해야 하는 상황이 온다면, 그건 사실 상속을 활용하면 안되는 상황에서 상속을 사용중이라는 신호로 봐야하는 것 아닐까?” 라는 생각이 들기 시작했습니다.
우리는 상속 관계 매핑을 지양해야 하는가?
상속관계 매핑을 지양해야 하는가? - 인프런 | 커뮤니티 질문&답변
누구나 함께하는 인프런 커뮤니티. 모르면 묻고, 해답을 찾아보세요.
www.inflearn.com
검색을 해보던 중 JPA 대가 영한 선생님이 인프런 질문에 답변하신 내용을 찾아 가져왔습니다.
- 객체의 상속관계를 활용할 것인가? (객체)
- 아니면 객체 내부에 타입을 두고 해당 타입으로 구분할 것인가 (자료구조)
의 문제로 보라는 힌트를 받았습니다. 클린 코드 6장의 내용을 조금 더 살펴보겠습니다.
객체는 동작을 공개하고 자료를 숨긴다. 그래서 기존 동작을 변경하지 않으면서 새 객체 타입을 추가하기 쉬운 반면, 기존 객체에 새 동작을 추가하기는 어렵다.
자료 구조는 별다른 동작 없이 자료를 노출한다. 그래서 기존 자료 구조에 새 동작을 추가하기는 쉬우나, 기존 함수에 새 자료 구조를 추가하기는 어렵다.
아무래도 고민하던 내용과 비슷한 것 같습니다.
그렇다면“우리 프로젝트와 같은 상황에서 상속 관계 매핑을 사용하는것이 맞는가?” 를 고민해봤을때
- 객체의 상속관계(다형성)을 이용할 것인가
- 객체 내부에 타입을 두고 구분할 것인가 (실질적 instanceOf 사용)
를 결정해야하고 전자는 엔티티를 객체, 후자는 자료구조로 보는 관점입니다.
그리고 우리는 다형성을 사용할 수 없는 상황이므로 후자를 선택해야만 합니다.
JPA 엔티티를 자료구조로 본다? 그럼 DDD는?
// 디미터 법칙을 위반하는 것처럼 보이는 코드
final String outputDir = ctxt.options.scratchDir.absolutePath;
클린 코드에선 자료구조는 해당 메서드 내부의 모든 내용을 자유롭게 접근할 수 있어야 하며 비즈니스 규칙 메서드를 추가해선 안된다고 이야기합니다. 디미터 법칙과 이를 어기는 "기차 충돌" 코드도 객체라면 문제가 있지만, 자료구조에선 용인해도 된다고 이야기하기도 합니다.
이러한 엔티티를 자료구조로 보는 관점은 DDD의 전술적 패턴과 부유한 도메인 모델을 전적으로 부정하는 일이 아닌가 하는 생각이 듭니다.
그런데 DDD는 항상 적용해야 하는 절대적인 개념인가라는 물음에 대한 대답은 “아니오” 라고 단언할 수 있습니다.
도메인 주도 개발 시작하기에서도 이런 이야기를 합니다.
비즈니스 로직이 간단한 CRUD를 벗어나지 않으며, 다른 도메인과 연관관계가 복잡하지 않다면, 때로는 트랜잭션 스크립트 패턴, 고전적인 컨트롤러 서비스 도메인 패턴을 사용하는 것이 유지보수에 더 유리할 수 있다.
우리의 파일 도메인은 어떨까요? 제 생각에는
비즈니스 로직은 CRUD 로직에 presigned-url을 발급하는 로직 정도만 추가되어 있으며,
다른 도메인과 연결될 가능성도 매우 적은 간단한 도메인입니다.
이런 경우, DDD에 매몰되어 객체로만 보기보단, 조금 더 유연한 사고가 필요할 때입니다.
이제 알잘딱하게 리팩토링해보자
1. 확장자를 이용해서 파일 타입을 알 수 있도록 하기
@Getter
@RequiredArgsConstructor
public enum FileExtension {
JPG("jpg", IMAGE),
XML("xml", DOCUMENT),
PDF("pdf", DOCUMENT),
TXT("txt", DOCUMENT);
private final String value;
private final FileType fileType;
}
public enum FileType {
DOCUMENT, IMAGE;
}
우선 위와 같이 확장자를 이용해서 파일 타입을 알 수 있도록 하기 위해서
확장자 enum 내부에 파일 타입을 분류할 수 있는 enum 항목을 추가했습니다.
사실 파일 타입은 확장자만으로 결정되며, 같은 확장자가 여러 파일 타입을 가질 확률도 매우 적어 이렇게 변경하기로 결정했습니다.
이제 글의 맨처음 문제가 발생했던 파일 생성 전 구분 로직, instanceOf를 사용하는 로직들을 다음과 같이 바꿀 수 있습니다.
public class FileService {
// == 변경 전 ==
private File createFile(FileCreateRequest request) {
FileExtension extension = request.extension();
if(isFileTypeDocument(extension)){
return createDocumentFile(request);
} else {
return createImageFile(request);
}
}
// 문제의 로직
private boolean isFileTypeDocument(FileExtension extension) {
return extension == PDF || extension == XML;
}
// == 변경 후 ==
public File createFile(FileCreateRequest request) {
FileExtension extension = request.extension();
if (extension.getFileType() == IMAGE) {
return ImageFile.create(
fileName,
extension,
resolution,
width,
height);
}
else if (extension.getFileType() == DOCUMENT) {
return DocumentFile.create(fileName, extension);
}
else {
throw new Exception("Invalid FileType");
}
}
//변경 전
public void updateFile(File file, FileRequest reqeust){
if (file instanceOf ImageFIle imageFile) {
imageFile.update(request.name(),
request.extension(),
request.resolution(),
request.weight(),
request.height());
}
if (file instanceOf DocumentFile documentFile) {
documentFile.update(request.name(), request.extension());
}
}
// 변경 후
public void updateFile(File file, FileRequest reqeust){
FileExtension extension
if (extension.getFileType() == IMAGE) {
ImageFile imageFile = (ImageFile) file;
imageFile.update(request.name(),
request.extension(),
request.resolution(),
request.weight(),
request.height());
}
if (extension.getFileType() == DOCUMENT) {
DocumentFile documentFile = (DocumentFile) file;
documentFile.update(request.name(), request.extension());
}
}
}
이제 새로운 파일 확장자가 추가되더라도, 파일 서비스는 변경할 필요 없이 FileExtension enum만 추가하면 됩니다.
2. 변경에 취약한 부분을 몰아넣기
사실 위와 같은 코드가 FileService의 비즈니스 로직의 전부는 아닙니다.
Presiend-URL을 받아오는 로직도 존재해야하고 파일 조회, 삭제와 같은 로직도 FileService 내부에 존재합니다.
사실 이 글의 예시 코드는 원활한 설명을 위해, 실제 코드를 참고하여 새롭게 만들어 온 것에 가깝습니다.
실제 코드는 조금 더 다양한 역할을 수행하고 있었습니다. 그런데 만약 새로운 파일 타입이 추가된다면..?
위의 생성, 수정 로직의 if문은 추가된 파일 타입에 맞춰 변경되어야 합니다.
그리고 하나씩 수정하는 과정에서 놓치는 부분이 있다면 역시 문제가 됩니다.
이걸 해결하는 방법은 여러가지가 있겠지만, 변경에 취약한 부분만 따로 몰아넣는 것도 좋은 해결책입니다.
그리고 사실 조건에 따라 알맞은 객체를 생성하여 반환하는 좋은 방법을 모두 알고 있습니다.
바로 팩토리 클래스를 활용하는 것입니다.
public class FileFactory {
public File createFile(String fileName, FileExtension extension, double resolution, int width, int height) {
if (extension.getFileType() == FileType.IMAGE) {
return ImageFile.create(
fileName,
extension,
resolution,
width,
height);
}
else if (extension.getFileType() == FileType.DOCUMENT) {
return DocumentFile.create(fileName, extension);
}
else {
throw new Exception("Invalid FileType");
}
}
}
렇게 팩토리 클래스를 도메인 서비스 처럼 만들어 관리한다면,
파일 타입 추가에 취약한 코드들을 어플리케이션 서비스에서 분리할 수 있습니다.
3. 상속 관계 매핑 해제하기
그리고 이렇게 파일 도메인 내부의 Extension을 이용해 파일 타입을 구분한다면 더 이상 상속을 사용해야만 하는 이유는 없어집니다.
어차피 단일 테이블을 사용해 데이터를 저장하니, 다음과 같이 합성을 이용하도록 변경해볼 수도 있겠습니다.
public class File {
private Long id;
private String name;
private FileExtension extension;
private ImageData imageData;
...
public void updateDocumentFile(String name, FileExtension extension){
....
}
public void updateImageFile(String name, FileExtension extension, ImageData imageData){
....
}
}
public class ImageProperties {
private double resolution;
private int width;
private int height;
}
그러나 여기까지 진행하진 않았습니다. 그 이유는
- 상속 관계의 엔티티 정의가 비즈니스 규칙을 명료하게 제공한다.
- 파일 타입에는 이미지 파일과 문서 파일이 있고.. 이미지 파일은 어떤 정보를 추가로 가지고… 와 같은 정보를 엔티티 정의를 본다면 명확하게 파악할 수 있습니다.
- 타입 캐스팅을 통해 혹시 모를 엔티티의 잘못된 사용을 예방할 수 있다.
- 문서 파일에서 해상도를 호출하려 하면 문제가 발생해야 합니다. 상속을 통해 문서 파일과 이미지 파일을 분리하면, 위와 같은 상황을 막을 수 있습니다.
- 잘 사용하고 있던 DB 테이블 구조 변경에 대한 부담
- 상속을 사용하지 않는다면 DB 테이블에서 더이상 dtype 컬럼을 사용할 필요가 없습니다. 그러나 “이미 잘 사용하고 있던 DB 테이블까지 바꾸면서 까지 고쳐야할 문제인가?” 는 아니라고 생각했습니다.
결론 & 후기
상속관계 매핑을 사용하던 중 instancOf를 사용할 수 밖에 없는 상황이 생겼다면 “엔티티 내부에 타입(Enum FileType)을 두고, 엔티티를 자료구조로 사용해 외부에서 타입을 읽고 사용하는 방법도 고려해봐야 한다.” 라는 결론입니다.
간단한 리팩토링이었지만 그 과정에서 “instanceOf 사용을 지양해야 하는가?”와 같이 블로그만 읽고 대충 알던 지식들을 제대로 공부해보고 상황에 맞춰 적용하기 위해 고민해보는 좋은 경험을 얻었습니다.
또한 "은탄환은 없다"와 가장 좋은 설계란 내 상황에 가장 알맞은 설계이다. 라는 자주 잊는 진리를 다시 한 번 깨닫게 됐습니다.
사실 어그로를 잔뜩 끈 서론에 비해 결론은 보잘것 없으며, 결론에 비해 결과물은 더 애매한 리팩토링으로 끝났습니다.
상속 구조는 아직도 사용 중이며, 서비스 코드와 팩토리 클래스엔 중복된 코드가 존재합니다.
리팩토링을 하던 도중 고민하던 저에게 A씨가 WET 원칙과 DRY 원칙을 반대하는 사람들의 의견에 대해 이야기해줬습니다.
“관리가 가능한 중복 코드는 너무 두려워 할 필요는 없다” 라는 이야기로 들렸습니다.
https://news.hada.io/topic?id=15102
코드를 성급하게 DRY하지 마세요 | GeekNews
코드 중복 제거를 너무 일찍 하지 말아야 하는 이유DRY(Don't Repeat Yourself) 원칙을 너무 엄격하게 적용하면 "Premature" 추상화를 유발하여 미래의 변경이 더 복잡해질 수 있음중복이 진짜로 불필요한
news.hada.io
비슷한 의미로 영한 선생님은 "어느날 고민 많은 주니어 개발자가 찾아왔다" 라는 세미나에서
“추상화도 비용이다”라는 이야기를 하셨습니다.
객체지향 원칙은 중요합니다.
하지만 변경의 범위를 한정하고, 변경에 취약한 코드를 한쪽으로 몰아넣는 선에서 중복 코드와 의존관계를 잘 관리할 수 있다면
성급한 추상화보다 좋은 해결책이 될 수 있습니다.
완벽한 객체지향 코드를 만드는 추상화의 비용이 때로는 중복 코드와 절차지향 코드를 사용하는 것보다 클 때도 있다는 사실을 잊지 않아야겠습니다.
분별있는 프로그래머는 모든 것이 객체라는 생각이 미신임을 잘 안다.
때로는 단순한 자료구조와 절차적인 코드가 가장 적합한 상황도 있다.
-클린 코드-
잘못된 내용을 보시거나 다른 견해가 있다면 언제든 댓글 부탁드립니다.
- 여담으로 저 리팩토링 이후 약 한 달 뒤 확장자를 추가해야 하는 상황이 생겼습니다.
enum 하나 추가하는 것으로 다른 코드 하나도 안건드리고 깔끔하게 확장자 추가에도 성공했는데 기분이 상당히 좋았습니다.
'Spring' 카테고리의 다른 글
사지방에서 Spring boot 프로젝트 #4 - 비즈니스 로직은 어디에 위치해야 하는가 (1) | 2024.01.14 |
---|---|
사지방에서 Spring boot 프로젝트 #3 - 패키지 구조 리팩토링 (적절한 패키지 구조는 무엇일까?) (0) | 2024.01.04 |
사지방에서 Spring boot로 토이 프로젝트를 진행하며 #2 - 테스트 코드 작성하기, 그 외 설정들 (0) | 2023.08.05 |