728x90

들어가며,

상품/주문/결제 각 페이지가 갖는 역할과 책임에 따라 코드를 짜기 위해 노력했습니다.

 

상품 페이지

상품 페이지는 홈 화면과 비슷하게 많은 데이터를 불러와 사용자에게 노출시키고, 비즈니스 로직의 다음 페이지로 이동하기 위한 역할을 하고 있습니다. 따라서 데이터, 뷰, 로직 이 3가지로 나누어 단일 책임 원칙에 따라 코드를 작성했습니다.

 

추상화도 비용이다

다만, '추상화도 비용이다'라는 점을 고려해 너무 작은 컴포넌트를 생성하지는 않았습니다.

화면을 그리기 위해서는 10가지의 부분적인 요소가 필요했지만, 중복적으로 필요한 부분을 제거하고 보니 6개의 컴포넌트와 3개의 커스텀 훅을 통해 구현할 수 있었습니다.

이를 통해 각 컴포넌트의 역할과 책임이 명확해지고 가독성이 크게 개선될 수 있었습니다.

 

주문 로직으로의 화면 전환

화면의 구성이 끝나고 주문 로직으로 전환하기 위해서는 몇 가지 제약조건이 등장했습니다.

1. 로그인 된 유저만 주문이 가능

2. 만약 로그인이 되지 않았다면, 알림을 통해 로그인 화면으로 이동 

3. 로그인 이후에는 홈화면이 아닌 기존 상품 페이지로 이동

전역 상태 관리

Recoil을 통해 관리하는 커스텀 훅을 사용하여 로그인된 유저의 정보를 불러오고, 유저의 정보가 없는 경우에는 로그인 페이지로 이동하면서 로컬 스토리지에 리다이렉트 주소를 저장하게 했습니다. 

따라서, 로그인이 완료되면 저장된 주소 값을 통해 돌아올 수 있습니다.

 

이번 프로젝트를 하면서는 로그인된 유저의 정보와 아래의 주문 로직에서 진행하고 있던 단계를 관리하기 위해서만 전역 상태 관리가 이루어졌습니다. 따라서 Recoil의 간결한 코드로도 충분히 관리할 수 있었지만, 추후에는 다른 상태관리 라이브러리를 사용하면서 다양한 코드를 짜보고 싶습니다.

 

react-query 데이터 캐시

react-query를 사용하여 상품 정보를 가져와 캐시에 저장하고 사용하는 방식을 사용했습니다. 정적인 콘텐츠를 한 번 불러와서 캐시로 사용함으로 인해 네트워크 요청을 줄이고 애플리케이션의 성능을 향상시킬 수 있었습니다. 또한 캐시를 사용하면서 동일한 데이터를 여러 컴포넌트에서 공유할 수 있었습니다. 데이터를 한 곳에서 관리하고 업데이트할 수 있으므로 일관된 상태를 유지하는 등 CSR 기반의 SPA 어플리케이션의 장점을 극대화 할 수 있었습니다.

 

주문 페이지

 

주문 로직은 3개의 페이지를 돌면서 정보를 수집해 저장하는 역할을 하고 있습니다.

 

 

저는 이 부분을 별개의 페이지로 구성하는 것이 아닌 컴포넌트로 구성해 하나의 페이지에서 step별로 다른 화면을 보여주며 정보를 수집하는 방식을 택했습니다. 컴포넌트 기반의 단일 페이지를 택함으로 인해 코드의 가독성과 유지보수성을 챙길 수 있었습니다. 또한, 페이지를 이동하지 않아 불필요한 돔조작이 일어나는 것을 방지했습니다. 

 

 

728x90

들어가며,

Next.js로 먼저 기능을 개발했었는데, 프로젝트의 방향이 React.js로 변경되면서 마이그레이션을 위한 대안점을 찾아야 했습니다.

 

기존에 사용하던 것과 바뀐 것들을 정리하면 다음과 같습니다.

