소프트웨어공학_0513_소프트웨어 아키텍처(2)

지원·2025년 5월 17일
0

소프트웨어공학

목록 보기
9/11
post-thumbnail

헥사고날 아키텍처

포트와 어댑터 아키텍처,
내부의 도메인 비지니스 로직이 외부 요소에 의존하지 않도록 설계된 아키텍처

핵심 비지니스 로직 중심으로 배치하고
외부 시스템과의 상호작용은 포트와 어댑터를 통해 이루어짐

포트
외부 영역과 내부 영역 사이의 연결을 추상화한 인터페이스
도메인 로직과 외부 세계 사이의 명확한 경계를 형성
핵심 도메인 로직을 구현하는 중심 계층으로 비지니스 규칙 담당

인바운드 포트: 내부영역 사용을 위해 노출
아웃바운드 포트: 외부영역 사용을 위해 노출

어댑터
외부 시스템과 상호작용하는 역할

인바운드: 입력 어댑터(HTTP 요청을 받는 웹 어댑터)
아웃바운드: 출력 어댑터(DB에 접근하여 데이터를 읽고 쓸 수 있는 아웃바운드 영속성 어댑터)

헥사고날 아키텍처의 구조

사용자 인터페이스

  • 웹, 모바일 앱, CLI 등 다양한 인터페이스를 통해 시스템에 접근

입력 어댑터

  • 외부 요청을 내부 도메인이 이해할 수 있는 형태로 변환

출력 어댑터

  • 내부 도메인의 요청을 외부 시스템이 이해할 수 있는 형태로 변환

핵심 도메인

  • 비지니스 로직이 구현된 시스템의 중심부

@transactional 어노테이션
모든 작업이 하나의 트랜잭션으로 처리되어야 함
과정 중 하나라도 실패하면 모든 변경사항이 롤백

=> 헥사고날 아키텍처의 핵심은 관심사 분리
비지니스 로직과 데이터 집근 로직이 명확히 분리되어 있어 용이성과 유지보수성이 향상

헥사고날 아키텍처의 입력 포트

애플리케이션 핵심 비지니스 로직에 접근하기 위한 진입점 역할을 하는 인터페이스

  • 외부 어댑터(Controller, UI)이 내부 비지니스 로직과 통신하기 위한 계약 정의
  • 특정 기술에 종속되지 않는 순수한 자바 인터페이스로 구현
  • 의존성 역전 원칙(DIP)를 적용하여 외부 시스템이 내부 비지니스 로직이 의존하게 함

헥사고날 아키텍처의 웹 어댑터

웹에서 들어오는 요청을 내부 시스템이 처리할 수 있는 형태

특징

  • 스프링 MVC와 같은 기술 관련 코드는 어댑터 안에 숨김
  • REST API 접속 지점 정의
  • 웹 요청/응답 처리만 담당
  • 업무 처리 로직에 직접 연결되지 않고 인터페이스를 통해 소통

의존성 역전 원칙을 적용해 핵심 업무 로직이 외부 기술에 의존하지 않도록 설계, 단일 책임 원칙에 따라 웹 요청 처리에 의해 한 가지 일만 담당

헥사고날 아키텍처의 출력 포트

헥사고날 아키텍처에서 애플리케이션 코어가 외부 시스템과 통신하기 위한 인터페이스를 정의

애플리케이션 코어에 정의되며,
실제 구현에는 JPA, MongoDB 등 다양한 기술을 사용할 수 있음

이를 통해 영속성 기술 변경 시 핵심 비즈니스 로직을 수정할 필요가 없어짐

헥사고날 아키텍처의 출력 어댑터
Spring Data JPA를 통해 데이터베이스와 상호작용 함

기술적 세부사항 은닉
JPA 관련 코드를 캡술화하여 도메인 계층이 데이터베이스 기술에 직접 의존하지 않도록 함

도메인/인프라 모델 반환
데이터베이스 엔터티와 도메인 객체 간 매핑 처리

트랜젝션 관리
데이터베이스 트랜젝션을 처리

