์ฃผ๋ณ์ ๊ฐ๋ฐ์ ๋ถ๋ค๊ณผ ๋ฆฌ์กํธ์ DI์ ๋ํ ์ด์ผ๊ธฐ๋ฅผ ํ ์ ์ด ์์ต๋๋ค.
Angular, Nest.js, Spring๊ณผ ๊ฐ์ ํ๋ ์์ํฌ์์ ์์กด์ฑ ์ฃผ์
์ ํตํด ํ์ฅ์ฑ ์๊ฒ ๊ฐ๋ฐ์ด ๊ฐ๋ฅํ๋ฐ ๋นํด
๋ฆฌ์กํธ์ ๊ฐ์ ๋ผ์ด๋ธ๋ฌ๋ฆฌ์์๋ ์์กด์ฑ ์ฃผ์
์ ๋์
ํ ์ ์๋ค๋ ์ด์ผ๊ธฐ๋ฅผ ํ์์ต๋๋ค.
๊ทธ๋ ๊ฒ ๊ด๋ จ ์๋ฃ๋ฅผ ์ฐพ๋ค๊ฐ Effect๋ผ๋ ํ์
์คํฌ๋ฆฝํธ ๊ธฐ๋ฐ์ ํจ์ํ ํ๋ก๊ทธ๋๋ฐ ๋ผ์ด๋ธ๋ฌ๋ฆฌ๋ฅผ ์๊ฒ ๋์๋๋ฐ
์ด๊ฒ์ ํจ์ํ ํ๋ก๊ทธ๋๋ฐ์ ๊ธฐ๋ฐ์ผ๋ก ์์กด์ฑ ์ฃผ์
๊ฐ๋
์ ๋์
ํ ์ ์์์ต๋๋ค. ๊ทธ๋์ ๊ฐ๋จํ๊ฒ Effect๋ก ๊ตฌํํด๋ณด์์ต๋๋ค.
๋ค์๊ณผ ๊ฐ์ ์ ์ฅ์๋ฅผ ๊ตฌํํ๋ ์ฝ๋๋ฅผ ์์ฑํด๋ด ์๋ค.
Effect๋ก ์ฝ๋๋ฅผ ์์ฑํ ๋์๋ ์ธํฐํ์ด์ค ๊ธฐ๋ฐ์ ํ๋ก๊ทธ๋๋ฐ์ ์ถ๊ตฌํฉ๋๋ค.
๊ทธ๋ ๊ธฐ ๋๋ฌธ์ ๊ฐ ๊ธฐ๋ฅ์ ๋ํด ์ธํฐํ์ด์ค๋ฅผ ๋ช
์ํ๊ณ ํด๋น ์ธํฐํ์ด์ค์ ๋ํ ๊ตฌํ์ฒด๋ฅผ ์์ฑํฉ๋๋ค.
์๋์ ์ฝ๋์์๋ ์ ์ ์ ์ฅ์์ ๋ก์ปฌ์คํ ๋ฆฌ์ง ์ ์ฅ์์ ์ธํฐํ์ด์ค๋ฅผ ์ ์ธํ๊ณ ๊ฐ ์ธํฐํ์ด์ค์์ ์ฌ์ฉ๋๋ ์๋ฌ ํ์
์ ์ ์ํฉ๋๋ค.
class UserRepositoryError extends Error {
readonly _tag = "UserRepositoryError"
constructor(message: string) {
super(`UserRepositoryError: ${message}`)
this.name = "UserRepositoryError"
}
static readonly NotFound = (id: number) => {
return new UserRepositoryError(`User with id ${id} not found`)
}
}
class LocalStorageError extends Error {
readonly _tag = "LocalStorageError"
constructor(message: string) {
super(`LocalStorageError: ${message}`)
this.name = "LocalStorageError"
}
static readonly parseError = (error: unknown) => {
if (error instanceof Error) {
return new LocalStorageError(error.message)
}
return new LocalStorageError("Unknown error")
}
static readonly stringifyError = (error: unknown) => {
if (error instanceof Error) {
return new LocalStorageError(error.message)
}
return new LocalStorageError("Unknown error")
}
static readonly NotFound = (key: string) => {
return new LocalStorageError(`LocalStorage key ${key} not found`)
}
}
class LocalStorageService extends Context.Tag("LocalStorageService")<
LocalStorageService,
{
getItem<T>(key: string): Effect.Effect<T | undefined, LocalStorageError>
setItem<T>(key: string, value: T): Effect.Effect<void, LocalStorageError>
removeItem(key: string): Effect.Effect<void, LocalStorageError>
}
>() {}
class UserRepository extends Context.Tag("UserRepository")<
UserRepository,
{
update: (user: User) => Effect.Effect<void, UserRepositoryError>
findById: (id: number) => Effect.Effect<User | undefined, UserRepositoryError>
findAll: () => Effect.Effect<Array<User>, UserRepositoryError>
}
>() {}
Effect๋ ์ ์์ ์ผ๋ก ๋ฐํ๋๋ ๋ฐ์ดํฐ, ์๋ฌ ํ์
, ์๊ตฌ์ฌํญ์ผ๋ก ๊ตฌ๋ถ๋๋ ๊ธฐ๋ณธ์ ์ธ ์ธํฐํ์ด์ค๋ฅผ ๊ฐ์ง๊ณ ์์ต๋๋ค.
์๋์ ํจ์์ ๋ฐํ๊ฐ์ ์์๋ก ์ฑ๊ณต์ผ ๋์๋ T ๋๋ undefined๋ฅผ ๋ฐํํ๊ณ ์๋ฌ์ผ ๋์๋ ๋ก์ปฌ์คํ ๋ฆฌ์ง ์๋ฌ ํ์
์ ๋ฐํํฉ๋๋ค.
getItem<T>(key: string): Effect.Effect<T | undefined, LocalStorageError>
update: (user: User) => Effect.Effect<void, UserRepositoryError>
์ด์ ๋ ์ธํฐํ์ด์ค๋ฅผ ์ด์ฉํด ๊ฐ ๊ตฌํ์ฒด๋ฅผ ์์ฑํด๋ด ์๋ค.
LocalStorageService ์ธํฐํ์ด์ค์ ๋ํ ์ค์ ๊ตฌํ์ ๊ตฌํํฉ๋๋ค.
์์ฒด์ ์ธ try/catch ํจ์๊ฐ ์์ด ๊ธฐ์กด์ try/catch ๋ฐฉ์์ ๋์ ํ ์ ์์ต๋๋ค. try/catch ๋ฌธ์ ์ด๋ ๊ฒ ์์ฑํ๋ฉด ์ข์ ์ ์ ๋ฌด์์ผ๊น์?
๊ธฐ์กด์ ๋ฌธ๋ฒ์ ์๋ฌ๊ฐ ์ ํ๋ ๋ ๊ฐ์ฅ ๊ฐ๊น์ด try/catch์์ ํด๊ฒฐ๋ฉ๋๋ค.
์ด๊ฒ์ ์์์ ์ผ๋ก ํด๊ฒฐ๋๋ฉฐ ์ด๋์ ์ด๋ค ์๋ฌ๋ฅผ ๋ฐํํ๋์ง ํ์
ํ ์ ์๊ฒ ํฉ๋๋ค.
ํ์ง๋ง Effect.try ๋ฉ์๋๋ฅผ ์ด์ฉํ๋ฉด ํด๋น ํจ์์ ๋ฐํ๊ฐ์ผ๋ก ์๋ฌ๊ฐ ๋ฐํ๋ ์ ์์์ ๋ํ๋ด๊ธฐ ๋๋ฌธ์
๋ช
์์ ์ผ๋ก ์๋ฌ ์ฒ๋ฆฌ๊ฐ ๋ฐ๋์ ํ์ํฉ๋๋ค.
const localStorageServiceLive = LocalStorageService.of({
getItem: <T>(key: string) =>
Effect.try<T | undefined, LocalStorageError>({
try: () => {
const item = localStorage.getItem(key)
if (!item) {
return undefined
}
return JSON.parse(item) as T
},
catch: (error) => LocalStorageError.parseError(error)
}),
setItem: <T>(key: string, value: T) =>
Effect.try<void, LocalStorageError>({
try: () => {
const stringifiedValue = JSON.stringify(value)
localStorage.setItem(key, stringifiedValue)
},
catch: (error) => LocalStorageError.stringifyError(error)
}),
removeItem: (key: string) =>
Effect.try<void, LocalStorageError>({
try: () => localStorage.removeItem(key),
catch: (error) => LocalStorageError.parseError(error)
})
})
yield* LocalStorageService ์ฝ๋๋ฅผ ์ดํด๋ณด๋ฉด ์ธํฐํ์ด์ค๋ฅผ ์ฌ์ฉํ๋ ๊ฒ์ ์ ์ ์์ต๋๋ค.
์ด๋ ๊ฒ ๊ตฌํ์ฒด๋ฅผ ์ด์ฉํ ํ๋ก๊ทธ๋๋ฐ์ด ์๋ ์์กด์ฑ์ ์ฃผ์
๋ฐ์ ์ธํฐํ์ด์ค ๊ธฐ๋ฐ์ ํ๋ก๊ทธ๋๋ฐ์ด ๊ฐ๋ฅํฉ๋๋ค.
์ธ๋ถ ๊ตฌํ์ฌํญ์ ๋ํด ์์กดํ์ง ์๊ณ ์ค์ง ์ธํฐํ์ด์ค๋ง์ผ๋ก ๊ฐ๋ฐ์ด ๊ฐ๋ฅํ์ฌ ์ฝ๋์ ๊ฒฐํฉ๋๋ฅผ ๋ฎ์ถ ์ ์์ต๋๋ค.
๊ทธ๋ฆฌ๊ณ pipe ๋ฉ์๋๋ฅผ ์ด์ฉํด์ ํจ์ํ ํ๋ก๊ทธ๋๋ฐ์ด ๊ฐ๋ฅํฉ๋๋ค. ํจ์๋ณ๋ก ์ญํ ์ ๋๋ ์ ์๊ธฐ ๋๋ฌธ์ ๊ฐ๋
์ฑ์ด ๋์์ง๋๋ค.
const userLocalStorageRepository = Effect.gen(function* () {
const localStorageService = yield* LocalStorageService
return UserRepository.of({
findById: (id: number) => {
return Effect.gen(function* () {
return yield* localStorageService.getItem<Array<User>>("users").pipe(
Effect.map((users) => users || []),
Effect.catchAll(() => Effect.succeed<Array<User>>([])),
Effect.map((users) => users.find((user) => user.id === id))
)
})
},
findAll: () => {
return Effect.gen(function* () {
return yield* localStorageService.getItem<Array<User>>("users").pipe(
Effect.map((users) => users || []),
Effect.catchAll(() => Effect.succeed<Array<User>>([]))
)
})
},
update: (user: User) => {
return Effect.gen(function* () {
return yield* localStorageService.getItem<Array<User>>("users").pipe(
Effect.map((users) => users || []),
Effect.map((users) => {
const index = users.findIndex((u) => u.id === user.id)
if (index === -1) {
return users
}
return [...users.slice(0, index), { ...user }, ...users.slice(index + 1)]
}),
Effect.flatMap((users) => localStorageService.setItem("users", users)),
Effect.catchAll(() => Effect.succeed(void 0))
)
})
}
})
})
์ธ ๋ฉ๋ชจ๋ฆฌ ๊ตฌํ์ฒด๋ ์ธ๋ถ ์๋น์ค๋ฅผ ์ฌ์ฉํ์ง ์๊ธฐ ๋๋ฌธ์ ๋ณ๋์ ์์กด์ฑ ์ฃผ์ ์์ด ๊ตฌํ ๊ฐ๋ฅํฉ๋๋ค.
const inMemoryUserRepository = Effect.gen(function* () {
return UserRepository.of({
findById: (id: number) => Effect.succeed(users.find((user) => user.id === id)),
findAll: () => Effect.succeed(users),
update: (user: User) => Effect.succeed((users[users.findIndex((u) => u.id === user.id)] = user))
})
})
Effect์์๋ ์ฌ๋ฌ ๋ฐฉ์์ผ๋ก ์์กด์ฑ์ ์ฃผ์
ํ ์ ์๋๋ฐ ๊ฐ์ฅ ์ฝ๊ฒ ๋ช
์์ ์ธ Effect.Layer๋ฅผ ์ด์ฉํด์ ๋ง๋ค์ด ๋ด
์๋ค.Layer๋ผ๋ ํจํค์ง์์ ์ ๊ณตํ๋ ํจ์๋ฅผ ์ด์ฉํด ์ธํฐํ์ด์ค์ ์ค์ ๊ตฌํ์ฒด๋ฅผ ์กฐํฉํ ์ ์์ต๋๋ค.
์๋ ์ฝ๋์์๋ ๋ก์ปฌ์คํ ๋ฆฌ์ง ์๋น์ค ์ธํฐํ์ด์ค์ ์ค์ ๊ตฌํ์ฒด๋ฅผ ๊ฒฐํฉํด์ localStorageLayer๋ฅผ ๋ง๋ญ๋๋ค.
์ด์ ๋์ผํ ๋ฐฉ์์ผ๋ก ์ธ ๋ฉ๋ชจ๋ฆฌ์ ๋ก์ปฌ์คํ ๋ฆฌ์ง ์ ์ฅ์์ ์์กด์ฑ์ ๊ตฌํํฉ๋๋ค.
์ด๋์ ๋ก์ปฌ์คํ ๋ฆฌ์ง ์ ์ฅ์๋ ๋ก์ปฌ์คํ ๋ฆฌ์ง ์๋น์ค๋ฅผ ์์กดํ๊ณ ์์ผ๋ฏ๋ก userLocalStorageRepository.pipe(Effect.provide(localStorageLayer))์ ๊ฐ์ด ๋ณ๋์ ์์กด์ฑ์ ์ถ๊ฐํ์ฌ ๋ ์ด์ด๋ฅผ ๋ง๋ญ๋๋ค.
๊ทธ๋ฆฌ๊ณ main ํจ์๋ฅผ ์คํํ๋ ์์ ์ ์ธ ๋ฉ๋ชจ๋ฆฌ ์ ์ฅ์ ๋ ์ด์ด์ ๋ก์ปฌ์คํ ๋ฆฌ์ง ์ ์ฅ์ ๋ ์ด์ด ์ค์ ์ด๋ค ๋ ์ด์ด๋ฅผ ์ฃผ์ ํ ์ง ๊ฒฐ์ ํ๊ฒ ๋๋ฉด main ํจ์๋ ์ธ๋ถ์ ์ด๋ค ์ ์ฅ์๋ก ๋์ํ๋์ง ์์ง ๋ชปํ ์ฑ ์คํ๋ ์ ์์ต๋๋ค.
์ค์ง ๋ก์ปฌ์คํ ๋ฆฌ์ง ์ ์ฅ์ ๋ ์ด์ด์์๋ง ๋ก์ปฌ์คํ ๋ฆฌ์ง ์๋น์ค๊ฐ ํ์ํจ์ ์ธ์งํ ์ ์์ต๋๋ค.
์ด๋ ๊ฒ ์ธํฐํ์ด์ค์ ์ค์ ๊ตฌํ์ฒด๋ฅผ ์ฐ๊ฒฐํ์ฌ ํ๋์ ๋ ์ด์ด๋ฅผ ๋ง๋ค๊ณ
์ด๋ฐ ๋ ์ด์ด๋ค์ ์กฐํฉํ์ฌ ์ค์ ํ๋ก๊ทธ๋จ์ด ์คํํ๋ ์์ ์ ์์กด์ฑ์ ์ฃผ์
ํ๋ ๋ฐฉ์์ผ๋ก ๋์ํฉ๋๋ค.
const localStorageLayer = Layer.succeed(LocalStorageService, localStorageServiceLive)
const userLocalStorageRepositoryLayer = Layer.effect(
UserRepository,
userLocalStorageRepository.pipe(Effect.provide(localStorageLayer))
)
const inMemoryUserRepositoryLayer = Layer.effect(UserRepository, inMemoryUserRepository)
const selectUserRepositoryLayer = (isDevelopment: boolean) =>
isDevelopment ? inMemoryUserRepositoryLayer : userLocalStorageRepositoryLayer
const main = Effect.gen(function* () {
const userRepository = yield* UserRepository
const user1 = yield* userRepository.findById(1)
const user2 = yield* userRepository.findById(2)
const all = yield* userRepository.findAll()
console.log("user1", user1)
console.log("user2", user2)
console.log("all", all)
})
Effect.runSync(main.pipe(Effect.provide(selectUserRepositoryLayer(process.env.BUN_ENV === "development"))))
์ด๋ ๊ฒ ์์ฑํ ํ๋ก๊ทธ๋จ์ ๊ธฐ์กด ์ผ๋ฐ์ ์ธ ํ์
์คํฌ๋ฆฝํธ ์ฝ๋์ ์ด๋ป๊ฒ ์ฐ๊ฒฐํ ๊น์?
์ฌ์ค Effect๋ ์คํํ๋ ์์ ์๋ ์ผ๋ฐ ํ์
์คํฌ๋ฆฝํธ์ ๋ค๋ฅผ ๋ฐ ์์ผ๋ฏ๋ก ๊ธฐ์กด์ ํจ์์ฒ๋ผ ์ด์ฉ์ด ๊ฐ๋ฅํฉ๋๋ค.
getUser๋ผ๋ ๊ธฐ์กด์ ํจ์์ ๋จ์ง Effect.run ๋ฉ์๋๋ค์ ์คํํด์ฃผ๋ฉด ๋ฉ๋๋ค.
์ด์ฒ๋ผ ๊ธฐ์กด ๋ก์ง๊ณผ ๊ฒฐํฉํ๊ธฐ๋ ์์ฃผ ์ฝ์ต๋๋ค.
const getUser = (id: number) => {
// return oldFindById(id);
return Effect.runSync(main.pipe(Effect.provide(selectUserRepositoryLayer(process.env.BUN_ENV === "development"))))
}
์ฝ๋๋ฅผ ๋ณด๋ฉด ๊ธฐ์กด์ ์ฝ๋๋ค๋ณด๋ค๋ ํจ์ฌ ์ฝ๋๋์ด ๋ง์ต๋๋ค.
์ด๋ ์ธํฐํ์ด์ค๋ฅผ ์ ์ธํด์ผ ํ๊ณ ์๋ฌ ํ์
์ ๋๋ถ๋ถ ๋ง๋ค์ด์ ์ฒ๋ฆฌํด์ฃผ๋ ๊ฒ์ ์งํฅํ๊ณ ์๊ธฐ ๋๋ฌธ์
๋๋ค.
๊ทธ๋ ๊ธฐ ๋๋ฌธ์ ๊ฐ๋จํ ๋ก์ง์์๋ ๋ถํ์ํ ์ ์์ต๋๋ค.
๊ทธ์ ๋ฐํด ๋์ผํ ๋์์ด ์ผ์ด์ค์ ๋ฐ๋ผ ์ธ๋ถ ๊ตฌํ์ด ๋ฌ๋ผ์ง๋ ๊ฒฝ์ฐ ์์ฃผ ์ ํฉํฉ๋๋ค.
์ฝ๋ ๊ฐ์ ๊ฒฐํฉ๋๋ฅผ ๋ฎ์ถ ์ ์๊ธฐ ๋๋ฌธ์ ํ์ฅ์ฑ์ ๋์ผ ์ ์๊ณ ์ฝ๊ฒ ์ ์ง๋ณด์ํ ์ ์์ต๋๋ค.
๊ทธ ์ธ์๋ Effect์์๋ stream๊ณผ ๊ฐ์ ๊ธฐ๋ฅ์ ์ ๊ณตํด์ ๋ฐํ๊ณผ ๊ตฌ๋ ๊ธฐ๋ฅ์ ์ง์ํ๊ณ ๋ ๋ง์ ๋ถ๊ฐ ๊ธฐ๋ฅ์ด ์์ต๋๋ค.
๋ชจ๋ ๊ธฐ๋ฅ์ ์ด์ฉํ ํ์๋ ์์ง๋ง ๋์ ๋ฐ๋ผ pipe ํจ์๋ฅผ ์ด์ฉํ ํจ์ํ ํ๋ก๊ทธ๋๋ฐ ๊ทธ๋ฆฌ๊ณ ์์กด์ฑ ์ฃผ์ ์ ํตํ ํ์ฅ์ฑ ๋ฑ์ ๊ฐ๋ ฅํ ๊ธฐ๋ฅ์ด ๋ ๊ฒ์ด๋ผ ์๊ฐํฉ๋๋ค.
๊ตฌํ ์์ - https://github.com/load28/effect