3계층 구조(3-Tier Architecture)의 이해와 실제 적용

park.js·2025년 4월 17일
1

FrontEnd Develop log

목록 보기
43/43
post-thumbnail

3계층 구조가 생긴 배경

웹 애플리케이션 개발이 점점 복잡해지면서, 초기의 단일 구조(Monolithic) 애플리케이션은 여러 문제점을 드러냈습니다. 이런 문제를 해결하기 위해 등장한 것이 3계층 구조(3-Tier Architecture) 입니다. IBM, Oracle, Microsoft 등의 기업들은 엔터프라이즈 애플리케이션 개발 가이드에서 단일 구조의 한계를 극복하기 위한 다계층 아키텍처의 중요성을 강조해왔습니다.

단일 구조의 문제점

모든 것이 한 곳에 섞임:

  • 한 파일에 HTML, CSS, JavaScript, 데이터베이스 쿼리가 모두 섞여 있어 변경이 어려웠습니다.
  • 화면 디자인만 바꾸고 싶어도 데이터 처리 코드까지 건드려야 했습니다.
  • TV 채널만 바꾸려고 할 때마다 전체 설정을 들어가서 바꿔야 하는 느낌.

코드 재활용이 어려움:

  • 모듈화가 되지 않으니 같은 기능이 필요할 때마다 처음부터 다시 코드를 작성해야 했습니다.

테스트가 복잡:

  • 특정 기능만 테스트하고 싶어도 전체를 실행해야 했습니다.

이러한 문제를 해결하기 위해 애플리케이션을 기능별로 분리하는 계층화된 접근 방식이 필요!

프레젠테이션 계층 (Presentation Tier) React, Redux, 컴포넌트, UI 비즈니스 계층 (Business Tier) UseCase, 비즈니스 로직, 인터페이스 데이터 계층 (Data Tier) Repository 구현체, API 호출, DB 접근

3계층 구조란?

3계층 구조는 애플리케이션을 논리적/물리적으로 세 개의 독립된 계층으로 나누어 구성하는 아키텍처 패턴입니다. 각 계층은 특정 책임을 담당하며, 서로 간의 의존성을 최소화합니다.

1. 프레젠테이션 계층 (Presentation Tier)

  • 역할: 사용자와 직접 상호작용하는 인터페이스 담당
  • 포함 요소: UI 컴포넌트, 사용자 입력 처리, 데이터 표시
  • 프론트엔드 영역: 사용자가 직접 마주하는 부분

2. 비즈니스 계층 (Business/Application Tier)

  • 역할: 비즈니스 로직과 규칙 처리
  • 포함 요소: 데이터 검증, 비즈니스 규칙 적용, UseCase 구현
  • 미들웨어 영역: 프레젠테이션과 데이터 계층 사이의 중개자

3. 데이터 계층 (Data Tier)

  • 역할: 데이터 저장 및 접근
  • 포함 요소: 데이터베이스, 데이터 접근 로직
  • 백엔드 영역: 실제 데이터 관리를 담당

코드로 보는 3계층 구조

실제 진행중인 프로젝트 코드를 통해 각 계층의 구현을 살펴보겠습니다.

1. 데이터 계층: Repository 구현체

// 데이터 계층: UserRepositoryImpl.ts
export class UserRepositoryImpl implements UserRepository {
  async getUser(): Promise<User> {
    const response = await fetch('https://XXXXXX.co.kr/product/user', {
      method: 'GET',
      headers: {
        'Content-Type': 'application/json',
        Authorization: `Bearer ${localStorage.getItem('XXXX-accessToken')}`,
      },
    });

    if (!response.ok) {
      throw new Error('존재하지 않는 id입니다. 관리자에게 문의해주세요.');
    }

    const data = await response.json();
    return {
      userId: data.userId,
      name: data.name,
      // 기타 필드...
    };
  }
}

데이터 계층은 UserRepositoryImpl 클래스로 구현되며, API 호출을 통해 실제 데이터를 가져오는 역할을 합니다.

2. 비즈니스 계층: UseCase 구현

// 비즈니스 계층: GetUserUseCase.ts
export class GetUserUseCase {
  constructor(private repository: UserRepository) {}
  
  async execute(): Promise<User> {
    return this.repository.getUser();
  }
}

비즈니스 계층은 GetUserUseCase 클래스로 구현되며, 데이터 계층에 의존하지만 구체적인 구현체가 아닌 인터페이스에 의존합니다. 개인적으로 이 부분이 아키텍처의 핵심이라고 생각합니다. 이유는 DIP와 SOLID와 연관되어있는데 밑에서 더 자세히 설명드리겠습니다.

