본문 바로가기

개발기술/설계|디자인패턴

디자인패턴

디자인패턴

 

싱글톤패턴

하나의 인스턴스만 사용되는 경우. 하나의 인스턴스가 사용되지 않는 경우에는 인스턴스의 멤버변수에 저장/입력된 값들이 다른 인스턴스에 공유되지 않는다.  이로인해서 예상한 데이터를 찾을 수 없어서 문제가 발생하기도 함.

빌더패턴

  • 장점1 : Setter대신 빌더패턴을 사용하면, 한번 생성된 instance의 값은 변경되지 못하게 하여 thread safe를 확보할 수 있음
  • 장점2 : 순차적으로 field를 하나씩 설정할 수 있어서 readbility가 증진됨. 필요한 데이터만 설정할 수 있어서 유연성이 증가됨
  • 장점3 : 필요한 데이터만 설정할 수 있어서 유연성이 증가됨

스태틱팩토리패턴

 

 

프록시패턴

프록시 패턴의 핵심은 바로 인터페이스의 동일성을 유지함으로써, 클라이언트가 실제로 대리 객체를 사용하고 있는지, 아니면 실제 객체를 사용하고 있는지 신경 쓸 필요가 없도록 만드는 데 있습니다.  서버는 대리객체를 통해서 원래 객체를 호출하고 추가적인 로직을 실행한다.

프록시의 장점은 원래 실제 객체에 기능을 추가하거나 변경하지 않고도, 그 객체에 대한 접근 방식이나 동작을 확장할 수 있게 만드는 것입니다.

 

  • 접근 제어: 프록시는 원래 객체에 대한 접근을 제어할 수 있습니다. 예를 들어, 특정 조건에서만 실제 객체에 접근하도록 하거나, 접근 전에 인증 및 권한 검사를 수행할 수 있습니다.
  • 기능 확장: 프록시는 원래 객체의 메서드를 호출하면서 추가적인 기능(예: 로깅, 트랜잭션 관리, 성능 모니터링 등)을 수행할 수 있습니다.
  • 지연 초기화(Lazy Initialization): 프록시를 사용하면 실제 객체를 필요한 시점까지 생성하지 않고 미뤄둘 수 있습니다.

퍼사드 패턴 (Facade Pattern)

퍼사드 패턴은 시스템의 복잡성을 숨기고 단순한 인터페이스를 제공하여 클라이언트가 시스템과 상호작용하기 쉽게 만드는 데 사용됩니다. 여러 개의 복잡한 서브시스템이나 서비스가 존재할 때, 퍼사드는 그 모든 기능을 감싸서 하나의 간단한 인터페이스를 제공합니다.

  • 예를 들어, 어떤 소프트웨어 시스템이 여러 개의 모듈(결제 모듈, 사용자 관리 모듈, 알림 모듈 등)을 포함하고 있다면, 퍼사드는 이러한 모듈들의 복잡한 API를 숨기고, 하나의 단순한 메서드 호출로 모든 작업을 수행할 수 있게 만들어줍니다.
  • 이를 통해 클라이언트는 내부 시스템의 복잡한 구성 요소를 알 필요 없이 단순히 퍼사드를 호출하여 작업을 완료할 수 있습니다.

퍼사드 패턴의 장점사용의 용이성내부 구현의 캡슐화입니다. 클라이언트는 시스템의 복잡한 내부 구현을 알 필요 없이 간단한 인터페이스만 사용하여 기능을 이용할 수 있습니다.

 

디자인원칙

Clean Architecture Principles

  클린 아키텍처는 소프트웨어 시스템을 여러 계층으로 구분하고, 각 계층 간의 의존성을 엄격하게 관리하는 것을 기본 원리로 합니다. 이를 위해서 의존성역전규칙, 계층간 책임분리을를 준수합니다. 

DIP : Dependency Inversion Principle 의존성 역전 원칙(교체)

  • 하위모듈의 변경이 상위모듈의 변경을 요구하는 의존성을 끊어내야한다.

https://en.wikipedia.org/wiki/Dependency_inversion_principle

1. High-level modules should not import anything from low-level modules. Both should depend on abstractions (e.g., interfaces). 

- 고수준 모듈(내부 계층, 비즈니스 로직)이 저수준 모듈(외부 계층, 구체적 구현)에 직접 의존해서는 안 됩니다.
대신, **추상화 (인터페이스)**를 통해 둘을 연결해야 합니다. 

 

Traditional layers pattern

 

Dependency inversion pattern

 

2.Abstractions should not depend on details. Details (concrete implementations) should depend on abstractions.

- 추상화 구체적인 세부 사항에 의존해서는 안 된다. 오히려 세부 사항(구체적 구현)이 추상화에 의존해야 한다.

 

-> 의존성규칙(Dependency rule) : Inward Dependency

의존성은 항상 안쪽으로(내부로), 즉 더 높은 수준의 추상화 계층(Entities와 Use Cases)으로 향해야 한다는 것입니다.

  • 컨트롤러나 필터는 어댑터를 호출하여 데이터를 추출하고, 내부 로직에서 사용할 수 있는 형태로 전달할 수 있습니다.반면, 어댑터는 컨트롤러나 필터를 의존해서는 안 됩니다

 