- Next.js -> React.js

- NextAuth.js -> Firebase/Auth

- Prizma ORM -> Firebase

- Postgres -> firebase/firestore

 

FireBase를 사용하게 되면서 대부분의 기능을 Firebase가 담당해주면서 비교적 빠른 속도로 개발할 수 있었습니다.

 

개발의 주안점

1. 리다이렉트 되어 돌아온 비회원
 로컬스토리지에 저장된 이전 페이지 값을 불러낸 뒤 변수에 저장.
로컬스토리지는 비우고 저장된 변수를 활용해 다시 돌려보냄

2. 로그인 정보 유지
로그인 정보를 유지하기 위해 AuthGuard 컴포넌트와 useUser 훅을 사용

미리보기

 

회원 가입과 로그인 기능의 파라미터 값은 최대한 기본적인 요소를 사용하되, 오류나 검증 로직을 작성해봤습니다.

 

회원 가입

function SignUpPage() {
  const navigate = useNavigate()

  const handleSubmit = async (formValues: FormValues) => {
    const { email, password, name } = formValues

    const { user } = await createUserWithEmailAndPassword(auth, email, password)
    await updateProfile(user, {
      displayName: name,
    })
    const newUser = {
      uid: user.uid,
      email: user.email,
      displayName: name,
    }
    await setDoc(doc(collection(store, 'USER'), user.uid), newUser)
    navigate('/')
  }
  return (
    <div>
      <Form onSubmit={handleSubmit} />
    </div>
  )
}

export default SignUpPage

 

Form 컴포넌트를 따로 두고 Page 컴포넌트에서는 firebase로 데이터를 보내는 역할에 집중하게 했습니다.

<코드 동작>
1.폼에서 받은 formValues를 이용하여 이메일, 비밀번호, 이름을 추출합니다.
2.createUserWithEmailAndPassword 함수를 사용하여 이메일과 비밀번호를 사용하여 새로운 사용자를 생성합니다.
3.updateProfile 함수를 사용하여 새로 생성된 사용자의 프로필을 업데이트합니다.
4.새로운 사용자의 정보를 newUser 객체에 저장합니다.
5.setDoc 함수를 사용하여 데이터베이스에 새로운 사용자의 정보를 저장합니다.
6. 페이지를 '/'로 이동합니다.

 

Axios 로 스프링 부트와 API통신

import Form from '@/components/signup/Form'
import { FormValues } from '@/models/signup'
import axios from 'axios'
import { useState } from 'react'

function SignUpPage() {
  const [error, setError] = useState('')

  const handleSubmit = async (formValues: FormValues) => {
    const { email, password, name } = formValues

    try {
      const memberId = '1'
      const newUser = { memberId, email, password, name }
      // 스프링 서버에 회원가입 정보를 전송
      await axios.post('http://localhost:8080/members/register', newUser)
      console.log('회원가입에 성공했습니다.')
    } catch (error) {
      setError('회원가입에 실패했습니다.')
      console.error(error)
    }
  }

  return (
    <div>
      {error && <p>{error}</p>}
      <Form onSubmit={handleSubmit} />
    </div>
  )
}

export default SignUpPage

 

주문로직에서 결제 API와 연동한 스프링부트 어플리케이션과의 통신을 연습해보기 위해 회원가입 기능에서 POST 방식의 API를 보내 DB에 데이터가 저장되는 것을 확인했습니다.

-> 연습용으로 사용한 스프링 부트 어플리케이션은 이전에 간단하게 회원가입/로그인 기능을 구현했던 것을 재활용했습니다.

 

로그인 기능

import { useAlertContext } from '@/contexts/AlertContext'
import { FirebaseError } from 'firebase/app'
import { signInWithEmailAndPassword } from 'firebase/auth'
import { useCallback } from 'react'
import { useNavigate } from 'react-router-dom'