3. 프레젠테이션 계층

// 프레젠테이션 계층: actions.ts
export const getUserAction = createAsyncThunk<User, void, { rejectValue: string }>(
  'user/getUser',
  async (_, { rejectWithValue }) => {
    const useCase = new GetUserUseCase(repository);
    try {
      const user = await useCase.execute();
      return user;
    } catch (error) {
      return rejectWithValue((error as Error).message);
    }
  },
);

이 예시에서는 Redux를 사용하여 프레젠테이션 계층을 구현했습니다. 이것은 3계층 구조에서 필수적인 기술이 아니라, 제가 진행중인 프로젝트의 요구사항에 맞게 선택된 구현 방식입니다.

사용자 정보 가져오기 과정 요약

  1. 사용자 요청프레젠테이션 계층 접근
    • 사용자가 프로필 페이지를 열거나 "정보 가져오기" 버튼 클릭
  1. Redux 액션비즈니스 계층 호출
    • getUserActionGetUserUseCase 호출
  1. UseCase데이터 계층 인터페이스 접근
    • GetUserUseCaseUserRepository 인터페이스의 메서드 호출
  1. Repository 구현체외부 API 요청
    • UserRepositoryImpl이 실제 서버 API에 HTTP 요청
  1. API 응답역순으로 데이터 전달화면 표시
    • 서버 응답 → Repository → UseCase → Redux → UI 컴포넌트

각 계층을 연결하는 인터페이스의 중요성

3계층 구조에서 핵심은 각 계층 간의 인터페이스입니다. 3계층 구조와 SOLID/DIP는 필수적인 상관관계를 갖는 것은 아니지만, 함께 사용될 때 코드 품질과 유지보수성을 크게 향상시킵니다.

SOLID/DIP를 적용한 3계층 구조 vs. 적용하지 않은 구조

DIP 적용 (현재 프로젝트)

// 인터페이스 (추상화)
interface UserRepository {
  getUser(): Promise<User>;
}

// 비즈니스 계층: 인터페이스에 의존
class GetUserUseCase {
  constructor(private repository: UserRepository) {}
  
  async execute(): Promise<User> {
    return this.repository.getUser();
  }
}

DIP 미적용

// 비즈니스 계층: 구체적인 구현체에 직접 의존
class GetUserUseCase {
  private repository = new UserRepository(); // 직접 의존!
  
  async execute(): Promise<User> {
    return this.repository.getUser();
  }
}

핵심 차이점

  1. 인터페이스 사용
    • DIP 적용: 인터페이스를 통한 추상화로 유연성 확보
    • DIP 미적용: 구체적인 구현체에 직접 의존으로 유연성 감소
  1. 의존성 주입
    • DIP 적용: 생성자를 통해 외부에서 의존성 주입
    • DIP 미적용: 내부에서 직접 의존성 생성
  1. 결합도
    • DIP 적용: 느슨한 결합으로 변경에 유연하게 대응
    • DIP 미적용: 강한 결합으로 변경 시 여러 부분 수정 필요
  1. 테스트 용이성
    • DIP 적용: Mock 객체로 쉽게 대체 가능
    • DIP 미적용: 테스트 시 실제 API 호출 필요

3계층 구조의 장점

  1. 관심사의 분리
    • 각 계층은 명확한 책임(이것이 3계층의 핵심)을 가져 코드 이해도가 향상됩니다.
  1. 재사용성 증가
    • 비즈니스 로직과 데이터 접근 로직을 여러 상황에서 재사용할 수 있습니다.
  1. 테스트 용이성
    • 각 계층을 독립적으로 테스트할 수 있어 품질 보증이 쉬워집니다.
  1. 유지보수성 향상
    • 특정 계층의 변경이 다른 계층에 미치는 영향을 최소화합니다.
  1. 확장성 개선
    • 각 계층을 독립적으로 확장할 수 있어 성장하는 애플리케이션에 적합합니다.

결론

3계층 구조는 복잡한 애플리케이션의 유지보수성, 확장성, 테스트 용이성을 높이는 효과적인 아키텍처 패턴입니다. 모든 상황에 적합한 것은 아니지만, 프로젝트가 성장함에 따라 그 가치가 더욱 분명해질 것 같은 느낌입니다.

핵심은 각 계층 간의 인터페이스를 통한 의존성 관리입니다. 이는 SOLID 원칙 중 DIP(의존성 역전 원칙)와 직접적으로 연관되며, 코드의 유연성과 테스트 용이성을 크게 향상시킵니다.

profile
참 되게 살자

0개의 댓글