테마 선택

📚 지식 노트

AI·하드웨어·주변기기·게임·컴퓨터 시스템·이론 컴퓨터 과학·수학·개발과 운영·보안·응용 분야에 대해 알아 두면 좋은 내용을 정리한 노트다. 위에서 분야를 고르고 아래에서 노트를 고른다.

GoF 너머: 의존성 주입·엔터프라이즈·동시성 패턴

GoF 이후 널리 쓰이게 된 패턴과, 언어와 프레임워크가 패턴을 흡수한 모습이다.

목차 (16)
  1. 의존성 주입과 제어의 역전
  2. DI 컨테이너: 객체 그래프를 대신 조립한다
  3. 서비스 로케이터와 무엇이 다른가
  4. 리포지터리와 유닛 오브 워크
  5. 액티브 레코드, 데이터 매퍼, DTO
  6. 표현 패턴: MVC, MVP, MVVM
  7. 단방향 데이터 흐름: Flux
  8. 널 객체와 명세
  9. 생산자-소비자: 블로킹 큐로 속도 차이를 흡수한다
  10. 스레드 풀, 퓨처와 프로미스
  11. 리액터: 이벤트 루프 하나로 많은 연결을
  12. 함수형 언어에서의 패턴: 언어가 흡수한 것들
  13. 시스템 단위 패턴은 시스템 설계 노트로
  14. 패턴 고르기 정리
  15. 흔한 오해
  16. 핵심 자료

의존성 주입과 제어의 역전

GoF 책이 나온 뒤 가장 널리 퍼진 패턴을 하나만 고르라면 의존성 주입(DI, Dependency Injection)이다. 생각은 간단하다. 객체가 함께 일할 객체를 스스로 new로 만들지 않고, 바깥에서 받는다. 받는 쪽은 인터페이스만 알고, 무엇을 꽂을지는 조립하는 곳이 정한다. 상위 정책이 구체적인 구현이 아니라 추상에 기대라는 SOLID의 의존성 역전 원칙을 코드로 옮기는 가장 흔한 방법이다.

왼쪽은 OrderService가 MailSender를 new로 직접 만들어 구체 클래스에 묶인 모습이고, 오른쪽은 OrderService가 Notifier 인터페이스만 생성자로 받고 MailSender와 SmsSender가 그 인터페이스를 구현하는 모습이다. 아래에 그림 기호 범례가 있다직접 만들기: 구체 클래스에 묶임주입: 인터페이스만 안다OrderService- sender = new MailSender()MailSendernew로 직접 생성메일을 문자로 바꾸려면OrderService를 고쳐야 하고,테스트에서 가짜를 꽂을 수 없다OrderService+ OrderService(Notifier n)«interface»NotifierMailSenderSmsSender무엇을 꽂을지는 바깥이 정한다그림 기호(이 노트 전체)상속구현(인터페이스)보유(합성)의존(사용, 생성)
왼쪽은 OrderService가 MailSender를 직접 만들어 그 클래스에 묶여 있다. 오른쪽은 Notifier 인터페이스만 받고, 메일이든 문자든 바깥에서 꽂는다. 아래 그림 기호는 이 노트의 모든 클래스 그림에 같이 쓴다.
// 주입 전: 서비스가 협력 객체를 직접 만든다
public class OrderService {
    private final MailSender sender = new MailSender();

    public void place(Order order) {
        // ... 주문 저장
        sender.send(order.customerEmail(), "주문이 접수되었습니다");
    }
}
// 주입 후: 인터페이스만 알고, 구현은 생성자로 받는다
public interface Notifier {
    void notify(String to, String message);
}

public class OrderService {
    private final Notifier notifier;

    public OrderService(Notifier notifier) {
        this.notifier = notifier;
    }

    public void place(Order order) {
        // ... 주문 저장
        notifier.notify(order.customerEmail(), "주문이 접수되었습니다");
    }
}

// 조립은 맨 바깥(main) 한 곳에서
OrderService service = new OrderService(new MailSender());
// 테스트에서는 가짜를 꽂는다
OrderService underTest = new OrderService((to, message) -> sent.add(to));

이름은 마틴 파울러의 2004년 글에서 굳었다. 당시 이런 경량 컨테이너들은 스스로를 "제어의 역전"(IoC, Inversion of Control) 컨테이너라고 불렀는데, 파울러는 IoC가 프레임워크 대부분에 해당하는 너무 넓은 말이라 헷갈린다고 보고, 여러 사람과 의논해 더 좁은 이름인 의존성 주입을 붙였다. 같은 글은 주입 방식을 셋으로 나눈다.

  • 생성자 주입: 생성자 인자로 받는다. 만들어진 순간 완전한 객체가 되고 필드를 final로 둘 수 있다. 파울러도 이것으로 시작하라고 권한다.
  • 세터 주입: 만든 뒤 세터로 넣는다. 선택적인 의존성이나 생성자 인자가 너무 많을 때 쓰지만, 넣기 전의 어중간한 상태가 생긴다.
  • 인터페이스 주입: 받는 쪽이 "이걸 주입받겠다"는 인터페이스를 구현한다. 지금은 거의 쓰지 않는다.

가장 큰 이득은 테스트다. 위 코드의 마지막 줄처럼 진짜 메일 대신 보낸 주소를 모으는 람다를 꽂으면, 메일 서버 없이 "주문하면 알림이 간다"를 검사할 수 있다. 생성자 인자가 대여섯 개를 넘으면 주입이 문제가 아니라 클래스가 너무 많은 일을 한다는 신호다.

다른 언어에서는: Go는 컨테이너 없이 생성 함수(NewOrderService(n Notifier))에 인터페이스를 넘기는 손 조립이 기본이다. Python과 JavaScript는 함수나 객체를 그냥 인자로 넘기면 되고, 테스트에서는 모듈을 바꿔치기하는 도구(unittest.mock.patch, Jest의 jest.mock)를 함께 쓴다. 어느 언어든 "만드는 곳과 쓰는 곳을 나눈다"는 뼈대는 같다.

DI 컨테이너: 객체 그래프를 대신 조립한다

객체가 수백 개가 되면 main에서 손으로 조립하기 힘들다. DI 컨테이너는 "무엇이 무엇을 받는가"라는 설정을 읽고, 요청받은 객체를 만들기 위해 의존성을 거슬러 내려가 먼저 만든 뒤 넣어 준다. 스프링 문서는 객체가 의존성을 생성자 인자, 팩터리 메서드 인자, 속성으로만 밝히고 컨테이너가 빈을 만들 때 그것을 넣는 것을 DI라 하고, 객체가 스스로 클래스를 만들거나 서비스 로케이터로 찾는 것의 반대라서 "제어의 역전"이라고 설명한다.