function SignInPage() {
  const navigate = useNavigate()
  const { open } = useAlertContext()

  const handleSubmit = useCallback(
    async (formValues: FormValues) => {
      const { email, password } = formValues
      const storedUrl = localStorage.getItem('redirectUrl')
      try {
        await signInWithEmailAndPassword(auth, email, password)
        if (storedUrl !== null) {
          localStorage.removeItem('redirectUrl') // 사용된 URL은 삭제
          navigate(storedUrl)
        } else {
          navigate('/')
        }
      } catch (e) {
        if (e instanceof FirebaseError) {
          if (e.code === 'auth/invalid-credential') {
            open({
              title: '이메일 및 패스워드를 다시 확인해주세요',
              onButtonClick: () => {
                console.log('Error', e)
              },
            })
            return
          }
        }
        open({
          title: '잠시 후 다시 시도해주세요',
          onButtonClick: () => {
            console.log('Error', e)
          },
        })
      }
    },
    [open, navigate],
  )

  return (
    <div>
      <Form onSubmit={handleSubmit} />
    </div>
  )
}

export default SignInPage

 

로그인 기능 역시 구현은 회원가입과 크게 다르지 않았습니다. firebase에서 제공하는 함수를 통해 쉽게 로그인을 성공시킬 수 있었습니다.

 

FORM컴포넌트에서의 검증 로직 : Validate

function validate(formValues: FormValues) {
  let errors: Partial<FormValues> = {}

  if (validator.isEmail(formValues.email) === false) {
    errors.email = '이메일 형식을 확인해주세요'
  }

  if (formValues.password.length < 8) {
    errors.password = '비밀번호를 8글자 이상 입력해주세요'
  }

  return errors
}

 

 

그러나, 본격적으로 로그인에 성공하면 로그인 정보를 유지해야 한다는 것과 회원 전용 페이지에 접근을 시도하다 리다이렉트되어 온 비회원에 대한 처리를 구현하려다 보니 몇가지 로직이 추가되었습니다.

 

1. 리다이렉트 되어 돌아온 비회원
 로컬스토리지에 저장된 이전 페이지 값을 불러낸 뒤 변수에 저장.
로컬스토리지는 비우고 저장된 변수를 활용해 다시 돌려보냄

2. 로그인 정보 유지
로그인 정보를 유지하기 위해 AuthGuard 컴포넌트와 useUser 훅을 사용

 

로그인 정보 유지와 관련된 내용은 Recoil 글에서 다루겠습니다.

728x90

Next.auth를 통해 인증인가 설정하는 이유

→ 유저의 Role에 따라 페이지 접근을 제어하기 때문입니다.

회원 정보를 담아야 하기 때문에 샘플 DB와 ORM을 설정할 필요가 있었습니다. 따라서, Prisma ORM 과 Postgre를 통해 입력값을 담을 준비를 마쳤습니다.

 

model Session {
  id           String   @id @default(cuid())
  sessionToken String   @unique
  userId       String
  expires      DateTime
  user         User     @relation(fields: [userId], references: [id], onDelete: Cascade)
}
enum UserType {
  USER
  ADMIN
}

model User {
  id            String    @id @default(cuid())
  name          String?
  hashedPassword String?
  email         String?   @unique
  emailVerified DateTime?
  image         String?
  accounts      Account[]
  sessions      Session[]
  UserType      UserType  @default(USER)
}

 

먼저 Prisma ORM의 스키마를 설정해서 디비에 저장할 테이블을 생성할 준비를 합니다.

version: "3"
services:
  db:
    image: postgres:latest
    restart: always
    ports:
      - "5432:5432"
    environment:
      POSTGRES_USER: "root"
      POSTGRES_PASSWORD: "1234"
    volumes:
      - ./data:/var/lib/postgresql/data

 

그리고 DB는 따로 설치를 하지 않고 도커컴포즈 파일을 작성해서 띄워 사용하였습니다.

DATABASE_URL="postgresql://root:1234@localhost:5432/next2db"

