macOS:14.2 / Nestjs / EC2
기존 EC2 내부 스토리지를 사용하다 S3를 연결했는데 한가지 신경쓰이는게 있었다.
이번 토이프로젝트의 목표중 하나가 무과금이였는데 S3프리티어는 5GB 스토리지와 2만 get요청, 그리고 2천 put요청을 제공해준다.
5기가라는 다소 조금 귀여운 용량은 그렇다치고 제공된 요청건수를 넘어가면 과금이 시작되는 방식인데 둘이서 진행하는 프로젝트라 이정도면 충분하지 않을까 생각하다 혹시몰라 바로바로 전환할 수 있게 코드를 작성해보자 했다.
그렇게 코드를 수정하는데, 전에 작성한 코드들이 아무리 봐도 클래스 탈을 뒤집어쓴 함수형 코드다.
그럴만도한게 nestjs를 처음 접해봤고, 프로젝트 시작시 동료분의 괴물같은 템포를 따라잡고자 예전에 사용하던 express의 코드를 복붙해와서
당장 사용할 수 있을정도로 수정만 해두고 덕지덕지 기워놓은 상태였다.
OOP는 아아아아아주 예전에 JAVA를 찍먹해볼때나(나사실코딩천재인건가하던시절) 개념정도만 알고 있던 터라, 함수형 프로그래밍이 손가락에 익어버려서 생각없이 휘갈기다보면 어느샌가 이렇게 클래스껍데기만 쓰고 있는 함수지향형 코드가 완성되어있다.
그나마 nest의 독재정치?로 어느정도 객체지향을 달성할 수 있지만 앞으로 의식하고 코드를 작성해야되지 않을까 생각한다.
@Injectable()
export class UploadService {
constructor(
@InjectRepository(User)
private userRepository: Repository<User>,
@InjectRepository(Post)
private postRepository: Repository<Post>,
) {}
async uploadProfileImage(userId: number, imageUrl: string): Promise<string> {
const user = await this.userRepository.findOne({
where: { id: userId },
});
if (process.env.NODE_ENV === "production") {
if (user.profileImg) {
const params = {
Bucket: process.env.AWS_BUCKET_NAME,
Key: user.profileImg.split(`${process.env.AWS_BUCKET_NAME}/`)[1],
};
const deleteObjectCommand = new DeleteObjectCommand(params);
await s3.send(deleteObjectCommand);
}
await this.userRepository.update({ id: userId }, { profileImg: imageUrl });
return imageUrl;
} else {
if (user.profileImg) {
const serverUrl: string = process.env.SERVER_URL;
const relativeImagePath: string = user.profileImg.replace(serverUrl, "").replace(/^\//, "");
const absoluteImagePath: string = path.join(__dirname, "..", "public", relativeImagePath);
if (fs.existsSync(absoluteImagePath)) {
fs.unlinkSync(absoluteImagePath);
}
}
const finalUrl: string = await createUploadUrl(imageUrl);
await this.userRepository.update({ id: userId }, { profileImg: finalUrl });
return finalUrl;
}
}
...다른 메서드들
}
아직 객체지향을 완벽하게 이해하지 못한 코린이의 주관적인 생각입니다
대충 눈에 보이는 문제점은 다음과 같았다
다시보니 굳이 객체지향을 적용하지 않아도 문제가 많아보인다 ㅋㅋ..
리펙터링 과정에서 특히 '의존성' 에 대해 이해하기가 쉽지 않았는데 조금 쉽게 풀어보자
의존성을 이해하는 데 도움이 될 수 있는 비유 중 하나는 팀 프로젝트를 생각해보면 된다. 예를 들어 한 사람은 디자인을 담당하고, 다른 사람은 코드를 작성하고, 또 다른 사람은 프로젝트 관리를 담당한다. 이렇게 각 팀원은 자신의 역할에 집중하면서 다른 팀원이 자신의 역할을 잘 수행할 것이라는 의존성을 가지게 된다. 이와 마찬가지로, 소프트웨어에서의 의존성은 한 부분이 다른 부분의 기능이나 동작에 의존하는 관계를 의미한다. 예를 들어, 서비스 클래스는 데이터베이스와의 통신 기능을 필요로 하는데, 이 기능을 직접 구현하는 대신 데이터베이스와의 통신 기능을 제공하는 다른 클래스에 의존할 수 있다. 이렇게 하면 서비스 클래스는 자신의 주요 역할에 집중할 수 있으며 데이터베이스와의 통신은 의존하고 있는 클래스가 담당하게 된다. 이러한 의존성 관리는 코드의 유연성을 높이고, 각 부분의 역할을 명확하게 하며, 코드의 재사용성을 높인다.
비유가 장황하지만 더 쉽게 말하면 모든 기능을 모듈화(분리)하고, 필요에 따라 의존성으로 주입받아 사용한다고 생각하면 이해하기 수월하다. (지극히 개인적인 코린이의 주장)
의존성은 캡슐화와도 관계가 있는데, 의존성 주입을 사용하면 클래스는 주입받은 객체의 인터페이스에만 의존하게 된다. 이는 클래스가 주입받은 객체의 구체적인 구현에 의존하지 않고, 대신 그 객체가 제공하는 메서드와 속성에 의존한다는 것을 의미한다.
class DatabaseService {
getData() {
// 데이터베이스에서 데이터를 가져오는 코드
}
saveData(data) {
// 데이터베이스에 데이터를 저장하는 코드
}
}
class UserService {
constructor(private databaseService: DatabaseService) {} // UserService는 DatabaseService 의존성 주입
getUser() {
return this.databaseService.getData();
}
saveUser(user) {
this.databaseService.saveData(user);
}
}
위 코드에서 UserService는 DatabaseService의 구체적인 구현에 의존하지 않고, 대신 DatabaseService가 제공하는 인터페이스인 getData와 saveData 메서드에 의존하게 된다.
더 쉽게 풀자면 UserService는 DatabaseService의 구체적인 내용을 알지 못하고 제공되는 메서드(인터페이스)인 getData와 saveData의 존재유무만 알 수 있다. 이로써 서로의 결합도가 낮춰지고 코드의 유연성이 높아짐에 따라 유지보수가 쉬워진다.
@Injectable()
export class UploadService {
private readonly isProduction: boolean;
private readonly bucketName: string;
constructor(
@InjectRepository(User)
private userRepository: Repository<User>,
@InjectRepository(Post)
private postRepository: Repository<Post>,
private configService: ConfigService,
) {
// 환경변수를 의존성 주입으로 결합을 낮춘다
this.isProduction = configService.get("NODE_ENV") === "production";
this.bucketName = configService.get("AWS_BUCKET_NAME");
}
// 각 메서드 내부에서 반복되던 로직 분리 및 삼항연산자를 활용한 확장성 증가
async deleteImage(
target: "user" | "post",
targetId: number,
field: "profileImg" | "backgroundImg" | "postImg",
isProduction: boolean,
) {
const repository = target === "user" ? this.userRepository : this.postRepository;
const entity = await repository.findOne({ where: { id: targetId } });
const image = entity[field];
if (image) {
if (isProduction) {
const params = {
Bucket: this.bucketName,
Key: image.split(`${this.bucketName}/`)[1],
};
const deleteObjectCommand = new DeleteObjectCommand(params);
await s3.send(deleteObjectCommand);
} else {
await deleteRelativeImage(image);
}
}
return;
}
// 각 메서드 내부에서 반복되던 로직 분리 및 삼항연산자를 활용한 확장성 증가
async updateImage(
target: "user" | "post",
targetId: number,
imageUrl: string,
field: "profileImg" | "backgroundImg" | "postImg",
isProduction: boolean,
): Promise<string> {
const repository = target === "user" ? this.userRepository : this.postRepository;
let finalUrl: string;
if (isProduction) {
finalUrl = imageUrl;
} else {
finalUrl = await createUploadUrl(imageUrl);
}
await repository.update({ id: targetId }, { [field]: finalUrl });
return finalUrl;
}
// 업데이트, 삭제 메서드를 분리해 SRP원칙을 지키고 가독성 향상 및 중복코드 제거
async uploadProfileImage(userId: number, imageUrl: string): Promise<string> {
const user = await this.userRepository.findOne({
where: { id: userId },
});
if (user.profileImg) await this.deleteImage("user", userId, "profileImg", this.isProduction);
return this.updateImage("user", userId, imageUrl, "profileImg", this.isProduction);
}
async uploadBackgroundImage(userId: number, imageUrl: string): Promise<string> {
const user = await this.userRepository.findOne({
where: { id: userId },
});
if (user.backgroundImg)
await this.deleteImage("user", userId, "backgroundImg", this.isProduction);
return this.updateImage("user", userId, imageUrl, "backgroundImg", this.isProduction);
}
async uploadPostImage(postId: number, imageUrl: string): Promise<string> {
const post = await this.postRepository.findOne({
where: {
id: postId,
},
});
if (post.postImg) await this.deleteImage("post", postId, "postImg", this.isProduction);
return this.updateImage("post", postId, imageUrl, "postImg", this.isProduction);
}
}