군만두의 IT 공부 일지

[스터디7] 02. 스프링 컨텍스트: 추상화 본문

프로그래밍/Java

[스터디7] 02. 스프링 컨텍스트: 추상화

mandus 2025. 4. 22. 21:34

목차

    4장. 스프링 컨텍스트: 추상화

    이 장에서 다룰 내용
    - 인터페이스를 사용하여 계약 정의하기
    - 스프링 컨텍스트에서 빈 추상화 사용하기
    - 추상화와 함께 의존성 주입 사용하기

    4.1 계약 정의를 위한 인터페이스 사용

    • 인터페이스: 자바에서 특정 책임을 선언하는 데 사용하는 추상 구조. '무엇이 발생해야 하는지(필요 대상)'를 지정.
    • 인터페이스를 구현하는 객체: '어떻게 그것이 발생해야 하는지(발생 방법)'를 지정.

    4.1.1 구현 분리를 위해 인터페이스 사용

    • 예시 1) 목적지로 이동하려고 우버(Uber) 같은 차량 공유 앱을 사용할 때,
      • 차량 공유 앱 = 인터페이스
      • 고객 = 이동을 요청함
      • 서비스를 제공할 수 있는 차를 가진 드라이버 = 고객 요청에 응할 수 있음
      • 고객과 드라이버는 앱(인터페이스)으로 분리되어 있어 서로에 대해 알 수 없다.
    • 예시 2) 배송 앱에서 배송할 패키지의 세부 정보를 인쇄해야 하는 객체를 구현한다고 가정할 때,
      • 인쇄된 세부 정보는 목적지 주소별로 정렬되어야 함
      • 세부 정보를 인쇄하는 객체는 배송 주소별로 패키지를 정렬하는 책임을 다른 객체에 위임해야 함
      • 객체의 책임을 변경할 때 변경된 책임을 사용하는 다른 객체까지 변경할 필요가 없도록 해야 한다.

    4.1.2 시나리오 요구 사항

    • 예시 3) 팀 업무 관리용 앱을 구현한다고 가정할 때,
      • 사용자가 업무에 대한 댓글을 담길 수 있도록 함
      • 사용자가 댓글을 게시하면 해당 댓글은 데이터베이스 등 어딘가에 저장되고, 앱은 설정된 특정 주소로 이메일을 보냄
      • 이 기능을 구현하려면 객체를 설계하고 올바른 책임과 추상화를 찾아야 한다.

    4.1.3 프레임워크 없이 요구 사항 구현

    • 서비스(service): 사용 사례를 구현하는 객체
    • 요구 사항에서 사용 사례가 댓글 저장과 댓글을 이메일로 보내는 두 가지 행동(action)으로 구성되어 있음을 알 수 있다. 서로 다른 두 책임(responsibility)으로 간주하여 두 개의 객체로 구현해야 한다.
    • 리포지토리(repository): 데이터베이스와 직접 작업하는 객체가 있을 때의 객체 이름. 데이터 액세스 객체(Data Access Object)라고도 한다.
    • 프록시(proxy): 실제 앱에서 앱 외부와 통신을 담당하는 객체 이름
    • 댓글 게시를 구현하는 서비스의 객체 이름은 'CommentService', 댓글 저장 기능을 구현하는 객체 이름은 'CommentRepository', 이메일 전송을 담당하는 객체 이름을 'CommentNotificationProxy' 로 지정한다.
    • 의존성을 변경해야 할 때, 의존성을 사용하는 객체까지 변경할 필요가 없도록 CommentService를 의존성 구현과 확실하게 분리해야 한다.
    public interface CommentRepository {
        void storeComment(Comment comment);
    }
    public class DBCommentRepository implements CommentRepository {
    
        @Override
        public void storeComment(Comment comment) {
            System.out.println("Storing comment: " + comment.getText());
        }
    }
    public interface CommentNotificationProxy {
        void sendComment(Comment comment);
    }
    public class EmailCommentNotificationProxy implements CommentNotificationProxy {
    
        @Override
        public void sendComment(Comment comment) {
            System.out.println("Sending notification for comment: " + comment.getText());
        }
    }
    public class CommentService {
    
        private final CommentRepository commentRepository;
        private final CommentNotificationProxy commentNotificationProxy;
    
        public CommentService(
            CommentRepository commentRepository,
            CommentNotificationProxy commentNotificationProxy
        ) {
            this.commentRepository = commentRepository;
            this.commentNotificationProxy = commentNotificationProxy;
        }
    
        public void publishComment(Comment comment) {
            // '댓글 저장'과 '알림 전송' 책임을 의존성에 위임
            commentRepository.storeComment(comment);
            commentNotificationProxy.sendComment(comment);
        }
    }

    4.2 추상화와 함께 의존성 주입

    4.2.1 스프링 컨텍스트에 포함될 객체 정하기

    • 스프링 컨텍스트에 객체를 추가하는 이유: 스프링이 객체를 제어하고 프레임워크가 제공하는 기능으로 객체를 보강할 수 있도록 하는 것
    • 객체가 컨텍스트로부터 주입해야 하는 의존성이 있거나 그 자체가 의존성인 경우 해당 객체를 스프링 컨텍스트에 추가해야 한다.
    • 객체 예시:
      • CommentService: CommentRepository와 CommentNotificationProxy 의존성 두 개를 갖고 있다.
      • DBCommentRepository: CommentRepository 인터페이스를 구현하며 CommentService의 의존성이다.
      • EmailCommentNotificationProxy: CommentNotificationProxy 인터페이스를 구현하며 CommentService의 의존성이다.
    • 스프링 컨텍스트에 객체를 추가하면 프레임워크가 제공하는 특정 기능을 사용하여 객체를 관리할 수 있다. 프레임워크에서 얻는 이점도 없는데 스프링이 관리할 객체만 추가하는 것은 오버엔지니어링(over-engineering) 구현을 하는 것이다.
    @Component
    public class DBCommentRepository implements CommentRepository {
    
        @Override
        public void storeComment(Comment comment) {
            System.out.println("Storing comment: " + comment.getText());
        }
    }
    @Component
    public class EmailCommentNotificationProxy implements CommentNotificationProxy {
    
        @Override
        public void sendComment(Comment comment) {
            System.out.println("Sending notification for comment: " + comment.getText());
        }
    }
    @Component
    public class CommentService {
    
        private final CommentRepository commentRepository;
        private final CommentNotificationProxy commentNotificationProxy;
    
        public CommentService(
            CommentRepository commentRepository,
            CommentNotificationProxy commentNotificationProxy
        ) {
            this.commentRepository = commentRepository;
            this.commentNotificationProxy = commentNotificationProxy;
        }
    
        public void publishComment(Comment comment) {
            commentRepository.storeComment(comment);
            commentNotificationProxy.sendComment(comment);
        }
    }
    @Configuration
    @ComponentScan(basePackages = {"proxies", "services", "repositories"})
    public class ProjectConfiguration {
    }

    4.2.2 추상화에 대한 여러 구현체 중에서 오토와이어링할 것을 선택

    • 서로 다른 두 클래스로 생성된 빈이 두 개 있고 이 두 빈이 CommentNotificationProxy 인터페이스를 구현한다고 가정할 때, 스프링은 빈 선택 메커니즘을 사용한다.
      1. @Primary 애너테이션으로 구현할 빈 중 하나를 기본값으로 표시한다.
      2. @Qualifier 애너테이션으로 빈 이름을 지정한 후 DI를 위해 해당 이름으로 참조한다.
    @Component
    public class CommentPushNotificationProxy implements CommentNotificationProxy {
    
        @Override
        public void sendComment(Comment comment) {
            System.out.println(
                "Sending push notification for comment: " + comment.getText()
            );
        }
    }

    @Primary로 주입에 대한 기본 구현 표시하기

    @Component
    @Primary
    public class CommentPushNotificationProxy implements CommentNotificationProxy {
    
        @Override
        public void sendComment(Comment comment) {
            System.out.println(
                "Sending push notification for comment: " + comment.getText()
            );
        }
    }

    @Quilfier로 의존성 주입에 대한 구현 이름 지정하기

    @Component
    @Qualifier("PUSH")
    public class CommentPushNotificationProxy implements CommentNotificationProxy {
        // 코드 생략
    }
    @Component
    @Qualifier("EMAIL")
    public class EmailCommentNotificationProxy implements CommentNotificationProxy {
        // 코드 생략
    }
    @Component
    public class CommentService {
    
        private final CommentRepository commentRepository;
        private final CommentNotificationProxy commentNotificationProxy;
    
        public CommentService(
            CommentRepository commentRepository,
            @Qualifier("PUSH") CommentNotificationProxy commentNotificationProxy
        ) {
            this.commentRepository = commentRepository;
            this.commentNotificationProxy = commentNotificationProxy;
        }
    
        // 코드 생략
    }

    4.3 스테레오타입 애너테이션으로 객체의 책임에 집중

    • 실제 프로젝트에서는 스테레오타입 애너테이션을 명시적으로 사용하여 컴포넌트 목적을 정의한다.
      • @Component가 사용되며 구현하는 객체의 책임에 대한 세부 정보는 제공하지 않는다.
    • 서비스(@Service)는 사용 사례를 구현하는 책임이 있는 객체이며, 리포지토리(@Repository)는 데이터 지속성을 관리하는 객체이다.
    • 세 가지(@Component, @Service, @Repository) 모두 스테레오타입 애너테이션이며 스프링이 애너테이션된 클래스의 인스턴스를 생성하고 스프링 컨텍스트에 추가하도록 지시한다.

     

    이 글은 『스프링 교과서』 책을 학습한 내용을 정리한 것입니다.
    Comments