특징

  • 도메인 객체와 JPA엔티티 간 변환을 통해 계층 간 명확한 경계 유지
  • 기본 CRUD 작업(생성, 조회, 삭제)을 구현
  • 데이터베이스 기술이 변경되어도 도메인 로직은 유지, 어댑터 계층만 교체하면 됨
  • 테스트 용이성과 시스템 유연성을 향상 시킴

헥사고날 아키텍처의 장점

  • 핵심 비즈니스 로직 독립성
  • 독립적인 테스트 용이성
  • 높은 확장성과 유지보수성
  • 의존성 역전 원칙(DIP)적용
    의존성이 도메인을 향하도록 강제됨 이는 견고하고 유연한 애플리케이션 구조를 만드는데 기여

헥사고날 아키텍처의 단점

  • 구조적 복잡성
  • 학습 곡선
  • 표준화 부족
    구현체마다 어댑터나 포트의 구조가 달라서 설계 일관성이 떨어질 수 있음

헥사고날 아키텍처를 이용해서 사용자 인증 시스템

조건

  • 사용자가 이메일과 비밀번호로 로그인

  • 인증 후 토큰 발급

  • 다양한 프론트엔드에서 인증 로직 사용

  • 향후 인증 채널이 추가될 수 있음

  • 포트 인터페이스를 상속받아 클래스 구현
    -- InMemoryUserRepository.java(Map<String, User>을 db로)
    --- 이메일, 비밀번호 사용자 추가
    -- FakePasswordEncoder.java(문자열 접두사 비교)
    --- 입력으로 들어오는 비밀번호에 'hashed' 비교
    -- SimpleTokenGenerator.java(문자열 토큰 생성기)
    --- 정상적으로 로그인 완료되었을 때 토큰 반환
    --- 토큰은 'TOKENFOR' + 메일주소 형식으로 리턴
    -- AuthController.java 구현
    --- 멤버변수로 AuthService authService 가지고 있음
    -- 구현 후 메인에서 로그인 성공/실패 테스트 수향

도메인 모델 User.java

package org.example;

//도메인 모델 User.java
//User는 PasswordEncoder라는 추상화에만 의존
//PasswordEncoder는 어댑터(adapter)가 구현하게 될 포트(port) 역할
//도메인은 외부로부터 독립적

public class User {
    //final은 생성 이후 값이 바뀌지 않음
    private final String email;
    private final String passwordHash;

    public User(String email, String passwordHash) {
        this.email = email;
        this.passwordHash = passwordHash;
    }

    //비밀번호 확인 로직
    //User 클래스가 해시 방법을 직접 몰라도 되도록 외부 인터페이스(encoder)에 위임했다는 점
    public boolean checkPassword(String rawPassword, PasswordEncoder encoder) {
        return encoder.matches(rawPassword, passwordHash);
    }

    public String getEmail() {
        return email;
    }
}

도메인이 외부에 의존하지 않도록 돕는 추상화 포트
포트: UserRepository/PasswordEncoder/TokenGenerator

UserRepository.java

package org.example;

public interface UserRepository {
    User findByEmail(String email);
}

PasswordEncoder.java

package org.example;

public interface PasswordEncoder {
    boolean matches(String rawPassword, String encodedPassword);
}

TokenGenerator.java

package org.example;

public interface TokenGenerator {
    String generateToken(String subject);
}

서비스 클래스(스켈레톤 코드)

package org.example;

public class AuthService {
    private final UserRepository userRepo;
    private final PasswordEncoder encoder;
    private final TokenGenerator tokenGen;

    public AuthService(UserRepository userRepo, PasswordEncoder encoder, TokenGenerator tokenGen) {
        this.userRepo = userRepo;
        this.encoder = encoder;
        this.tokenGen = tokenGen;
    }

    public String login(String email, String password) {
        // TODO: 다음 요구사항을 만족하도록 구현하세요.
        // 1. 이메일로 사용자를 조회한다. 존재하지 않으면 예외를 던진다.
        // 2. 비밀번호가 일치하는지 확인한다. 불일치하면 예외를 던진다.
        // 3. 일치할 경우 토큰을 발급하여 반환한다.

        User user = userRepo.findByEmail(email);
        if (user == null) {
            throw new RuntimeException("사용자 없음");
        }

        if (!user.checkPassword(password, encoder)) {
            throw new RuntimeException("비밀번호 불일치");
        }

        return tokenGen.generateToken(user.getEmail());
    }
}

