상세 컨텐츠

본문 제목

NestJS 실무에서 merge 문법 사용하기

NestJS

by aeongiii 2025. 8. 17. 21:43

본문

NestJS 개발에서 merge는 데이터 병합객체 결합에 꼭 사용되는 문법 패턴입니다.

TypeORM의 Repository merge부터 JavaScript의 객체 병합까지, 백엔드 개발자가 반드시 알아야 할 merge 활용법을 살펴보겠습니다.

merge가 필요한 이유

데이터베이스 업데이트나 객체 조합 시 기존 데이터를 유지하면서 새로운 데이터만 덮어쓰는 경우가 많습니다. 이때 merge를 사용하면 안전하고 효율적으로 처리할 수 있습니다.

// 문제 상황: 사용자 정보 일부만 업데이트
const existingUser = { id: 1, name: '김철수', email: 'kim@example.com', age: 30 };
const updateData = { email: 'newemail@example.com' }; // name, age는 유지해야 함

TypeORM Repository merge

1. 기본 merge 사용법

TypeORM의 merge 메서드는 기존 엔티티와 새로운 데이터를 안전하게 병합합니다.

// user.service.ts
@Injectable()
export class UserService {
  constructor(
    @InjectRepository(User)
    private userRepository: Repository<User>,
  ) {}

  async updateUser(id: number, updateUserDto: UpdateUserDto): Promise<User> {
    // 1. 기존 사용자 조회
    const existingUser = await this.userRepository.findOne({ 
      where: { id } 
    });
    
    if (!existingUser) {
      throw new NotFoundException('사용자를 찾을 수 없습니다.');
    }

    // 2. merge로 안전하게 병합
    const mergedUser = this.userRepository.merge(existingUser, updateUserDto);
    
    // 3. 저장
    return await this.userRepository.save(mergedUser);
  }
}

- merge는 기존 엔티티의 모든 속성을 유지하면서 새로운 데이터만 덮어씁니다.

2. 여러 객체 merge하기

@Injectable()
export class UserService {
  async updateUserWithProfile(
    id: number, 
    userUpdate: UpdateUserDto,
    profileUpdate: UpdateProfileDto
  ): Promise<User> {
    const existingUser = await this.userRepository.findOne({
      where: { id },
      relations: ['profile']
    });

    // 여러 객체를 순차적으로 merge
    let mergedUser = this.userRepository.merge(existingUser, userUpdate);
    
    // 추가 데이터가 있다면 계속 merge
    mergedUser = this.userRepository.merge(mergedUser, {
      profile: { ...existingUser.profile, ...profileUpdate }
    });

    return await this.userRepository.save(mergedUser);
  }
}

3. 관계 데이터와 함께 merge

@Injectable()
export class PostService {
  async updatePostWithTags(
    id: number,
    updateData: UpdatePostDto,
    tagIds: number[]
  ): Promise<Post> {
    const existingPost = await this.postRepository.findOne({
      where: { id },
      relations: ['tags', 'author']
    });

    // 태그 정보 조회
    const tags = await this.tagRepository.findByIds(tagIds);

    // merge로 관계 데이터까지 함께 병합
    const mergedPost = this.postRepository.merge(existingPost, {
      ...updateData,
      tags, // 새로운 태그 관계 설정
      updatedAt: new Date() // 업데이트 시간 갱신
    });

    return await this.postRepository.save(mergedPost);
  }
}

JavaScript 객체 merge 패턴

1. Spread 연산자를 이용한 merge

// user.service.ts
@Injectable()
export class UserService {
  async createUserResponse(user: User, additionalData?: any): Promise<UserResponseDto> {
    const baseResponse = {
      id: user.id,
      email: user.email,
      name: user.name,
      createdAt: user.createdAt
    };

    // spread 연산자로 객체 merge
    return {
      ...baseResponse,
      ...additionalData, // 추가 데이터가 있다면 병합
      isActive: user.lastLoginAt > new Date(Date.now() - 30 * 24 * 60 * 60 * 1000)
    };
  }
}

2. Object.assign을 이용한 merge

// config.service.ts
@Injectable()
export class ConfigService {
  private getDefaultConfig(): AppConfig {
    return {
      database: {
        host: 'localhost',
        port: 3306,
        username: 'root'
      },
      cache: {
        ttl: 3600,
        max: 100
      }
    };
  }

  mergeConfig(userConfig: Partial<AppConfig>): AppConfig {
    const defaultConfig = this.getDefaultConfig();
    
    // Object.assign으로 deep merge 구현
    return {
      ...defaultConfig,
      database: Object.assign({}, defaultConfig.database, userConfig.database),
      cache: Object.assign({}, defaultConfig.cache, userConfig.cache)
    };
  }
}

3. Lodash merge (복잡한 객체 병합)

import * as _ from 'lodash';

@Injectable()
export class SettingsService {
  async updateUserSettings(
    userId: number,
    newSettings: Partial<UserSettings>
  ): Promise<UserSettings> {
    const currentSettings = await this.getUserSettings(userId);
    
    // lodash merge로 deep merge 수행
    const mergedSettings = _.merge({}, currentSettings, newSettings);
    
    await this.settingsRepository.update(userId, mergedSettings);
    return mergedSettings;
  }
}

// 사용 예시
const currentSettings = {
  notifications: {
    email: { marketing: true, updates: false },
    push: { marketing: false, updates: true }
  },
  privacy: { profileVisible: true }
};

const newSettings = {
  notifications: {
    email: { marketing: false } // updates는 그대로 유지됨
  }
};

// 결과: notifications.email.updates는 false로 유지
// notifications.email.marketing만 false로 변경