DI 컨테이너의 생성 순서 단계 그림: OrderService는 OrderRepository와 PaymentGateway를, 각각은 DataSource와 HttpClient를 받는다. 컨테이너는 DataSource, OrderRepository, HttpClient, PaymentGateway, OrderService 순서로 만든다OrderService5번째OrderRepository2번째PaymentGateway4번째DataSource1번째HttpClient3번째만든 순서(아직 없음)만든 순서(아직 없음)만든 순서(아직 없음)만든 순서1 DataSource만든 순서1 DataSource, 2 OrderRepository만든 순서1 DataSource, 2 OrderRepository만든 순서1 DataSource, 2 OrderRepository만든 순서1 DataSource, 2 OrderRepository, 3 HttpClient만든 순서1 DataSource, 2 OrderRepository, 3 HttpClient4 PaymentGateway만든 순서1 DataSource, 2 OrderRepository, 3 HttpClient4 PaymentGateway, 5 OrderService
1 / 10

필요: OrderService. 생성자 인자 OrderRepository, PaymentGateway부터 찾아 내려간다.

컨테이너에 OrderService를 달라고 하면, 생성자 인자를 따라 내려가 잎(DataSource, HttpClient)부터 만들고 올라온다. 테두리는 지금 찾는 객체, 채움은 이미 만든 객체다. 순서는 계산 함수로 구했다.
@Configuration
public class AppConfig {
    @Bean
    DataSource dataSource() {
        return new HikariDataSource();
    }

    @Bean
    OrderRepository orderRepository(DataSource dataSource) {
        return new JdbcOrderRepository(dataSource);
    }

    @Bean
    OrderService orderService(OrderRepository repository, PaymentGateway gateway) {
        return new OrderService(repository, gateway);
    }
}

// 컨테이너가 인자를 보고 의존성부터 만들어 넣는다
ApplicationContext context = new AnnotationConfigApplicationContext(AppConfig.class);
OrderService service = context.getBean(OrderService.class);
  • 빈과 컨테이너: 스프링에서 컨테이너가 만들고 조립하고 관리하는 객체를 빈(bean)이라 한다. BeanFactory가 기본 기능을, 그 하위 인터페이스인 ApplicationContext가 이벤트 발행, 국제화 메시지, AOP 연동 같은 기능을 더한다.
  • 순환 의존: A가 B를, B가 다시 A를 생성자로 받으면 어느 쪽도 먼저 만들 수 없다. 위 그림의 계산 함수도 이 경우 오류를 낸다. 컨테이너가 순환을 알려 주면 대개 책임을 잘못 나눈 것이므로 세 번째 객체로 공통 부분을 빼낸다.
  • 수명: 컨테이너는 객체를 하나만 만들어 나눠 쓸지(싱글턴 범위), 요청마다 새로 만들지도 정한다. 직접 짠 싱글턴 대신 컨테이너가 하나만 만들게 하면 테스트에서 바꿔 끼울 수 있다.

컨테이너는 편하지만 마법이 되기 쉽다. 설정이 흩어지면 "이 인터페이스에 지금 무엇이 꽂혀 있나"를 실행해 봐야 안다. 작은 프로그램은 손 조립으로 충분하고, 컨테이너를 쓰더라도 클래스는 컨테이너를 모르게(애너테이션을 생성자 정도에만) 두면 나중에 떼어 내기 쉽다.

서비스 로케이터와 무엇이 다른가

서비스 로케이터는 등록소 하나를 두고, 필요한 객체가 그 등록소에 "Notifier를 달라"고 묻는다. 이것도 구현을 바깥에서 갈아 끼울 수 있다는 점은 DI와 같다. 차이는 방향이다. 로케이터는 필요한 쪽이 당겨 오고, 주입은 바깥이 밀어 넣는다.

왼쪽 서비스 로케이터에서는 OrderService가 로케이터에게 notifier를 달라고 직접 묻고, 로케이터가 구현을 돌려준다. OrderService는 로케이터를 안다. 오른쪽 의존성 주입에서는 컨테이너가 Notifier를 만들어 OrderService의 생성자에 넣고, OrderService는 Notifier만 안다서비스 로케이터: 당겨 온다의존성 주입: 밀어 넣는다OrderServiceLocatorNotifier① 달라② 받음모든 사용자가 로케이터에 기댄다무엇을 쓰는지 생성자만 봐선 모른다ContainerOrderServiceNotifier생성자로 건넨다OrderService는 컨테이너를 모른다생성자가 곧 필요한 것의 목록이다
로케이터에서는 모든 서비스가 로케이터를 알고, 무엇을 쓰는지는 본문을 읽어야 안다. 주입에서는 서비스가 컨테이너를 모르고, 생성자가 곧 필요한 것의 목록이다.
// 서비스 로케이터: 필요한 것을 스스로 찾아온다
public class OrderService {
    private final Notifier notifier = ServiceLocator.get(Notifier.class);
}

파울러는 로케이터를 쓰면 모든 사용자가 로케이터에 의존하게 되고, 주입은 그렇지 않다는 점을 핵심 차이로 든다. 다만 그는 로케이터를 나쁜 패턴으로 몰지 않는다. 테스트하기 쉬운 정도는 둘 다 비슷하게 만들 수 있고, 한 응용 프로그램 안에서만 쓰는 클래스라면 동작이 더 곧바로 보이는 로케이터가 조금 낫다고까지 적는다. 여러 응용 프로그램이 가져다 쓰는 부품이라면, 쓰는 쪽이 모르는 로케이터에 기대게 하지 말고 주입을 쓴다. 그리고 둘 중 무엇을 고르느냐보다 "설정과 사용을 떼어 놓는 것"이 더 중요하다고 결론짓는다.

요즘 코드에서 로케이터가 문제가 되는 흔한 모양은 아무 데서나 부르는 context.getBean(...)이나 전역 등록소다. 숨은 의존성이 생기고, 등록을 빠뜨리면 컴파일이 아니라 실행 중에 터진다.

리포지터리와 유닛 오브 워크

마틴 파울러의 『Patterns of Enterprise Application Architecture』(PoEAA, 2003)는 GoF가 다루지 않은 업무용 응용 프로그램의 패턴, 특히 객체와 관계형 데이터베이스 사이의 패턴을 모았다. 그 가운데 가장 많이 쓰이는 둘이 리포지터리와 유닛 오브 워크다.

