본문 바로가기
Study/디자인 패턴

[C#/Unity][디자인패턴] 책임 연쇄 패턴(Chain of Responsibility Pattern)

by 스테디코디스트 2023. 12. 29.
반응형

1. 책임 연쇄 패턴이란?

- 사용자의 요청을 처리함에 있어, 연쇄(Chain)적으로 연결되어있는 처리 객체(Handler)들을 이용하는 패턴.

- 클라이언트로부터의 요청을 처리할 수 있는 처리 객체를 집합(Chain)으로 만들어 부여함으로 결합을 느슨하게 하기 위해 만들어진 디자인 패턴.

- 일반적으로 요청을 처리할 수 있는 객체를 찾을 때까지 집합 안에서 요청을 전달한다.

- 각각의 인스턴스의 책임들이 체인처럼 연쇄되어 있다는 뜻이다.

- 요청을 보내는 쪽과 처리하는 쪽을 분리시키고, 요청을 보내는 쪽에서 해당 요청을 처리하는 핸들러가 어떤 구체적인 타입인지에 상관없이 디커플링된 상태에서 요청을 처리할 수 있게끔 해주는 패턴.

- 클라이언트의 요청에 대한 세세한 처리를 하나의 객체가 몽땅 하는 것이 아닌, 여러 개의 처리 객체(handler)들로 나누고, 이들을 사슬(chain)처럼 연결해 집합 안에서 연쇄적으로 처리하는 행동 패턴.

 

2. 책임 연쇄 패턴을 사용하는 경우

- 요청을 처리할 수 있는 객체가 여러 개이고, 처리 객체의 유형과 순서를 미리 알 수 없는 경우 사용

- 특정 순서로 여러 핸들러를 실행해야 할 때 사용

- 핸들러들의 집합과 그들의 순서가 런타임에 변경되어야 하는 경우 사용

- if-else문을 최적화하는데 사용

 

3. 책임 연쇄 패턴의 장점

- 결합도를 낮추고, 요청의 발신자와 수신자를 분리시킬 수 있다.

- 클라이언트는 처리 객체의 집합 내부 구조를 알 필요가 없다.

- 집합 내의 처리 순서를 변경하거나 처리 객체를 추가 또는 삭제할 수 있어 유연성이 좋다.

- 새로운 요청에 대한 처리 객체 생성이 편리해진다.

 

4. 책임 연쇄 패턴의 단점

- 충분한 디버깅을 거치지 않았을 경우 집합 내부에서 무한 사이클이 발생할 수 있다.

- 디버깅 및 테스트가 쉽지 않다.

- 일부 요청들은 처리되지 않을 수 있다.

- 처리가 지연될 수 있다.

 

5. 코드 구현

1) 구조

- Handler

: 요청을 수신하고 처리 객체들의 집합을 정의하는 인터페이스

 

- Concrete Handler

: 요청을 처리하는 실제 처리 객체

: 핸들러에 대한 필드를 내부에 가지고 있으며 메서드를 통해 다음 핸들러를 체인시킨다.

: 자신이 처리할 수 없는 요구가 나오면 바라보고 있는 다음 체인의 핸들러에게 요청을 떠넘긴다.

 

- Client

: 요청을 Handler에게 전달한다.

 