Main.java

package org.example;

//TIP 코드를 <b>실행</b>하려면 <shortcut actionId="Run"/>을(를) 누르거나
// 에디터 여백에 있는 <icon src="AllIcons.Actions.Execute"/> 아이콘을 클릭하세요.
public class Main {
    public static void main(String[] args) {
// 학습자가 직접 구현한 어댑터 인스턴스 사용
        UserRepository repo = new InMemoryUserRepository();
        PasswordEncoder encoder = new FakePasswordEncoder();
        TokenGenerator tokenGen = new SimpleTokenGenerator();

        AuthService service = new AuthService(repo, encoder, tokenGen);
        AuthController controller = new AuthController(service);

        controller.simulateLogin("test@example.com", "123");   // 성공
        controller.simulateLogin("test@example.com", "wrong"); // 실패
        controller.simulateLogin("nobody@example.com", "123"); // 실패

    }
}

InMemoryUserRepository.java

package org.example;

import java.util.HashMap;
import java.util.Map;
import java.util.Optional;

public class InMemoryUserRepository implements UserRepository{
    private final Map<String, User> db = new HashMap<>();
    @Override
    public User findByEmail(String email) {

        return db.get(email);
    }

    public InMemoryUserRepository() {
        db.put("test@example.com", new User("test@example.com", "hashed123"));
    }
}

FakePasswordEncoder.java

package org.example;

public class FakePasswordEncoder implements PasswordEncoder{
    @Override
    public boolean matches(String rawPassword, String encodedPassword) {

        return encodedPassword.equals("hashed" + rawPassword);
    }
}

SimpleTokenGenerator.java

package org.example;

public class SimpleTokenGenerator implements TokenGenerator{
    @Override
    public String generateToken(String subject) {
        return "TOKEN_FOR_" + subject;
    }
}

AuthController.java

package org.example;

public class AuthController {
    private final AuthService authService;

    public AuthController(AuthService authService){
        this.authService=authService;
    }

    public void simulateLogin(String email, String password){
        try{
            String token = authService.login(email, password);
            System.out.println("로그인 성공!" + token);
        }catch(RuntimeException e){
            System.out.println("로그인 실패 "+e.getMessage());
        }
    }
}

Main.java

package org.example;

//TIP 코드를 <b>실행</b>하려면 <shortcut actionId="Run"/>을(를) 누르거나
// 에디터 여백에 있는 <icon src="AllIcons.Actions.Execute"/> 아이콘을 클릭하세요.
public class Main {
    public static void main(String[] args) {
// 학습자가 직접 구현한 어댑터 인스턴스 사용
        UserRepository repo = new InMemoryUserRepository();
        PasswordEncoder encoder = new FakePasswordEncoder();
        TokenGenerator tokenGen = new SimpleTokenGenerator();

        AuthService service = new AuthService(repo, encoder, tokenGen);
        AuthController controller = new AuthController(service);

        controller.simulateLogin("test@example.com", "123");   // 성공
        controller.simulateLogin("test@example.com", "wrong"); // 실패
        controller.simulateLogin("nobody@example.com", "123"); // 실패

    }
}

클라이언트 서버 스타일

가장 기본적인 분산 시스템 구조

서비스를 제공하는 서버와
서비스를 요청하는 하나 이상의 클라이언트로 구성

Web Layer (Controller)
사용자의 요청을 받아 처리하는 계층
HTTP 요청을 처리하고 적절한 응답을 반환

Application Logic (Service Domain)
비즈니스 로직을 처리하는 계층

Data Access Layer (Repository DB 연결)
데이터베이스와의 상호작용을 담당하는 계층
데이터 저장, 조회, 수정, 삭제 등의 기능을 제공

클라이언트

클라이언트는 사용자 인터페이스에서 필요한 정보를 서버에 요청하고, 서버로부터 받은 데이터를 사용자에게 표시할 수 있음

서버

클라이언트의 요청을 받아 처리하고, 적절한 응답을 반환

DB서버

SQL 쿼리를 통해 계좌 정보를 관리

클라이언트 서버 스타일 장점

명확한 역할 분리
시스템 구조가 명확하고 이해하기 쉬워
자신의 책임에만 집중할 수 있어 개발과 유지보수가 용이

