8 레이어드 아키텍처

개발 99·2025년 4월 23일

8.1 레이어드 아키텍처의 최소 조건

애플리케이션을 레이어로 나누고 각 레이어에 역할을 정한다.
( 프레젠테이션, 비즈니스, 인프라스트럭처 )

컴포넌트에 맞춰 레이어를 분류하는 것은 폴더를 관리하는 것과 다를 바 없다.


--exe
	.game.exe
    .pointer.exe
    .browser.exe
--image
	.game.ico
    .game_character.png
...

핵심은 아래와 같다.

  1. 레이어 구조를 사용한다.

  2. 레이어 간 의존 방향은 단방향으로 유지한다.

  3. 레이어 간 통신은 인접한 레이어에서만 이뤄지게 한다.

아키텍처란 무엇인가?
정책과 제약 조건(규칙)을 이용해 목적을 달성한다.
(해야 하는 것과 하지 말아야 할 것을 구분 짓는다.)

따라서 아키텍처(정책과 제약을 정하는 과정)는 형태가 아니라 정책에 가깝다.

그러나, 목적에 따라서 제약 조건이 달라질 수 있음을 명심한다.
(Ex. 마감임박이 닥친 경우, 제약조건 다 따지면서 만들 수는 없다.)

레이어드 아키텍처를 위한 최소 제약 조건은 무엇인가?

  1. 레이어 구조를 사용한다.

  2. 레이어 간 의존 방향은 단방향으로 유지한다.

8.2 잘못된 레이어드 아키텍처

문제점과 한계 분석

8.2.1 JPA 엔티티 우선 접근

데이터베이스 테이블을 어떻게 만들지 고민하는 경우.

@Entity
public class AccountJpaEntity{
	
    @Id
    @GeneratedValue(...)
    private Long id;
    
    private String email;
    
    private String nickname;
    
}

아래와 같은 구조가 잘못된 레이어드 아키텍처이다.

  • 레이어를 지나치게 추상화해서 각 레이어가 무엇을 설명하고 싶은지 알 수 없다.

  • 다른 한편으로 레이어에 대응하는 스프링 컴포넌트는 또 구체적으로 정해져 있다.

즉 목적은 모호한데, 레이어의 실체는 구체적인, 편의주의를 추구하고 있다.

또한 역방향으로 상향식 설계 구조도 문제다.

(테이블이 먼저 만들어지지 않은 경우 아무런 개발 시작을 할 수 없다.)

이 방식은 애플리케이션 요구사항에 맞는 데이터베이스가 선정되는 것이 아니라 데이터베이스에 맞는 기능을 개발하는 방향으로 발전할 수 밖에 없다.

그래서 JPA 엔티티를 우선 개발 = 상향식으로 레이어드 아키텍처 설계

8.2.2 API 엔드포인트 우선 접근

인터페이스 관점에서 어떻게 사용해야 할지를 먼저 설계
(아까 레이어드 아키텍처에서 하향식 접근법)

이 방식은 시스템을 도메인 요구사항 관점에서 보기 시작하는 것을 의미하므로
JPA 엔티티를 우선했던 방식보단 조금 낫다.

이 방식은 프로젝트가 스프링 프레임워크에 종속될 위험이 있다.

(도메인이 무엇인지 파악조차 못한 상태로 진행한 것이다.)

웹소캣용 서버가 될 수도, 원격 호출을 위한 gRPC 서버가 될 수도, 메시지 큐의 컨슈머가 될 수도 있다.
즉 어떤 서버가 될지도 모르는데, 도메인을 무시하고 설계하는 것 자체가 잘못되었다.

따라서 이런 기술 스펙은 도메인 요구사항을 분석하고 거기에 대응하는 해결 수단으로 선택돼야 한다.

또한 요청과 응답 형식을 고민하면 -> 데이터를 고민하고 -> JPA 엔티티 고민해서 -> 트랜잭션 스크립트 코드 탄생

8.2.3 본질을 다시 생각하기