NEXTAUTH_SECRET=nextAuthSecret
NEXTAUTH_URL=http://localhost:3000

JWT_SECRET=jwt-secret

 

마지막으로 .env파일을 작성해 Next.js 프로젝트를 DB와 연결하였습니다.

 

본격적으로 NextAuth를 통해 회원 가입 / 로그인 기능 개발

const prisma = new PrismaClient()

export const authOptions: NextAuthOptions = {
  adapter: PrismaAdapter(prisma),
  providers: [
    GoogleProvider({
      clientId: process.env.GOOGLE_CLIENT_ID!,
      clientSecret: process.env.GOOGLE_CLIENT_SECRET!,
    }),
    CredentialsProvider({
      // The name to display on the sign in form (e.g. "Sign in with...")
      name: "Credentials",
      // `credentials` is used to generate a form on the sign in page.
      // You can specify which fields should be submitted, by adding keys to the `credentials` object.
      // e.g. domain, username, password, 2FA token, etc.
      // You can pass any HTML attribute to the <input> tag through the object.
      credentials: {
        username: { label: "Username", type: "text", placeholder: "jsmith" },
        password: { label: "Password", type: "password" }
      },
      async authorize(credentials, req) {
        // Add logic here to look up the user from the credentials supplied
        const user = { id: "1", name: "J Smith", email: "jsmith@example.com", role: "USER" }
  
        if (user) {
          // Any object returned will be saved in `user` property of the JWT
          return user
        } else {
          // If you return null then an error will be displayed advising the user to check their details.
          return null
  
          // You can also Reject this callback with an Error thus the user will be sent to the error page with the error message as a query parameter
        }
      }
    })
  ],

 

https://next-auth.js.org/providers/credentials

 

Credentials | NextAuth.js

Overview

next-auth.js.org

 

NextAuth 공식 문서에는 NextAuth를 연결하기만 하면 기본적으로 회원가입과 로그인 기능이 제공되고 있습니다.

공식문서에서 제공해주는 구글 로그인과 자체 로그인 코드를 토대로 개발을 진행했습니다.

 

// 세션 관리는 jwt로 지정
  session: {
    strategy: 'jwt',
  },
// jwt 설정 -> 시크릿은 env파일에 설정해둠 + 유효기간 30일
  jwt: {
    secret: process.env.JWT_SECRET,
    maxAge: 30 * 24 * 60 * 60 // 30 days
  },
//토큰 내에 사용자 정보가 포함되어, 이후 세션 생성 시 이용
  callbacks: {
    async jwt({token, user}) {
      return {...token, ...user}

    },
    async session({session, token}) {
      session.user = token
      return session
    }
  }
}

 

제공되는 코드에 추가로 설정할 부분은 로그인 이후 세션을 어떻게 관리할 지 정의하는 것이었습니다.

JWT 토큰을 통해 세션 정보를 유지하기 위해 콜백함수로 관리했습니다.

 

JWT 콜백 : 사용자가 로그인할 때 또는 토큰이 갱신될 때 실행
→ 사용자가 로그인하는 순간, user 객체에 포함된 정보를 token 객체와 병합하여 반환
session 콜백 : 서버 측에서 생성된 세션 정보를 클라이언트 측에 맞게 조정하거나 추가 데이터를 세션에 포함
→ session 객체 내에 user 속성을 추가하고, 이를 jwt 콜백에서 반환된 token 객체로 설정

 

import { getToken } from 'next-auth/jwt';
import { NextRequest, NextResponse } from 'next/server';

export {default} from 'next-auth/middleware'