중앙집중식 데이터 관리
서버에 데이터를 중앙 집중형으로 관리하여 백업, 보안, 권한 관리가 용이
모든 데이터가 한 곳에서 관리되므로 데이터 일관성과 정확성을 유지하기 쉬움

확장성
클라이언트 수가 증가하더라도 대응이 가능
시스템의 성능과 용량을 필요에 따라 조절할 수 있게 해줌

보안 통제 집중화
인증, 접근 제어 등 보안 정책을 서버에 집중시킬 수 있어 효율적인 보안 관리가 가능
표준화된 통신 프로토콜을 사용함으로써 상호운용성이 보장

클라이언트 서버 스타일 단점

서버 의존성
서버가 다운되면 전체 시스템이 마비되므로 서버에 대한 의존성이 매우 높음

성능 병목
트래픽 급증 시 서버 확장이 안되면 병목 현상이 초래됨
많은 클라이언트가 동시에 요청할 경우 서버의 처리 능력을 초과하여
응답 시간이 같아지거나 서비스가 중단될 수 있음

네트워크 의존성
클라인언트와 서버가 통신하므로 네트워크 연결에 의존성이 높음

인프라 비용
서버 구축, 운영, 보안, 백업 등 인프라 편리 비용이 필요하고
추가 개발 요구됨, 초기 구축 비용과 유지보수 비용을 증가시킴

브로커 스타일

여러 노드에 투명하게 소프트웨어 시스템을 분산하는 방식
중간에 브로커가 위치하여 클라이언트와 서버 간의 통신을 중계한다는 점

구성요소

클라이언트: 서비스를 요청하는 주체

서버: 요청받은 서비스를 제공하는 주체

브로커: 이 둘 사이에서 클라이언트의 요청을 적절한 서버로 라우팅하고
서버의 응답을 다시 클라이언트에게 전달하는 중개자 역할
서비스 등록, 서비스 발견, 메시지 라우팅, 요청/응답 관리 기능을 제공하여
분산 시스템의 복잡성을 감추고 단순화된 인터페이스를 제공

분산 시스템에서 컴포넌트 간의 통신을 단순화하고 위치 투명성을 제공하여 시스템의 확장성과 유연성을 높이는데 기여

브로커 스타일 장점

  • 위치 투명성
    -- 클라이언트는 서비스의 실제 위치(IP나 포트)를 알 필요없이 이름만으로
    서비스를 접근할 수 있음, 서비스 제공자의 물리적 위치가 변경되더라도
    클라이언트 코드를 수정할 필요가 없게 함

  • 낮은 결합도
    -- 컴포넌트 간의 직접적 참조 없이 브로커를 통해 통신하므로 결합도가 낮아짐
    시스템 유연성을 높이고, 한 컴포넌트의 변경이 다른 컴포넌트에 미치는 영향을 최소화 함

  • 이기종 시스템 간 통신
    -- 서로 다른 언어, 플랫폼, 시스템 간의 통신이 가능해짐
    -- 브로커는 다양한 시스템 간의 메시지 변환과 프로토콜 통합을 담당하여
    이기종 환경에서의 상호운용성을 제공

  • 모듈성 및 재사용성
    -- 컴포넌트가 독립적으로 설계, 모듈성 및 재사용성이 증가
    런타임 중에 컴포넌트를 브로커에 등록하거나 제거할 수 있어 유연한 시스템 확장 가능

브로커 스타일 단점

  • 단일 장애점 위험
    -- 중앙 브로커에 장애가 발생하면 전체 통신이 중단될 위험이 있음

  • 성능 오버헤드
    -- 모든 요청이 브로커를 가지므로 통신 지연이나 처리 병목 현상이 발생할 수 있음

  • 구현 복잡성
    -- 서비스 등록, 요청 라우팅, 객체 직렬화/역직렬화 등 브로커 내부의 구현이 복잡
    브로커를 통해 간접적으로 통신하므로 디버깅과 추적이 어려워질 수 있음

  • 높은 의존성
    -- 시스템 구조가 브로커에 크게 의존하게 되어 브로커 대체나 마이그레이션이 어려울 수 있음, 특정 브로커 기술에 종속되면 기술 변경 시 시스템 전체를 재설계해야 할 수도 있음