OrderService는 OrderRepository 인터페이스의 findById, add, remove만 쓴다. 구현인 JpaOrderRepository가 매퍼를 거쳐 데이터베이스에 닿는다. 아래 유닛 오브 워크는 새로 만든 주문, 바뀐 주문, 지운 주문을 모았다가 commit 때 INSERT, UPDATE, DELETE를 한 트랜잭션으로 보낸다OrderService«interface»OrderRepositoryfindById(id)add(order)remove(order)JpaOrderRepository컬렉션처럼 쓴다SQL을 모른다매퍼, SQLDB유닛 오브 워크: 바뀐 것을 모아 두었다가 한 번에새로 만든 것Order #7바뀐 것Order #3지운 것Order #5commitINSERTUPDATEDELETE트랜잭션 하나로 묶어 보낸다. 중간에 실패하면 셋 다 없던 일이 된다EF Core의 DbContext는 리포지터리와 유닛 오브 워크를 합친 것이다
서비스는 컬렉션처럼 보이는 리포지터리만 쓰고 SQL을 모른다. 아래 유닛 오브 워크는 한 작업에서 새로 만든 것, 바뀐 것, 지운 것을 모았다가 커밋 때 한 트랜잭션으로 보낸다.
  • 리포지터리(Repository): PoEAA 요약에 따르면 도메인과 데이터 매핑 층 사이에서 컬렉션처럼 생긴 인터페이스로 도메인 객체를 꺼내고 넣게 한다. 질의를 만드는 코드가 한곳에 모이고, 도메인 층은 매핑 층을 모른 채 한 방향으로만 기댄다.
  • 유닛 오브 워크(Unit of Work): 한 업무 트랜잭션 동안 데이터베이스에 영향을 줄 변경(읽은 객체의 변경, 새 객체, 지운 객체)을 추적하다가, 끝나면 무엇을 써야 하는지 계산해 한꺼번에 쓴다. 객체를 바꿀 때마다 UPDATE를 보내지 않으니 왕복이 줄고, 쓰는 순서와 동시성 충돌도 한곳에서 다룬다.
public interface OrderRepository {
    Optional<Order> findById(OrderId id);
    List<Order> findUnpaidSince(LocalDate date);
    void add(Order order);
    void remove(Order order);
}

// 서비스는 컬렉션을 다루듯 쓴다. SQL도 표 이름도 모른다
public void cancel(OrderId id) {
    Order order = orders.findById(id).orElseThrow();
    order.cancel();
}

둘은 보통 ORM 안에 들어 있다. EF Core 문서는 DbContext를 유닛 오브 워크와 리포지터리 패턴을 합친 것이라고 소개한다. 그래서 ORM 위에 리포지터리를 한 겹 더 씌우는 것이 늘 이득은 아니다. 도메인에 맞는 이름의 질의(findUnpaidSince)를 모으고 테스트에서 메모리 구현으로 바꿀 수 있다면 값어치가 있고, ORM 메서드를 그대로 넘기기만 한다면 층만 늘어난다.

액티브 레코드, 데이터 매퍼, DTO

객체를 데이터베이스에 어떻게 저장하느냐에 PoEAA는 두 큰 길을 둔다. 액티브 레코드는 객체 하나가 표의 한 행을 감싸고, 저장과 삭제를 스스로 하며, 그 데이터에 도메인 로직을 더한다. 데이터 매퍼는 매퍼 층을 따로 두어, 도메인 객체는 데이터베이스가 있는지조차 모르고 SQL도 스키마도 품지 않는다.

왼쪽 액티브 레코드에서는 Order 객체 하나가 표의 한 행을 감싸고 save와 delete를 스스로 한다. 오른쪽 데이터 매퍼에서는 Order가 데이터베이스를 모르고, OrderMapper가 객체와 행 사이를 옮긴다. 아래는 서버의 도메인 객체를 OrderDto로 바꿔 JSON으로 보내는 모습이다액티브 레코드: 객체가 자기를 저장데이터 매퍼: 매퍼가 옮긴다Ordertotal()save()delete()id | total3 | 120007 | 8000orders 표한 행이 곧 한 객체. 간단한 CRUD에 빠르다Rails의 Active Record가 이 방식이다Ordertotal()cancel()OrderMapperfind(id)insert(o)id | total3 | 120007 | 8000SQL 없음도메인과 표 모양이 서로 몰라도 된다DTO: 경계를 넘을 때 필요한 값만 묶는다Ordercancel()조립기OrderDtoid, total, itemCountJSON다른 프로세스행동은 없고 값만 있다. 호출 한 번에 필요한 것을 다 실어 왕복을 줄인다
액티브 레코드는 객체가 곧 행이라 짧고 빠르다. 데이터 매퍼는 매퍼가 둘 사이를 옮겨 도메인과 표가 서로 다르게 자랄 수 있다. 아래 DTO는 경계를 넘을 때 필요한 값만 묶는다.
  • 언제 액티브 레코드: 표 모양과 도메인 모양이 거의 같은 CRUD 위주 앱. Rails 안내서는 Rails의 Active Record가 바로 이 패턴의 구현이라고 밝힌다. 도메인 규칙이 복잡해지면 저장 코드와 규칙이 한 클래스에 엉킨다.
  • 언제 데이터 매퍼: 상속, 값 객체, 컬렉션처럼 관계형 표에 없는 구조를 도메인에 쓰고 싶을 때. 파울러 요약도 객체 모델과 관계형 스키마가 어긋나는 문제를 이 패턴의 출발점으로 든다. Hibernate 같은 JPA 구현이 이쪽이다.
  • DTO(Data Transfer Object): 원래 뜻은 원격 호출 한 번에 필요한 데이터를 몰아 실어 호출 횟수를 줄이는 객체다. 직렬화할 수 있어야 하고, 서버 쪽에서는 조립기(assembler)가 도메인 객체와 DTO 사이를 옮긴다. 지금은 API 응답 모양을 도메인 객체와 떼어 놓는 용도로 더 자주 쓴다. 엔터티를 그대로 JSON으로 내보내면 내부 필드가 새고, 필드 하나 바꿀 때 API가 깨진다.
// 경계를 넘는 값 묶음. 행동은 없고, 직렬화하기 쉽다
public record OrderSummaryDto(String id, long total, int itemCount) {
    static OrderSummaryDto from(Order order) {
        return new OrderSummaryDto(order.id().value(), order.total(), order.items().size());
    }
}