export async function middleware(req: NextRequest) {
  const session = await getToken({req, secret: process.env.JWT_SECRET});
  
  const pathName = req.nextUrl.pathname;

  // 관리자 페이지 접근 권한이 없는 경우 메인 페이지로 리다이렉트
  if (pathName.startsWith('/admin') && session?.role !== 'ADMIN') {
    return NextResponse.redirect(new URL("/", req.url));
  }
  // 로그인되지 않은 상태에서 유저 페이지 접근시 로그인 페이지로 리다이렉트
  if (pathName.startsWith('/user') && !session) {
    return NextResponse.redirect(new URL("/auth/login", req.url));
  }
  // 로그인 상태에서 로그인 페이지, 회원가입 페이지 접근시 메인 페이지로 리다이렉트
  if (pathName.startsWith('/auth') && session) {
    return NextResponse.redirect(new URL("/", req.url));
}
  return NextResponse.next();
}

 

미들웨어 파일을 두고 세션의 속성 값에 따라 접근 가능한 경로를 제어했습니다

/admin:path*와 같은 와일드카드 패턴을 사용해, 하위 모든 경로를 제어할 수 있었습니다.

 

 

728x90

CSS in JS

CSS-in-JS는 단어 그대로 자바스크립트 코드에서 CSS를 작성하는 방식을 말합니다.

 

CSS in JS의 장점

- 별도의 css 파일을 만들어 관리하는 것이 아닌, 컴포넌트 별로 CSS를 작성해 관리할 수 있습니다. 

-> 따라서 CSS명을 지정하는데 큰 어려움 없이 작성할 수 있습니다.

- 변수와 함께 동적 스타일링을 구성할 수 있습니다.

- 사용되는 스타일만 번들에 포함되어 코드 분할에 이점을 가질 수 있습니다.

 

Emotion VS TailwindCSS

기존의 토이 프로젝트를 연습할 때는 TailwindCSS를 자주 사용하였으나, 최근 AWS 부트캠프에서의 과제에서는 Emotion을 도입하게 되었습니다.

TailwindCSS를 사용했던 이유는 Next.js를 연습하고 있었기도 했고, 비교적 러닝커브가 낮아 금방 적용해 볼  수 있었습니다.

다만, css가 코드 안에서 여기저기 존재하다보니, 가끔 코드가 지저분해지는 단점을 느꼈습니다.

function App () {
	return (
    	<div classname={'flex ...기타 등등'}>Hello World</div>
        )
    }
import {css} from '@emotion/react'

const containerStyles = css`
	display: flex;
    ...기타 등등
`

function App() {
	return (
    	<div css={containerStyles}>Hello World</div>
     	)
    }

 

이런 식으로 사용하는데 지금은 코드가 짧아 Tailwind가 더 가독성이 좋아 보이지만, 코드가 길어지면 꽤나 지저분해 보이는 단점이 있습니다.

 

// colors.ts
export const colors = {
  primary: "#0d6efd",
  border: "#ddd",
  /* ... */
};
// MyComponent.tsx
function MyComponent({ fontSize }) {
  return (
    <p
      css={{
        color: colors.primary,
        fontSize,
        border: `1px solid ${colors.border}`,
      }}
    >
      ...
    </p>
  );
}

 

또한, Emotion에서는 자바스크립트 변수를 style에 활용하기도 합니다.

여러가지 장점들이 많지만, 단점들도 존재합니다. 제가 경험한 토이 프로젝트에서는 체감하기 어려웠지만 SSR에서 구축하기 매우 어렵다는 점과 렌더링 이슈 등이 있다고 알고 있습니다.

728x90

Yarn이란?

yarn은 자바스크립트로 작성된 프로젝트의 종속성을 관리하는데 사용되는 오픈 소스입니다.

기존의 npm이라는 방대한 오픈소스가 가진 문제점을 보완하고자 등장하였으며, 그중에서도 비효율적인 의존성 검색 부분을 크게 개선시켰습니다.

 

NPM의 문제점

 

Yarn도 NPM을 통해 설치해야 하는오픈소스일 정도로 NPM은 Node 환경에서 기본으로 제공되고 있는 종속성 관리 오픈소스입니다. 다만, 방대한 라이브러리를 가진 장점은 반대로 검색의 비효율을 가져왔습니다.

 