계층간 책임 분리

 

  • Entities
    • 엔티티는 소프트웨어에서 문제 해결을 위해 핵심이 되는 업무 규칙들이 속하는 계층으로, 도메인이라고 할 수 있다.
    • 엔티티는 단순히 속성들로만 구성된 객체일수도, 속성과 메소드로 구성된 객체일 수 있다.
  • Usecase
    • 유즈케이스는 어플리케이션 수준의 업무 규칙들이 속하는 계층이다. 즉, 비즈니스로직이 존재하는 곳이다.
      • 비즈니스 로직은 소프트웨어 애플리케이션에서 데이터가 처리, 저장, 변경되는 방식을 안내하는 일련의 알고리즘과 규칙입니다.
    • 유즈케이스는 어플리케이션 상에서 엔티티 간 데이터 통신을 관장하고, 또한 엔티티의 업무 규칙들이 어플리케이이션 상황에서 실행되도록 하는 코드들이 포함된다.
  • Interface adapter
    • 인터페이스 어댑터는 외부에서 유입되는 데이터를 유즈케이스와 엔티티에서 사용하기 편한 방식으로, 또 반대 방식으로 데이터를 변환하는 것을 담당하는 계층이다.
    • 계층형 아키텍처에서 presentation에 속하는 controller와 같은 것이 이 계층에 속할 수 있다.
  • Frameworks and drivers
    • 소프트웨어의 기술적 세부 사항(웹 프레임워크, 데이터베이스, 외부 시스템 등)으로, 외부 입력(예: HTTP 요청, 데이터베이스 드라이버, 메시지 등)을 내부 계층으로 전달하거나 외부 시스템과 통신하는 역할을 담당하는 구성 요소입니다.

https://www.spaceteams.de/en/insights/clean-architecture-a-deep-dive-into-structured-software-design

CleanArchitecture : Division of Layer

 

 

Client:

  • 프론트엔드 모듈을 의미하며, 사용자 인터페이스를 통해 서버와 상호작용합니다.
  • 사용자가 입력한 데이터를 서버로 보내고, 서버로부터 받은 데이터를 화면에 표시합니다.

 

DTO (Data Transfer Object):

  • 계층 간 데이터 교환을 위한 객체입니다.
  • 주로 클라이언트에서 서버까지 데이터를 전달할 때 사용됩니다.
  • DTO는 범용으로 쓰지 않고 개별적인 용도로 써야 오류가 나지 않는다. 메소드의 필요에 따라 1:1로 만들것. (Controller DTO는 Controller에서만, Service DTO는 Service에서만)
  • Service DTO는 Entity에서 일부 데이터를 추출하여 Web Layer에서 필요로 할만한  데이터 전송에 최적화된 형태로 설계된다.
    • 반면 Request DTO는 Web의 요구에 따라서 변경이 잦으므로, Service Layer에 침투되지 않도록 분리한다. 필요시에 Service DTO로 변환하여 전달한다.

Filter

  • 인증, 권한 확인, 로깅, CORS 처리, 요청/응답 데이터 변환 등을 처리합니다.요청이나 응답을 수정하거나, 특정 조건에서 요청 처리를 중단할 수도 있습니다.

구체적인 책임:

  • 인증(Authentication): 요청 헤더에 포함된 인증 토큰(JWT 등)을 검증하고 인증된 사용자인지 확인합니다.
  • 권한 부여(Authorization): 사용자의 권한을 검사하여 특정 리소스에 접근할 수 있는지 확인합니다.
  • 로깅(Logging): 요청/응답 데이터를 기록하여 디버깅 및 추적에 도움을 줍니다.
  • CORS 처리: 다른 도메인에서의 요청을 허용하거나 차단합니다.
  • 요청/응답 변환: 요청 본문을 수정하거나 추가 헤더를 삽입하고, 응답 데이터를 변환합니다.

예외처리:

  • 필터 단계에서 발생한 예외는 주로 Spring Security의 예외 번역기(ExceptionTranslationFilter) 또는 전역 예외 처리기를 통해 처리됩니다.

Controller:

  • 클라이언트로부터 HTTP 요청을 받아들이는 역할을 합니다.
  • 요청을 적절한 서비스 계층으로 전달하고, 서비스로부터 받은 결과를 클라이언트에 반환합니다.
  • 주로 @Controller나 @RestController 애노테이션을 사용하여 정의합니다.
  • 예외처리 : 우선, Contoller별로 custon excpetion Handler로 1차 처리 후, Global Handler을 통해서 전체 예외처리