다른 언어에서는: Django ORM과 Laravel의 Eloquent는 액티브 레코드 쪽이고, SQLAlchemy는 데이터 매퍼와 유닛 오브 워크(세션) 쪽이다. TypeScript에서 DTO는 보통 클래스 없이 타입 선언(type OrderSummary = { id: string; total: number })과 경계에서의 스키마 검사로 쓴다.

표현 패턴: MVC, MVP, MVVM

화면 코드에서 가장 오래된 패턴은 MVC(Model-View-Controller)다. 트뤼그베 렌스카우그가 1970년대 말 제록스 PARC에서 Smalltalk-79를 다루며 만들었고, 처음에는 네 부분이던 것이 Smalltalk 개발자들 손에서 셋으로 정리됐다. 세 패턴 모두 핵심은 파울러가 "분리된 표현"이라 부른 생각, 즉 도메인 객체는 화면을 전혀 모르게 두는 것이다. 차이는 화면과 모델을 무엇이 잇느냐다.

세 열 비교. MVC는 컨트롤러가 모델을 갱신하고 모델이 뷰에 변경을 알린다. MVP는 뷰가 입력을 프레젠터에 넘기고 프레젠터가 모델을 읽고 쓴 뒤 뷰를 직접 그린다. MVVM은 뷰와 뷰모델이 데이터 바인딩으로 이어지고 뷰모델이 모델을 읽고 쓰며, 뷰모델은 뷰를 모른다MVCControllerModelView갱신변경 알림입력은 컨트롤러가 받고,모델이 바뀌면 뷰에 알린다MVPViewPresenterModel입력 전달그려라읽기, 쓰기뷰는 수동적이다.프레젠터가 뷰를 직접 그린다MVVMViewViewModelModel입력바인딩읽기, 쓰기뷰모델은 뷰를 모른다.바인딩이 화면을 맞춘다입력
같은 세 역할을 다르게 잇는다. MVC는 모델이 뷰에 알리고, MVP는 프레젠터가 뷰를 직접 그리고, MVVM은 바인딩이 뷰를 뷰모델에 맞춘다. 강조 화살표가 화면을 바꾸는 길이다.
패턴화면을 바꾸는 쪽뷰가 아는 것잘 맞는 곳
MVC모델의 변경 알림(옵저버)을 받은 뷰모델서버 렌더링 웹 프레임워크(요청을 컨트롤러가 받는다)
MVP프레젠터가 뷰 인터페이스를 불러서프레젠터에 입력을 넘길 뿐바인딩이 약한 UI 툴킷, 뷰를 가짜로 바꿔 테스트할 때
MVVM데이터 바인딩이 뷰모델 속성을 따라뷰모델의 속성과 명령바인딩이 있는 플랫폼(WPF, 안드로이드 ViewModel, Vue)
  • MVP: 파울러에 따르면 1990년대 IBM과 Taligent에서 처음 나왔고 Dolphin Smalltalk 개발자들이 퍼뜨렸다. 뷰를 거의 아무 로직 없는 "수동적인 뷰"로 만들면, 프레젠터만 단위 테스트로 검사할 수 있다.
  • MVVM: 마이크로소프트의 켄 쿠퍼와 테드 피터스가 만들었고, 존 고스먼이 2005년 블로그에서 알렸다. 파울러의 프레젠테이션 모델을 바꾼 것으로, 프레젠터와 달리 뷰모델은 뷰를 참조하지 않는다. 뷰가 뷰모델 속성에 묶이고, 동기화 코드는 바인딩이 대신 쓴다.
  • 옵저버 동기화의 값: 파울러는 모델이 바뀌면 지켜보던 뷰가 스스로 갱신하는 방식(옵저버 동기화)이 결합을 낮추는 대신, 코드만 읽어서는 무슨 일이 일어나는지 알기 어렵다고 짚는다. 정해진 순간에 코드가 직접 화면을 고치는 흐름 동기화는 그 반대다. 다음 절의 단방향 흐름은 이 문제에 대한 한 가지 답이다.

단방향 데이터 흐름: Flux

화면이 커지면 양방향 연결이 꼬인다. 페이스북이 공개한 Flux 문서가 든 예가 그렇다. 대화를 읽음으로 표시하면 대화 목록과 따로 있는 "안 읽은 수"가 함께 바뀌어야 하는데, 모델이 다른 모델을, 그것이 또 다른 모델을 고치는 연쇄 갱신이 생기면 결과를 예측하기 어렵다. Flux는 흐름을 한 방향으로 묶는다.

Flux의 한 방향 흐름. 액션이 디스패처로, 디스패처가 스토어로, 스토어가 뷰로 가고, 뷰에서 사용자가 누르면 새 액션이 생겨 다시 처음으로 돈다. 뷰는 스토어의 상태를 직접 고치지 않는다데이터는 한 방향으로만 흐른다Action무슨 일이 있었나Dispatcher모든 스토어에 배달Store상태를 바꾸는 유일한 곳View상태를 그린다사용자가 누르면 새 액션이 생긴다양방향 바인딩이 얽히면: 모델 A가 B를 바꾸고, B가 뷰를, 뷰가 다시 A를 바꾼다한 방향으로 돌면 어떤 상태 변화도 액션 하나로 거슬러 올라가 찾을 수 있다
액션이 디스패처를 거쳐 스토어로 가고, 스토어가 바뀐 상태를 뷰에 내보낸다. 뷰는 상태를 고치지 않고 새 액션을 보낸다.
  • 액션: 종류와 내용을 가진 단순한 객체. 사용자의 입력이나 서버 응답이 액션이 된다.
  • 디스패처: 모든 액션을 등록된 모든 스토어에 배달하는 중앙의 한 곳. 스토어 사이의 갱신 순서도 정한다.
  • 스토어: 한 영역의 상태와 규칙을 갖고, 액션에만 반응해 바뀐 뒤 변경을 알린다. 바깥에서 스토어의 데이터를 직접 넣을 수 없다. 문서는 이것을 제어를 스토어 쪽으로 뒤집은 것이라고 표현한다.
  • 뷰: 스토어의 변경을 듣고 다시 그린다. 그래서 사용자 동작 하나는 한 바퀴의 변경으로 끝난다.
// 스토어: 액션을 받아 새 상태를 돌려주는 곳이 하나뿐이다
function cartStore(state, action) {
  switch (action.type) {
    case 'item-added':
      return { ...state, items: [...state.items, action.item] };
    case 'cart-cleared':
      return { ...state, items: [] };
    default:
      return state;
  }
}

// 뷰는 상태를 고치지 않고 액션을 보낸다
dispatch({ type: 'item-added', item: { id: 'book-1', price: 12000 } });

