계층 분리
공책에 기록할 수 있는 필기구 즉, 연필, 볼펜 등은 첫 번째 서랍장에
기록을 수정할 수 있는 역할인 지우개, 화이트 등은 두 번째 서랍장에 두는 등
기준을 정의하고, 그 기준에 부합한 것들끼리 분리해 놓는 것이
어플리케이션을 설계할 때도 존재한다. 이를 계층(Layer)이라 하고,
이번 프로젝트에서는 3계층(controller, service, repository)이 아닌
4계층(presentation, application, domain, infrastructure)으로 진행하였다.
4계층으로 프로젝트 개발을 진행하면서 알게된 것들을 기록하는 차원에서 정리하게 되었다.
4계층은 presentation, application, domain, infrastructure로 구성된다.
presentation layer는 사용자 인터페이스를 제공하는 역할을 한다.
사용자 인터페이스를 제공하는 역할이라는 것이 사용자와 직접 맞닿는 부분을 의미하고,
이 역할을 하는 곳이 controller와 view다. 사용자의 요청을(데이터와 uri) 받고,
요청에 맞는 응답을 보내주는 인터페이스 역할을 하는 곳이 presentation layer다.
사용자의 요청(http 요청)을 맨 처음 받아들이는 역할이 presentation layer의
핵심 컴포넌트인 controller다.
application layer는 presentation layer와 domain layer사이에서 데이터를 전달하고,
처리하는 역할을 한다. 사용자 입력을 처리하여 domain layer에 전달하고,
domain layer에 반환된 데이터를 presentation layer에 전달한다.
domain layer에서도 비즈니스 로직을 담당한다.
데이터 베이스와 연동하여 데이터를 저장하고, 관리하는 역할을 하는 곳
infrastructure layer는 데이터베이스, 서버, 네트워크 등의 인프라를 관리하는 곳이다.
데이터베이스와 연동하여 데이터를 저장하는 역할을 한다.
domain layer와 infrastructure layer 모두 db와 연동하여 데이터를 저장하고 관리하는 역할을 하는데,
그 역할의 개념이 다르다. domain의 db repository 인터페이스 파일을
infrastructure에서 db를 구현하는 클래스가 구현하고,
해당 클래스에서 SimpleJpaRepository 구현체를 필드로 갖는다.
도메인 주도 디자인 사용자의 기준에 따라(관점을 기준으로) 설계되고,
사용자에 따라 설계되기 때문에 사용자가 달라지면 설계가 달라져야 한다.
설계가 달라질 수 있기에 서로 의존하는 관계를 낮춰야 하고,
의존성과 결합성을 낮춘 디자인 패턴이 도메인 주도 설계(디자인)라는 것이다.
도메인 주도 설계는 복잡한 비즈니스 도메인을 명확하게 모델링하고,
도메인 로직을 중심으로 애플리케이션을 구성하도록 한다.
이 구조는(DDD) 계층 간의 결합도를 낮추어 설계 변경에 유연하고,
유지보수 및 확장에 용이한 시스템을 만들 수 있게 한다.
각 레이어는 하나의 관심사에만 집중할 수 있고,
4계층 구조를 올바르게 구현하기 위해 2가지를 주의하면 된다.
1. 위의 계층에서 아래 계층에 접근이 가능하지만 아래 계층에서
위 계층으로 접근하는 것은 불가한 것을 기본으로 한다.
ex. presentation 계층에서 application계층에 접근하는 것은 가능하지만
application계층에서 presentation계층에 접근하는 것은 설계 원칙 위반이다.
단, 상위 계층에서 인터페이스를 정의하고, 그 인터페이스를 하위 계층에서 구현하는 방식이라면
하위 계층이 상위 계층의 추상화에만 의존하게 되어 의존성 방향을 유지하면서도
상위 계층의 요구사항을 하위 계층에서 구현할 수 있게 된다.
헷갈리는 부분
application layer의 service에서 비즈니스 로직을 담당한다고 생각했는데,
domain layer(ex. entity 클래스)에서 비즈니스 로직을 담당한다고 하는 글을 많이 봤다.
두 계층에서 비즈니스 로직을 담당하는 것은 맞는데 application layer에서는 비즈니스 흐름을
domain layer에서는 '핵심' 비즈니스 규칙과 로직을 담당하는 것으로 나뉘어진다고 한다.
우선 '비즈니스'는 소프트웨어로 해결하려는 현실 세계의 문제 영역 전체를 의미한다고 한다.
쉽게 말해서 어플리케이션(유저들이 사용하고자 하는 서비스)은 현실 세계를 추상화한 공간이고,
현실에서 사람이 하던 일을 대신 수행하도록 설계하는 것인데, 그 과정에서 어떤 순서로 해야하는가,
즉 흐름은 service가 담당하고, 순서에 따라 세부적으로 조건을 검사하는 등의
문제 해결의 핵심적인 부분 등을 domain에서 담당하게 된다.
즉 일의 흐름(순서)과 흐름에 따라 문제를 해결하는 과정을 설계하고
구현하는 것이 비즈니스 로직이고, 그 전체 영역을 비즈니스라고 한다.
1. 현실 세계의 문제를 어플리케이션에게 위임한다.
[현실 세계 문제] ⎯ (추상화) → [어플리케이션]
2. 어플리케이션 비즈니스 영역 살펴보기.
[어플리케이션]
↓
(비즈니스) 흐름[순서]: application layer의 service에서 순서를 제어한다.
(비즈니스) 규칙: domain layer에서 문제해결, 판단, 계산 등을 처리한다.
비즈니스 흐름과 규칙을 정의하고, 수행되는 과정 전체를 비즈니스라고 한다.
정적 팩토리 메서드
정적 팩토리 메서드는 개발자가 작성한 static 메서드를 통해 간접적으로 생성자를 호출하여 객체를 생성하는 디자인 패턴이다.
클래스로부터 인스턴스화할 때[객체 생성] 직접적으로 생성자를 호출하지 않고, 객체를 생성하는 클래스 메서드를 통해
간접적으로 객체 생성을 유도하는 것이다.
정적 팩토리 메서드 작성 과정
- 생성자의 접근 제한자를 private 으로 해서 외부의 접근 방지(외부에서 해당 생성자를 호출하지 못함)
- 정적 팩토리 메서드에서 private 인 생성자를 호출하고 생성된 인스턴스를 반환하도록 한다.
결국 직접적으로 생성자를 호출하지 못하도록 하고,
그 생성자를 호출할 수 있도록 별도의 메서드를 작성(정적 팩토리 메서드)
그리고 외부에서 별도의 메서드를 호출하여 간접적으로 생성자를 호출하고,
생성된 객체를 반환받도록 설계하는 것인데,
이렇게까지 우회적으로 접근하는 이유가 있는지가 의문이었다.
왜 정적 팩토리 메서드를 사용해야 하는가?
1. 생성 목적에 대한 이름 표현이 가능하다.
개발하면서 개념 정리(기록용)
@Component는 스프링이 관리하는 기본 컴포넌트로,
애플리케이션이 실행되면 @Component가 작성된 클래스는
스프링이 객체로 만든 후 스프링 컨텍스트 안에서 관리한다.
→ 스프링이 관리하는 (스프링 컨텍스트 안에 존재하는) 객체를 빈(Bean)이라 한다.
클래스의 역할을 명확히 구분할 수 있는 @Component 기반 어노테이션
@Controller, @Service, @Repository
- @Controller: 컨트롤러 역할인(웹 요청을(http 요청) 처리하는) 클래스임을 선언
- @Service: 서비스 역할인(비즈니스 로직, 특히 비즈니스 흐름(순서)을 제어하는) 클래스임을 선언
- @Repository: 레포지토리 역할인(db연결) 클래스임을 선언
* 개발자간 '해당 클래스가 컨트롤러 역할을 하는구나' 정도를 인지하게 해주는 표시같음.
서비스 클래스에 @Controller를 작성했는데 서버 실행과 기능 작동에 문제가 없었음.
1. @Controller와 @RestController의 차이
- @Controller와 @RestController 모두 웹 요청을 처리하고, 응답을 반환하는 역할을 한다.
- @Controller는 뷰(View: html, jsp 파일 등)를 반환하고,
@RestController는 데이터를(JSON/XML 등) 반환한다.
→ @RestController: @Controller+@ResponseBody
2. 기존 Repository 클래스는 필드로 EntityManager 객체를 필드로 주입받고,
insert 명령이 요청되면 insert 하는 메서드의 내부에 persist() 메서드가 호출되고,
persist() 메서드로 인해 해당 객체가 (EntityManager가 관리하는) 영속성 컨텍스트에서 관리된다.
3. Pageable
Pageable 인터페이스를 구현한 PageRequest 객체를 만든다. PageRequest 의 Of() 메서드를 통해
Pageable 인터페이스를 구현한 PageRequest 객체가 만들어진다. PageRequest.Of() 메서드를 호출할 때
2개의 매개변수를 전달하는데, 첫 번째는 (클라이언트가 선택한 페이지, 클라이언트에게 보여줄 페이지) 페이지 번호를
두 번째는 한 페이지에 보여줄 게시물의 수다. 페이지 번호는 0부터 시작하기에 클라이언트가 1번의 페이지를 보기위해
1페이지를 클릭했고, 페이지 번호로 1을 전달받으면, 해당 데이터에서 1을 감산한 값을
PageRequest.Of() 메서드에 전달한다.
또 PageRequest.Of() 메서드에 정렬 객체(Sort)를 전달할 수도 있다.
PageRequest 클래스의 필드에 Sort 가 있음. private final Sort sort;
PageRequest.of() 메서드의 매개변수로 페이지 번호(int pageNumber)와
한 페이지에 보여줄 게시물 갯수(int pageSize)만 전달했을 때 내부적으로 Sort 객체는
unsorted() 메서드를 호출하며 Sort 객체는 비어있는 상태로(정렬 조건이 없는 상태)
PageRequest 객체가 생성된다.
- 페이징 처리 프로세스
1) PageRequest 객체를 이용해서 Pageable의 정보를 담아 객체화 한다.
→ 페이지네이션에 필요한 페이지 번호, 한 페이지에 보여줄 게시물의 수, 정렬 조건이 있다면 정렬 객체
를 담아서 PageRequest 에 담아 Pageable 타입을 구현한 객체로(PageRequest) 만든다.
2) Pageable을 JpaRepository가 상속된 인터페이스를 메서드에 entity 객체와 함께 전달한다.
3) 2번 메서드는 Page<entity> 타입으로 결과를 반환한다.
4) 반환된 결과값인 Page<entity> 에 담겨진 Page 정보를 바탕으로 로직으로 처리한다.
4. SimpleJpaRepository 클래스의 findById() 는 Optional 타입을 반환한다.
만약 Optional이 감싸는 T 타입 객체가 null 인 것을 확인하고, null 이라면
예외를 던지려고 할 때 orElseThrow() 메서드를 사용한다.
Optional은 값이 있을 수도 있고, 없을 수도 있다.
orElseThrow() 는 값이 있으면 꺼내서 반환하고,
값이 없으면(==null) 예외를 던진다.
- Optional.orElse(default): Optional 타입을 반환받을 때,
Optional 안에 null 이라면 orElse() 메서드의 매개값으로 전달받은 값이 설정된다.
- Optional.orElseThrow(()->); Optional 타입을 반환받을 때,
Optional 안에 null 이라면 예외를 발생시킨다.
- Optional.ifPresent(): Optional 타입 안에 값이 있다면,
ifPresent() 로 전달한 람다식이 실행되고, 존재하지 않는다면 무시한다.
- orElse()와 orElseGet() 의 차이:
orElse()과 orElseGet() Optional 타입이 비어있으면 기본 값을 설정하는데,
orElse()는 기본 값을 무조건 먼저 생성한다, orElseGet()은 비어있는걸 알았을 때,
필요할 때만 기본 값을 생성한다. orElseGet()은 필요할 때만 생성하는 지연 생성(lazy)
5. JPQL은 Java Persistence Query Language
SQL에서 지원하지 않는 기능을 JPQL을 통해 구현할 수 있다.
Table이 아닌 Entity(객체) 기준으로 작성하는 쿼리가 JPQL이다.
EntityManager 또는 @Query 어노테이션을 통해 JPQL 쿼리를 사용할 수 있다.
6. cascade 속성
@ManyToOne, @OneToMany 어노테이션의 속성으로 cascade 를 작성할 수 있는데
보편적으로 @OneToMany 어노테이션의 속성으로 사용한다.
@OneToMany에서 cascade 사용하는 것이 보편적이다.
cascade 속성은 JPA에서 연관된 엔티티에 어떤 작업(영속화, 삭제 등)을 함께 적용할지를 결정하는 옵션이다.
CascadeType.PERSIST는 부모 엔티티를 persist()하면 자식 엔티티도 자동으로 persist()된다.
entity 클래스 분석하기
entity 클래스에 @NoArgsConstructor(access = AccessLevel.PROTECTED)를 작성했는데
매개변수가 없는 생성자를(기본 생성자) 생성하되, 접근 제어자는 protected로 설정하라는 것이다.
예를 들어 Hospital entity 클래스에 @NoArgsConstructor(access = AccessLevel.PROTECTED)
작성시 기본 생성자는 하기와 같이 작성된다.
protected Hospital() {
}
- entity 클래스에 기본 생성자를 꼭 작성해야 하는 이유
JPA는 엔터티 객체를 생성할 때 리플렉션을 통해 객체를 만들기 때문에
pulic 또는 protected 접근 제어자를 가진 기본 생성자가 반드시 있어야한다.
리플렉션을 통해 객체를 만든다는 것은 JPA나 스프링 프레임워크 같은 라이브러리들이 코드를 직접 new해서 만들지 않고,
클래스 정보를 읽어와서 런타임에 동적으로 객체를 생성한다는 것이다. Java 리플렉션은 클래스, 메서드, 필드 등의 정보를
런타임에 다룰 수 있게 해주는 기능이다. 예를 들어 클래스 이름만 알고 있어도 하기와 같이 객체를 만들 수 있다.
Class<T> clazz = Class.forName("프로젝트 경로가 포함된 풀네임 클래스명");
Object obj = clazz.getDeclaredConstructor().newInstance();
new 연산자와 생성자를 호출하지 않고, 객체를 생성하는 방법이다.
JPA가 객체를 생성하는 방식
JPA가 데이터를 db에서 읽어올 때, 엔티티 클래스로부터 new를 통해 객체를 생성하는 것이 아니라
상기와 같이 리플렉션으로 인스턴스를(객체) 만든다. 그래서 기본생성자가 없으면 newInstance()에서 오류가 발생한다.
리플렉션으로 객체를 생성할 때는 기본 생성자가 꼭 있어야 한다. getDeclaredConstructor() 메서드는
파라미터가 없는 생성자만 찾는다. 생성자가 private이면 접근이 안되므로 최소 protected여야 한다.
@NoArgsConstructor(access = AccessLevel.PROTECTED)는 JPA가 리플렉션으로 객체를 만들 수 있도록
기본 생성자를 만들어주고, 외부에서 접근해서 생성자를 통해 객체를 쉽게 생성하지 못하도록 막는 패턴이다.
만약 @NoArgsConstructor 만 작성한다면 Hospital의 기본 생성자의 접근 제어자가 public이 되고,
외부 코드에서 new Entity()로 엔터티 객체를 막 생성할 수 있게된다.
엔티티 무결성, 불변성, 제약 조건 등을 우회할 수 있는 위험이 있다.
근데 JPA가 필드가 아무것도 없는 리플렉션을 엔터티 객체를 생성하고, 그것을 db에 저장하면 이 또한 문제가 아닌가?
하는 생각에 chat gpt에게 물어보니 중요한 건 "언제, 왜 JPA가 리플렉션으로 객체를 생성하는가" 라고 했다.
JPA가 리플렉션으로 객체를 생성할 때는 조회할 때다(SELECT).
저장 또는 업데이트할 때는 JPA가 리플렉션을 사용하지 않는다.
결론: JPA가 리플렉션을 통해 entity 클래스로부터 객체를 생성하는 것은 조회를 할 때이며,
빈 엔터티 객체를 생성하고, db에서 조회한 데이터를 필드에 채워넣어 반환한다.
저장, 업데이트할 때는 개발자가 설계한 로직에 따라 객체가 생성되기 때문에
생성될 객체를 무결하게 만들도록 설계해야 문제가 없다.
작성 날짜와 작성자를 기록할 수 있는 JPA의 Auditing 기술
작성 날짜와 같은 시간은 @CreatedDate 등의 어노테이션으로 자동 기입이 되지만
작성자를 자동으로 필드에 대입하기 위해서는 AuditorAware 구현체를 만들어주어야 한다.
ex.
@CreatedDate
private LocalDateTime createdAt; // (약간의 별도 설정 필요) 수정한 시간이 자동 기입된다.
@LastModifiedBy
private Long createdBy; // 이렇게 누가 작성했는지에 대한 데이터는 별도의 구현체가 필요하다.
AuditorAware 구현체
package com._7.bookinghospital.hospital_service.infrastructure.repository;
import jakarta.servlet.http.HttpServletRequest;
import lombok.extern.slf4j.Slf4j;
import org.springframework.data.domain.AuditorAware;
import org.springframework.stereotype.Component;
import org.springframework.util.StringUtils;
import org.springframework.web.context.request.RequestContextHolder;
import org.springframework.web.context.request.ServletRequestAttributes;
import java.util.Optional;
@Component
@Slf4j
public class UserInfoAuditorAware implements AuditorAware<Long> {
private static final Long DEFAULT_AUDITOR = -1L;
@Override
public Optional<Long> getCurrentAuditor() {
try{
ServletRequestAttributes attrs =
(ServletRequestAttributes)RequestContextHolder.getRequestAttributes();
if(attrs == null) {
log.debug("요청 컨텍스트가 존재하지 않습니다.");
return Optional.of(DEFAULT_AUDITOR);
}
HttpServletRequest request = attrs.getRequest();
String userId = request.getHeader("X-User-Name");
log.info("UserInfoAuditorAware.java - userId: {}", userId);
// if(userId == null || userId.isBlank())
if(!StringUtils.hasText(userId)) {
log.debug("요청 헤더에 X-User-Name이 존재하지 않습니다.");
return Optional.of(DEFAULT_AUDITOR);
}
return Optional.of(Long.valueOf(userId));
} catch (Exception e) {
log.warn("정보 조회 중 오류 발생: ", e);
return Optional.of(DEFAULT_AUDITOR);
}
}
}
그리고 해당 서비스에서 JPA auditing 기능을 활성화하면서 위 구현체를
참조할 수 있도록 어플리케이션 진입점인 main() 을 갖는 class 에 하기와 같이 작성해준다.
@EnableJpaAuditing(auditorAwareRef="userInfoAuditorAware")
참고
1. ddd를 활용해서 커머스 아키텍처를 설계해보자: https://blog.hansolbangul.com/post/ddd-post-step1
2. DDD(Domain-Driven Design) 계층구조(Layered Architecture) 알아보기: https://dev-coco.tistory.com/166
3. 💠 정적 팩토리 메서드 패턴 (Static Factory Method): https://inpa.tistory.com/entry/GOF-%F0%9F%92%A0-%EC%A0%95%EC%A0%81-%ED%8C%A9%ED%86%A0%EB%A6%AC-%EB%A9%94%EC%84%9C%EB%93%9C-%EC%83%9D%EC%84%B1%EC%9E%90-%EB%8C%80%EC%8B%A0-%EC%82%AC%EC%9A%A9%ED%95%98%EC%9E%90
'[부트캠프] kdt 심화 과정' 카테고리의 다른 글
도커 실습(2) (0) | 2025.04.23 |
---|---|
docker 관련 정리(1) (0) | 2025.04.22 |
2025년 4월 7일 (0) | 2025.04.07 |
MSA(Microservice Architecture)_컨피그 서버(Config Server) (0) | 2025.03.13 |
MSA(Microservices Architecture) 서킷브레이커 (0) | 2025.03.12 |