2) 소스코드(C#)

사용자로부터 url 문자열을 입력받으면, 각 url의 프로토콜, 도메인, 포트를 파싱해서 정보를 출력해주는 프로그램을 만든다고 해보자.

 

- 패턴 적용 전

using System;
using System.Diagnostics;
using System.Globalization;

namespace StudyCSharp
{
    class UrlParser
    {
        public static void run(string url)
        {
            // 파싱 인덱스
            int startIndex = url.IndexOf("://");
            int lastIndex = url.LastIndexOf(":");

            // 1. 프로토콜 파싱
            if (startIndex != -1)
            {
                Console.WriteLine("프로토콜 : " + url.Substring(0, startIndex));
            }
            else
            {
                Console.WriteLine("프로토콜 : 없음");
            }

            // 2, 도메인 파싱
            Console.Write("도메인 : ");

            if (startIndex == -1)
            {
                // 프로토콜이 없는 경우

                if (lastIndex == -1)
                {
                    // 포트 없음
                    Console.WriteLine(url);
                }
                else
                {
                    // 포트 있음
                    Console.WriteLine(url.Substring(0, lastIndex));
                }
            }
            else if (startIndex != lastIndex)
            {
                // 온전한 url인 경우
                Console.WriteLine(url.Substring(startIndex + 3, lastIndex - (startIndex + 3)));
            }
            else
            {
                // 포트만 없는 경우
                Console.WriteLine(url.Substring(startIndex + 3));
            }

            // 3. 포트 파싱            
            if (startIndex != lastIndex && lastIndex != -1)
            {
                string strPort = url.Substring(lastIndex + 1);

                try
                {
                    int port = int.Parse((strPort));
                    Console.WriteLine("포트 : " + port);
                }
                catch(Exception e)
                {
                    // 포트가 잘못 인식된 경우
                    Console.Error.WriteLine(e.StackTrace);
                } 
            }
            else
            {
                Console.WriteLine("포트 : 없음");
            }
        }
    }

    
    class Program
    {
        static void Main(string[] args)
        {
            Console.WriteLine("------------------------------------------------------");

            string url1 = "https://steadycodist.tistory.com:443";
            Console.WriteLine("입력 URL : " + url1);
            UrlParser.run(url1);

            Console.WriteLine("------------------------------------------------------");

            string url2 = "http://www.youtube.com:80";
            Console.WriteLine("입력 URL : " + url2);
            UrlParser.run(url2);

            Console.WriteLine("------------------------------------------------------");

            string url3 = "localhost:8080";
            Console.WriteLine("입력 URL : " + url3);
            UrlParser.run(url3);

            Console.WriteLine("------------------------------------------------------");

            string url4 = "steadycodist.tistory.com";
            Console.WriteLine("입력 URL : " + url4);
            UrlParser.run(url4);

            Console.WriteLine("------------------------------------------------------");

            string url5 = "https://steadycodist.tistory.com";
            Console.WriteLine("입력 URL : " + url5);
            UrlParser.run(url5);

            Console.WriteLine("------------------------------------------------------");
        }
    }
}

 

- 패턴 적용 후

using System;
using System.Diagnostics;
using System.Globalization;

namespace StudyCSharp
{
    // Handler : 구체적인 핸들러를 묶는 인터페이스(추상클래스)
    abstract class Handler
    {
        // 다음 핸들러 - 체인으로 연결
        protected Handler nextHandler = null;

        // 다음 핸들러 연결
        public Handler setNext(Handler handler)
        {
            this.nextHandler = handler;
            return handler;
        }

        // 추상 메서드
        protected abstract void process(string url);

        // 핸들러가 요청에 대해 처리하는 메서드
        public void run(string url)
        {
            process(url);

            // 다음 핸들러가 있는 경우 -> 다음으로 떠넘김
            if (nextHandler != null)
                nextHandler.run(url);
        }
    }

    // Concrete Handler 1
    class ProtocolHandler : Handler
    {
        protected override void process(string url)
        {
            int index = url.IndexOf("://");

            if (index != -1)
            {
                Console.WriteLine("프로토콜 : " + url.Substring(0, index));
            }
            else
            {
                Console.WriteLine("프로토콜 : 없음");
            }
        }
    }

    // Concrete Handler 2
    class DomainHandler : Handler
    {
        protected override void process(string url)
        {
            int startIndex = url.IndexOf("://");
            int lastIndex = url.LastIndexOf(":");

            Console.Write("도메인 : ");

            if (startIndex == -1)
            {
                // 프로토콜이 없는 경우

                if (lastIndex == -1)
                {
                    // 포트 없음
                    Console.WriteLine(url);
                }
                else
                {
                    // 포트 있음
                    Console.WriteLine(url.Substring(0, lastIndex));
                }
            }
            else if (startIndex != lastIndex)
            {
                // 온전한 url인 경우
                Console.WriteLine(url.Substring(startIndex + 3, lastIndex - (startIndex + 3)));
            }
            else
            {
                // 포트만 없는 경우
                Console.WriteLine(url.Substring(startIndex + 3));
            }
        }
    }

    // Concrete Handler 3
    class PortHandler : Handler
    {
        protected override void process(string url)
        {
            int index = url.LastIndexOf(":");

            if (url.IndexOf("://") != index && index != -1)
            {
                string strPort = url.Substring(index + 1);

                try
                {
                    int port = int.Parse((strPort));
                    Console.WriteLine("포트 : " + port);
                }
                catch (Exception e)
                {
                    // 포트가 잘못 인식된 경우
                    Console.Error.WriteLine(e.StackTrace);
                }
            }
            else
            {
                Console.WriteLine("포트 : 없음");
            }
        }
    }
    
    class Program
    {
        static void Main(string[] args)
        {
            // 1. 핸들러 생성
            Handler handler1 = new ProtocolHandler();
            Handler handler2 = new DomainHandler();
            Handler handler3 = new PortHandler();

            // 2. 핸들러 체인 연결
            handler1.setNext(handler2).setNext(handler3);

            // 3. 요청에 대한 처리 연쇄 실행
            Console.WriteLine("------------------------------------------------------");

            string url1 = "https://steadycodist.tistory.com:443";
            Console.WriteLine("입력 URL : " + url1);
            handler1.run(url1);

            Console.WriteLine("------------------------------------------------------");

            string url2 = "http://www.youtube.com:80";
            Console.WriteLine("입력 URL : " + url2);
            handler1.run(url2);

            Console.WriteLine("------------------------------------------------------");

            string url3 = "localhost:8080";
            Console.WriteLine("입력 URL : " + url3);
            handler1.run(url3);

            Console.WriteLine("------------------------------------------------------");

            string url4 = "steadycodist.tistory.com";
            Console.WriteLine("입력 URL : " + url4);
            handler1.run(url4);

            Console.WriteLine("------------------------------------------------------");

            string url5 = "https://steadycodist.tistory.com";
            Console.WriteLine("입력 URL : " + url5);
            handler1.run(url5);

            Console.WriteLine("------------------------------------------------------");
        }
    }
}

 

- 실행 결과(적용 전,후 동일)

 

cf) 소스코드2(C#) - 중첩된 if-else 문 재구성

 

 

서버가 있고 사용자가 미들웨어를 통해 로그인을 하는 과정

 

- 패턴 적용 전

using System;
using System.Collections.Generic;

namespace StudyCSharp
{
    // 서버
    class Server
    {
        // 유저 정보
        private Dictionary<string, string> users = new Dictionary<string, string>();
        public Dictionary<string, string> Users { get {  return users; } }

        // 서버에 유저 등록
        public void register(string id, string password)
        {
            users.Add(id, password);
        }

        // 서버에 해당 이메일 계정이 가입되어 있는지 판단
        public bool hasID(string id)
        {
            return users.ContainsKey(id); 
        }

        // 서버에 해당 계정의 비밀번호가 일치하는지 판단.
        public bool isValidPassword(string id, string password)
        {
            return users[id] == password;
        }
    }
    
    // 미들웨어
    class Middleware
    {
        private int limit = 3;
        private int count = 0;

        Server _server;

        // 생성자 - 서버 연결
        public Middleware(Server server)
        {
            this._server = server;
        }

        // 로그인 횟수 제한
        public bool limitLoginAttempt()
        {
            if (count >= limit)
            {
                Console.WriteLine("\n로그인 시도 가능 횟수를 초과하였습니다.(최대 3회)");
                return false;
            }

            count++;
            return true;
        }

        // 아이디, 패스워드 인증
        public bool Login(string id, string password)
        {
            if (!_server.hasID(id))
            {
                Console.WriteLine("\n가입된 계정이 없습니다.\n");
                return false;
            }

            if (!_server.isValidPassword(id, password))
            {
                Console.WriteLine("\n패스워드가 다릅니다.\n");
                return false;
            }

            Console.WriteLine("\n로그인 완료!!");
            return true;
        }

        // 관리자 계정인지 여부 판단.
        public bool authentication(string id)
        {
            if (id == "steadycodist")
            {
                Console.WriteLine("관리자님, 환영합니다.");
                return true;
            }
            else
            {
                Console.WriteLine("{0}님 환영합니다.", id);
                return false;
            }
        }

        // 일반 유저의 경우 -> 유저의 요청에 대해선 모두 로깅(로그 기록 생성)
        public void logging()
        {
            Console.WriteLine();
            Console.WriteLine("-----------------------------");
            Console.WriteLine("로그 기록을 생성합니다.");
            Console.WriteLine("-----------------------------");
            Console.WriteLine();
        }

        // 관리자인 경우 -> 모든 유저의 정보를 열람
        public void userInfo()
        {
            int count = 0;

            Console.WriteLine("\n[등록된 유저 정보]");
            Console.WriteLine("-----------------------------");
            
            foreach (string userID in _server.Users.Keys)
            {
                count++;

                Console.WriteLine("{0}번째 유저 : {1}", count, userID);                
            }

            Console.WriteLine("-----------------------------");
            Console.WriteLine();
        }
    }
    
    class Program
    {
        static void Main(string[] args)
        {
            // 1, 서버 생성 및 등록
            Server server = new Server();
            server.register("steadycodist", "123456789");
            server.register("user1", "11111111");
            server.register("user2", "22222222");

            // 2. 인증 로직을 처리하는 미들웨어 생성
            Middleware middleware = new Middleware(server);

            // 3. 로그인 시도
            do
            {
                // 로그인 시도 횟수가 아직 남아있는 경우
                if (middleware.limitLoginAttempt())
                {
                    Console.Write("이메일을 입력하세요: ");
                    string ID = Console.ReadLine();

                    Console.Write("비밀번호를 입력하세요: ");
                    string PW = Console.ReadLine();

                    // 로그인 시도
                    if (middleware.Login(ID, PW))
                    {
                        // 로그인에 성공한 경우 -> 관리자 계정인지 확인
                        if (middleware.authentication(ID))
                        {
                            middleware.userInfo();
                        }
                        else
                        {
                            middleware.logging();
                        }

                        break;
                    }
                    else
                    {
                        // 로그인 실패
                        continue;
                    }
                }
                else
                {
                    throw new Exception("로그인 시도 횟수 초과로 프로그램을 종료합니다.");
                }
            }
            while (true);
        }
    }
}

 

- 패턴 적용 후

using System;
using System.Collections.Generic;
using System.Security.Cryptography.X509Certificates;

namespace StudyCSharp
{
    // while문을 빠져나가기 위한 조건값으로 사용하기 위한 enum
    public enum Flag
    {
        Idle,       // 그대로 처리
        Exception,  // 예외처리
        Break,      // break
        Continue,   // continue
    }

    // 서버
    class Server
    {
        // 유저 정보
        private Dictionary<string, string> users = new Dictionary<string, string>();
        public Dictionary<string, string> Users { get { return users; } }

        // 서버에 유저 등록
        public void register(string id, string password)
        {
            users.Add(id, password);
        }

        // 서버에 해당 이메일 계정이 가입되어 있는지 판단
        public bool hasID(string id)
        {
            return users.ContainsKey(id);
        }

        // 서버에 해당 계정의 비밀번호가 일치하는지 판단.
        public bool isValidPassword(string id, string password)
        {
            return users[id] == password;
        }
    }

    // 미들웨어 -> Handler 역할
    abstract class Middleware
    {
        // 다음 체인으로 연결될 핸들러
        protected Middleware nextMiddleware = null;

        // 다음 핸들러 연결
        public Middleware setNext(Middleware middleware)
        {
            this.nextMiddleware = middleware;
            return middleware;
        }       

        public virtual Flag check(string id, string pw)
        {
            Flag flag = Flag.Idle;

            // 다음 핸들러가 있는 경우 -> 다음으로 떠넘김
            if (nextMiddleware != null)
            {
                flag = nextMiddleware.check(id, pw);
            }

            return flag;
        }
    }

    // Concrete Handler 1
    class LimitLoginAttemptMiddleware : Middleware
    {
        // 로그인 횟수 제한
        private int limit = 3;
        private int count = 0;

        public override Flag check(string id = null, string pw = null)
        {
            Flag flag;

            if (count >= limit)
            {
                Console.WriteLine("\n로그인 시도 가능 횟수를 초과하였습니다.(최대 3회)");
                flag = Flag.Exception;
            }
            else
            {
                Console.Write("이메일을 입력하세요: ");
                id = Console.ReadLine();

                Console.Write("비밀번호를 입력하세요: ");
                pw = Console.ReadLine();

                // 로그인이 가능한 경우 -> 부모의 메서드 실행
                flag = base.check(id, pw);
            }

            count++;

            return flag;
        }
    }

    // Concrete Handler 2
    class LoginMiddleware : Middleware
    {
        // 아이디, 패스워드 인증
        private Server _server;

        public LoginMiddleware(Server server)
        {
            this._server = server;
        }

        public override Flag check(string id, string pw )
        {
            Flag flag;

            if (!_server.hasID(id))
            {
                Console.WriteLine("\n가입된 계정이 없습니다.\n");
                flag = Flag.Continue;
            }
            else if (!_server.isValidPassword(id, pw))
            {
                Console.WriteLine("\n패스워드가 다릅니다.\n");
                flag = Flag.Continue;
            }
            else
            {
                Console.WriteLine("\n로그인 완료!!");
                flag = base.check(id, pw);
            }

            return flag;
        }
    }

    // Concrete Handler 3
    class AuthenticationMiddleware : Middleware
    {
        // 관리자 계정인지 여부 판단.
        public override Flag check(string id, string pw)
        {
            Flag flag;

            if (id == "steadycodist")
            {
                Console.WriteLine("관리자님, 환영합니다.");
                flag = base.check(id, pw);
            }
            else
            {
                Console.WriteLine("{0}님 환영합니다.", id);
                flag = base.check(id, pw);
            }

            return flag;
        }
    }

    // Concrete Handler 4
    class LoggingMiddleware : Middleware
    {
        // 일반 유저의 경우 -> 유저의 요청에 대해선 모두 로깅(로그 기록 생성)
        public override Flag check(string id, string pw)
        {
            // 관리자 계정인 경우
            if (id == "steadycodist") 
                return base.check(id, pw);

            Console.WriteLine();
            Console.WriteLine("-----------------------------");
            Console.WriteLine("로그 기록을 생성합니다.");
            Console.WriteLine("-----------------------------");
            Console.WriteLine();

            return Flag.Break;
        }
    }

    // Concrete Handler 5
    class UserInfoMiddleware : Middleware
    {
        private Server _server;

        public UserInfoMiddleware(Server server)
        {
            this._server = server;
        }

        // 관리자인 경우 -> 모든 유저의 정보를 열람
        public override Flag check(string id, string pw)
        {
            int count = 0;

            Console.WriteLine("\n[등록된 유저 정보]");
            Console.WriteLine("-----------------------------");

            foreach (string userID in _server.Users.Keys)
            {
                count++;

                Console.WriteLine("{0}번째 유저 : {1}", count, userID);
            }

            Console.WriteLine("-----------------------------");
            Console.WriteLine();

            return Flag.Break;
        }
    }

    class Program
    {
        static void Main(string[] args)
        {
            // 1, 서버 생성 및 등록
            Server server = new Server();
            server.register("steadycodist", "123456789");
            server.register("user1", "11111111");
            server.register("user2", "22222222");

            // 2. 인증 로직을 처리하는 핸들러 생성
            LimitLoginAttemptMiddleware middleware1 = new LimitLoginAttemptMiddleware();
            LoginMiddleware middleware2 = new LoginMiddleware(server);
            AuthenticationMiddleware middleware3 = new AuthenticationMiddleware();
            LoggingMiddleware middleware4 = new LoggingMiddleware();
            UserInfoMiddleware middleware5 = new UserInfoMiddleware(server);

            // 3. 핸들러 체이닝
            middleware1
                .setNext(middleware2)
                .setNext(middleware3)
                .setNext(middleware4)
                .setNext(middleware5);

            // 4. 로그인 시도
            do
            {
                // 핸들러에서 flag를 받아옴
                Flag result = middleware1.check();

                // flag에 따라 루프문 다음 동작을 처리
                if (result == Flag.Exception)
                {
                    throw new Exception("로그인 시도 횟수 초과로 프로그램을 종료합니다.");
                }
                else if (result == Flag.Break)
                {
                    break;
                }
                else if(result == Flag.Continue)
                {
                    continue;
                }
            }
            while (true);
        }
    }
}

 

- 실행 결과(패턴 적용 전, 후 동일)


<참고 사이트>

 

💠 Chain Of Responsibility 패턴 - 완벽 마스터하기

Chain Of Responsibility Pattern 책임 연쇄 패턴(Chain Of Responsibility Pattern, COR)은 클라이어트의 요청에 대한 세세한 처리를 하나의 객체가 몽땅 하는 것이 아닌, 여러개의 처리 객체들로 나누고, 이들을 사

inpa.tistory.com

 

[디자인 패턴] 책임 연쇄 패턴(Chain of Responsibility Pattern)

1) 개요클라이언트로부터의 요청을 처리할 수 있는 처리객체를 집합(Chain)으로 만들어 부여함으로 결합을 느슨하기 위해 만들어진 디자인 패턴입니다.일반적으로 요청을 처리할 수 있는 객체를

always-intern.tistory.com

 

책임 연쇄 패턴

/ 디자인 패턴들 / 행동 패턴 책임 연쇄 패턴 다음 이름으로도 불립니다: CoR, 커맨드 사슬, Chain of Responsibility 의도 책임 연쇄 패턴은 핸들러들의 체인​(사슬)​을 따라 요청을 전달할 수 있게

refactoring.guru

 

책임 연쇄 패턴 (Chain-of-Responsibility) 패턴

책임 연쇄 패턴은 영어로 Chain-of-Responsibility라고하는데 각각의 인스턴스의 책임들이 체인처럼 연쇄되어 있다는 뜻입니다. 요청을 보내는쪽과 요청을 처리하는 쪽을 분리시키는 패턴인데 요청을

velog.io