본 내용은 내일배움캠프에서 활동한 내용을 기록한 글입니다.
해설 강의 17번에서 공연 생성 기능을 구현하고 있었음
튜터님께서는 각각의 Repository에 접근해서 데이터를 넣는 것이 아니라 공연 엔티티에 정의되어 있는 관계를 통해서 각 Repository에 데이터를 넣는 방법을 사용하셨음
내가 작성한 기존의 코드는 순차적으로 공연 데이터, 시간 데이터, 장소 데이터, 좌석 데이터 와 같이 순차대로 Repository에 접근해서 데이터를 저장했음
튜터님께서 사용하신 방법으로도 도전하고자 공연 생성 코드를 전체적으로 수정함
아래가 기존에 작성한 공연 생성 코드임
// 공연 등록
async createShow(createShowDto: CreateShowDto, files: Express.Multer.File[]) {
const {
title,
content,
category,
runningTime,
times,
placeName,
seatA,
seatS,
seatR,
seatVip,
priceA,
priceS,
priceR,
priceVip,
} = createShowDto;
// 트랜젝션 연결 설정 초기화
const queryRunner = this.dataSource.createQueryRunner();
await queryRunner.connect();
await queryRunner.startTransaction();
// 이미지 업로드 후 반환되는 이미지 URL 배열
let images: string[];
try {
// show 테이블에 데이터 저장
const show = this.showRepository.create({
title,
content,
category,
runningTime,
});
// 다른 테이블에서 show.id를 사용하기 때문에 따로 저장
await queryRunner.manager.save(show);
// 이미지 데이터를 업로드
images = await this.awsService.uploadImage(files);
// show_price 테이블에 데이터 저장
const showPrice = this.showPriceRepository.create({
showId: show.id,
priceA,
priceS,
priceR,
priceVip,
});
// show_time 테이블에 데이터 저장
const showTimes = times.map((time) =>
this.showTimeRepository.create({
showId: show.id,
showTime: time,
}),
);
// show_image 테이블에 데이터 저장
const showImages = images.map((image) =>
this.showImageRepository.create({
showId: show.id,
imageUrl: image,
}),
);
// queryRunner를 병렬로 처리해서 데이터베이스에 저장
await Promise.all([
queryRunner.manager.save(showPrice),
queryRunner.manager.save(showTimes),
queryRunner.manager.save(showImages),
]);
// show_place 테이블에 데이터 저장
const totalSeat: number = seatA + (seatS ?? 0) + (seatR ?? 0) + (seatVip ?? 0);
const showPlace = showTimes.map((showTime) =>
this.showPlaceRepository.create({
showId: show.id,
showTimeId: showTime.id,
placeName,
totalSeat,
seatA,
seatS,
seatR,
seatVip,
}),
);
await queryRunner.manager.save(showPlace);
// 장소(시간)별, 등급별 좌석 정보 추가하기
for (let k = 0; k < showPlace.length; k++) {
let seatNumber = 1;
// A 좌석 데이터 저장
for (let i = 1; i <= showPlace[k].seatA; i++) {
await queryRunner.manager.save(
this.seatService.createSeat(
show.id,
showPlace[k].showTimeId,
seatNumber,
Grade.A,
showPrice.priceA,
),
);
seatNumber++;
}
// S 좌석 데이터 저장
for (let i = 1; i <= showPlace[k].seatS; i++) {
await queryRunner.manager.save(
this.seatService.createSeat(
show.id,
showPlace[k].showTimeId,
seatNumber,
Grade.S,
showPrice.priceS,
),
);
seatNumber++;
}
// R 좌석 데이터 저장
for (let i = 1; i <= showPlace[k].seatR; i++) {
await queryRunner.manager.save(
this.seatService.createSeat(
show.id,
showPlace[k].showTimeId,
seatNumber,
Grade.R,
showPrice.priceR,
),
);
seatNumber++;
}
// Vip 좌석 데이터 저장
for (let i = 1; i <= showPlace[k].seatVip; i++) {
await queryRunner.manager.save(
this.seatService.createSeat(
show.id,
showPlace[k].showTimeId,
seatNumber,
Grade.VIP,
showPrice.priceVip,
),
);
seatNumber++;
}
}
// 출력 형식 지정
const createdShow = {
id: show.id,
title,
content,
runningTime,
placeName,
totalSeat,
priceA,
priceS,
priceR,
priceVip,
showTimes: showTimes.map((time) => time.showTime),
showImages: showImages.map((image) => image.imageUrl),
createdAt: show.createdAt,
updatedAt: show.updatedAt,
};
await queryRunner.commitTransaction();
return createdShow;
} catch (err) {
await queryRunner.rollbackTransaction();
// 트랜젝션 실패 시 S3 이미지도 롤백
await this.awsService.rollbackS3Image(images);
throw new InternalServerErrorException(SHOW_MESSAGE.CREATE_SHOW.FAIL);
} finally {
await queryRunner.release();
}
}
위 코드도 정상적으로 데이터가 저장되었기에 과제 제출은 위의 코드로 제출함
아래 코드는 튜터님께서 사용하신 방법을 토대로 간단하게 구조를 변경했음
// 공연 등록
async createShow(createShowDto: CreateShowDto) {
const { showImages, showSchedules, showPrice, showPlace, ...restOfShow } = createShowDto;
// 트랜젝션 연결 설정 초기화
const queryRunner = this.dataSource.createQueryRunner();
await queryRunner.connect();
await queryRunner.startTransaction();
// 총 좌석 수
const totalSeat: number =
showPlace.seatA + (showPlace.seatS ?? 0) + (showPlace.seatR ?? 0) + (showPlace.seatVip ?? 0);
try {
const show = this.showRepository.create({
...restOfShow,
showImages,
showPrice,
showSchedules: showSchedules.map((showSchedule) => {
return {
...showSchedule,
...showPlace,
totalSeat,
};
}),
});
await queryRunner.manager.save(show);
for (let i = 0; i < showSchedules.length; i++) {
let seatNumber = 1;
// A 좌석 데이터 저장
for (let j = 1; j <= showPlace.seatA; j++) {
await queryRunner.manager.save(
this.seatService.createSeat(
show.id,
show.showSchedules[i].id,
seatNumber,
Grade.A,
showPrice.priceA,
),
);
seatNumber++;
}
// S 좌석 데이터 저장
for (let j = 1; j <= showPlace.seatS; j++) {
await queryRunner.manager.save(
this.seatService.createSeat(
show.id,
show.showSchedules[i].id,
seatNumber,
Grade.S,
showPrice.priceS,
),
);
seatNumber++;
}
// R 좌석 데이터 저장
for (let j = 1; j <= showPlace.seatR; j++) {
await queryRunner.manager.save(
this.seatService.createSeat(
show.id,
show.showSchedules[i].id,
seatNumber,
Grade.R,
showPrice.priceR,
),
);
seatNumber++;
}
// Vip 좌석 데이터 저장
for (let j = 1; j <= showPlace.seatVip; j++) {
await queryRunner.manager.save(
this.seatService.createSeat(
show.id,
show.showSchedules[i].id,
seatNumber,
Grade.VIP,
showPrice.priceVip,
),
);
seatNumber++;
}
}
await queryRunner.commitTransaction();
return {
...restOfShow,
showImages: show.showImages.map((image) => image.imageUrl),
showSchedules: show.showSchedules.map((schedule) => {
return {
showTime: schedule.showTime,
seatA: schedule.seatA,
seatS: schedule.seatS,
seatR: schedule.seatR,
seatVip: schedule.seatVip,
};
}),
showPrice: {
priceA: show.showPrice.priceA,
priceS: show.showPrice.priceR,
priceR: show.showPrice.priceR,
priceVip: show.showPrice.priceVip,
},
};
} catch (err) {
console.log(err);
await queryRunner.rollbackTransaction();
// 트랜젝션 실패 시 S3 이미지도 롤백
await this.awsService.rollbackS3Image(showImages.map((image) => image.imageUrl));
throw new InternalServerErrorException(SHOW_MESSAGE.CREATE_SHOW.FAIL);
} finally {
await queryRunner.release();
}
}
코드의 분량 자체는 크게 변화하지 않았지만 가장 큰 변화는 공연 Repository에만 접근해서 관계를 형성한 Repository에 데이터를 저장하는 방식임
기존에는 각각의 Repository에 접근해서 데이터를 넣었지만 위 코드는 해당 엔티티의 관계를 통해서 데이터를 저장했음
위 코드처럼 구조를 변경한 이유는 그저 이런 저런 방식으로 코드를 작성하고 싶었기 때문임
그리고 위 코드에서 관계를 형성할 때, 필요한 외래키를 명시한적이 없는데 이건 아래와 같이 엔티티에서 { cascade: true }
라는 관계 옵션을 사용했기 때문에 자동으로 외래키가 적용됨
@Entity({
name: SHOW_CONSTANT.ENTITY.SHOW.NAME,
})
export class Show {
@PrimaryGeneratedColumn()
id: number;
/**
* 공연 제목
* @example "공연 제목 테스트"
*/
@Index()
@IsString()
@IsNotEmpty({ message: SHOW_MESSAGE.DTO.TITLE.IS_NOT_EMPTY })
@Column({ type: 'varchar', nullable: false })
title: string;
//...
@OneToMany(() => ShowImage, (showImage) => showImage.show, { cascade: true })
showImages: ShowImage[];
@OneToMany(() => ShowSchedule, (showSchedule) => showSchedule.show, { cascade: true })
showSchedules: ShowSchedule[];
}
내일은 플러스 주차 팀프로젝트 발제를 하는 날임
주제는 프로젝트 협업 도구인 트렐로
를 만드는 것임
그래도 트렐로가 어떤 툴인지 알기 때문에 기본적인 이해에는 도움이 될 것 같음
발제 첫 날이기 때문에 팀 노션에 기본적인 깃허브 룰, 커밋 룰, 코드 컨벤션을 작성할 예정
그리고 프로젝트에 대한 와이어프레임과 ERB, API 명세서를 작성할 예정
시간만 된다면 깃허브 레포지터리 생성과 기본적인 프로젝트의 틀을 생성하는 것이 목표임
오늘은 해설 강의를 바탕으로 공연 등록관련 코드를 리팩토링함
사실 공연 등록 메서드의 코드를 대부분 수정했다고 해도 과언이 아님
그 과정에서 1:1 관계로 연결한 공연 장소 엔티티와 시간 엔티티를 하나의 엔티티로 합침
튜터님께서 1:1 관계는 컬럼의 수가 너무 많거나 자주 접근하지 않는 경우를 제외하면 사용하지 않는 것이 좋다고 하셨기에 엔티티부터해서 관계있는 코드들을 수정했음
튜터님 말씀대로 수정하니 1:1 관계일 때 데이터 저장에서 발생하는 문제를 해결했음
모든 데이터를 따로따로 저장하던 코드에서 엔티티의 관계를 이용한 방법으로 저장하도록 코드를 리팩토링함
리팩토링을 진행하던 중 공연 생성을 할 때 1:1 관계를 맺고 있는 공연 시간 엔티티와 공연 장소 엔티티에서 시간 데이터는 저장되었지만 장소 데이터는 제대로 저장되지 않음
공연은 여러 개의 시간대를 가지고 있기 때문에 공연과 시간은 1:N관계고, 그 여러 개의 시간은 각각 하나의 장소 데이터를 가지고 있기 때문에 시간과 장소는 1:1관계를 가짐
결국 방법을 찾지 못해서 튜터님께 갔음
튜터님께서 엔티티의 구조부터가 좋지 않은 형태라고 하셨음
1:1 관계일 때 굳이 엔티티를 따로 뺄 필요가 없다고 하셨음
1:1 관계는 정말 접근할 일이 많이 없을 때에나 사용한다고 하셨음
그렇기에 기존에 있던 문제는 시간과 장소 엔티티를 하나의 엔티티로 사용하면 해결되는 문제라고 하셨기에 코드 전체를 새롭게 리팩토링 함
정말 프로젝트의 전체를 고쳐야 하기 떄문에 그 과정을 따로 기록을 하지 못함