자료출처: https://toss.tech/article/node-modules-and-yarn-berry

 

라이브러리를 찾기 위해 트리 구조로 뻗어 나가는 NPM은 상위 디렉토리를 계속 탐색해 나가야 합니다. 따라서 첫 번째 단점은 속도 면에서 찾을 수 있었습니다.

그리고 node_modules 디렉토리의 비효율적인 용량 문제도 있습니다. NPM의 이러한 트리구조를 이루기 위해 과한 부피의 node_modules 디렉토리를 구성하게 됩니다. 정작 중요한 패키지를 구별하기 어려워지고, 이는 의존성의 유효성 검증이 어려워짐을 보입니다.

 

Plug'n'Play를 통한 .yarn/cache 활용

yarn의 최신 버전을 이용하는 pnp 방식은 기존의 npm이나 yarn 1 버전에서 발생하는 문제를 해결해주고 있습니다.

의존성을 Zip 아카이브로 관리하면서 용량에 대한 문제, 속도에 대한 문제를 크게 개선시켜줍니다. 

예를 들면, 일반적인 node_modules 는 1.2GB 크기이고 13만 5천개의 파일로 구성되어 있는 반면, Yarn PnP의 의존성은 139MB 크기의 2천개의 압축 파일로 구성된다고 합니다.

 

Zero-Install

Zero-Install을 사용하면 앞서 줄어든 의존성 관리를 git을 통해 할 수 있게 됩니다. Yarn 캐시와 PnP 로더 파일을 저장소에 추가함으로써, 브랜치를 변경할 때마다 yarn install을 실행할 필요 없이 의존성을 관리할 수 있습니다. 또한, CI에서 의존성 설치 시간을 크게 절약할 수 있습니다.

 

728x90

돔 DOM

먼저, DOM은 html, body, div와 같은 태그들을 javascript가 이용할 수 있게 브라우저가 HTML 문서의 구조를 메모리에 트리 구조로 표현한 객체 모델이다.

 

즉, javascript 코드를 실행하면 D 따라서 사용자는 콘텐츠는 누락된 텅 빈 로딩 화면을 볼 수 있게 된다. 이후 데이터 쿼리 동작을 수행하며, 서버로부터 데이터를 받아와 빈 공간들을 채워 웹페이지를 완성하게 된다. OM 객체를 생성하여 HTML/CSS와 같은 기본 ‘뼈대’ 역할을 하는 렌더 쉘을 생성한다.

 

이러한 과정을 따르는 것이 CSR이다. client side rendering의 약어로 빈 페이지를 생성하고 이후에 서버로부터 데이터를 받아 온전한 페이지를 반환한다.

이 과정에서 발생되는 문제는 빈번한 DOM 조작으로 인한 성능 저하 문제다. 트리 구조로 생성된 DOM은 조작 시에 브라우저의 레이아웃 재계산(Reflow)과 화면 재그리기(Repaint)를 유발할 수 있으며, 자원을 많이 소모하게 된다.

 

React는 문제를 해결하기 위해 가상 돔(virtual DOM) 도입으로 DOM을 추상화해 문제를 해결한다.

 

 

가상 돔에서는 컴포넌트의 변경이 발생하면, 변경된 부분을 가상 돔에 업데이트하고 가상 돔끼리 비교(diffing)하여 변경된 부분만 실제 돔에 적용(patching)한다. 이를 통해 돔 변경의 빈도수를 줄이고 성능 최적화를 이룰 수 있다.

 

컴포넌트

컴포넌트는 웹 어플리케이션 개발에 필요한 UI를 아래와 같은 기준에 따라 설계하고 관리하는 코드 블록이다.

  • 재사용 가능하도록 설계: Input, Button 등 다양한 페이지에서 같은 컴포넌트를 사용한다. → 생산성
  • 독립적으로 설계 : 다른 컴포넌트와의 의존성을 줄여 독립적으로 관리한다. → 유지보수
  • 조합할 수 있도록 설계: 작게 설계하여 조합을 통해 큰 기능을 가진 컴포넌트를 생성한다. → 확장성