브로커 스타일 이용한 이미지 처리 스타일

조건

  • 클라이언트는 이미지를 업로드
  • 브로커는 처리 요청을 중개
  • 워커는 이미지를 리사이징, 압축 후 반환
  • ResizeImage.java(크기 조정)
  • CompressImage.java(압축)
  • ServiceBroker.register()
  • ServiceBroker.dispatch()
  • Main

구현 요구사항

  • ImageService 인터페이스를 구현한 크기조저ㅇ, 압축 클래스
  • ServiceBroker, Client의 스켈레톤 부분 채우기
    -- Client.run()에서는 resize, compress 요청 후 결과 출력
  • Main 클래스에서 서비스를 등록하고 클라이언트를 실행

공통 인터페이스: ImageService.java

package org.example;

public interface ImageService {
    String process(String image);
}

ResizeImage.java

package org.example;

public class ResizeImage implements ImageService{
    @Override
    public String process(String image) {
        return "[Resized]" + image;
    }
}

CompressImage.java

package org.example;

public class CompressImage implements ImageService{
    @Override
    public String process(String image) {
        return "[Compressed]"+image;
    }
}

브로커 클래스(스켈레톤) ServiceBroker.java

package org.example;

import java.util.HashMap;
import java.util.Map;

public class ServiceBroker {
    private final Map<String, ImageService> registry = new HashMap<>();

    // 서비스를 등록하는 메서드
    public void register(String command, ImageService service) {
        // TODO: 명령어와 서비스를 registry에 등록하세요
        registry.put(command, service);
    }

    // 명령어로 해당 서비스에 요청 위임
    public String dispatch(String command, String imagePath) {
        // TODO: 명령어에 해당하는 서비스를 찾아 실행 결과를 반환하세요
        return registry.get(command).process(imagePath);

    }
}

클라이언트 클래스 (스켈레톤) Client.java

package org.example;

public class Client {
    private final ServiceBroker broker;

    public Client(ServiceBroker broker) {
        this.broker = broker;
    }

    public void run() {
        // TODO: 아래 명령어 요청을 broker에 전달하고 결과를 출력하세요.
        System.out.println(broker.dispatch("resize", "dog.png"));
        System.out.println(broker.dispatch("compress", "cat.png"));
    }
}

Main.java

package org.example;

public class Main {
    public static void main(String[] args) {
        ServiceBroker broker = new ServiceBroker();
        broker.register("resize", new ResizeImage());
        broker.register("compress", new CompressImage());

        Client client = new Client(broker);
        client.run();
    }
}

트랜잭션 처리 스타일

연달아 들어오는 입력을 하나씩 읽어서 처리하는 방식의 아키텍처
각 입력은 트랜잭션을 명시하며,
이는 시스템에 저장되어 있는 데이터를 조작하는 명령들로 구성

핵심요소는 트랜잭션 사령(dispatcher)컴포넌트
: 트랜잭션을 어디서 처리할 것인지를 결정하고 적절한 컴포넌트에 배치하는 역할

특징
사령 컴포넌트가 프로시저 호출이나 메시지를 통해 트랜잭션을 처리할 컴포넌트에 전달
데이터의 일관성과 무결성을 유지하면서 복잡한 업무 처리를 가능하게 함

금융시스템, 예약 시스템, 재고 관리 시스템 등 데이터의 정확성과 일관성이 중요한 영역에서 널리 사용됨
@Transactional

트랜잭션 처리 스타일 장점

  • 데이터 일관성 보장
    -- 여러 작업이 한 개의 단위로 처리, 중간 상태 없이 일관된 데이터 유지가 가능
    -- 데이터베이스의 ACID(원자성, 일관성, 고립성, 지속성) 보장

  • 오류 복구 용이
    -- 오류가 발생했을 때, 전체 작업을 쉽게 원상복구(롤백)할 수 있음
    -- 견고성을 높이고 데이터 손상 위험을 줄여줌

  • 무결성 유지
    -- 데이터들이 논리적으로 함께 처리되므로 제약조건, 비즈니스 규칙 위반을 방지할 수 있음
    -- 애플리케이션의 비즈니스 로직이 항상 유효한 상태를 유지하도록 보장

  • 명확한 경계와 재사용성
    -- 트랜잭션 경계가 명확하여 코드 재사용과 유지보수가 용이
    -- 또한 동시성 문제 해결 기반을 제공하여 트랜잭션 간 충돌이나 경쟁 조건을 방지할 수 있음