이 모양은 GoF의 커맨드(액션은 요청을 객체로 만든 것)와 옵저버(스토어가 뷰에 알린다)를 합친 것으로 읽을 수도 있다. 스토어를 "이전 상태와 액션을 받아 새 상태를 돌려주는 함수" 하나로 줄인 것이 위 코드의 꼴이고, 상태를 고치지 않고 새로 만들므로 이전 상태와 비교하거나 되돌리기가 쉽다.

널 객체와 명세

GoF 23가지에는 없지만 자주 쓰는 작은 패턴 둘이다. 널 객체는 "없음"을 null 대신 인터페이스를 구현하되 아무것도 하지 않는 객체로 나타낸다. 1996년 토마스 퀴네가 "Void Value"로 먼저 발표했고, 바비 울프가 『Pattern Languages of Program Design』 시리즈(1998)에 "Null Object"로 실었으며, 파울러의 『Refactoring』에는 "Insert Null Object"라는 리팩터링으로 들어 있다. 명세(Specification)는 업무 조건 하나를 isSatisfiedBy 하나짜리 객체로 만들고, and, or, not으로 엮어 새 조건을 만든다. 에릭 에번스의 『도메인 주도 설계』(2004)와 에번스, 파울러의 글이 널리 알렸다.

왼쪽은 Logger 인터페이스를 FileLogger와 NullLogger가 구현하고, NullLogger의 log는 아무것도 하지 않아 부르는 쪽이 null 검사를 하지 않아도 된다. 오른쪽은 연체됐고 추심 중이 아닌 청구서를 고르는 명세로, overdue와 not(inCollection)을 and로 묶은 나무다널 객체: 없음도 객체로명세: 조건을 객체로 엮는다«interface»Loggerlog(msg)FileLoggerlog: 파일에 쓴다NullLoggerlog: { }로그가 꺼져 있어도 logger.log()를그냥 부른다. if (logger != null)이 사라진다andoverduenotinCollectionisSatisfiedBy(invoice)가 true면 추심같은 조건을 조회에도, 검증에도 다시 쓴다
왼쪽: 로그가 꺼져 있으면 NullLogger를 꽂아 null 검사를 없앤다. 오른쪽: 연체됐고 추심 중이 아닌 청구서를 고르는 조건을 객체 셋의 나무로 엮었다.
public interface Logger {
    void log(String message);
}

public final class NullLogger implements Logger {
    public static final Logger INSTANCE = new NullLogger();

    @Override
    public void log(String message) {
        // 일부러 아무것도 하지 않는다
    }
}

// 쓰는 쪽에는 if (logger != null)이 없다
Logger logger = config.loggingEnabled() ? new FileLogger(path) : NullLogger.INSTANCE;
public interface Specification<T> {
    boolean isSatisfiedBy(T candidate);

    default Specification<T> and(Specification<T> other) {
        return candidate -> isSatisfiedBy(candidate) && other.isSatisfiedBy(candidate);
    }

    default Specification<T> not() {
        return candidate -> !isSatisfiedBy(candidate);
    }
}

Specification<Invoice> overdue = invoice -> invoice.dueDate().isBefore(today);
Specification<Invoice> inCollection = Invoice::inCollection;
Specification<Invoice> sendToCollection = overdue.and(inCollection.not());
  • 널 객체의 함정: 오류가 정상 실행처럼 보이게 숨을 수 있다. "할인 없음"처럼 아무것도 안 하는 것이 맞는 동작일 때만 쓰고, "고객을 못 찾음"처럼 부르는 쪽이 알아야 하는 경우에는 쓰지 않는다. 그런 경우에는 Java의 Optional이나 Kotlin의 ?처럼 "없을 수 있음"을 타입에 드러내는 편이 낫다.
  • 명세의 쓰임: 같은 조건을 메모리 안 검증, 화면 필터, 저장소 질의에 다시 쓴다. 리포지터리에 findBy(spec)를 두면 질의 메서드가 끝없이 늘어나는 것을 막는다. Spring Data JPA의 Specification이 이 생각이다. 조건이 두세 개뿐이면 그냥 메서드 하나가 더 읽기 쉽다.

생산자-소비자: 블로킹 큐로 속도 차이를 흡수한다

동시성 패턴의 맏형은 생산자-소비자다. 만드는 쪽과 쓰는 쪽 사이에 크기가 정해진 버퍼를 두고, 버퍼가 가득 차면 생산자가, 비면 소비자가 기다린다. 에츠허르 데이크스트라가 1965년부터 이 문제를 다뤘고, 빈칸과 찬 칸의 수를 세는 세마포어로 풀었다. 흔히 보는 풀이는 여기에 버퍼를 지키는 상호 배제 하나를 더한다. 지금은 그 동기화를 큐 하나가 감춰 준다.

생산자-소비자 단계 그림: 생산자가 A, B, C를 넣어 큐가 가득 차고, D를 넣으려다 멈춘다. 소비자가 하나 꺼내면 D가 들어간다. 소비자가 모두 꺼낸 뒤 빈 큐에서 멈추고, 생산자가 E를 넣자 곧바로 받아 간다생산자소비자BlockingQueue (용량 3)puttake일하는 중일하는 중소비자가 받은 순서(아직 없음)A일하는 중일하는 중소비자가 받은 순서(아직 없음)AB일하는 중일하는 중소비자가 받은 순서(아직 없음)ABC일하는 중일하는 중소비자가 받은 순서(아직 없음)ABC멈춤: D 들고 대기일하는 중소비자가 받은 순서(아직 없음)BCD일하는 중일하는 중소비자가 받은 순서ACD일하는 중일하는 중소비자가 받은 순서A → BD일하는 중일하는 중소비자가 받은 순서A → B → C일하는 중일하는 중소비자가 받은 순서A → B → C → D일하는 중멈춤: 빈 큐에서 대기소비자가 받은 순서A → B → C → D일하는 중일하는 중소비자가 받은 순서A → B → C → D → E넣은 순서와 받은 순서가 같다(FIFO). 멈춘 동안 스레드는 CPU를 쓰지 않는다
1 / 11

처음: 큐는 비어 있고 생산자와 소비자가 함께 돈다.

생산자 하나, 소비자 하나, 용량 3인 블로킹 큐. 큐는 왼쪽으로 들어가 오른쪽으로 나온다. 빨간 글자는 멈춘 쪽이다. 프레임은 계산 함수로 구했다.
BlockingQueue<Job> queue = new ArrayBlockingQueue<>(3);
Job poison = new Job("STOP");