DTO 업데이트 패턴

1. Partial DTO를 이용한 안전한 merge

// update-user.dto.ts
export class UpdateUserDto {
  @IsOptional()
  @IsString()
  name?: string;

  @IsOptional()
  @IsEmail()
  email?: string;

  @IsOptional()
  @IsInt()
  @Min(0)
  age?: number;
}

// user.service.ts
@Injectable()
export class UserService {
  async updateUser(id: number, updateDto: UpdateUserDto): Promise<UserResponseDto> {
    const user = await this.userRepository.findOne({ where: { id } });
    
    if (!user) {
      throw new NotFoundException('사용자를 찾을 수 없습니다.');
    }

    // undefined 값은 제거하고 merge
    const cleanUpdateDto = this.removeUndefinedFields(updateDto);
    const updatedUser = await this.userRepository.save({
      ...user,
      ...cleanUpdateDto,
      updatedAt: new Date()
    });

    return this.toResponseDto(updatedUser);
  }

  private removeUndefinedFields(obj: any): any {
    return Object.fromEntries(
      Object.entries(obj).filter(([_, value]) => value !== undefined)
    );
  }
}

2. 조건부 merge 처리

@Injectable()
export class ProductService {
  async updateProduct(id: number, updateDto: UpdateProductDto): Promise<Product> {
    const product = await this.productRepository.findOne({ where: { id } });
    
    const updateData: Partial<Product> = {};

    // 조건부로 merge할 데이터 준비
    if (updateDto.name !== undefined) {
      updateData.name = updateDto.name.trim();
    }

    if (updateDto.price !== undefined && updateDto.price > 0) {
      updateData.price = updateDto.price;
      updateData.priceUpdatedAt = new Date(); // 가격 변경 시에만 업데이트
    }

    if (updateDto.categoryId !== undefined) {
      // 카테고리 존재 여부 확인 후 merge
      const category = await this.categoryRepository.findOne({ 
        where: { id: updateDto.categoryId } 
      });
      if (category) {
        updateData.category = category;
      }
    }

    // merge 후 저장
    const mergedProduct = this.productRepository.merge(product, updateData);
    return await this.productRepository.save(mergedProduct);
  }
}

주의사항과 모범 사례

1. 참조 타입 merge 주의

// 위험한 방법: 참조 공유
const badMerge = (original: User, update: Partial<User>) => {
  return { ...original, ...update }; // 객체 참조 공유 위험
};

// 안전한 방법: 깊은 복사
const safeMerge = (original: User, update: Partial<User>) => {
  return {
    ...original,
    ...update,
    // 중첩 객체는 별도로 처리
    profile: update.profile 
      ? { ...original.profile, ...update.profile }
      : original.profile
  };
};

2. 타입 안전성 확보

// Generic을 이용한 타입 안전한 merge
export class MergeUtil {
  static safeMerge<T extends Record<string, any>>(
    target: T,
    source: Partial<T>
  ): T {
    const result = { ...target };
    
    for (const key in source) {
      if (source[key] !== undefined) {
        result[key] = source[key] as T[keyof T];
      }
    }
    
    return result;
  }
}

// 사용
const mergedUser = MergeUtil.safeMerge(existingUser, updateData);

3. 성능 최적화

@Injectable()
export class OptimizedUserService {
  async bulkUpdateUsers(updates: Array<{id: number, data: UpdateUserDto}>): Promise<User[]> {
    // 한 번에 모든 사용자 조회
    const userIds = updates.map(update => update.id);
    const users = await this.userRepository.findByIds(userIds);
    
    // Map으로 빠른 조회 구조 생성
    const userMap = new Map(users.map(user => [user.id, user]));
    
    // 병합된 데이터 준비
    const mergedUsers = updates.map(update => {
      const existingUser = userMap.get(update.id);
      if (!existingUser) return null;
      
      return this.userRepository.merge(existingUser, update.data);
    }).filter(Boolean);

    // 벌크 저장
    return await this.userRepository.save(mergedUsers);
  }
}

Controller에서의 활용

@Controller('users')
export class UserController {
  constructor(private readonly userService: UserService) {}

  @Patch(':id')
  async updateUser(
    @Param('id') id: number,
    @Body() updateUserDto: UpdateUserDto
  ) {
    const updatedUser = await this.userService.updateUser(id, updateUserDto);
    
    return {
      success: true,
      message: '사용자 정보가 업데이트되었습니다.',
      data: updatedUser
    };
  }

  @Patch(':id/settings')
  async updateSettings(
    @Param('id') id: number,
    @Body() settingsDto: Partial<UserSettingsDto>
  ) {
    // 기존 설정과 새 설정 merge
    const updatedSettings = await this.userService.updateSettings(id, settingsDto);
    
    return {
      success: true,
      data: updatedSettings
    };
  }
}

정리

  1. TypeORM Entity 업데이트: 기존 데이터 보존하며 일부만 수정
  2. DTO 병합: 클라이언트에서 전달받은 부분 업데이트 데이터 처리
  3. 설정 객체 조합: 기본 설정과 사용자 설정 병합
  4. 응답 데이터 구성: 여러 소스의 데이터를 하나로 결합

예시:

  • TypeORM의 merge 메서드 활용으로 안전한 엔티티 병합
  • Spread 연산자(...)를 이용한 얕은 복사 병합
  • Lodash merge를 이용한 깊은 객체 병합
  • 타입 안전성을 위한 Generic 활용
  • undefined 값 처리로 예상치 못한 덮어쓰기 방지

'NestJS' 카테고리의 다른 글

NestJS에서 map 문법 사용하기  (1) 2025.08.17

관련글 더보기