컴포넌트에서 서버와 클라이언트 간의 상호작용

 

useEffect(() => {
  fetch('https://example.com/data')
    .then(response => response.json())
    .then(data => setData(data)); // 상태 업데이트
}, []); // 빈 배열은 컴포넌트가 마운트될 때만 실행되도록 함

 

etch는 비동기적으로 서버에 데이터를 요청하고 받을 수 있는 함수이다.

React 컴포넌트가 마운트될 때 useEffect 훅을 사용하여 데이터를 요청할 수 있다.

클라이언트 컴포넌트와 서버 컴포넌트

React 18버전 이전까지는 주로 클라이언트 컴포넌트만 이용하며, 페이지의 동적인 부분들을 관리해왔다. 그러나 JS번들의 크기가 커지면서 브라우저의 파싱과 실행 시간을 늘어났으며, 이는 사용자 경험의 저하 문제를 일으켰다.

React 18에서 도입된 서버 컴포넌트는 초기 로딩 성능을 향상시키고, 데이터 페칭 및 템플릿 렌더링 같은 작업을 서버에서 처리하며 문제를 해결했다.

 

 

Next.js와 같은 메타 프레임워크(React를 기반으로 구축된 프레임워크)는 미리 데이터 쿼리와 렌더링을 서버 사이드에서 수행하기 때문에 정적 사이트 생성(SSG) 또는 서버 사이드 렌더링(SSR) 방식을 구현할 수 있다.

728x90

들어가며


프로젝트의 규모가 방대해지면서, 개발팀과 운영팀으로 나뉘어 진행하기도 하며 인수인계 등의 여러 과정에서는 특정 버전을 설치하고 환경을 세팅하는 일에 많은 시간이 소요되곤 했었다. 그러나 도커를 이용하면 이와 같은 문제가 해결된다. 

도커 컨테이너와 이미지란?


 

 

컨테이너란 어플리케이션과 그에 필요한 모든종속성, 즉 코드, 라이브러리, 리소스 등을 포함하여 어디서든 실행이 가능한 상태로 패키징하기 위한 하나의 단위이다.

 

이미지란 어플리케이션을 실행하기 위해 필요한 모든 환경 시스템을 포함한 템플릿이다.

 

일반적으로 서비스를 도커파일을 통해 이미지로 빌드하고, 빌드한 이미지를 컨테이너로 실행한다. 

 

 

사용자는 클라이언트를 통해 도커 데몬과 통신하여 컨테이너의 생성, 실행, 빌드 등을 요청하고 결과를 받는다.

도커 데몬은 도커 API를 통해 클라이언트 요청을 처리하는데 컨테이너의 실행과 관리를 담당한다.

도커 레지스트리는 이미지 저장소로 직접 이미지를 관리할 수 있다.

 

 

 

도커는 위 그림과 같이 레지스트리에 등록된 이미지를 pull 해서 다운받아 사용한다.
다운받은 이미지를 run 명령어를 통해 실행하면 컨테이너가 생성된다.
컨테이너의 이름 --name, 컨테이너 포트 -p 등의 명령어를 통해 지정할 수 있다.
실행되고 있는 컨테이너는 ps를 통해 확인이 가능하며, exec를 통해 내부에 접속할 수도 있다.

 

 

 

'Docker, K8S, AWS' 카테고리의 다른 글

Kubernetes 쿠버네티스란?  (0) 2023.12.12
728x90

들어가며,

DB에 접근하며 데이터를 저장하고 호출하는 과정에서 수많은 예외가 발생합니다.

개발자는 발생할 수 있는 예외에 대해 미리 지정하여 안정적으로 어플리케이션이 동작할 수 있도록 처리해야 합니다.

예외 계층

예외 종류