Thread producer = new Thread(() -> {
    try {
        for (Job job : incomingJobs()) {
            queue.put(job);        // 가득 차면 여기서 멈춘다
        }
        queue.put(poison);         // 끝났다는 표시
    } catch (InterruptedException e) {
        Thread.currentThread().interrupt();
    }
});

Thread consumer = new Thread(() -> {
    try {
        while (true) {
            Job job = queue.take(); // 비어 있으면 여기서 멈춘다
            if (job == poison) {
                return;
            }
            job.run();
        }
    } catch (InterruptedException e) {
        Thread.currentThread().interrupt();
    }
});
  • Java의 BlockingQueue: API 문서는 이 인터페이스가 주로 생산자-소비자 큐에 쓰려고 만들어졌다고 적는다. 넣기와 꺼내기마다 네 가지 꼴이 있다. 예외를 던지는 add, 실패하면 false나 null을 돌려주는 offer와 poll, 될 때까지 멈추는 put과 take, 시간 제한이 있는 offer와 poll이다.
  • 끝내는 법: 큐에는 "닫기"가 없다. 문서가 권하는 흔한 방법은 위 코드처럼 생산자가 약속된 끝 표시(poison) 객체를 넣는 것이다. null은 poll의 실패 값이라 넣을 수 없다.
  • 왜 크기를 정하나: 크기 없는 큐는 소비자가 느릴 때 메모리가 끝없이 찬다. 가득 차면 생산자를 멈추게 하는 것이 곧 배압(backpressure)이다.

스레드와 락, 원자 연산이 어떻게 이런 큐를 받치는지는 병렬 프로그래밍 노트에 있다. 같은 모양이 서비스 사이로 커지면 메시지 큐가 된다. 다른 언어에서는: Go는 크기를 준 채널(make(chan Job, 3))이 바로 블로킹 큐이고, 다 보낸 뒤 close(ch)로 끝을 알린다. Python은 queue.Queue(maxsize=3), asyncio에서는 asyncio.Queue를 쓴다.

스레드 풀, 퓨처와 프로미스

작업마다 스레드를 새로 만들면 만드는 비용과 메모리가 작업 수만큼 든다. 스레드 풀은 정해진 수의 스레드를 미리 두고, 작업을 큐에 받아 비는 스레드가 꺼내 가게 한다. 안을 보면 앞 절의 생산자-소비자 그대로다. 퓨처(future)는 제출하는 순간 돌려받는 "나중에 결과가 들어올 자리"다.

일꾼 둘인 스레드 풀의 시간표. 길이 3, 1, 2, 2, 1인 작업 다섯 개를 제출 순서대로 먼저 비는 일꾼이 맡아 5단위 만에 끝난다. 하나씩 하면 9단위다. 아래는 submit이 곧바로 Future를 돌려주고 get이 결과가 나올 때까지 기다리는 모습이다newFixedThreadPool(2)에 작업 다섯 개일꾼 1일꾼 2작업 1작업 2작업 3작업 4작업 5012345시간하나씩 하면 9, 둘이 나누면 5. 스레드를 작업마다 새로 만들지 않고 다시 쓴다pool.submit(task)→ 곧바로Future결과를 받을 자리표.get()은 끝날 때까지 기다린다CompletableFuture와 JS Promise는 기다리지 않고 "끝나면 이것을 하라"를 잇는다
일꾼 둘인 풀에 길이 3, 1, 2, 2, 1인 작업을 넣으면 먼저 비는 일꾼이 다음 작업을 맡는다. 시간표는 계산 함수로 구했다. 아래는 submit이 곧바로 Future를 돌려주는 모습이다.
try (ExecutorService pool = Executors.newFixedThreadPool(2)) {
    Future<Report> future = pool.submit(() -> buildReport(month));
    // ... 그동안 다른 일
    Report report = future.get();   // 끝날 때까지 기다린다
}   // Java 19부터 close()가 종료를 요청하고 끝나기를 기다린다

// 기다리지 않고 "끝나면 이것을" 잇기
CompletableFuture<Receipt> receipt =
    CompletableFuture.supplyAsync(() -> loadCart(userId))
        .thenCombine(CompletableFuture.supplyAsync(() -> loadCoupon(userId)), Cart::apply)
        .thenApply(payments::charge)
        .exceptionally(error -> Receipt.failed(error));
  • ExecutorService: Executors.newFixedThreadPool(n)은 n개 스레드를 다시 쓰고, 남는 작업은 큐에서 기다리게 한다. submit은 Future를 돌려주고, get()은 끝날 때까지 기다린다. Java 19부터 ExecutorService가 AutoCloseable이라 위처럼 try로 감싸면 닫을 때 종료를 기다린다.
  • CompletableFuture: get()으로 막혀 기다리는 대신 "끝나면 이것을 하라"를 잇는다. thenApply는 결과를 바꾸고, thenCombine은 두 결과를 합치고, exceptionally는 실패를 대체 값으로 바꾼다. 실행기를 주지 않은 비동기 메서드는 ForkJoinPool.commonPool()에서 돈다. 입출력처럼 오래 막히는 일은 따로 만든 풀을 넘긴다.
  • 풀의 크기: CPU만 쓰는 일은 코어 수 근처, 입출력을 기다리는 일은 그보다 크게 잡는다. 한 풀에서 다른 작업의 결과를 get()으로 기다리면 일꾼이 모두 서로를 기다리는 교착이 생길 수 있다.

다른 언어에서는: JavaScript의 Promise가 같은 생각이다. 대기(pending)에서 이행(fulfilled)이나 거부(rejected)로 한 번만 바뀌고, then이 새 프로미스를 돌려줘 이어 붙인다. Promise.all은 모두 이행되면 결과 배열로, 하나라도 거부되면 그 이유로 끝난다. Python은 concurrent.futures.ThreadPoolExecutor와 asyncio.gather, Go는 고루틴 여러 개를 sync.WaitGroup이나 errgroup으로 기다린다.

const [user, posts] = await Promise.all([fetchUser(id), fetchPosts(id)]);

리액터: 이벤트 루프 하나로 많은 연결을

연결마다 스레드를 하나씩 두면, 연결이 만 개일 때 대부분의 스레드는 읽을 데이터를 기다리며 메모리와 문맥 전환만 쓴다. 웹 서버의 C10k 문제에서 나온 리액터는 입출력이 준비된 뒤에만 반응한다. 더글러스 슈미트가 1995년 『Pattern Languages of Program Design』에 정리했다.

