NestJS 개발에서 merge는 데이터 병합과 객체 결합에 꼭 사용되는 문법 패턴입니다.
TypeORM의 Repository merge부터 JavaScript의 객체 병합까지, 백엔드 개발자가 반드시 알아야 할 merge 활용법을 살펴보겠습니다.
데이터베이스 업데이트나 객체 조합 시 기존 데이터를 유지하면서 새로운 데이터만 덮어쓰는 경우가 많습니다. 이때 merge를 사용하면 안전하고 효율적으로 처리할 수 있습니다.
// 문제 상황: 사용자 정보 일부만 업데이트
const existingUser = { id: 1, name: '김철수', email: 'kim@example.com', age: 30 };
const updateData = { email: 'newemail@example.com' }; // name, age는 유지해야 함
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는 기존 엔티티의 모든 속성을 유지하면서 새로운 데이터만 덮어씁니다.
@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);
}
}
@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);
}
}
// 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)
};
}
}
// 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)
};
}
}
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로 변경
// 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)
);
}
}
@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);
}
}
// 위험한 방법: 참조 공유
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
};
};
// 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);
@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('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
};
}
}
예시:
| NestJS에서 map 문법 사용하기 (1) | 2025.08.17 |
|---|