OOP 원칙에 따른 리펙터링 (feat. S3)

DaeChan Jo·2023년 12월 8일
0

nest.js

목록 보기
1/3
post-thumbnail

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;
    }
  }
  
...다른 메서드들

}

아직 객체지향을 완벽하게 이해하지 못한 코린이의 주관적인 생각입니다
대충 눈에 보이는 문제점은 다음과 같았다

  • 중복 코드 파티
    유저와 게시글 엔터티는 각각 하나의 이미지만 가질 수 있도록 계획했기에 새롭게 이미지를 업데이트하면 기존 이미지는 삭제되어야 한다. 그렇게에 각각의 메서드에 삭제와 업데이트 로직이 중복되고 있었다.
  • SRP 위반
    각 함수들이 여러가지 책임(삭제, 업데이트)을 가지게 되었고 이로인해 중복과 복잡도가 증가
  • if문 중첨
    아직도 벗어나지 못한 if문 중첩..
  • 확장성 부족
    추후 다른 이미지 필드가 추가된다면 또 같은 로직을 반복해서 작성해야되므로 악순환의 반복이 예상
  • 강한 의존성
    환경변수로 프로덕션환경 여부와 버켓의 이름을 직접적으로 사용해 강한 의존성으로 유연성 저하

다시보니 굳이 객체지향을 적용하지 않아도 문제가 많아보인다 ㅋㅋ..


리펙터링 과정에서 특히 '의존성' 에 대해 이해하기가 쉽지 않았는데 조금 쉽게 풀어보자



의존성(종속성)

의존성을 이해하는 데 도움이 될 수 있는 비유 중 하나는 팀 프로젝트를 생각해보면 된다. 예를 들어 한 사람은 디자인을 담당하고, 다른 사람은 코드를 작성하고, 또 다른 사람은 프로젝트 관리를 담당한다. 이렇게 각 팀원은 자신의 역할에 집중하면서 다른 팀원이 자신의 역할을 잘 수행할 것이라는 의존성을 가지게 된다. 이와 마찬가지로, 소프트웨어에서의 의존성은 한 부분이 다른 부분의 기능이나 동작에 의존하는 관계를 의미한다. 예를 들어, 서비스 클래스는 데이터베이스와의 통신 기능을 필요로 하는데, 이 기능을 직접 구현하는 대신 데이터베이스와의 통신 기능을 제공하는 다른 클래스에 의존할 수 있다. 이렇게 하면 서비스 클래스는 자신의 주요 역할에 집중할 수 있으며 데이터베이스와의 통신은 의존하고 있는 클래스가 담당하게 된다. 이러한 의존성 관리는 코드의 유연성을 높이고, 각 부분의 역할을 명확하게 하며, 코드의 재사용성을 높인다.

비유가 장황하지만 더 쉽게 말하면 모든 기능을 모듈화(분리)하고, 필요에 따라 의존성으로 주입받아 사용한다고 생각하면 이해하기 수월하다. (지극히 개인적인 코린이의 주장)

의존성은 캡슐화와도 관계가 있는데, 의존성 주입을 사용하면 클래스는 주입받은 객체의 인터페이스에만 의존하게 된다. 이는 클래스가 주입받은 객체의 구체적인 구현에 의존하지 않고, 대신 그 객체가 제공하는 메서드와 속성에 의존한다는 것을 의미한다.

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);
  }
}
profile
BackEnd Developer

0개의 댓글