리액터 구조. 왼쪽 소켓 네 개 가운데 리슨 소켓과 연결 2만 준비됐다. 가운데 디멀티플렉서(epoll, kqueue)가 준비된 핸들만 알려 주면, 이벤트 루프인 디스패처가 새 연결은 AcceptHandler에, 읽을 데이터는 ReadHandler에 넘긴다. 핸들러가 끝나면 루프는 다시 기다린다핸들이벤트 루프(스레드 하나)핸들러리슨 소켓연결 1연결 2연결 3디멀티플렉서select, epoll, kqueue디스패처준비된 것만 차례로 넘긴다AcceptHandlerReadHandlerWriteHandler핸들러가 끝나면 루프는 다시 기다린다연결마다 스레드를 두지 않아 수만 개 연결도 스레드 하나로 지킨다핸들러가 오래 걸리면 모든 연결이 멈춘다. 무거운 일은 스레드 풀로 넘긴다
디멀티플렉서가 소켓 넷을 한꺼번에 지켜보다 준비된 둘(리슨 소켓, 연결 2)만 알려 주고, 디스패처가 각각 맞는 핸들러에 넘긴다. 모두 스레드 하나에서 돈다.
  • 핸들: 소켓이나 파일 기술자처럼 입출력의 대상을 가리키는 것.
  • 디멀티플렉서: 여러 핸들을 한꺼번에 지켜보고 준비된 것을 알려 준다. 운영체제의 select, epoll, kqueue가 이것이다.
  • 디스패처: 이벤트 루프. 핸들러를 등록받고, 준비된 핸들에 맞는 핸들러를 부른다.
  • 핸들러: 한 종류의 일을 하는 콜백. 기본적으로 디스패처의 스레드에서 돌므로 오래 걸리면 모든 연결이 멈춘다.

Netty, Nginx, Node.js가 이 구조다. 코어마다 루프를 하나씩 두는 다중 리액터, 무거운 핸들러를 앞 절의 스레드 풀로 넘기는 변형이 흔하다. 콜백으로 흐름이 쪼개져 디버깅이 어렵다는 단점은 async/await가 많이 덜어 줬다. 이벤트 루프가 언어마다 어떻게 보이는지는 프로그래밍 언어 비교 노트의 동시성 모델 절에 있다.

함수형 언어에서의 패턴: 언어가 흡수한 것들

패턴의 상당수는 언어에 없는 기능을 클래스로 메우는 방법이기도 하다. 피터 노빅은 1996년 발표 "Design Patterns in Dynamic Programming"에서 GoF 23가지 중 16가지가 Lisp나 Dylan에서는 보이지 않거나 더 단순해진다고 세었다. 그중 일급 함수 덕분에 사라지는 것으로 든 넷이 커맨드, 전략, 템플릿 메서드, 방문자다. 자세한 분류는 패턴의 유래 노트에 있다.

왼쪽은 클래스로 쓴 전략: DiscountPolicy 인터페이스와 이를 구현하는 클래스 세 개, 그리고 정책을 보유한 Checkout. 오른쪽은 같은 일을 함수 값으로 한 것: Checkout이 UnaryOperator 하나를 받고, 정책은 람다 세 줄이다클래스로 쓴 전략함수 값으로 쓴 전략Checkout«interface»DiscountPolicyapply(price)NoDiscountPercent10VipDiscount정책 하나에 클래스 하나.상태가 없으면 대부분 한 줄짜리다CheckoutCheckout(UnaryOperator f)p -> pp -> p * 0.9p -> p - 3000상태나 여러 메서드가 필요하면 클래스로
같은 할인 전략을 인터페이스와 구현 클래스 셋으로 쓴 것(왼쪽)과 함수 값 하나로 쓴 것(오른쪽). 상태가 없는 전략은 람다 한 줄이면 된다.
// 전략 인터페이스와 구현 클래스 셋 대신 함수 하나
public class Checkout {
    private final UnaryOperator<Long> discount;

    public Checkout(UnaryOperator<Long> discount) {
        this.discount = discount;
    }

    public long pay(long price) {
        return discount.apply(price);
    }
}

Checkout normal = new Checkout(price -> price);
Checkout sale = new Checkout(price -> price * 9 / 10);

// 커맨드도 Runnable 하나로: 실행 취소 목록에 함수를 쌓는다
Deque<Runnable> undo = new ArrayDeque<>();
undo.push(() -> document.delete(range));
  • 전략과 커맨드: 메서드 하나짜리 인터페이스는 함수 타입(Function, Runnable, Comparator)이면 된다. list.sort(Comparator.comparing(Order::total))는 전략 패턴을 쓴 것이지만 아무도 그렇게 부르지 않는다. 각 패턴의 클래스 꼴은 행위 패턴 1 노트에 있다.
  • 템플릿 메서드: 상속 대신 "바뀌는 단계"를 함수 인자로 받는 고차 함수가 된다.
  • 이터레이터, 옵저버: 언어와 표준 라이브러리에 이미 들어 있다(for-each, 제너레이터, 이벤트 리스너, 리액티브 스트림).
  • 모나드는 짧게: 함수형 언어에서 자주 듣는 모나드는 "값을 감싼 상자와, 상자를 벗기지 않고 다음 계산을 잇는 방법(flatMap)"의 짝이라고 보면 된다. Optional.flatMap, CompletableFuture.thenCompose, Promise.then이 같은 모양이라, null 검사나 콜백 중첩 없이 단계를 이어 붙인다.

그렇다고 패턴 공부가 쓸모없어지지는 않는다. 함수가 되든 클래스가 되든 "바뀌는 것을 떼어 내 인자로 받는다"는 설계 판단은 남고, 함수가 여러 개 필요하거나 상태를 품어야 하면 다시 객체가 낫다.

시스템 단위 패턴은 시스템 설계 노트로

같은 생각이 더 큰 크기에서도 반복된다. 객체 사이의 프록시는 서비스 앞의 서킷 브레이커가 되고, 커맨드와 메멘토는 이벤트 소싱이 되고, 유닛 오브 워크의 트랜잭션은 여러 서비스에 걸치면 사가가 된다. 이 노트는 한 애플리케이션 안까지만 다루고, 네트워크를 건너는 패턴은 시스템 설계 노트에서 다룬다.

겹친 세 영역. 가장 안쪽은 객체 몇 개 사이의 GoF 패턴(전략, 옵저버, 어댑터, 팩터리), 그 바깥은 한 애플리케이션 안의 패턴(의존성 주입, 리포지터리, MVC, 생산자-소비자), 가장 바깥은 여러 서비스 사이의 패턴(서킷 브레이커, 사가, CQRS, 이벤트 소싱)이다같은 생각, 다른 크기여러 서비스 사이: 시스템 설계 노트서킷 브레이커, 사가, CQRS, 이벤트 소싱한 애플리케이션 안: 이 노트의존성 주입, 리포지터리, MVC, MVVM, 생산자-소비자객체 몇 개 사이: GoF 23가지전략, 옵저버, 어댑터, 팩터리
패턴이 다루는 크기. 가운데 강조된 영역이 이 노트의 범위다.

