포스트

corou : 싱글톤 패턴과 의존성 주입

TypeORM

TypeORM 부분 관련 며칠째 수정하는지 모르겠다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
@Entity('item_order')
export class ItemOrder {
    @PrimaryGeneratedColumn()
    order_key!: number;

    @Column({ type: 'date' })
    order_at!: Date;

    @Column({ type: 'varchar', length: 255 })
    status!: 'ORDERED' | 'CANCELLED' | 'DELIVERED';

    @Column({ type: 'int' })
    price_total!: number;

    @ManyToOne(() => User)
    @JoinColumn({ name: 'user_key' })
    user_key!: number;

    @ManyToOne(() => Address)
    @JoinColumn({ name: 'address_key' })
    address_key!: number;
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
@Entity('item_order')
export class ItemOrder {
    @PrimaryGeneratedColumn()
    order_key!: number;

    @Column()
    user_key!: number;

    @Column()
    address_key!: number;

    @Column({ type: 'date' })
    order_at!: Date;

    @Column({ type: 'varchar', length: 255 })
    status!: 'ORDERED' | 'CANCELLED' | 'DELIVERED';

    @Column({ type: 'int' })
    price_total!: number;

    @ManyToOne(() => User)
    @JoinColumn({ name: 'user_key' })
    user!: User;

    @ManyToOne(() => Address)
    @JoinColumn({ name: 'address_key' })
    address!: Address;
}

이 두 코드는 모두 같은 테이블 구조를 갖는다.

  • order_key: PrimaryGeneratedColumn, number
  • user_key: Column, number
  • address_key: Column, number
  • order_at: Column, date
  • status: Column, varchar(255)
  • price_total: Column, int

둘의 차이는, 참조 여부에서 나뉜다.

1번 코드는 단순 숫자 값으로 키를 저장을 하고, 이를 인자로 전달하여 데이터베이스를 탐색하는 과정을 거침으로 해당 데이터를 가져올 수 있다.

2번 코드는 엔터티의 인스턴스를 실제로 참조한다.

효율 면에서 두 번째 코드가 객체 참조로 인해 엔터티에 쉽게 접근할 수 있으며, 레이지 로딩을 통해 성능 최적화에 도움이 된다고 하여 이 방식으로 다시 변경했다. 엔터티 코드를 3일동안 바꾸고 있는 사람

메소드 변경

위 엔터티 내용 변경에 따라 서비스 객체에 있는 메소드들의 변경도 불가피하게 되었다.

1
2
3
4
5
6
7
8
9
10
11
12
13
    async addAddress(user_key: number, name: string, addr: string, addr_detail: string, zip: string, tel: string, request: string, is_default: 'Y' | 'N'): Promise<Address> {
        const newAddress = this.addressRepository.create({
            user_key,
            name,
            addr,
            addr_detail,
            zip,
            tel,
            request,
            is_default
        });
        return await this.addressRepository.save(newAddress);
    }

기존 addAddress 메소드

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
    async addAddress(user_key: number, name: string, addr: string, addr_detail: string, zip: string, tel: string, request: string, is_default: 'Y' | 'N'): Promise<Address> {
        try {
            const user = await UserService.getUser(user_key);
            if (!user) {
                throw new Error('해당 유저를 찾을 수 없습니다.');
            }
            const newAddress = this.addressRepository.create({
                user_key,
                name,
                addr,
                addr_detail,
                zip,
                tel,
                request,
                is_default
            });
            return await this.addressRepository.save(newAddress);
        } catch (error) {
            throw new Error('주소를 추가하는데 실패했습니다.');
        }
    }

새로운 addAddress 메소드

다만 이렇게 하면 오류가 생긴다.

UserSerivceAddressService에서 바로 사용할 수 있는 것이 아니라, 선언을 해주어야 한다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
import { UserService } from './user.service`

const userService = new UserService; 

export class AddressService {
    private addressRepository = AppDataSource.getRepository(Address);