체크 예외 : 

  • SQLException: 데이터베이스에 대한 SQL 쿼리 수행 중 문제가 발생한 경우.
  • IOException: 데이터베이스 연결 또는 파일 입출력과 관련된 문제가 발생한 경우.

언체크 예외 : 프로그램 실행 중에 발생할 수있는 예외 

  • NullPointerException (NPE): 객체 참조가 없는 상태에서 메소드를 호출할 때
  • IllegalArgumentException: 메소드에 전달된 인수가 잘못된 경우

예외 규칙

 

기본적인 MVC 구조에서 예외는 Repository에서 발생하게 되며, 예외는 Repository를 호출한 Service에서 처리를 하거나 다시 던지거나 선택을 할 수 있습니다.

처리를 하는 것은 Catch로 잡고 던질 경우에는 Throw로 던지면 됩니다.

 

Catch

@Test
void checked_catch() {
	Service service = new Service();
    service.callCatch();
    
    static class MyCheckedException extends Exception {
        public MycheckedException(String message) {
            super(message);
        }
    }

    public void callCatch() {
        try {
            repository.call();
        } catch (MyCheckedException e) {
            log.info("예외 처리, message={}", e.getMessage(), e);
        }
    }

    static class Repository {
        public void call() throw MyCheckedException {
            throw new MyCheckedException("ex");
        }
    }
}
catch(MycheckedException e)를 통해 MycheckedException 예외를 잡아 처리하는 Test를 진행했습니다.
해당 예외 처리에서는 log.info를 통해 예외에 대한 정보를 기록하게 됩니다.

 

Throw

@Test
void checked_throw() {
	Service service = new Service();
   	assertThatThrowBy(() -> service.callThrow())
    		.isInstanceOf(MyCheckedException.class);
}

static class MyCheckedException extends Exception {
	public MyCheckedException (String message) {
    	super(message);
    }
}

static class Service {
	Repository repository = new Repository();
    
    public void callThrow() throws MyCheckedException {
    	repository.call();
    }
}

static class Repository {
	public void call() throws My checkedException {
    	throw new MyCheckedException("ex");
        }
    }
}

 

예외를 던지는 경우, assertj의 메서드를 통해 예외를 검증했습니다.
() -> service.callThrow()는 해당 메서드를 호출하는 람다식을 정의합니다.
.isInstanceOf(MyCheckedException.class)에서는 ()안의 예외 유형의 인스턴스와 같은지 검증하게 됩니다.
따라서 service에서 호출한 callThrow 메서드의 결과가 MycheckedException의 인스턴스와 같아야만 테스트가 성공합니다. 

 

체크 예외와 언체크 예외 비교

체크 예외에서는 반드시 던지거나 처리를 해야만 했습니다. 그렇지 않을 경우 시스템이 종료되는 문제가 발생하기 때문입니다. 반면, 언체크 예외는 컴파일러가 예외를 체크하지 않기 때문에 생략이 가능합니다. 생략할 경우에는 자동으로 예외를 던지게 됩니다.

 

실제 적용 시에는?

기본적으로 언체크 예외를 사용하는 것이 좋습니다. 체크 예외는 비즈니스 로직상 의도적으로 던지는 경우에만 사용하는 것이 좋습니다.

 

상위 트리에 있는 Exception을 던지면?

결론을 먼저 말하면 throws Exception을 통해 모든 예외를 던지는 방법은 선택해서는 안됩니다. 앞서 실제 적용 시에 체크 예외는 의도적으로 던지는 경우에 사용하는 것이 좋은데, 모든 예외를 던지게 되면 어떤 예외를 잡고 던지는 것인지 불명확해지기 때문입니다.

 

기존 예외를 포함한 후 스택 트레이스 출력

로그를 출력할 때 마지막 파라미터에 기존 예외를 꼭 포함해야 한다.

log.info("message={}", "message", ex)` , 여기에서 마지막에 `ex` 를 전달하는 것을 확인할 수 있다.

+ Recent posts