패턴 고르기 정리

패턴은 증상에서 출발해 고른다. "이 패턴을 써 보자"에서 출발하면 문제가 없는 곳에 층만 늘어난다. 아래는 이 노트의 패턴을 증상으로 다시 묶은 것이다.

증상에서 패턴으로. 테스트에서 가짜를 꽂을 수 없으면 생성자 주입, SQL이 흩어졌으면 리포지터리, 화면 코드가 로직을 다 알면 MVVM이나 단방향 흐름, null 검사가 곳곳에 있으면 널 객체나 Optional, 생산과 소비 속도가 다르면 블로킹 큐, 연결마다 스레드가 들면 리액터다이런 증상이 보이면먼저 떠올릴 패턴테스트에서 가짜를 꽂을 수 없다생성자 주입서비스 곳곳에 SQL이 흩어졌다리포지터리화면 코드가 로직을 다 안다MVVM, 단방향 흐름if (x != null)이 곳곳에 있다널 객체, Optional만드는 쪽과 쓰는 쪽 속도가 다르다블로킹 큐연결마다 스레드가 하나씩 든다리액터, 이벤트 루프
증상 여섯 가지와 먼저 떠올릴 패턴. 증상이 없으면 패턴도 필요 없다.
패턴푸는 문제치르는 값쓰지 않아도 될 때
의존성 주입구체 클래스에 묶여 바꾸거나 테스트할 수 없음조립 코드나 컨테이너 설정바뀔 일 없는 값 객체, 유틸 함수
리포지터리질의가 서비스 곳곳에 흩어짐층 하나, 질의 메서드 관리ORM 메서드를 그대로 넘기기만 할 때
유닛 오브 워크변경마다 왕복, 부분 저장추적 비용, 언제 쓰이는지 덜 보임대개 ORM이 이미 해 준다
데이터 매퍼도메인과 표 모양이 다름매핑 설정, 학습 비용표 그대로의 CRUD(액티브 레코드로 충분)
DTO내부 모델이 API로 샘, 원격 왕복변환 코드같은 프로세스 안의 호출
MVVM, 단방향 흐름화면과 로직이 엉킴, 연쇄 갱신보일러플레이트, 간접 층화면 몇 개짜리 도구
널 객체null 검사가 곳곳에오류가 숨을 수 있음"없음"을 부르는 쪽이 알아야 할 때
명세조건 조합이 늘어남객체 수조건이 두세 개일 때
생산자-소비자만드는 속도와 쓰는 속도가 다름끝내기, 오류 전달이 까다로움순서대로 바로 처리해도 될 때
스레드 풀, 퓨처스레드 생성 비용, 결과 기다리기풀 크기 조정, 교착 위험작업이 몇 개뿐일 때
리액터연결 수만큼 스레드가 듦막는 코드를 쓰면 전체가 멈춤연결이 적고 처리가 무거울 때

흔한 오해

  • "DI를 하려면 스프링 같은 컨테이너가 필요하다" — 주입은 생성자로 받는 것일 뿐이다. 컨테이너는 객체가 많아졌을 때 조립을 대신해 주는 도구다.
  • "제어의 역전과 의존성 주입은 같은 말이다" — IoC는 프레임워크가 내 코드를 부르는 넓은 생각이고, DI는 그 가운데 의존성을 바깥에서 넣는 한 방식이다. 이름이 따로 생긴 이유가 바로 그 혼동이었다.
  • "서비스 로케이터는 안티패턴이다" — 숨은 의존성이 생기는 것은 사실이지만, 파울러는 한 응용 프로그램 안에서는 쓸 만하다고 봤다. 중요한 것은 설정과 사용을 나누는 것이다.
  • "리포지터리는 ORM 위에 늘 한 겹 더 씌워야 한다" — ORM이 이미 리포지터리와 유닛 오브 워크를 품고 있는 경우가 많다. 도메인 이름의 질의를 모을 때 값어치가 있다.
  • "DTO는 계층마다 하나씩 만든다" — 원래 목적은 경계(프로세스, 공개 API)를 넘는 것이다. 같은 프로세스 안 계층마다 복사본을 두면 변환 코드만 는다.
  • "MVVM이 MVC보다 새롭고 낫다" — 셋은 화면을 무엇이 갱신하느냐의 선택이다. 바인딩이 없는 환경에서 MVVM을 흉내 내면 바인딩 코드를 손으로 쓰게 된다.
  • "함수형 언어에는 패턴이 없다" — 패턴 일부가 언어 기능으로 사라질 뿐, 바뀌는 것을 떼어 내는 판단은 그대로 남고 함수형 쪽만의 패턴(모나드, 렌즈 등)도 생긴다.

핵심 자료

요지는 직접 정리한 한 줄이다.

자료한 줄 요지
Martin Fowler — Inversion of Control Containers and the Dependency Injection pattern (2004)"의존성 주입"이라는 이름이 나온 글. 세 가지 주입 방식과 서비스 로케이터와의 차이를 비교한다.
Spring — IoC 컨테이너 소개빈, BeanFactory, ApplicationContext. 객체가 의존성을 스스로 찾지 않는다는 뜻의 "역전".
Martin Fowler — PoEAA 패턴 목록리포지터리, 유닛 오브 워크, 데이터 매퍼, 액티브 레코드, DTO의 한 쪽짜리 요약.
Martin Fowler — GUI Architectures (2006)MVC에서 MVP, 프레젠테이션 모델까지. 옵저버 동기화와 흐름 동기화의 차이.
Flux — In-Depth Overview액션, 디스패처, 스토어, 뷰가 한 방향으로 도는 이유를 MVC의 연쇄 갱신과 비교한다.
Java SE 21 — BlockingQueue넣기, 꺼내기의 네 가지 꼴(예외, 특수값, 멈춤, 시간 제한)과 생산자-소비자 예.
Reactor pattern — Wikipedia핸들, 디멀티플렉서, 디스패처, 핸들러. C10k 문제에서 나온 배경과 단점.
Peter Norvig — Design Patterns in Dynamic Programming (1996)일급 함수와 일급 타입 같은 언어 기능이 GoF 패턴 다수를 얼마나 단순하게 만드는지 센다.