    // 사용자 주소 추가 
    async addAddress(user_key: number, name: string, addr: string, addr_detail: string, zip: string, tel: string, request: string, is_default: 'Y' | 'N'): Promise<Address> {
        try {
            const user = await userService.getUser(user_key);
            if (!user) {
                throw new Error('해당 유저를 찾을  없습니다.');
            }
            const newAddress = this.addressRepository.create({
                user_key,
                name,
                addr,
                addr_detail,
                zip,
                tel,
                request,
                is_default
            });
            return await this.addressRepository.save(newAddress);
        } catch (error) {
            throw new Error('주소를 추가하는데 실패했습니다.');
        }
    }

다만, 찾아보니 UserService의 인스턴스를 클래스 외부에서 생성하는 것 보다, AddressService 클래스 내부에서 생성하는 것이 더 좋다고 한다. 우선 이렇게 내부에서 생성하면 독립적인 인스턴스를 가지므로, 상태공유로 인한 경쟁 상태를 방지할 수 있다. 다만, 서버의 메모리 사용량이 증가하지만, 일반적으로 이는 상대적으로 덜 중요한 사항이기에 내부에서 생성하는 것이 옳다고 본다.

내부에서 생성할 때 두 가지 방법이 있는데, 각각은 다음과 같다:

  • 싱글톤 패턴: UserService가 상태를 가지지 않거나, 상태가 공유되어도 문제가 없는 경우 싱글톤 패턴을 사용하면 메모리 사용량을 줄이며 초기화 비용을 최소화한다.

  • 의존성 주입: UserService가 상태를 가지며, 별도의 단일 인스턴스를 보장한다.

나의 경우에는 각 서비스들의 결합도를 낮추기 위해 의존성 주입 방법으로 진행하기로 했다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
import { Repository } from 'typeorm';
import { AppDataSource } from '../config/ormconfig';
import { User } from '../entities/user.entity';

export class UserService {
    private userRepository: Repository<User>;

    constructor(userRepository: Repository<User>) {
        this.userRepository = userRepository;
    }

    // 사용자 생성
    async createUser(email: string, password: string, username: string, birth_date: Date, gender: 'M' | 'F'): Promise<User> {
        let user = await this.userRepository.findOneBy({ email });
        if (user) {
            throw new Error('해당 이메일은 이미 사용중입니다.');
        }

        user = await this.userRepository.findOneBy({ username });
        if (user) {
            throw new Error('해당 닉네임은 이미 사용중입니다.');
        }

        const newUser = this.userRepository.create({
            email,
            password,
            username,
            birth_date,
            gender
        });
        return await this.userRepository.save(newUser);
    }
    // 닉네임 중복 확인
    async checkUsername(username: string): Promise<boolean> {
        const user = await this.userRepository.findOneBy({ username });
        if (user) {
            return true;
        }
        return false;
    }
    // 사용자 로그인
    async loginUser(email: string, password: string): Promise<User> {
        const user = await this.userRepository.findOneBy({ email });
        if (!user) {
            throw new Error('해당 이메일로 가입된 계정이 없습니다.');
        }

        if (user.password !== password) {
            throw new Error('비밀번호가 일치하지 않습니다.');
        }

        return user;
    }
    // 모든 사용자 정보 조회
    async getAllUsers(): Promise<User[]> {
        const users = await this.userRepository.find();
        if (!users) {
            throw new Error('유저 정보를 불러오는데 실패했습니다.');
        }
        return users;
    }
    // 사용자 정보 조회
    async getUser(user_key: number): Promise<User> {
        const user = await this.userRepository.findOneBy({ user_key });
        if (!user) {
            throw new Error('해당 유저를 찾을 수 없습니다.');
        }
        user.password = '';
        return user;
    }
}

const userRepository = AppDataSource.getRepository(User);
const userService = new UserService(userRepository);

수정된 user.service.ts

무엇이 바뀌었는지 하나하나 짚어보겠다.

1
2
3
4
5
6
7
export class UserService {
    private userRepository: Repository<User>;

    constructor(userRepository: Repository<User>) {
        this.userRepository = userRepository;
    }

우선 클래스 위쪽 부분에 자체 초기화 방식을 의존성 주입 방식으로 바꾸었다.

이 방식을 사용하면, 클래스의 인스턴스를 만들 때 필요한 의존성(Dependency)를 외부에서 전달받는다. 즉, 클래스가 필요한 것을 스스로 만들지 않고, 외부에서 받아서 사용한다는 말이다. 즉, userRepository를 외부에서 받아야 한다는 것이다.

그렇기 때문에 아래와 같이 userRepository를 별도로 별도로 클래스 밖에서 선언해준 후 인자로 userService에 전달해준다:

1
2
const userRepository = AppDataSource.getRepository(User);
const userService = new UserService(userRepository);

이 기사는 저작권자의 CC BY 4.0 라이센스를 따릅니다.