Service:

  • 비즈니스 로직을 처리하는 계층입니다.
    • 그렇다면 비즈니스 로직이란 ?
    • 상세 구현 로직은 잘 모르더라도 비즈니스의 흐름은 이해 가능한 로직이어야 한다. 신규 개발자 외에도 사업 담당자나 영업 담당자에게까지 코드를 보면서 `이런 흐름이다`라고 설명이 가능한 수준이면 가장 이상적일 것 같습니다.반대로말하면, 비즈니스로직에 상세구현로직은 들어갈필요가 없다. 그리고 비즈니스의 흐름에 대한 것은 모두 비즈니스 로직에 담겨야한다. 
    • Business logic is a set of algorithms and rules that guide how data is processed, stored and changed in a software application
  • Controller로부터 받은 DTO를 사용하여 필요한 로직을 수행하고, 이를 사용하여 Entity를 생성하거나 업데이트합니다. 로직이 다 수행되었다면 Controller에 응답을 위해서 return값을 설정하고 주로 동일한 DTO를 쓴다.
  • 주로 @Service 애노테이션을 사용하여 정의합니다.
  • 예외처리 : 비즈니스 로직은 예외처리까지 포함하는 범위이며 로직의 원자성을 위해서 excpetion은 Service 내에서만 발생하도록 하는 것이 best practice.

비즈니스레이어에는 어떤 것이 들어가야하는가 ?

https://geminikims.medium.com/%EC%A7%80%EC%86%8D-%EC%84%B1%EC%9E%A5-%EA%B0%80%EB%8A%A5%ED%95%9C-%EC%86%8C%ED%94%84%ED%8A%B8%EC%9B%A8%EC%96%B4%EB%A5%BC-%EB%A7%8C%EB%93%A4%EC%96%B4%EA%B0%80%EB%8A%94-%EB%B0%A9%EB%B2%95-97844c5dab63

 

지속 성장 가능한 소프트웨어를 만들어가는 방법

스프링은 국내에서 정말 많이 쓰이고 있습니다, 개인적으로 많은 회사를 다녀보며 주니어/시니어를 막론하고 많은 분들이 스프링에 함몰되어 개발을 하고 있다는 느낌을 받을 때가 많았고 이

geminikims.medium.com

 

 

Entity (Domain):

  • DB의 테이블과 1:1로 대응되는 객체입니다.
  • 테이블의 컬럼과 엔티티의 필드가 매핑되며, 여러 엔티티 간의 연관관계(관계형 데이터베이스의 외래키 등)를 정의합니다.
  • 주로 @Entity 애노테이션을 사용하여 정의합니다.

Repository:

  • DB와 직접 연결되어 CRUD(Create, Read, Update, Delete) 작업을 수행하는 계층입니다.
  • Entity에 접근하기 위한 메서드들을 정의하며, Spring Data JPA의 경우 JpaRepository 인터페이스를 상속받아 구현합니다.
  • 주로 @Repository 애노테이션을 사용하여 정의합니다.

DAO (Data Access Object):

  • 데이터베이스에 접근하는 객체로, 퍼시스턴스 계층이라고도 불립니다.
  • 서비스 계층이 DB와 통신할 수 있도록 도와줍니다.
  • Spring Data JPA를 사용할 경우, Repository가 DAO의 역할을 합니다.

ServiceImpl, DAOImpl:

  • 서비스와 DAO는 인터페이스로 정의되며, 실제 구현 클래스는 Impl 접미사를 붙여서 구현합니다.
  • 인터페이스를 통해 의존성을 주입받아 테스트 용이성과 코드의 유연성을 높입니다.

Scheduler :

  • Fetching the list of participations eligible for deposit return is triggered periodically, making it a natural responsibility for a scheduler.
  • The scheduler is not performing the core business logic; it is merely triggering and coordinating the execution of this logic.

 

Design Concern

  • Seperation between the database architecture(Db Interaction,Entity) from the API(User Interaction, DTO)
    • 1. Performance Concerns :
      • lazy-loaded
    • 2. Transactional Concerns :
      • where lazy loading triggers outside of transactional boundaries, causing exceptions like LazyInitializationException.
    • 3. Data Safety :
      • If entities are exposed directly, any modifications made to the returned entities could inadvertently affect the database once the transaction commits.
      • + sensitive info in Entity
    • 4. Division of work
      • API와 DB에서 요구하는 데이터가 상이할 수 있다. API의 요구에 따라서 ENtity를 수정하는 것은, DB에 영향을 주기에 불가능하고 API요구에 맞도록 DTO를 별도로 만들어주는 것이 올바른 접근이다. 혹시 API의 요구사항 변경이 생겼을때 더욱 유연하게 대응할 수 있음. 
      • API Stability vs. Database Changes: . Using DTOs allows the database model to evolve without necessarily impacting the API.
    • Entity에서 DTO로 넘겨보내기 위해서 DTO 내부구조를 아래와 같이 entity에서 정보를 뽑아서 DTO를 생성하도록 주로함.
@Getter
@Setter
@AllArgsConstructor
@NoArgsConstructor
@Builder
public class AccountDto {
    private Long userId;
    private String accountNumber;
    private Long balance;

    private LocalDateTime registeredAt;
    private LocalDateTime unregisteredAt;

    public static AccountDto fromEntity(Account account) {
        return AccountDto.builder().
                userId(account.getAccountUser().getId())
                .accountNumber(account.getAccountNumber())
                .balance(account.getBalance())
                .registeredAt(account.getRegisteredAt())
                .unregisteredAt(account.getUnregisteredAt()).
                build();