트랜잭션 처리 스타일 단점

  • 성능 저하 가능성
    -- 트랜잭션 지속 시간 동안 자원을 점유하므로 성능 저하나 병목 유발 가능성이 높이

  • 분산 시스템 복잡성
    -- 여러 시스템에 걸친 트랜잭션은 구현이 복잡하고 신뢰성 유지가 어려움

  • 경계 설정의 어려움
    -- 업무 로직이 복잡한 경우, 어디서 트랜잭션을 시작하고 끝낼지 경계 정의가 어려울 수 있음

  • 롱 트랜잭션 위험
    -- 트랜잭션이 길어지면 락 점유 시간 증가로 교착 상태나 성능 저하가 유발
    트랜잭션은 가능한 짧게 유지하는 것이 좋지만, 비즈니스 요구사항과 충돌할 수 있음

    트랜잭션 스타일 이용한 은행 계좌 이체

  • AccountRepository.java : JpaRepository<Account, Long>상속

  • BankTransaction.transfer() : 출금 계좌에서 인출 -> 입금 계좌에 입금 예외 발생 시 전체 롤백

  • AppRunner.run() : 정상 이체/실패 이체 각각 테스트 코드 작성

    계좌 클래스: Account.java

package com.example.banking;
>
import javax.persistence.Entity;
import javax.persistence.GeneratedValue;
import javax.persistence.Id;
>
@Entity
public class Account {
>
    @Id
    @GeneratedValue
    private Long id;
>
    private String owner;
    private int balance;
>
    protected Account() {} // JPA 기본 생성자
>
    public Account(String owner, int balance) {
        this.owner = owner;
        this.balance = balance;
    }
>
    public void withdraw(int amount) {
        if (balance < amount) {
            throw new IllegalStateException("잔액 부족");
        }
        balance -= amount;
    }

    public void deposit(int amount) {
        balance += amount;
    }
>
    public String getOwner() {
        return owner;
    }
>
    public int getBalance() {
        return balance;
    }
>
    public Long getId() {
        return id;
    }
}

AccountRepository.java

package com.example.banking;

import org.springframework.data.jpa.repository.JpaRepository;

// TODO: JPA Repository로 구현하세요
public interface AccountRepository extends JpaRepository<Account, Long> {
}

이체 로직 클래스: BankTransaction.java

package com.example.banking;

import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;

@Service
public class BankTransaction {

    private final AccountRepository repository;

    public BankTransaction(AccountRepository repository) {
        this.repository = repository;
    }

    @Transactional
    public void transfer(Long fromId, Long toId, int amount) {
        // TODO:
        // 1. 출금 계좌와 입금 계좌를 ID로 조회
        // 2. 출금 → 입금 처리
        // 3. 예외 발생 시 전체 트랜잭션이 롤백되도록 구현
        Account from = repository.findById(fromId)
                .orElseThrow(()->new RuntimeException("보내는 계좌 없음"));
        Account to = repository.findById(toId)
                .orElseThrow(()-> new RuntimeException("받는 계좌 없음"));
        from.withdraw(amount);

        if(true){
            throw new RuntimeException("예외 발생으로 롤백되는지 확인");
        }

        to.deposit(amount);
    }
}

AppRunner.java (스켈레톤)

package com.example.banking;

import org.springframework.boot.CommandLineRunner;
import org.springframework.stereotype.Component;

@Component
public class AppRunner implements CommandLineRunner {

    private final AccountRepository repository;
    private final BankTransaction service;

    public AppRunner(AccountRepository repository, BankTransaction service) {
        this.repository = repository;
        this.service = service;
    }