앞선 예제는 JPA나 스프링에 지나치게 의존하는데 이는 JPA나 스프링에 대한 의존성을 빼면 구조가 무너진다.

스프링이나 JPA 없이도 성립할 수 있는 애플리케이션을 만들 줄 알아야 한다.

(프레임워크 의존성을 제거하더라고 구조상 문제가 없어야 하며, 다른 프레임워크 이식을 하더라도 문제가 없다.)

특정 기술에 절대 종속되어서는 안된다!

애플리케이션의 본질은 도메인이다.

도메인을 파악하고, 이에 따른 도메인 모델을 구성하고, 설계를 진행해야만 한다.

세부 사항에 대한 결정(JPA, 스프링부트)을 미뤄라!

8.3 진화하는 아키텍처

시스템 개발의 첫 시작을 도메인으로 둔다.( 도메인 분석, 개발 )

8.3.1 인지 모델 변경하기

프레젠테이션, 인프라스트럭처가 아닌 "비즈니스"레이어 먼저 개발을 해야 한다.

( 도메인 먼저 개발 )

Presentaion(Controller) -> Business(Service) -> Domain -> Infrastructure(JpaRepository)

패키지명이 common, core = 프로젝트에서 사용하는 공통 코드들을 한 군데에 모아서 관리
(유틸 클래스, 설정을 위한 클래스)

아래와 같이 패키지를 구성할 수 있다.
즉 모든 패키지가 레이어드 아키텍처에서 말하는 레이어에 일대일로 대응해야 하는 것은 아니다.

-presentation
-business
-domain
-infrastructure
-aop
-config
-util

또한 common, core는 순수 자바 코드로 작성이 되어야만 한다.
(즉, 외부 라이브러리에 의존하지 않기 위함이다.)
이 말은 @Entity, @Service 같은 스프링 에너테이션도 없어야만 한다.

추가로, 도메인 레이어의 외부 라이브러리 사용은 엄격하게 통제해서 외부 연동이 필요하면 언제든지 컴포넌트가 교체가 되도록 한다.

위 구조에서 비즈니스 레이어 -> 애플리케이션 서비스로 바꾸자(@Servuce)

(왜냐하면, 결국 비즈니스 레이어도 도메인에 포함되기 때문이다.)

Presentation(Controller) -> Application(Service) -> Domain(Domain) 
				
                			Application(Service) -> Infrastructure(JpaRepository)

비즈니스 레이어 = 애플리케이션 레이어 + 도메인 레이어
이로써 애플리케이션 레이어는 도메인에 있는 코드를 실행하는 역할만 담당한다.

그리고 도메인 레이어를 먼저 설계함으로써 외부 의존성에 영향받지 않는 애플리케이션이 가능하다.

Ex)

@Builder
@RequiredArgsConstructor
public class Account{
	public final Long id;
    public final String email;
    public final String nickname;
    
    public Account withNickname(String nickname){ ... }
}

JPA나 스프링 없이도 도메인 레이어에 애플리케이션에 필요한 주요 기능들을 개발할 수 있음을 명심하자.

(항상 도메인 레이어 설계부터 고민.)

따라서, 먼저 도메인 객체들을 만들고 -> 나중에 JPA나 스프링부트를 얹는다.

JPA 엔티티는 아래와 같이 써먹는다.

@Entity
public class AccountJpaEntity{
	fields ...
    
    public static AccountJpaEntity from(Account account){...}
    
    public Account toModel(){...}
    
}

그래서 코드를 다음과 같이 쓸 수 있다.

@Service
public class AccountService{
	
    @Transactional
    public Account updateNicknameById(long id,String nickname){
    	// 영속성 객체 -> 도메인 객체로 변환
    	Account account = accountJpaRepository.findById(id).toModel();
        
        // 도메인 객체에 작업을 위임
        account = account.withNickname(nickname);
        
        // 도메인 객체 -> 영속성 객체
        accountJpaRepository.save(AccountJpaEntity.from(account));
        
    }
}