    @Override
    public void run(String... args) throws Exception{
        Account a = repository.save(new Account("철수", 10000));
        Account b = repository.save(new Account("영희", 5000));
        try{
            service.transfer(a.getId(), b.getId(), 3000);
            System.out.println("이체 성공!");
        }catch(Exception e){
            System.out.println("이체 실패: "+e.getMessage());
        }
        Account updatedA = repository.findById(a.getId())
                .orElseThrow(()->new RuntimeException("A 계좌 없음"));
        Account updatedB = repository.findById(b.getId())
                .orElseThrow(()-> new RuntimeException("B 계좌 없음"));
        System.out.println(updatedA.getOwner() + " 잔액: "+updatedA.getBalance());
        System.out.println(updatedB.getOwner() + " 잔액: "+updatedB.getBalance());
        // TODO: service.transfer(...) 호출하여 이체 수행 및 예외 테스트
    }
}

BankingApplication.java(메인 클래스)

package com.example.banking;

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;

@SpringBootApplication
public class BankingApplication {

	public static void main(String[] args) {
		SpringApplication.run(BankingApplication.class, args);
	}
}

파이프필터 스타일

시스템을 구성하는 각 컴포넌트는 필터라고 부르고 필터 간의 연결관계를 파이프라고 부름
데이터 스트림이 여러 처리 단계(필터)를 순차적으로 통과하며 변환되는 구조

특징
매우 유연한 구조를 가짐
거의 모든 컴포넌트가 필요에 따라 삭제, 대체, 추가될 수 있으며
일부 컴포넌트는 순서를 재배치할 수 있음

각 필터는 독립적으로 동작하며 특정 작업만을 수행하므로,
시스템 전체의 복잡성을 줄이고 유지보수성을 향상시킴

단순한 인터페이스를 통해 다양한 필터를 일관된 방식으로 구현하고 조합할 수 있음
각 필터는 이 인터페이스를 구현함으로써 파이프라인에 쉽게 추가되거나 제거될 수 있음
서로 다른 필터들이 동일한 인터페이스를 통해 상호작용할 수 있게 됨

새로운 필터가 필요할 경우, 기존 코드를 수정하지 않고 이 인터페이스를 구현하는
새로운 클래스를 추가하기만 하면 됨

파이프필터 아키텍처의 고려사항

  • 필터 독립성
    필터들은 서로 상태정보를 공유하지 않아야 함

  • 필터 간 무지성
    필터는 자신에 앞서 수행하는 필터나
    자신의 출력 결과를 받는 필터에 대한 정보를 알지 못함

  • 병렬 처리와 동기화
    필터는 병렬적으로 수행되며 파이프를 통해 동기화가 이루어짐
    시스템의 처리량을 높일 수 있음
    동시에 데이터 흐름의 일관성을 유지하기 위한 동기화 매커니즘이 필요함

파이프필터 스타일 장점

  • 재사용성
  • 병렬 처리
    대용량 데이터 처리가 필요한 시스템에서 성능 향상에 기여
  • 확장성
    새로운 필터 추가가 용이하여 시스템의 확장성이 뛰어남

파이프필터 스타일 단점

  • 상태 관리의 어려움
    상태를 저장하지 않는(stateless) 스타일
    각 필터는 입력을 받아 출력을 생성할 뿐, 과거 처리 내역이나 컨텍스트 정보를 유지하지 않음

  • 유연성 제한
    처리 흐름이 고정되어 있어 동적으로 처리 경로를 변경하기 어려움

  • 복잡한 제어 흐름 부적합
    분기, 반복, 조건부 실행과 같은 복잡한 제어 흐름을 표현하기 어려움
    단순한 변환 작업의 순차적 적용에 최적화되어 있음

아키텍처 문서화의 중요성

  • 설계품질 향상
  • 이해관계자 간 소통

아키텍처 문서 구조

  • 목적
  • 우선순위
  • 설계 개요
  • 주요 설계 이슈
  • 설계 상세 사항

아키텍처 평가의 목적과 방법

아키텍처 평가
소프트웨어 아키텍처나 디자인 패턴이 속성, 강점 및 약점을 체계적으로 분석하는 과정
개발자가 선택한 아키텍처가 기능적 및 비기능적 품질 요구상항을 모두 충족시킬 수 있는지 검증할 수 있음

주요 평가 방법

  • SAAM
    시나리오 기반 평가 방법
    아키텍처가 특정 시나리오를 얼마나 잘 지원하는지 분석
    변경 용이성, 유지보수성 등을 평가하는데 효과적