여기서 핵심은 도메인 레이어는 인프라스트럭처 또는 외부 레이어를 참조하지 않는다 = 사용하지 않는다.

(JPA 라이브러리. 스프링 라이브러리를 임포트 하면 안됨!)

8.3.2 JPA와의 결합 끊기

아직까지 JPA에 의존적이다.
그래서 의존성 역전을 사용한다.(인터페이스를 사용하자.)

@Service
public class AccountService{
	private final AccountRepository accountRepository;
    
    ...
}

public interface AccountRepository{
	public Account findById(long id);
    public void save(Account account);
}


@Repository
public class AccountRepositoryImpl implements AccountRepository{

	@Override 
    public Account findById(long id){...}
    
    
    @Override
    public void save(Account account){...}

}

여기서 도메인 모델, JPA 엔티티는 그냥 유지한다.

스프링의 의존성 주입은 타입을 기반으로 동작하기 때문에 인터페이스를 구현한 구현체가 알아서 주입된다.( 중복되면 @Qualifier쓰고 )

그래서 나중에 바꿔야 하면, AccountRepositoryImpl만 바꾸면 된다.
(RDB, MongoDB 등 데이터베이스 마음대로 변경가능)

8.3.3 웹 프레임워크와 결합 끊기

이 원리를 프레젠테이션 레이어에도 적용하자
(즉, Service에서 인터페이스를 설정하자.)


//
@RestController
@RequestMapping("/accounts")
public class AccountController{

	private final AccountService account Service;

	...
    
    @PatchMapping(...)
    public ResponseEntity<Account> patchProperties(...){
    	...
    }
}

// Interface
public interface AccountService{
	public Account updateNickNameById(...);
}

// 구현체
@Service
public class AccountServiceImpl implements AccountService{
	...
}

각 레이어 접합 부분을 인터페이스로 설명을 한다.

헥사고날 아키텍처 구조(포트-어댑터 패턴)

내부 세계(도메인)와 바깥 세계(웹 요청, 데이터베이스 등) 구분

사용자는 입력 어댑터를 통해 API를 호출을 하고, 입력 어댑터는 입력 포트를 통해 입력받은 내용을 ServiceImpl 컴포넌트로 전달한다.

  • ServiceImpl 컴포넌트 = 애플리케이션 서비스
    DB에서 도메인을 가져오고, 네트워크 호출을 하고, 도메인에 업무를 위임

핵심은 도메인인 부분과 아닌 경계를 구분하고, 그 경계를 인터페이스로 작성해서 유지보수에 용이하게 하자

(제어의 역전을 적극적으로 사용하자)

스프링 프레임워크의 가치는 IoC 컨테이너라는 것이며, DI를 훌륭하게 지원하는 도구라는 것이다.

프레젠테이션 레이어에 의존성을 적용할지에 대한 평가

  1. 긍정적 의견
  • 경계를 강제할 수 있다.

  • 외부 세계와 내부 세계에서 벌어지는 모든 일에 일관된 패턴을 적용할 수 있다.

  • 프레젠테이션 레이어에 있는 컴포넌트를 테스트해야 할 때 테스트가 쉬워진다.

  1. 부정적 의견
  • 의존성 역전을 적용했을 때 얻을 수 있는 실효성이 모호함.
    프레젠테이션 레이어는 그대로인데 애플리케이션 레이어가 변경되는 일은 없다.
    (프레젠테이션이 바뀌면, 나머지도 싹다 바뀌어야 한다.)

  • 의존성 역전을 적용하지 않아도 도메인 모델은 외부 세계에 독립적

  • 애플리케이션 레이어가 프레젠테이션 레이어에 의존하는 것은 부자연스럽다.

8.4 새로운 접근법

도메인부터 접근하는 방식은 상향식 접근법인다.
(도메인 -> 애플리케이션 서비스 -> 애플리케이션 서비스가 사용할 인터페이스 -> 컨트롤러 및 JPA 시스템 구축 완성)

profile
구구구구구!

0개의 댓글