시나리오 도출
시나리오 분류
아키텍처 평가
결과 분석 및 개선

  • ATAM
    여러 품질 속성에 초점을 맞춰 아키텍처를 평가하고
    다양한 속성 간의 트레이드오프를 분석
    성능, 보안, 가용성, 수정 용이성 등 여러 품질요소를 종합적으로 고려

위험 포인트 식별
설계 결정이 품질 요구사항을 충족시키지 못할 가능성이 있는 위험 포인트를 식별
여러 품질 속성에 영향을 미치는 민감한 지점과 트레이드오프 포인트를 분석

MVC 스타일 실습

Book.java

package org.example;

public class Book {
    private final String title;
    private final String author;

    public Book(String title, String author) {
        this.title = title;
        this.author = author;
    }

    public String getInfo() {
        return "\"" + title + "\" by " + author;
    }

    public boolean matches(String keyword) {
        return title.toLowerCase().contains(keyword.toLowerCase());
    }
}

Model.java

package org.example;

import java.util.*;
import java.util.stream.Collectors;

public class Model {
    private final List<Book> books = new ArrayList<>();

    public Model() {
        books.add(new Book("Effective Java", "Joshua Bloch"));
        books.add(new Book("Clean Code", "Robert C. Martin"));
        books.add(new Book("Java Concurrency in Practice", "Brian Goetz"));
    }

    public List<Book> search(String keyword) {
        return books.stream()
                .filter(book -> book.matches(keyword))
                .collect(Collectors.toList());
    }
}

View.java

package org.example;
import java.util.List;
import java.util.Scanner;
public class View {
    private final Scanner scanner = new Scanner(System.in);

    public String getSearchKeyword(){
        System.out.println("검색할 키워드 입력");
        return scanner.nextLine();
    }

    public void displayResults(List<Book> books){
        if (books.isEmpty()){
            System.out.println("해당 도서를 찾을 수 없음");
        }else{
            System.out.println("검색 결과");
            for (Book book : books) System.out.println(" - " + book.getInfo());
        }
    }
}

Controller.java

package org.example;

import java.util.List;

public class Controller {
    private Model model;
    private View view;

    public Controller(Model model, View view){
        this.model=model;
        this.view=view;
    }

    public void run(){
        String result = view.getSearchKeyword();
        List<Book> results = model.search(result);
        view.displayResults(results);
    }
}

Main.java

package org.example;

public class Main {
    public static void main(String[] args) {
        Model model = new Model();
        View view = new View();
        Controller controller = new Controller(model, view);
        controller.run();
    }
}

EDA 스타일 실습

이벤트 기반 스타일
SensorEvent.java

package org.example;

public class SensorEvent {
    public final double temperature;

    public SensorEvent(double temperature){
        this.temperature=temperature;
    }
}

EventBus.java

package org.example;

import java.util.ArrayList;
import java.util.List;

public class EventBus {
    public interface EventListener{
        void handle(SensorEvent event);
    }

    private final List<EventListener> listeners = new ArrayList<>();

    public void register(EventListener listener){
        listeners.add(listener);
    }

    public void publish(SensorEvent event){
        for (EventListener listener : listeners) listener.handle(event);
    }
}

AlarmListener.java

package org.example;

public class AlarmListener implements EventBus.EventListener{
    @Override
    public void handle(SensorEvent event) {
        if (event.temperature>30){
            System.out.println("온도 초과! "+event.temperature+"도");
        }
    }
}

LoggerListener.java

package org.example;

public class LoggerListener implements EventBus.EventListener{
    @Override
    public void handle(SensorEvent event) {
        System.out.println("로그 기록: 현재온도 "+event.temperature+"도");
    }
}

Main.java

package org.example;

public class Main {
    public static void main(String[] args) {
        AlarmListener alarm = new AlarmListener();
        LoggerListener logger = new LoggerListener();
        EventBus bus = new EventBus();
        bus.register(alarm);
        bus.register(logger);

        double[] temperatures={25.3, 29.8, 30.1, 32.5, 27.4};
        for (double temp: temperatures) bus.publish(new SensorEvent(temp));
    }
}

0개의 댓글